From 37c3e46f5b6ab1fbe620922ab88f886e9402a314 Mon Sep 17 00:00:00 2001 From: Max Mehl Date: Tue, 13 Aug 2024 10:54:03 +0200 Subject: [PATCH 1/3] restructure SBOM commands --- .github/workflows/selftest.yaml | 2 +- README.md | 6 +- complassist/main.py | 99 +++++++++++++++++++-------------- 3 files changed, 62 insertions(+), 45 deletions(-) diff --git a/.github/workflows/selftest.yaml b/.github/workflows/selftest.yaml index 788a098..4612578 100644 --- a/.github/workflows/selftest.yaml +++ b/.github/workflows/selftest.yaml @@ -40,7 +40,7 @@ jobs: path: ${{ runner.temp }} # Run compliance-assistant sbom-enrich - name: Enrich SBOM - run: poetry run compliance-assistant -v sbom-enrich -f ${{ runner.temp }}/sbom-raw.json -o ${{ runner.temp }}/sbom-enriched.json + run: poetry run compliance-assistant -v sbom enrich -f ${{ runner.temp }}/sbom-raw.json -o ${{ runner.temp }}/sbom-enriched.json # Show and upload enriched SBOM - name: Print SBOM content run: cat ${{ runner.temp }}/sbom-enriched.json diff --git a/README.md b/README.md index 5939499..4d2d03c 100644 --- a/README.md +++ b/README.md @@ -108,9 +108,9 @@ For each command, you can get detailed options, e.g. `compliance-assistant sbom- ### Examples -* Create an SBOM for the current directory: `compliance-assistant sbom-generate -d .` -* Enrich an SBOM with ClearlyDefined data: `compliance-assistant sbom-enrich -f /tmp/my-sbom.json -o /tmp/my-enriched-sbom.json` -* Extract certain data from an SBOM: `compliance-assistant sbom-parse -f /tmp/my-enriched-sbom.json -e purl,copyright,name` +* Create an SBOM for the current directory: `compliance-assistant sbom generate -d .` +* Enrich an SBOM with ClearlyDefined data: `compliance-assistant sbom enrich -f /tmp/my-sbom.json -o /tmp/my-enriched-sbom.json` +* Extract certain data from an SBOM: `compliance-assistant sbom parse -f /tmp/my-enriched-sbom.json -e purl,copyright,name` * Gather ClearlyDefined licensing/copyright information for one package: `compliance-assistant clearlydefined -p pkg:pypi/inwx-dns-recordmaster@0.3.1` * Get license outbound candidate based on licenses from SBOM: `compliance-assistant licensing outbound -f /tmp/my-enriched-sbom.json` diff --git a/complassist/main.py b/complassist/main.py index 109f309..c144cd8 100644 --- a/complassist/main.py +++ b/complassist/main.py @@ -25,11 +25,27 @@ from ._sbom_parse import extract_items_from_cdx_sbom parser = argparse.ArgumentParser(description=__doc__) + +# General flags +parser.add_argument("-v", "--verbose", action="store_true", help="Verbose output") +parser.add_argument("--version", action="version", version="%(prog)s " + __version__) + +# Subcommands subparsers = parser.add_subparsers(dest="command", help="Available commands", required=True) +# SBOM commands +parser_sbom = subparsers.add_parser( + "sbom", + help="Commands to generate, enrich, and parse SBOMs", +) +subparser_sbom = parser_sbom.add_subparsers( + dest="sbom_command", + help="Available sbom commands", +) + # SBOM Generator -parser_sbom_gen = subparsers.add_parser( - "sbom-generate", +parser_sbom_gen = subparser_sbom.add_parser( + "generate", help="Generate a CycloneDX SBOM using the cdxgen Docker image", ) parser_sbom_gen.add_argument( @@ -48,8 +64,8 @@ ) # Enrich a SBOM with ClearlyDefined data -parser_sbom_enrich = subparsers.add_parser( - "sbom-enrich", +parser_sbom_enrich = subparser_sbom.add_parser( + "enrich", help="Enrich a CycloneDX SBOM and its licensing/copyright data via ClearlyDefined", ) parser_sbom_enrich.add_argument( @@ -66,8 +82,8 @@ ) # SBOM Parser -parser_sbom_read = subparsers.add_parser( - "sbom-parse", +parser_sbom_read = subparser_sbom.add_parser( + "parse", help="Parse a CycloneDX SBOM and extract contained information", formatter_class=argparse.ArgumentDefaultsHelpFormatter, ) @@ -130,65 +146,60 @@ "licensing", help="Help with checking and reaching Open Source license compliance", ) -licensing_subparser = parser_licensing.add_subparsers( +subparser_licensing = parser_licensing.add_subparsers( dest="licensing_command", help="Available licensing commands", ) # List licenses -licensing_list = licensing_subparser.add_parser( +parser_licensing_list = subparser_licensing.add_parser( "list", help="List all detected licenses", ) -licensing_list.add_argument( +parser_licensing_list.add_argument( "-f", "--file", help="Path to the CycloneDX SBOM (JSON format) from which licenses are read", required=True, ) -licensing_list.add_argument( +parser_licensing_list.add_argument( "-o", "--output", default="json", choices=["json", "dict", "plain", "none"], help="Desired output format.", ) -licensing_list.add_argument( +parser_licensing_list.add_argument( "--no-simplify", help="Do not simplify SPDX license expression using flict. May increase speed", action="store_true", ) # License outbound candidate -licensing_outbound = licensing_subparser.add_parser( +parser_licensing_outbound = subparser_licensing.add_parser( "outbound", help="Suggest possible outbound licenses based on found licenses in an SBOM", ) -licensing_outbound.add_argument( +parser_licensing_outbound.add_argument( "-f", "--file", help="Path to the CycloneDX SBOM (JSON format) from which licenses are read", required=True, ) -licensing_outbound.add_argument( +parser_licensing_outbound.add_argument( "-o", "--output", default="json", choices=["json", "dict", "plain", "none"], help="Desired output format. json and dict contain the most helpful output", ) -licensing_outbound.add_argument( +parser_licensing_outbound.add_argument( "--no-simplify", help="Do not simplify SPDX license expression using flict. May increase speed", action="store_true", ) -# General flags -parser.add_argument("-v", "--verbose", action="store_true", help="Verbose output") -parser.add_argument("--version", action="version", version="%(prog)s " + __version__) - - def configure_logger(args) -> logging.Logger: """Set logging options""" log = logging.getLogger() @@ -213,26 +224,32 @@ def main(): # pylint: disable=too-many-branches, too-many-statements logging.debug(args) # Generate SBOM with cdxgen - if args.command == "sbom-generate": - generate_cdx_sbom(args.directory, args.output) - - # Enrich SBOM by ClearlyDefined data - elif args.command == "sbom-enrich": - enrich_sbom_with_clearlydefined(args.file, args.output) - - # Parse info from SBOM - elif args.command == "sbom-parse": - # Convert comma-separated information to list - info = args.extract.split(",") - extraction = extract_items_from_cdx_sbom( - args.file, information=info, use_flict=not args.no_simplify - ) - if args.output == "json": - print(dict_to_json(extraction)) - elif args.output == "dict": - print(extraction) - elif args.output == "none": - pass + # SBOM commands + if args.command == "sbom": + if args.sbom_command == "generate": + generate_cdx_sbom(args.directory, args.output) + + # Enrich SBOM by ClearlyDefined data + elif args.sbom_command == "enrich": + enrich_sbom_with_clearlydefined(args.file, args.output) + + # Parse info from SBOM + elif args.sbom_command == "parse": + # Convert comma-separated information to list + info = args.extract.split(",") + extraction = extract_items_from_cdx_sbom( + args.file, information=info, use_flict=not args.no_simplify + ) + if args.output == "json": + print(dict_to_json(extraction)) + elif args.output == "dict": + print(extraction) + elif args.output == "none": + pass + + # No sbom subcommand given, show help + else: + parser_sbom.print_help() # Get ClearlyDefined license/copyright data for a package elif args.command == "clearlydefined": @@ -275,7 +292,7 @@ def main(): # pylint: disable=too-many-branches, too-many-statements elif args.output == "none": pass - # No subcommand given, show help + # No licensing subcommand given, show help else: parser_licensing.print_help() From a2966da242cfd710a48d45183d4b63de834cf913 Mon Sep 17 00:00:00 2001 From: Max Mehl Date: Tue, 13 Aug 2024 11:19:18 +0200 Subject: [PATCH 2/3] make -v available for all subcommands directly, feels more natural this way --- .github/workflows/selftest.yaml | 2 +- README.md | 8 ++++---- complassist/main.py | 16 ++++++++++++---- 3 files changed, 17 insertions(+), 9 deletions(-) diff --git a/.github/workflows/selftest.yaml b/.github/workflows/selftest.yaml index 4612578..140cf35 100644 --- a/.github/workflows/selftest.yaml +++ b/.github/workflows/selftest.yaml @@ -40,7 +40,7 @@ jobs: path: ${{ runner.temp }} # Run compliance-assistant sbom-enrich - name: Enrich SBOM - run: poetry run compliance-assistant -v sbom enrich -f ${{ runner.temp }}/sbom-raw.json -o ${{ runner.temp }}/sbom-enriched.json + run: poetry run compliance-assistant sbom enrich -v -f ${{ runner.temp }}/sbom-raw.json -o ${{ runner.temp }}/sbom-enriched.json # Show and upload enriched SBOM - name: Print SBOM content run: cat ${{ runner.temp }}/sbom-enriched.json diff --git a/README.md b/README.md index 4d2d03c..799a7cb 100644 --- a/README.md +++ b/README.md @@ -92,19 +92,19 @@ compliance-assistant poetry run compliance-assistant ``` -In the following, we will just use `compliance-assistant` +In the following, we will just use `compliance-assistant`. ### Command Structure ```bash -compliance-assistant [global-options] [command-options] +compliance-assistant [] [subcommand-options] ``` ### Commands Please run `compliance-assistant --help` to get an overview of the commands and global options. -For each command, you can get detailed options, e.g. `compliance-assistant sbom-enrich --help`. +For each command, you can get detailed options, e.g. `compliance-assistant sbom enrich --help`. ### Examples @@ -160,7 +160,7 @@ jobs: path: ${{ runner.temp }} # Run compliance-assistant sbom-enrich - name: Enrich SBOM - run: compliance-assistant sbom-enrich -f ${{ runner.temp }}/sbom-raw.json -o ${{ runner.temp }}/sbom-enriched.json + run: compliance-assistant sbom enrich -f ${{ runner.temp }}/sbom-raw.json -o ${{ runner.temp }}/sbom-enriched.json # Upload enriched SBOM as artifact - name: Store enriched SBOM as artifact uses: actions/upload-artifact@v4 diff --git a/complassist/main.py b/complassist/main.py index c144cd8..065fea4 100644 --- a/complassist/main.py +++ b/complassist/main.py @@ -24,15 +24,17 @@ from ._sbom_generate import generate_cdx_sbom from ._sbom_parse import extract_items_from_cdx_sbom +# Main parser with root-level flags parser = argparse.ArgumentParser(description=__doc__) - -# General flags -parser.add_argument("-v", "--verbose", action="store_true", help="Verbose output") parser.add_argument("--version", action="version", version="%(prog)s " + __version__) -# Subcommands +# Initiate first-level subcommands subparsers = parser.add_subparsers(dest="command", help="Available commands", required=True) +# Common flags, usable for all effective subcommands +common_flags = argparse.ArgumentParser(add_help=False) # No automatic help to avoid duplication +common_flags.add_argument("-v", "--verbose", action="store_true", help="Verbose output") + # SBOM commands parser_sbom = subparsers.add_parser( "sbom", @@ -47,6 +49,7 @@ parser_sbom_gen = subparser_sbom.add_parser( "generate", help="Generate a CycloneDX SBOM using the cdxgen Docker image", + parents=[common_flags], ) parser_sbom_gen.add_argument( "-d", @@ -67,6 +70,7 @@ parser_sbom_enrich = subparser_sbom.add_parser( "enrich", help="Enrich a CycloneDX SBOM and its licensing/copyright data via ClearlyDefined", + parents=[common_flags], ) parser_sbom_enrich.add_argument( "-f", @@ -86,6 +90,7 @@ "parse", help="Parse a CycloneDX SBOM and extract contained information", formatter_class=argparse.ArgumentDefaultsHelpFormatter, + parents=[common_flags], ) parser_sbom_read.add_argument( "-f", @@ -116,6 +121,7 @@ parser_cd = subparsers.add_parser( "clearlydefined", help="Gather license information from ClearlyDefined for a package", + parents=[common_flags], ) parser_cd_exclusive = parser_cd.add_mutually_exclusive_group(required=True) parser_cd_exclusive.add_argument( @@ -155,6 +161,7 @@ parser_licensing_list = subparser_licensing.add_parser( "list", help="List all detected licenses", + parents=[common_flags], ) parser_licensing_list.add_argument( "-f", @@ -179,6 +186,7 @@ parser_licensing_outbound = subparser_licensing.add_parser( "outbound", help="Suggest possible outbound licenses based on found licenses in an SBOM", + parents=[common_flags], ) parser_licensing_outbound.add_argument( "-f", From 0590e43acecb652480f6e002fbc05f0e42b225b3 Mon Sep 17 00:00:00 2001 From: Max Mehl Date: Tue, 13 Aug 2024 11:31:38 +0200 Subject: [PATCH 3/3] separate clearlydefined commands into two subcommands --- README.md | 2 +- complassist/main.py | 52 ++++++++++++++++++++++++++++++--------------- 2 files changed, 36 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index 799a7cb..d4629b1 100644 --- a/README.md +++ b/README.md @@ -111,7 +111,7 @@ For each command, you can get detailed options, e.g. `compliance-assistant sbom * Create an SBOM for the current directory: `compliance-assistant sbom generate -d .` * Enrich an SBOM with ClearlyDefined data: `compliance-assistant sbom enrich -f /tmp/my-sbom.json -o /tmp/my-enriched-sbom.json` * Extract certain data from an SBOM: `compliance-assistant sbom parse -f /tmp/my-enriched-sbom.json -e purl,copyright,name` -* Gather ClearlyDefined licensing/copyright information for one package: `compliance-assistant clearlydefined -p pkg:pypi/inwx-dns-recordmaster@0.3.1` +* Gather ClearlyDefined licensing/copyright information for one package: `compliance-assistant clearlydefined fetch -p pkg:pypi/inwx-dns-recordmaster@0.3.1` * Get license outbound candidate based on licenses from SBOM: `compliance-assistant licensing outbound -f /tmp/my-enriched-sbom.json` ### Run as GitHub workflow diff --git a/complassist/main.py b/complassist/main.py index 065fea4..7a4e222 100644 --- a/complassist/main.py +++ b/complassist/main.py @@ -120,33 +120,50 @@ # ClearlyDefined parser_cd = subparsers.add_parser( "clearlydefined", - help="Gather license information from ClearlyDefined for a package", + help="Use ClearlyDefined to fetch licensing and copyright information, and run coversions", +) +subparser_cd = parser_cd.add_subparsers( + dest="clearlydefined_command", + help="Available clearlydefined commands", +) + +# ClearlyDefined convert subcommand +parser_cd_convert = subparser_cd.add_parser( + "convert", + help="Convert a Package URL to ClearlyDefined coordinates", parents=[common_flags], ) -parser_cd_exclusive = parser_cd.add_mutually_exclusive_group(required=True) -parser_cd_exclusive.add_argument( +parser_cd_convert.add_argument( + "-p", + "--purl", + help="A Package URL (purl) to convert to ClearlyDefined coordinates.", +) + +# ClearlyDefined fetch subcommand +parser_cd_fetch = subparser_cd.add_parser( + "fetch", + help="Fetch licensing and copyright information of packages from ClearlyDefined", + parents=[common_flags], +) +parser_cd_fetch_exclusive = parser_cd_fetch.add_mutually_exclusive_group(required=True) +parser_cd_fetch_exclusive.add_argument( "-p", "--purl", help=( "The purl for which ClearlyDefined licensing information is searched. " - "If -c is used, this is preferred." + "Cannot be combined with -c" ), ) -parser_cd_exclusive.add_argument( +parser_cd_fetch_exclusive.add_argument( "-c", "--coordinates", help=( - "The ClearlyDefined coordinates for which ClearlyDefined licensing information is searched" - ), -) -parser_cd_exclusive.add_argument( - "--purl-to-coordinates", - help=( - "Convert a Package URL (purl) to ClearlyDefined coordinates, and show result. " - "Cannot be combined with -p and -c." + "The ClearlyDefined coordinates for which licensing information is searched. " + "Canot be combined with -p." ), ) + # License Compliance parser_licensing = subparsers.add_parser( "licensing", @@ -259,12 +276,13 @@ def main(): # pylint: disable=too-many-branches, too-many-statements else: parser_sbom.print_help() - # Get ClearlyDefined license/copyright data for a package + # ClearlyDefined commands elif args.command == "clearlydefined": - if args.purl_to_coordinates: - print(purl_to_cd_coordinates(args.purl_to_coordinates)) + # ClearlyDefined conversion + if args.clearlydefined_command == "convert": + print(purl_to_cd_coordinates(args.purl)) - elif args.coordinates or args.purl: + elif args.clearlydefined_command == "fetch": if args.purl: coordinates = purl_to_cd_coordinates(args.purl) else: