Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Create GitHub actions for code formatting and code quality #1

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions .github/workflows/code-format.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# This is a rather superficial check that only verifies compliance with the formatting style.

name: Java code format checks

on: push

jobs:
build:

runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v3
- name: Set up JDK 11
uses: actions/setup-java@v3
with:
java-version: '11'
distribution: 'temurin'
cache: maven
- name: Check code formatting compliance
run: mvn spotless:check
61 changes: 61 additions & 0 deletions .github/workflows/code-quality.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
# Run various code quality checks when pushing to main or when opening pull requests.
# - This will run the tests, as well as
# - Perform an analysis via SonarCloud
# - Examine whether any of the dependencies contain known vulnerabilities

name: Code quality checks
on:
push:
branches:
- main
pull_request:
types: [opened, synchronize, reopened]
jobs:
analyze_sonar:
name: Run unit tests and Sonar analysis
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis
- name: Set up JDK 17
uses: actions/setup-java@v3
with:
java-version: 17
distribution: 'temurin'
- name: Cache SonarCloud packages
uses: actions/cache@v3
with:
path: ~/.sonar/cache
key: ${{ runner.os }}-sonar
restore-keys: ${{ runner.os }}-sonar
- name: Cache Maven packages
uses: actions/cache@v3
with:
path: ~/.m2
key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }}
restore-keys: ${{ runner.os }}-m2
- name: Analyze code quality
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
# note that we deliberately turn off the OWASP dependency checker here, it will run in a separate job,
# such that its results can be viewed independently of what Sonar has to say
run: |
mvn -B verify sonar:sonar -Dsonar.projectKey=kiron-mx_LightweightCmpRa -Ddependency-check.skip=true

analyze_dependencies_owasp:
name: Check dependencies with OWASP
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Set up JDK 11
uses: actions/setup-java@v3
with:
java-version: 11
distribution: 'temurin'
- name: Analyze dependencies
# this will run the OWASP dependency checker only
run: mvn -B verify -DskipTests
24 changes: 24 additions & 0 deletions .github/workflows/licence-compliance.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Produce a software bill of materials (SBOM) and ensure that the licenses of the dependencies are compatible
# with the project

name: License compatibility checks

on: push

jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up JDK 11
uses: actions/setup-java@v3
with:
java-version: '11'
distribution: 'temurin'
cache: maven
- name: Produce SBOM in CycloneDX format
run: mvn cyclonedx:makeAggregateBom
- name: Produce concise license report
run: /usr/bin/python3 scripts/license-check.py --sbom target/bom.json --report
- name: Check license compliance
run: /usr/bin/python3 scripts/license-check.py --sbom target/bom.json --enforce resources/allowed-licenses.txt
57 changes: 49 additions & 8 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@
<!-- Copyright (c) 2020 Siemens AG Licensed under the Apache License, Version
2.0 SPDX-License-Identifier: Apache-2.0 -->
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.siemens.pki</groupId>
<artifactId>LightweightCmpRa</artifactId>
Expand All @@ -18,6 +18,14 @@
<maven.compiler.target>11</maven.compiler.target>
<maven-failsafe-plugin.version>3.0.0-M3</maven-failsafe-plugin.version>
<maven-site-plugin.version>3.8.2</maven-site-plugin.version>
<sonar.organization>kiron-mx</sonar.organization>
<sonar.host.url>https://sonarcloud.io</sonar.host.url>
<sonar.java.coveragePlugin>jacoco</sonar.java.coveragePlugin>
<sonar.dynamicAnalysis>reuseReports</sonar.dynamicAnalysis>
<sonar.coverage.jacoco.xmlReportPaths>
${project.basedir}/target/site/jacoco/jacoco.xml</sonar.coverage.jacoco.xmlReportPaths>
<sonar.language>java</sonar.language>
<sonar.verbose>true</sonar.verbose>
</properties>
<build>
<plugins>
Expand Down Expand Up @@ -74,7 +82,6 @@
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-javadoc-plugin</artifactId>
<version>3.6.2</version>
<configuration />
</plugin>
<plugin>
<groupId>com.diffplug.spotless</groupId>
Expand All @@ -93,14 +100,14 @@
<importOrder />
<removeUnusedImports />
<formatAnnotations />
<endWithNewline />
<endWithNewline/>
</java>
</configuration>
</plugin>
<plugin>
<groupId>org.owasp</groupId>
<artifactId>dependency-check-maven</artifactId>
<version>8.4.2</version>
<version>8.4.3</version>
<executions>
<execution>
<goals>
Expand Down Expand Up @@ -141,7 +148,7 @@
<configuration>
<serverId>ossrh</serverId>
<nexusUrl>https://s01.oss.sonatype.org/</nexusUrl>
<autoReleaseAfterClose>true</autoReleaseAfterClose>
<autoReleaseAfterClose>false</autoReleaseAfterClose>
</configuration>
</plugin>
<plugin>
Expand All @@ -150,12 +157,26 @@
<executions>
<execution>
<id>attach-source</id>
<phase>package</phase>
<goals>
<goal>jar</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.cyclonedx</groupId>
<artifactId>cyclonedx-maven-plugin</artifactId>
<version>2.7.9</version>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>makeAggregateBom</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
<dependencies>
Expand Down Expand Up @@ -202,13 +223,27 @@
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.15.3</version>
<version>2.16.0</version>
</dependency>
<dependency>
<groupId>commons-cli</groupId>
<artifactId>commons-cli</artifactId>
<version>1.5.0</version>
</dependency>
<dependency>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
<version>${jacoco.version}</version>
</dependency>
<dependency>
<!-- Indirect dependency of jacoco-maven-plugin. We add this one
explicitly, otherwise the included version-->
<!-- would pull maven-artifact-manager-2.0.2 and lead to
CVE-2021-26291.-->
<groupId>org.apache.maven.shared</groupId>
<artifactId>file-management</artifactId>
<version>3.1.0</version>
</dependency>

</dependencies>
<scm>
Expand All @@ -221,4 +256,10 @@
<name>Lightweight CMP RA</name>
<description>This project provides a Proof of Concept (PoC) implementation
of the Lightweight CMP Profile for CMP [RFC 4210].</description>
</project>
<licenses>
<license>
<name>The Apache License, Version 2.0</name>
<url>http://www.apache.org/licenses/LICENSE-2.0.txt</url>
</license>
</licenses>
</project>
8 changes: 8 additions & 0 deletions resources/allowed-licenses.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# This is a list of licenses, one per line, that will be treated as acceptable. Any license in the SBOM that
# is not in this list will cause the pipeline to fail. The file is prepared for use by scripts/license-check.py
Apache-2.0
MIT
EPL-2.0
BSD-3-Clause
EPL-1.0
Bouncy Castle Licence
132 changes: 132 additions & 0 deletions scripts/license-check.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
#!/usr/bin/python3
"""Tools to parse and process a software bill of materials (SBOM) in CycloneDX format, to check whether all the
dependencies used in the project use compatible licenses."""

import json
import logging
import sys
import argparse
import pathlib


def discover_licenses(sbom):
"""Return a set of licenses featured in a given SBOM

:param sbom: dict, loaded SBOM data from a JSON file
:returns: set of strings that correspond to licenses featured in the SBOM"""
found_licenses = set()
for item in sbom['components']:
name, version, licenses = item['name'], item['version'], item['licenses']
if not item['licenses']:
logging.warning(f'{name} {version}: Unspecified license!')

for entry in licenses:
# special treatment for bouncy castle
# https://github.com/spdx/license-list-XML/issues/910
if 'id' not in entry['license'] and entry['license']['name'] == 'Bouncy Castle Licence':
found_licenses.add('Bouncy Castle Licence')
else:
found_licenses.add(entry['license']['id'])
return found_licenses


def generate_license_report(sbom):
"""Generate a list of licenses from the given SBOM

:param sbom: dict, loaded SBOM data from a JSON file
:returns: list of tuples(name, version, license), each tuple element is a string"""
entries = []
for item in sbom['components']:
name, version, licenses = item['name'], item['version'], item['licenses']
if not item['licenses']:
entries.append((name, version, None))

for entry in licenses:
# according to the JSON schema of components/licenses/license, either `id` or `name` must be there
try:
license_id = entry['license']['id']
except KeyError:
license_id = entry['license']['name']
entries.append((name, version, license_id))
return entries


def pretty_report(licenses):
"""Stringify the license list of the SBOM as an ASCII table

:param licenses: list of tuples(name, version, license), each tuple element is a string
:returns: str, tabular representation of the data
"""
result = 'Component \tVersion \tLicense\n'
for name, version, license in licenses:
result += f'{name: <20}\t{version: <8}\t{license}\n'
return result


def check_license_compliance(sbom, allowed):
"""Verify if the licenses in the SBOM correspond to the ones allowed in the project

:param sbom: dict, loaded SBOM data from a JSON file
:param allowed: set of str, containing allowed licenses
:returns: bool, True if everything is compliant, otherwise False; violations will be logged"""
errors = [] # list of tuples, each entry is (component name, version, error type)
for item in sbom['components']:
name, version, licenses = item['name'], item['version'], item['licenses']
if not item['licenses']:
errors.append((name, version, 'no license specified'))

for license in licenses:
# according to the JSON schema of components/licenses/license, either `id` or `name` must be there
try:
license_id = license['license']['id']
except KeyError:
license_id = license['license']['name']
if license_id not in allowed:
errors.append((name, version, f'`{license_id}` not allowed'))

if not errors:
return True

logging.warning('License issues found: %i', len(errors))
for error in errors:
logging.warning(error)
return False


def load_allowed_licenses(path):
result = set()
with open(path, 'r') as f:
for line in f:
if line.startswith('#'):
continue
result.add(line.strip())
return result


if __name__ == '__main__':
logging.basicConfig(level=logging.DEBUG, format="%(levelname)7s %(message)s")

parser = argparse.ArgumentParser()
parser.add_argument("-s", "--sbom", help="Path to SBOM JSON file", type=str, required=True)
parser.add_argument("--enforce", help="Path to list of allowed licenses", type=pathlib.Path)
parser.add_argument('--report', help="Print a report of components and licenses", action='store_true')
args = parser.parse_args()

raw_sbom = open(args.sbom, 'r').read()
parsed_sbom = json.loads(raw_sbom)

discovery = discover_licenses(parsed_sbom)
logging.info('Licenses discovered: %s', discovery)

if args.report:
summary = pretty_report(generate_license_report(parsed_sbom))
logging.info('Producing license report: \n%s', summary)

if args.enforce:
allowed = load_allowed_licenses(args.enforce)
valid = check_license_compliance(parsed_sbom, allowed)
if valid:
logging.info('SUCCESS: No license issues found')
sys.exit(0)

sys.exit(1)
Loading