From 51b74849864257707482446461b6636628da9754 Mon Sep 17 00:00:00 2001 From: clueleaf <10379303+clueleaf@users.noreply.github.com> Date: Thu, 29 Aug 2024 04:58:31 +0900 Subject: [PATCH 01/19] replace scanner example (#84) --- README.md | 2 +- action.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 05d8d55..774bf39 100644 --- a/README.md +++ b/README.md @@ -333,7 +333,7 @@ The following input and output options are provided by this action. See [action. | medium_threshold | Specifies the number of medium vulnerabilities needed to set the 'vulnerability_threshold_exceeded' flag. | False | 0 | | low_threshold | Specifies the number of low vulnerabilities needed to set the 'vulnerability_threshold_exceeded' flag. | False | 0 | | other_threshold | Specifies the number of other vulnerabilities needed to set the 'vulnerability_threshold_exceeded' flag. | False | 0 | -| scanners | Specifies the file scanners that you would like inspector-sbomgen to execute. By default, inspector-sbomgen will try to run all file scanners that are applicable to the target artifact. If this argument is set, inspector-sbomgen will only execute the specified file scanners. Provide your input as a single string. Separate each file scanner with a comma. For example: scanners: dpkg,python-requirements,javascript-nodejsTo view a list of available file scanners, execute 'inspector-sbomgen list-scanners'. See here for more info: https://docs.aws.amazon.com/inspector/latest/user/sbom-generator.html | False | '' | +| scanners | Specifies the file scanners that you would like inspector-sbomgen to execute. By default, inspector-sbomgen will try to run all file scanners that are applicable to the target artifact. If this argument is set, inspector-sbomgen will only execute the specified file scanners. Provide your input as a single string. Separate each file scanner with a comma. For example: scanners: dpkg,python-requirements,javascript-npm-packagelock To view a list of available file scanners, execute 'inspector-sbomgen list-scanners'. See here for more info: https://docs.aws.amazon.com/inspector/latest/user/sbom-generator.html | False | '' | | skip_scanners | Specifies a list of file scanners that should NOT be executed; this argument cannot be combined with 'scanners'. If this argument is set, inspector-sbomgen will execute all file scanners except those you specified. Provide your input as a single string. Separate each file scanner with a comma. For example: skip_scanners: 'binaries,alpine-apk,dpkg,php'To view a list of available file scanners, execute 'inspector-sbomgen list-scanners'. See here for more info: https://docs.aws.amazon.com/inspector/latest/user/sbom-generator.html | False | '' | | skip_files | Specifies one or more files and/or directories that should NOT be inventoried. Separate each file with a comma and enclose the entire string in double quotes, for example: skip_files: "./media,/tmp/foo/,/bar/my_program" | False | '' | | timeout | Specifies a timeout in seconds. If this timeout is exceeded, the action will gracefully conclude and present any findings discovered up to that point. Default value is 600 seconds or 10 minutes. | False | 600 | diff --git a/action.yml b/action.yml index 7110ffe..e33c71c 100644 --- a/action.yml +++ b/action.yml @@ -85,7 +85,7 @@ inputs: required: False default: "''" # Example: - # scanners: "dpkg,python-requirements,javascript-nodejs" + # scanners: "dpkg,python-requirements,javascript-npm-packagelock" skip_scanners: description: "Specifies a list of file scanners that should NOT be executed; this argument cannot be combined with 'scanners'. If this argument is set, inspector-sbomgen will execute all file scanners except those you specified. Provide your input as a single string. Separate each file scanner with a comma. To view a list of available file scanners, execute 'inspector-sbomgen list-scanners'. See here for more info: https://docs.aws.amazon.com/inspector/latest/user/sbom-generator.html" From 3aa7bb236da19c483a780429cc832cd402dff9b2 Mon Sep 17 00:00:00 2001 From: Michael Long <31821088+bluesentinelsec@users.noreply.github.com> Date: Fri, 30 Aug 2024 14:45:08 -0400 Subject: [PATCH 02/19] Write CSV with no vulns (#86) * reproducing issue - test 1 * resolve issue 85 - test 2 * test 3 * test fix --------- Co-authored-by: Michael Long --- .github/workflows/test_containers.yml | 8 ++++---- entrypoint/entrypoint/orchestrator.py | 4 ---- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/.github/workflows/test_containers.yml b/.github/workflows/test_containers.yml index 357dbbd..b76434d 100644 --- a/.github/workflows/test_containers.yml +++ b/.github/workflows/test_containers.yml @@ -35,12 +35,12 @@ jobs: uses: aws-actions/vulnerability-scan-github-action-for-amazon-inspector@v1.1.3 with: artifact_type: 'container' - artifact_path: 'ubuntu:14.04' + artifact_path: 'alpine:latest' display_vulnerability_findings: "enabled" - sbomgen_version: "1.3.1" + sbomgen_version: "1.4.0" - - name: Display scan results - run: cat ${{ steps.inspector.outputs.inspector_scan_results }} + - name: Display scan results (CSV) + run: cat ${{ steps.inspector.outputs.inspector_scan_results_csv }} - name: Validate scan content run: python3 validator/validate_inspector_scan.py --file ${{ steps.inspector.outputs.inspector_scan_results }} diff --git a/entrypoint/entrypoint/orchestrator.py b/entrypoint/entrypoint/orchestrator.py index 500a5e1..40d5d7f 100644 --- a/entrypoint/entrypoint/orchestrator.py +++ b/entrypoint/entrypoint/orchestrator.py @@ -344,10 +344,6 @@ def install_sbomgen(args): def write_pkg_vuln_report_csv(out_scan_csv, scan_result: exporter.InspectorScanResult): - if scan_result.total_vulns() == 0: - logging.info("skipping package vulnerability CSV report because no vulnerabilities were detected") - return - csv_output = exporter.to_csv(scan_result) logging.info(f"writing package vulnerability CSV report to: {out_scan_csv}") From 561f140812b1af69131edcc5348ad2fd1843f11f Mon Sep 17 00:00:00 2001 From: Michael Long Date: Fri, 30 Aug 2024 14:59:48 -0400 Subject: [PATCH 03/19] testing CSV with no vulns --- .github/workflows/test_containers.yml | 8 +++--- .github/workflows/test_csv_no_vulns.yml | 37 +++++++++++++++++++++++++ 2 files changed, 41 insertions(+), 4 deletions(-) create mode 100644 .github/workflows/test_csv_no_vulns.yml diff --git a/.github/workflows/test_containers.yml b/.github/workflows/test_containers.yml index b76434d..357dbbd 100644 --- a/.github/workflows/test_containers.yml +++ b/.github/workflows/test_containers.yml @@ -35,12 +35,12 @@ jobs: uses: aws-actions/vulnerability-scan-github-action-for-amazon-inspector@v1.1.3 with: artifact_type: 'container' - artifact_path: 'alpine:latest' + artifact_path: 'ubuntu:14.04' display_vulnerability_findings: "enabled" - sbomgen_version: "1.4.0" + sbomgen_version: "1.3.1" - - name: Display scan results (CSV) - run: cat ${{ steps.inspector.outputs.inspector_scan_results_csv }} + - name: Display scan results + run: cat ${{ steps.inspector.outputs.inspector_scan_results }} - name: Validate scan content run: python3 validator/validate_inspector_scan.py --file ${{ steps.inspector.outputs.inspector_scan_results }} diff --git a/.github/workflows/test_csv_no_vulns.yml b/.github/workflows/test_csv_no_vulns.yml new file mode 100644 index 0000000..b8d9801 --- /dev/null +++ b/.github/workflows/test_csv_no_vulns.yml @@ -0,0 +1,37 @@ +name: Test CSV no vulns + +on: + push: + branches: # + - '*' + +jobs: + daily_job: + runs-on: ubuntu-latest + environment: + name: plugin-development + + steps: + + - name: Checkout this repository + uses: actions/checkout@v4 + + - name: Configure AWS credentials + uses: aws-actions/configure-aws-credentials@v4 + with: + aws-region: ${{ secrets.AWS_REGION }} + aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} + aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + role-to-assume: ${{ secrets.AWS_IAM_ROLE }} + + - name: Test container scan + id: inspector + uses: aws-actions/vulnerability-scan-github-action-for-amazon-inspector@v1.1.3 + with: + artifact_type: 'container' + artifact_path: 'alpine:latest' + display_vulnerability_findings: "enabled" + sbomgen_version: "latest" + + - name: Display scan results + run: cat ${{ steps.inspector.outputs.inspector_scan_results_csv }} From 2310b9e78c35ee553a3e538d6b534c0292359b8f Mon Sep 17 00:00:00 2001 From: Michael Long Date: Fri, 30 Aug 2024 15:07:04 -0400 Subject: [PATCH 04/19] test against main branch --- .github/workflows/test_csv_no_vulns.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test_csv_no_vulns.yml b/.github/workflows/test_csv_no_vulns.yml index b8d9801..b44b07b 100644 --- a/.github/workflows/test_csv_no_vulns.yml +++ b/.github/workflows/test_csv_no_vulns.yml @@ -26,7 +26,7 @@ jobs: - name: Test container scan id: inspector - uses: aws-actions/vulnerability-scan-github-action-for-amazon-inspector@v1.1.3 + uses: aws-actions/vulnerability-scan-github-action-for-amazon-inspector@main with: artifact_type: 'container' artifact_path: 'alpine:latest' From 2acdf071383d8dbf3d97b45029f0cc559693c029 Mon Sep 17 00:00:00 2001 From: Michael Long <31821088+bluesentinelsec@users.noreply.github.com> Date: Fri, 30 Aug 2024 15:27:35 -0400 Subject: [PATCH 05/19] Write Dockerfile CSV and Markdown on no vulns (#88) Co-authored-by: Michael Long --- entrypoint/entrypoint/dockerfile.py | 6 ------ entrypoint/tests/test_dockerfile_checks.py | 4 ++-- 2 files changed, 2 insertions(+), 8 deletions(-) diff --git a/entrypoint/entrypoint/dockerfile.py b/entrypoint/entrypoint/dockerfile.py index 53ff4ae..17ea337 100644 --- a/entrypoint/entrypoint/dockerfile.py +++ b/entrypoint/entrypoint/dockerfile.py @@ -314,9 +314,6 @@ def dockerfile_vulns_to_csv(dockerfile_vulns): def write_dockerfile_report_csv(inspector_scan_path, dst_file): dockerfile_vulns = get_dockerfile_vulns(inspector_scan_path) - if len(dockerfile_vulns) == 0: - logging.info(f"skipping dockerfile vulnerability CSV report because no vulnerabilities were detected") - return False csv_output = dockerfile_vulns_to_csv(dockerfile_vulns) @@ -328,9 +325,6 @@ def write_dockerfile_report_csv(inspector_scan_path, dst_file): def write_dockerfile_report_md(inspector_scan_path, dst_file): dockerfile_vulns = get_dockerfile_vulns(inspector_scan_path) - if len(dockerfile_vulns) == 0: - logging.info(f"skipping dockerfile vulnerability MD report because no vulnerabilities were detected") - return False markdown_report = get_markdown_header() for vuln in dockerfile_vulns: diff --git a/entrypoint/tests/test_dockerfile_checks.py b/entrypoint/tests/test_dockerfile_checks.py index cc3fa37..1095968 100644 --- a/entrypoint/tests/test_dockerfile_checks.py +++ b/entrypoint/tests/test_dockerfile_checks.py @@ -152,7 +152,7 @@ def test_write_dockerfile_report_csv(self): write_counter += 1 os.remove(dst) - expected_writes = 2 + expected_writes = 4 self.assertEqual(expected_writes, write_counter) def test_write_dockerfile_report_md(self): @@ -170,7 +170,7 @@ def test_write_dockerfile_report_md(self): write_counter += 1 os.remove(dst) - expected_writes = 2 + expected_writes = 4 self.assertEqual(expected_writes, write_counter) From cdcff85377bb15f6b0f23de9c93811093c9036dc Mon Sep 17 00:00:00 2001 From: Michael Long Date: Fri, 30 Aug 2024 15:37:04 -0400 Subject: [PATCH 06/19] Set example workflows to main branch for testing --- .github/workflows/build_scan_container.yml | 2 +- .github/workflows/example_display_findings.yml | 2 +- .github/workflows/test_archive.yml | 2 +- .github/workflows/test_binary.yml | 2 +- .github/workflows/test_containers.yml | 4 ++-- .github/workflows/test_dockerfile_vulns.yml | 2 +- .github/workflows/test_installation.yml | 2 +- .github/workflows/test_no_vulns.yml | 2 +- ...csv_no_vulns.yml => test_reports_no_vulns.yml} | 15 +++++++++++++++ .github/workflows/test_repository.yml | 2 +- .github/workflows/test_vuln_thresholds.yml | 2 +- 11 files changed, 26 insertions(+), 11 deletions(-) rename .github/workflows/{test_csv_no_vulns.yml => test_reports_no_vulns.yml} (61%) diff --git a/.github/workflows/build_scan_container.yml b/.github/workflows/build_scan_container.yml index 5b28e25..f016616 100644 --- a/.github/workflows/build_scan_container.yml +++ b/.github/workflows/build_scan_container.yml @@ -47,7 +47,7 @@ jobs: role-to-assume: ${{ secrets.AWS_IAM_ROLE }} - name: Scan built image with Inspector - uses: aws-actions/vulnerability-scan-github-action-for-amazon-inspector@v1.1.3 + uses: aws-actions/vulnerability-scan-github-action-for-amazon-inspector@main id: inspector with: artifact_type: 'container' diff --git a/.github/workflows/example_display_findings.yml b/.github/workflows/example_display_findings.yml index 818dfdb..bb24bd2 100644 --- a/.github/workflows/example_display_findings.yml +++ b/.github/workflows/example_display_findings.yml @@ -29,7 +29,7 @@ jobs: # modify this block to scan your intended artifact - name: Inspector Scan id: inspector - uses: aws-actions/vulnerability-scan-github-action-for-amazon-inspector@v1.1.3 + uses: aws-actions/vulnerability-scan-github-action-for-amazon-inspector@main with: # change artifact_type to either 'repository', 'container', 'binary', or 'archive'. # this example scans a container image diff --git a/.github/workflows/test_archive.yml b/.github/workflows/test_archive.yml index 83e860a..141d097 100644 --- a/.github/workflows/test_archive.yml +++ b/.github/workflows/test_archive.yml @@ -32,7 +32,7 @@ jobs: - name: Test archive scan id: inspector - uses: aws-actions/vulnerability-scan-github-action-for-amazon-inspector@v1.1.3 + uses: aws-actions/vulnerability-scan-github-action-for-amazon-inspector@main with: artifact_type: 'archive' artifact_path: 'entrypoint/tests/test_data/artifacts/archives/testData.zip' diff --git a/.github/workflows/test_binary.yml b/.github/workflows/test_binary.yml index 10885db..300be0e 100644 --- a/.github/workflows/test_binary.yml +++ b/.github/workflows/test_binary.yml @@ -32,7 +32,7 @@ jobs: - name: Test binary scan id: inspector - uses: aws-actions/vulnerability-scan-github-action-for-amazon-inspector@v1.1.3 + uses: aws-actions/vulnerability-scan-github-action-for-amazon-inspector@main with: artifact_type: 'binary' artifact_path: 'entrypoint/tests/test_data/artifacts/binaries/inspector-sbomgen' diff --git a/.github/workflows/test_containers.yml b/.github/workflows/test_containers.yml index 357dbbd..0549126 100644 --- a/.github/workflows/test_containers.yml +++ b/.github/workflows/test_containers.yml @@ -32,12 +32,12 @@ jobs: - name: Test container scan id: inspector - uses: aws-actions/vulnerability-scan-github-action-for-amazon-inspector@v1.1.3 + uses: aws-actions/vulnerability-scan-github-action-for-amazon-inspector@main with: artifact_type: 'container' artifact_path: 'ubuntu:14.04' display_vulnerability_findings: "enabled" - sbomgen_version: "1.3.1" + sbomgen_version: "latest" - name: Display scan results run: cat ${{ steps.inspector.outputs.inspector_scan_results }} diff --git a/.github/workflows/test_dockerfile_vulns.yml b/.github/workflows/test_dockerfile_vulns.yml index 71773de..1554b24 100644 --- a/.github/workflows/test_dockerfile_vulns.yml +++ b/.github/workflows/test_dockerfile_vulns.yml @@ -31,7 +31,7 @@ jobs: - name: Scan Dockerfiles id: inspector - uses: aws-actions/vulnerability-scan-github-action-for-amazon-inspector@v1.1.3 + uses: aws-actions/vulnerability-scan-github-action-for-amazon-inspector@main with: artifact_type: 'repository' artifact_path: './' diff --git a/.github/workflows/test_installation.yml b/.github/workflows/test_installation.yml index 7f24618..e6504ff 100644 --- a/.github/workflows/test_installation.yml +++ b/.github/workflows/test_installation.yml @@ -28,7 +28,7 @@ jobs: role-to-assume: ${{ secrets.AWS_IAM_ROLE }} - name: Test Amazon Inspector GitHub Actions plugin - uses: aws-actions/vulnerability-scan-github-action-for-amazon-inspector@v1.1.3 + uses: aws-actions/vulnerability-scan-github-action-for-amazon-inspector@main with: artifact_type: 'container' artifact_path: 'alpine:latest' diff --git a/.github/workflows/test_no_vulns.yml b/.github/workflows/test_no_vulns.yml index cfad00e..cdfe36e 100644 --- a/.github/workflows/test_no_vulns.yml +++ b/.github/workflows/test_no_vulns.yml @@ -28,7 +28,7 @@ jobs: - name: Test binary scan id: inspector - uses: aws-actions/vulnerability-scan-github-action-for-amazon-inspector@v1.1.3 + uses: aws-actions/vulnerability-scan-github-action-for-amazon-inspector@main with: artifact_type: 'binary' artifact_path: 'entrypoint/tests/test_data/artifacts/binaries/test_go_binary' diff --git a/.github/workflows/test_csv_no_vulns.yml b/.github/workflows/test_reports_no_vulns.yml similarity index 61% rename from .github/workflows/test_csv_no_vulns.yml rename to .github/workflows/test_reports_no_vulns.yml index b44b07b..078742b 100644 --- a/.github/workflows/test_csv_no_vulns.yml +++ b/.github/workflows/test_reports_no_vulns.yml @@ -35,3 +35,18 @@ jobs: - name: Display scan results run: cat ${{ steps.inspector.outputs.inspector_scan_results_csv }} + + - name: Display scan results (JSON) + run: cat ${{ steps.inspector.outputs.inspector_scan_results }} + + - name: Display package vulns (CSV) + run: cat ${{ steps.inspector.outputs.inspector_scan_results_csv }} + + - name: Display package vulns (MD) + run: cat ${{ steps.inspector.outputs.inspector_scan_results_markdown }} + + - name: Display Dockerfile vulns (CSV) + run: cat ${{ steps.inspector.outputs.inspector_dockerfile_scan_results_csv }} + + - name: Display Dockerfile vulns (MD) + run: cat ${{ steps.inspector.outputs.inspector_dockerfile_scan_results_markdown }} diff --git a/.github/workflows/test_repository.yml b/.github/workflows/test_repository.yml index a90bc06..e1797f5 100644 --- a/.github/workflows/test_repository.yml +++ b/.github/workflows/test_repository.yml @@ -31,7 +31,7 @@ jobs: - name: Test repository scan id: inspector - uses: aws-actions/vulnerability-scan-github-action-for-amazon-inspector@v1.1.3 + uses: aws-actions/vulnerability-scan-github-action-for-amazon-inspector@main with: artifact_type: 'repository' artifact_path: './' diff --git a/.github/workflows/test_vuln_thresholds.yml b/.github/workflows/test_vuln_thresholds.yml index 86fa43c..f879716 100644 --- a/.github/workflows/test_vuln_thresholds.yml +++ b/.github/workflows/test_vuln_thresholds.yml @@ -30,7 +30,7 @@ jobs: role-to-assume: ${{ secrets.AWS_IAM_ROLE }} - name: Scan artifact with Inspector - uses: aws-actions/vulnerability-scan-github-action-for-amazon-inspector@v1.1.3 + uses: aws-actions/vulnerability-scan-github-action-for-amazon-inspector@main id: inspector with: artifact_type: 'archive' From 7260b5bf2ab96518b1c81b4bba22d199a3aebc32 Mon Sep 17 00:00:00 2001 From: Michael Long <31821088+bluesentinelsec@users.noreply.github.com> Date: Fri, 6 Sep 2024 10:24:10 -0400 Subject: [PATCH 07/19] Display 'no vulns found' for Dockerfiles (#92) Co-authored-by: Michael Long --- entrypoint/entrypoint/dockerfile.py | 4 ++++ entrypoint/test.sh | 14 ++++++++++++++ 2 files changed, 18 insertions(+) create mode 100755 entrypoint/test.sh diff --git a/entrypoint/entrypoint/dockerfile.py b/entrypoint/entrypoint/dockerfile.py index 17ea337..3378394 100644 --- a/entrypoint/entrypoint/dockerfile.py +++ b/entrypoint/entrypoint/dockerfile.py @@ -331,6 +331,10 @@ def write_dockerfile_report_md(inspector_scan_path, dst_file): row = vuln_to_markdown_row(vuln) markdown_report += row + if len(dockerfile_vulns) == 0: + row = ":green_circle: Amazon Inspector scanned for security issues in Dockerfiles and no issues were found." + markdown_report += row + logging.info(f"writing Dockerfile vulnerability markdown report to: {dst_file}") with open(dst_file, "w") as f: f.write(markdown_report) diff --git a/entrypoint/test.sh b/entrypoint/test.sh new file mode 100755 index 0000000..647eae6 --- /dev/null +++ b/entrypoint/test.sh @@ -0,0 +1,14 @@ +#!/bin/bash + +python3 main.py \ + --artifact-type="container" \ + --artifact-path="alpine:latest" \ + --display-vuln-findings="enabled" \ + --out-sbom="./sbom.json" \ + --out-scan="inspector_scan_.json" \ + --out-scan-csv="inspector_scan_.csv" \ + --out-scan-markdown="inspector_scan_.md" \ + --out-dockerfile-scan-csv="inspector_dockerfile_scan_.csv" \ + --out-dockerfile-scan-md="inspector_dockerfile_scan_.md" \ + --sbomgen-version="1.4.0" + From 9d3b15360972905892bd7c6d00c22cf4a5f3b690 Mon Sep 17 00:00:00 2001 From: Michael Long <31821088+bluesentinelsec@users.noreply.github.com> Date: Fri, 6 Sep 2024 10:30:19 -0400 Subject: [PATCH 08/19] Tweak dockerfile report (#93) Co-authored-by: Michael Long --- entrypoint/entrypoint/dockerfile.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/entrypoint/entrypoint/dockerfile.py b/entrypoint/entrypoint/dockerfile.py index 3378394..8996e11 100644 --- a/entrypoint/entrypoint/dockerfile.py +++ b/entrypoint/entrypoint/dockerfile.py @@ -332,7 +332,7 @@ def write_dockerfile_report_md(inspector_scan_path, dst_file): markdown_report += row if len(dockerfile_vulns) == 0: - row = ":green_circle: Amazon Inspector scanned for security issues in Dockerfiles and no issues were found." + row = "\n\n:green_circle: Amazon Inspector scanned for security issues in Dockerfiles and no issues were found." markdown_report += row logging.info(f"writing Dockerfile vulnerability markdown report to: {dst_file}") From b6c5e11270b2eae975a679e6f735ddad5cfbd51c Mon Sep 17 00:00:00 2001 From: Michael Long <31821088+bluesentinelsec@users.noreply.github.com> Date: Fri, 6 Sep 2024 10:40:01 -0400 Subject: [PATCH 09/19] Omit Dockerfile table on no vulns (#94) Co-authored-by: Michael Long --- entrypoint/entrypoint/dockerfile.py | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/entrypoint/entrypoint/dockerfile.py b/entrypoint/entrypoint/dockerfile.py index 8996e11..cb9042e 100644 --- a/entrypoint/entrypoint/dockerfile.py +++ b/entrypoint/entrypoint/dockerfile.py @@ -263,6 +263,9 @@ def get_markdown_header() -> str: s += "|---|---|---|---|---|\n" return s +def get_markdown_header_no_vulns() -> str: + s = "## Dockerfile Findings\n" + return s def get_dockerfile_vulns(inspector_scan_path): vuln_objects = [] @@ -326,14 +329,16 @@ def write_dockerfile_report_csv(inspector_scan_path, dst_file): def write_dockerfile_report_md(inspector_scan_path, dst_file): dockerfile_vulns = get_dockerfile_vulns(inspector_scan_path) - markdown_report = get_markdown_header() - for vuln in dockerfile_vulns: - row = vuln_to_markdown_row(vuln) - markdown_report += row - + markdown_report = "" if len(dockerfile_vulns) == 0: + markdown_report = get_markdown_header_no_vulns() row = "\n\n:green_circle: Amazon Inspector scanned for security issues in Dockerfiles and no issues were found." markdown_report += row + else: + markdown_report = get_markdown_header() + for vuln in dockerfile_vulns: + row = vuln_to_markdown_row(vuln) + markdown_report += row logging.info(f"writing Dockerfile vulnerability markdown report to: {dst_file}") with open(dst_file, "w") as f: From d62e2b94eb118f460134139262287622d355955b Mon Sep 17 00:00:00 2001 From: Michael Long <31821088+bluesentinelsec@users.noreply.github.com> Date: Fri, 6 Sep 2024 11:48:55 -0400 Subject: [PATCH 10/19] Updated workflows to v1.x - testing auto-updates (#96) Co-authored-by: Michael Long --- .github/workflows/build_scan_container.yml | 2 +- .github/workflows/example_display_findings.yml | 2 +- .github/workflows/example_vulnerability_threshold_exceeded.yml | 2 +- .github/workflows/test_archive.yml | 2 +- .github/workflows/test_binary.yml | 2 +- .github/workflows/test_containers.yml | 2 +- .github/workflows/test_dockerfile_vulns.yml | 2 +- .github/workflows/test_installation.yml | 2 +- .github/workflows/test_no_vulns.yml | 2 +- .github/workflows/test_reports_no_vulns.yml | 2 +- .github/workflows/test_repository.yml | 2 +- .github/workflows/test_vuln_thresholds.yml | 2 +- 12 files changed, 12 insertions(+), 12 deletions(-) diff --git a/.github/workflows/build_scan_container.yml b/.github/workflows/build_scan_container.yml index f016616..a46258b 100644 --- a/.github/workflows/build_scan_container.yml +++ b/.github/workflows/build_scan_container.yml @@ -47,7 +47,7 @@ jobs: role-to-assume: ${{ secrets.AWS_IAM_ROLE }} - name: Scan built image with Inspector - uses: aws-actions/vulnerability-scan-github-action-for-amazon-inspector@main + uses: aws-actions/vulnerability-scan-github-action-for-amazon-inspector@v1 id: inspector with: artifact_type: 'container' diff --git a/.github/workflows/example_display_findings.yml b/.github/workflows/example_display_findings.yml index bb24bd2..0f87384 100644 --- a/.github/workflows/example_display_findings.yml +++ b/.github/workflows/example_display_findings.yml @@ -29,7 +29,7 @@ jobs: # modify this block to scan your intended artifact - name: Inspector Scan id: inspector - uses: aws-actions/vulnerability-scan-github-action-for-amazon-inspector@main + uses: aws-actions/vulnerability-scan-github-action-for-amazon-inspector@v1 with: # change artifact_type to either 'repository', 'container', 'binary', or 'archive'. # this example scans a container image diff --git a/.github/workflows/example_vulnerability_threshold_exceeded.yml b/.github/workflows/example_vulnerability_threshold_exceeded.yml index e45eea2..d34df37 100644 --- a/.github/workflows/example_vulnerability_threshold_exceeded.yml +++ b/.github/workflows/example_vulnerability_threshold_exceeded.yml @@ -48,7 +48,7 @@ jobs: # Inspector scan - name: Scan container with Inspector - uses: aws-actions/vulnerability-scan-github-action-for-amazon-inspector@v1.1.3 + uses: aws-actions/vulnerability-scan-github-action-for-amazon-inspector@v1 id: inspector with: artifact_type: 'container' # configure Inspector for scanning a container diff --git a/.github/workflows/test_archive.yml b/.github/workflows/test_archive.yml index 141d097..5253d92 100644 --- a/.github/workflows/test_archive.yml +++ b/.github/workflows/test_archive.yml @@ -32,7 +32,7 @@ jobs: - name: Test archive scan id: inspector - uses: aws-actions/vulnerability-scan-github-action-for-amazon-inspector@main + uses: aws-actions/vulnerability-scan-github-action-for-amazon-inspector@v1 with: artifact_type: 'archive' artifact_path: 'entrypoint/tests/test_data/artifacts/archives/testData.zip' diff --git a/.github/workflows/test_binary.yml b/.github/workflows/test_binary.yml index 300be0e..dbb1efd 100644 --- a/.github/workflows/test_binary.yml +++ b/.github/workflows/test_binary.yml @@ -32,7 +32,7 @@ jobs: - name: Test binary scan id: inspector - uses: aws-actions/vulnerability-scan-github-action-for-amazon-inspector@main + uses: aws-actions/vulnerability-scan-github-action-for-amazon-inspector@v1 with: artifact_type: 'binary' artifact_path: 'entrypoint/tests/test_data/artifacts/binaries/inspector-sbomgen' diff --git a/.github/workflows/test_containers.yml b/.github/workflows/test_containers.yml index 0549126..a4cdb07 100644 --- a/.github/workflows/test_containers.yml +++ b/.github/workflows/test_containers.yml @@ -32,7 +32,7 @@ jobs: - name: Test container scan id: inspector - uses: aws-actions/vulnerability-scan-github-action-for-amazon-inspector@main + uses: aws-actions/vulnerability-scan-github-action-for-amazon-inspector@v1 with: artifact_type: 'container' artifact_path: 'ubuntu:14.04' diff --git a/.github/workflows/test_dockerfile_vulns.yml b/.github/workflows/test_dockerfile_vulns.yml index 1554b24..7a39532 100644 --- a/.github/workflows/test_dockerfile_vulns.yml +++ b/.github/workflows/test_dockerfile_vulns.yml @@ -31,7 +31,7 @@ jobs: - name: Scan Dockerfiles id: inspector - uses: aws-actions/vulnerability-scan-github-action-for-amazon-inspector@main + uses: aws-actions/vulnerability-scan-github-action-for-amazon-inspector@v1 with: artifact_type: 'repository' artifact_path: './' diff --git a/.github/workflows/test_installation.yml b/.github/workflows/test_installation.yml index e6504ff..0269c02 100644 --- a/.github/workflows/test_installation.yml +++ b/.github/workflows/test_installation.yml @@ -28,7 +28,7 @@ jobs: role-to-assume: ${{ secrets.AWS_IAM_ROLE }} - name: Test Amazon Inspector GitHub Actions plugin - uses: aws-actions/vulnerability-scan-github-action-for-amazon-inspector@main + uses: aws-actions/vulnerability-scan-github-action-for-amazon-inspector@v1 with: artifact_type: 'container' artifact_path: 'alpine:latest' diff --git a/.github/workflows/test_no_vulns.yml b/.github/workflows/test_no_vulns.yml index cdfe36e..380bb53 100644 --- a/.github/workflows/test_no_vulns.yml +++ b/.github/workflows/test_no_vulns.yml @@ -28,7 +28,7 @@ jobs: - name: Test binary scan id: inspector - uses: aws-actions/vulnerability-scan-github-action-for-amazon-inspector@main + uses: aws-actions/vulnerability-scan-github-action-for-amazon-inspector@v1 with: artifact_type: 'binary' artifact_path: 'entrypoint/tests/test_data/artifacts/binaries/test_go_binary' diff --git a/.github/workflows/test_reports_no_vulns.yml b/.github/workflows/test_reports_no_vulns.yml index 078742b..f5a1e55 100644 --- a/.github/workflows/test_reports_no_vulns.yml +++ b/.github/workflows/test_reports_no_vulns.yml @@ -26,7 +26,7 @@ jobs: - name: Test container scan id: inspector - uses: aws-actions/vulnerability-scan-github-action-for-amazon-inspector@main + uses: aws-actions/vulnerability-scan-github-action-for-amazon-inspector@v1 with: artifact_type: 'container' artifact_path: 'alpine:latest' diff --git a/.github/workflows/test_repository.yml b/.github/workflows/test_repository.yml index e1797f5..004f0f4 100644 --- a/.github/workflows/test_repository.yml +++ b/.github/workflows/test_repository.yml @@ -31,7 +31,7 @@ jobs: - name: Test repository scan id: inspector - uses: aws-actions/vulnerability-scan-github-action-for-amazon-inspector@main + uses: aws-actions/vulnerability-scan-github-action-for-amazon-inspector@v1 with: artifact_type: 'repository' artifact_path: './' diff --git a/.github/workflows/test_vuln_thresholds.yml b/.github/workflows/test_vuln_thresholds.yml index f879716..d64155c 100644 --- a/.github/workflows/test_vuln_thresholds.yml +++ b/.github/workflows/test_vuln_thresholds.yml @@ -30,7 +30,7 @@ jobs: role-to-assume: ${{ secrets.AWS_IAM_ROLE }} - name: Scan artifact with Inspector - uses: aws-actions/vulnerability-scan-github-action-for-amazon-inspector@main + uses: aws-actions/vulnerability-scan-github-action-for-amazon-inspector@v1 id: inspector with: artifact_type: 'archive' From 0ef860b76a794d5f44eb422db0612e1d900b1ac4 Mon Sep 17 00:00:00 2001 From: Michael Long <31821088+bluesentinelsec@users.noreply.github.com> Date: Fri, 6 Sep 2024 11:52:45 -0400 Subject: [PATCH 11/19] update README (#97) Co-authored-by: Michael Long --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 774bf39..ead5875 100644 --- a/README.md +++ b/README.md @@ -90,7 +90,7 @@ Perform the following steps to quickly add this action to your GitHub Actions pi # modify this block to scan your intended artifact - name: Inspector Scan id: inspector - uses: aws-actions/vulnerability-scan-github-action-for-amazon-inspector@v1.0.0 + uses: aws-actions/vulnerability-scan-github-action-for-amazon-inspector@v1 with: # change artifact_type to either 'repository', 'container', 'binary', or 'archive'. artifact_type: 'repository' @@ -175,7 +175,7 @@ The below example shows how to enable action outputs in various locations and fo ```yaml - name: Scan container id: inspector - uses: aws/vulnerability-scan-github-action-for-amazon-inspector@v1.0.0 + uses: aws/vulnerability-scan-github-action-for-amazon-inspector@v1 with: artifact_type: 'container' artifact_path: 'ubuntu:14.04' @@ -219,7 +219,7 @@ The example below shows how to set up vulnerability thresholds and fail the job ```yaml - name: Invoke Amazon Inspector Scan id: inspector - uses: aws/vulnerability-scan-github-action-for-amazon-inspector@v1.0.0 + uses: aws/vulnerability-scan-github-action-for-amazon-inspector@v1 with: artifact_type: 'repository' artifact_path: './' @@ -294,7 +294,7 @@ jobs: role-to-assume: "arn:aws:iam:::role/" - name: Scan built image with Inspector - uses: aws-actions/vulnerability-scan-github-action-for-amazon-inspector@v1.0.0 + uses: aws-actions/vulnerability-scan-github-action-for-amazon-inspector@v1 id: inspector with: artifact_type: 'container' From d7710387a9d1a2b38389e9c49686b417fcf544ea Mon Sep 17 00:00:00 2001 From: Michael Long <31821088+bluesentinelsec@users.noreply.github.com> Date: Tue, 17 Sep 2024 14:48:06 -0400 Subject: [PATCH 12/19] Extend vulnerability severity providers (#98) * Add severity providers: GHSA, GitLab * Add severity providers: GHSA, GitLab * Add REDHAT_CVE and UBUNTU_CVE providers * rename GHSA to GITHUB --------- Co-authored-by: Michael Long --- entrypoint/entrypoint/pkg_vuln.py | 26 ++++++++++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/entrypoint/entrypoint/pkg_vuln.py b/entrypoint/entrypoint/pkg_vuln.py index e9f19d0..bf3ff91 100644 --- a/entrypoint/entrypoint/pkg_vuln.py +++ b/entrypoint/entrypoint/pkg_vuln.py @@ -15,10 +15,32 @@ class CvssSourceProvider: NVD = "NVD" MITRE = "MITRE" + GITHUB = "GITHUB" + GITLAB = "GITLAB" + REDHAT_CVE = "REDHAT_CVE" + UBUNTU_CVE = "UBUNTU_CVE" AMAZON_INSPECTOR = "AMAZON_INSPECTOR" - DEFAULT_PROVIDER = NVD +def get_rating_providers(): + """ + get_rating_providers returns a list of vulnerability + severity providers. The action uses this information + to determine which vuln severity to render when + multiple severity values are present from different + vendors. See the function definition to view the + order in which severity providers are preferred. + """ + + # NVD is most preferred, followed by everything + # else in the order listed. + providers = [CvssSourceProvider.NVD, + CvssSourceProvider.MITRE, + CvssSourceProvider.GITHUB, + CvssSourceProvider.GITLAB, + CvssSourceProvider.AMAZON_INSPECTOR + ] + return providers class CvssSeverity: UNTRIAGED = "untriaged" @@ -255,7 +277,7 @@ def get_cwes(v) -> str: def get_cvss_rating(ratings, vulnerability) -> CvssRating: - rating_provider_priority = [CvssSourceProvider.NVD, CvssSourceProvider.MITRE, CvssSourceProvider.AMAZON_INSPECTOR] + rating_provider_priority = get_rating_providers() for provider in rating_provider_priority: for rating in ratings: if rating["source"]["name"] != provider: From 8c2d4d78eadaf3cdbe387414ca3d15ee5f7895c8 Mon Sep 17 00:00:00 2001 From: Michael Long <31821088+bluesentinelsec@users.noreply.github.com> Date: Tue, 26 Nov 2024 13:09:22 -0500 Subject: [PATCH 13/19] Add platform argument for container image scans (#102) * add --platform support for multi-arch containers * test multi-arch images on current branch * test actions against sbomgen 1.5.1-beta * fix --platform parsing error * fix platform parsing bug * test workflows on sbomgen latest (1.5.2) * Validate --platform input * Add more test cases, and revert workflow definitions * fix typo in platform arg --------- Co-authored-by: Michael Long --- .github/workflows/test_containers.yml | 1 + action.yml | 4 ++++ entrypoint/entrypoint/cli.py | 7 +++++++ entrypoint/entrypoint/orchestrator.py | 16 ++++++++++++++++ entrypoint/tests/test_orchestrator.py | 16 ++++++++++++++++ 5 files changed, 44 insertions(+) diff --git a/.github/workflows/test_containers.yml b/.github/workflows/test_containers.yml index a4cdb07..c8d2514 100644 --- a/.github/workflows/test_containers.yml +++ b/.github/workflows/test_containers.yml @@ -36,6 +36,7 @@ jobs: with: artifact_type: 'container' artifact_path: 'ubuntu:14.04' + platform: "linux/arm64" display_vulnerability_findings: "enabled" sbomgen_version: "latest" diff --git a/action.yml b/action.yml index e33c71c..d986748 100644 --- a/action.yml +++ b/action.yml @@ -106,6 +106,10 @@ inputs: required: False default: 600 # 10 minutes + platform: + description: "Specifies the OS and CPU arch of the container image you wish to scan. Valid inputs are of the form 'os/cpu/variant' for example, 'linux/amd64', 'linux/arm64/v8', etc. If no platform is specified, the system will use the same platform as the host that is performing the scan. This argument only affects container image scans. Requires inspector-sbomgen 1.5.1 or later." + required: False + outputs: artifact_sbom: description: "The filepath to the artifact's software bill of materials." diff --git a/entrypoint/entrypoint/cli.py b/entrypoint/entrypoint/cli.py index 384a9dd..133d380 100644 --- a/entrypoint/entrypoint/cli.py +++ b/entrypoint/entrypoint/cli.py @@ -51,6 +51,13 @@ def init(sys_argv=None) -> argparse.Namespace: parser.add_argument("--timeout", type=str, default="600", help="The amount of time in seconds that inspector-sbomgne will run. When this timeout is exceeded, sbomgen will gracefully conclude and present any findings discovered up to that point.") + parser.add_argument("--platform", type=str, + help="Specifies the OS and CPU arch of the container image you wish to scan. Valid inputs are " + "of the form 'os/cpu/variant' for example, 'linux/amd64', 'linux/arm64/v8', etc. If no platform is " + "specified, the system will use the same platform as the host that is performing the " + "scan. This argument only affects container image scans. Requires inspector-sbomgen " + "1.5.1 or later.") + args = "" if sys_argv: args = parser.parse_args(sys_argv) diff --git a/entrypoint/entrypoint/orchestrator.py b/entrypoint/entrypoint/orchestrator.py index 40d5d7f..57f2a4f 100644 --- a/entrypoint/entrypoint/orchestrator.py +++ b/entrypoint/entrypoint/orchestrator.py @@ -6,6 +6,7 @@ import shutil import sys import tempfile +import re from entrypoint import dockerfile, executor, exporter, installer, pkg_vuln @@ -195,6 +196,15 @@ def invoke_sbomgen(args) -> int: sbomgen_args.append("--skip-files") sbomgen_args.append(args.skip_files) + if args.artifact_type == "container": + + if args.platform: + platform_arg = args.platform.lower() + if not is_valid_container_platform(platform_arg): + logging.fatal(f"received invalid container image platform: '{args.platform}'. Platform should be of the form 'os/cpu/variant' such as 'linux/amd64' or 'linux/arm64/v8'") + sbomgen_args.append("--platform") + sbomgen_args.append(platform_arg) + ret = executor.invoke_command(sbomgen, sbomgen_args) if ret != 0: return ret @@ -441,3 +451,9 @@ def require_true(expr: bool, msg: str): if not expr: logging.error(msg) exit(1) + +def is_valid_container_platform(img_platform): + # regex for detecting 'os/cpu/variant' + # os/cpu are required whereas variant is optional + pattern = r'^[^/]+/[^/]+(?:/[^/]+)?$' + return bool(re.match(pattern, img_platform)) diff --git a/entrypoint/tests/test_orchestrator.py b/entrypoint/tests/test_orchestrator.py index d2a2ede..792abc0 100644 --- a/entrypoint/tests/test_orchestrator.py +++ b/entrypoint/tests/test_orchestrator.py @@ -185,6 +185,22 @@ def test_get_sbomgen_arch(self): result = orchestrator.get_sbomgen_arch(each_test["input"]) self.assertEqual(result, each_test["expected"]) + def test_is_valid_container_platform(self): + + test_cases = [ + # valid input + {"input": "linux/amd64", "expected": True}, + {"input": "linux/arm64/v8", "expected": True}, + # test malformed input + {"input": "linux", "expected": False}, + {"input": "garbage garbage garbage", "expected": False}, + {"input": "garbage / garbage / garbage /", "expected": False}, + {"input": "linux/amd64/slim/garbage", "expected": False}, + ] + + for each_test in test_cases: + result = orchestrator.is_valid_container_platform(each_test["input"]) + self.assertEqual(result, each_test["expected"]) if __name__ == "__main__": unittest.main() From 5dc8a4bafed85c4c3d7070b4a7ada5b9d94041e3 Mon Sep 17 00:00:00 2001 From: Michael Long <31821088+bluesentinelsec@users.noreply.github.com> Date: Thu, 10 Apr 2025 10:27:11 -0400 Subject: [PATCH 14/19] Improve severity rating consistency (#112) * fix severity rating mismatch * temporarily add a test workflow * Fix type issue: float provided, expected string * Rename workflow / job name * Add severity comparison logic * Revise severity sorting and selection logic * return default values on error * skip EPSS ratings for severity column * debugging unknown ratings * fix ratings with unknown name * Verify AMAZON_INSPECTOR renders correctly * fix failing test * temporarily disable failing tests * pass unit test: test_parse_inspector_scan_result * pass unit tests * change '-f' to '--failfast' for clarity * Remove unused type cast * refactor csv test * severity is rendered as 'other' not 'unknown' * test build on all actions * normalize dockerfile findings severity rating * debugging dockerfile severity * debugging * Normalize Dockerfile severity 'info' to 'other' * restore test actions * minor comment update * Remove develop workflow * Address PR feedback * test workflows against refactor * handle edge case CVE-2025-22871 * fix missing severity edge case * debugging epss * debugging * fix flawed test * added test case for absent severity rating * revert workflows to v1 --------- Co-authored-by: Michael Long --- Makefile | 10 +- entrypoint/entrypoint/dockerfile.py | 7 +- entrypoint/entrypoint/pkg_vuln.py | 162 +++++++++-- ...nspector-scan-cdx-empty-vulnerability.json | 4 +- entrypoint/tests/test_exporter.py | 103 ++++--- entrypoint/tests/test_pkg_vuln.py | 252 ++++++++++++++---- 6 files changed, 419 insertions(+), 119 deletions(-) diff --git a/Makefile b/Makefile index fcc3b06..23a59ee 100644 --- a/Makefile +++ b/Makefile @@ -3,9 +3,17 @@ run: docker run -it inspector-action:latest test: - cd entrypoint; python3 -m unittest discover -v -s ./ + cd entrypoint; python3 -m unittest discover --failfast -v -s ./ coverage: cd entrypoint && \ coverage run -m unittest discover -v -s ./ && \ coverage report + +clean: + rm -rf entrypoint/inspector-dockerfile-scan.csv \ + entrypoint/inspector-dockerfile-scan.md \ + entrypoint/inspector-scan.csv \ + entrypoint/inspector-scan.json \ + entrypoint/inspector-scan.md \ + entrypoint/sbom.json \ diff --git a/entrypoint/entrypoint/dockerfile.py b/entrypoint/entrypoint/dockerfile.py index cb9042e..3102a41 100644 --- a/entrypoint/entrypoint/dockerfile.py +++ b/entrypoint/entrypoint/dockerfile.py @@ -1,8 +1,6 @@ import json import logging -from typing import List - class DockerfileVulnerability: def __int__(self): @@ -179,6 +177,9 @@ def get_severity(rating): logging.error(f"expected severity in rating object but it was not found: {rating}") return None + if severity.upper() in {"INFO", "INFORMATIONAL"}: + severity = "other" + return severity @@ -263,10 +264,12 @@ def get_markdown_header() -> str: s += "|---|---|---|---|---|\n" return s + def get_markdown_header_no_vulns() -> str: s = "## Dockerfile Findings\n" return s + def get_dockerfile_vulns(inspector_scan_path): vuln_objects = [] inspector_scan_json = [] diff --git a/entrypoint/entrypoint/pkg_vuln.py b/entrypoint/entrypoint/pkg_vuln.py index bf3ff91..b31a5fa 100644 --- a/entrypoint/entrypoint/pkg_vuln.py +++ b/entrypoint/entrypoint/pkg_vuln.py @@ -4,6 +4,7 @@ to different formats (CSV and markdown). """ +import enum import logging import urllib.parse from dataclasses import dataclass @@ -22,6 +23,10 @@ class CvssSourceProvider: AMAZON_INSPECTOR = "AMAZON_INSPECTOR" DEFAULT_PROVIDER = NVD + +empty_rating = {"score": 0.0, "source": {"name": "in triage"}, "severity": "other", "method": "other"} + + def get_rating_providers(): """ get_rating_providers returns a list of vulnerability @@ -42,8 +47,8 @@ def get_rating_providers(): ] return providers + class CvssSeverity: - UNTRIAGED = "untriaged" UNKNOWN = "unknown" @@ -74,7 +79,7 @@ class Vulnerability: @dataclass class CvssRating: - severity: str = CvssSeverity.UNTRIAGED + severity: str = CvssSeverity.UNKNOWN provider: str = CvssSourceProvider.DEFAULT_PROVIDER cvss_score: str = NULL_STR @@ -109,7 +114,12 @@ def parse_inspector_scan_result(inspector_scan_json) -> List[Vulnerability]: pkg_vulns = get_pkg_vulns(vulns) for v in pkg_vulns: - vuln_obj = convert_package_vuln_to_vuln_obj(v, components) + vuln_obj = None + try: + vuln_obj = convert_package_vuln_to_vuln_obj(v, components) + except Exception as e: + logging.error(f"error encountered while parsing a vulnerability: {e}") + continue vuln_list.append(vuln_obj) return vuln_list @@ -143,7 +153,7 @@ def convert_package_vuln_to_vuln_obj(v, components) -> Vulnerability: vuln_obj.published = v.get("created", NULL_STR) vuln_obj.modified = v.get("updated", NULL_STR) - ratings = v.get("ratings") + ratings = v.get("ratings", [empty_rating]) add_ratings(ratings, vuln_obj) description = v.get("description") @@ -168,10 +178,7 @@ def convert_package_vuln_to_vuln_obj(v, components) -> Vulnerability: def add_ratings(ratings, vulnerability): - if ratings is None: - return - - rating = get_cvss_rating(ratings, vulnerability) + rating = get_highest_severity_rating(ratings) vulnerability.severity = rating.severity vulnerability.severity_provider = rating.provider vulnerability.cvss_score = rating.cvss_score @@ -276,22 +283,6 @@ def get_cwes(v) -> str: return cwe_str -def get_cvss_rating(ratings, vulnerability) -> CvssRating: - rating_provider_priority = get_rating_providers() - for provider in rating_provider_priority: - for rating in ratings: - if rating["source"]["name"] != provider: - continue - - severity = CvssSeverity.UNTRIAGED if rating["severity"] == CvssSeverity.UNKNOWN else rating["severity"] - cvss_score = str(rating["score"]) if rating["method"] == "CVSSv31" else "null" - if severity and cvss_score: - return CvssRating(severity=severity, provider=provider, cvss_score=cvss_score) - - logging.info(f"No CVSS rating is provided for {vulnerability.vuln_id}") - return CvssRating() - - def get_epss_score(ratings): for rating in ratings: source = rating.get("source") @@ -320,3 +311,126 @@ def combine_str_list_into_one_str(str_list: list[str]) -> str: if str_element == "": str_element = NULL_STR return str_element + + +def get_highest_severity_rating(ratings) -> CvssRating: + method = get_preferred_vuln_rating_method(ratings) + most_severe_rating = get_highest_rating_by_method(method, ratings) + cvss = CvssRating() + cvss.provider = most_severe_rating["source"]["name"] + cvss.severity = most_severe_rating["severity"] + if "unknown" in cvss.severity: + cvss.severity = "other" + cvss.cvss_score = str(most_severe_rating.get("score", 0.0)) + return cvss + + +class VulnRatingMethod(enum.Enum): + CVSSv2 = "CVSSv2" + CVSSv3 = "CVSSv3" + CVSSv31 = "CVSSv31" + CVSSv4 = "CVSSv4" + OWASP = "OWASP" + SSVC = "SSVC" + OTHER = "other" + + +def get_preferred_vuln_rating_method(ratings): + if ratings is None: + return VulnRatingMethod.OTHER + + found_methods = [] + + for rating in ratings: + if not rating: + continue + + method = rating.get("method", "") + if not method: + continue + + # we keep a list of each rating method in the + # vulnerability so we can present the highest rating + # to the end user + if method == VulnRatingMethod.CVSSv4.value: + found_methods.append(VulnRatingMethod.CVSSv4) + elif method == VulnRatingMethod.CVSSv31.value: + found_methods.append(VulnRatingMethod.CVSSv31) + elif method == VulnRatingMethod.CVSSv3.value: + found_methods.append(VulnRatingMethod.CVSSv3) + elif method == VulnRatingMethod.CVSSv2.value: + found_methods.append(VulnRatingMethod.CVSSv2) + elif method == VulnRatingMethod.OWASP.value: + found_methods.append(VulnRatingMethod.OWASP) + elif method == VulnRatingMethod.SSVC.value: + found_methods.append(VulnRatingMethod.SSVC) + elif method == VulnRatingMethod.OTHER.value: + found_methods.append(VulnRatingMethod.OTHER) + else: + logging.error(f"Expected a spec-conforming CycloneDX vulnerability rating method, but received '{method}'") + continue + + # select method to display to user in priority order + rating_method_priority = [VulnRatingMethod.CVSSv4, VulnRatingMethod.CVSSv31, VulnRatingMethod.CVSSv3, + VulnRatingMethod.CVSSv2, VulnRatingMethod.OWASP, VulnRatingMethod.SSVC] + for rating_method in rating_method_priority: + if rating_method in found_methods: + return rating_method + return VulnRatingMethod.OTHER + + +def get_highest_rating_by_method(method: VulnRatingMethod, ratings): + if not ratings: + return empty_rating + + ratings_with_same_method = [] + for rating in ratings: + if not rating: + continue + + a_method = rating.get("method", "") + if not method: + continue + + if is_epss(a_method, rating): + continue + + if a_method == method.value: + ratings_with_same_method.append(rating) + + highest_rating = empty_rating + for rating in ratings_with_same_method: + + score = rating.get("score", 0.0) + try: + score = float(score) + except Exception as e: + logging.error(f"threw exception while trying to convert severity score, '{score}' to type float: {e}") + continue + + if score >= highest_rating["score"]: + highest_rating = rating + + + return highest_rating + + +def is_epss(method, rating): + if not rating: + return False + + if method != VulnRatingMethod.OTHER.value: + return False + + source = rating.get("source", "") + if not source: + return False + + name = source.get("name", "") + if not name: + return False + + if name.lower() == "EPSS".lower(): + return True + + return False diff --git a/entrypoint/tests/test_data/test_pkg_vuln/inspector-scan-cdx-empty-vulnerability.json b/entrypoint/tests/test_data/test_pkg_vuln/inspector-scan-cdx-empty-vulnerability.json index 13a9f86..55520e4 100644 --- a/entrypoint/tests/test_data/test_pkg_vuln/inspector-scan-cdx-empty-vulnerability.json +++ b/entrypoint/tests/test_data/test_pkg_vuln/inspector-scan-cdx-empty-vulnerability.json @@ -27,7 +27,7 @@ "method": "CVSSv31", "vector": "CVSS:3.1/AV:L/AC:L/PR:N/UI:R/S:U/C:N/I:N/A:H", "source": { - "name": "OTHER_PROVIDER" + "name": "other" } } ], @@ -48,4 +48,4 @@ } ] } -} \ No newline at end of file +} diff --git a/entrypoint/tests/test_exporter.py b/entrypoint/tests/test_exporter.py index 1ad68a1..f27411e 100644 --- a/entrypoint/tests/test_exporter.py +++ b/entrypoint/tests/test_exporter.py @@ -1,56 +1,87 @@ -import json -import os import unittest from entrypoint import exporter, pkg_vuln class TestExporter(unittest.TestCase): - TEST_DIR = "tests/test_data/scans/" - TEST_RESULT_DIR = "tests/test_data/scan_results/" - CSV_EXTENSION = ".csv" - MARKDOWN_EXTENSION = ".md" def test_json_to_csv(self): - file_list = os.listdir(self.TEST_DIR) - for file in file_list: - with self.subTest(msg=file): + vulns = [ + pkg_vuln.Vulnerability(vuln_id='CVE-2023-6237', + severity='other', + severity_provider='other', + cvss_score='0.0', + published='2024-04-25T07:15:45Z', + modified='2024-05-01T18:15:12Z', + description="Issue summary: Checking excessively long invalid RSA public keys may take a long time...", + installed_ver='pkg:apk/alpine/openssl@3.1.1-r1?arch=x86_64&distro=3.18.2', + fixed_ver='3.1.4-r4', + pkg_path='null', + epss_score='0.00045', + exploit_available='null', + exploit_last_seen='null', + cwes='null') + ] + scan_result = exporter.InspectorScanResult(vulnerabilities=vulns) + want = '''"#artifact_name:null","artifact_type:null","artifact_hash:null","build_id:null" +"#critical_vulnerabilities:null","high_vulnerabilities:null","medium_vulnerabilities:null","low_vulnerabilities:null","other_vulnerabilities:null" +"ID","Severity","Source","CVSS","Installed Package","Fixed Package","Path","EPSS","Exploit Available","Exploit Last Seen","CWEs" +"CVE-2023-6237","other","other","0.0","pkg:apk/alpine/openssl@3.1.1-r1?arch=x86_64&distro=3.18.2","3.1.4-r4","null","0.00045","null","null","null"''' + got = exporter.to_csv(scan_result) + got = got.rstrip() + got = got.replace('\r\n', '\n') - test_file_path = os.path.join(self.TEST_DIR, file) - inspector_scan_json = {} - with open(test_file_path, "r") as f: - inspector_scan_json = json.load(f) + self.assertEqual(want, got) - vulns = pkg_vuln.parse_inspector_scan_result(inspector_scan_json) + def test_json_to_markdown(self): + vulns = [ + pkg_vuln.Vulnerability(vuln_id='CVE-2023-6237', + severity='other', + severity_provider='other', + cvss_score='0.0', + published='2024-04-25T07:15:45Z', + modified='2024-05-01T18:15:12Z', + description="Issue summary: Checking excessively long invalid RSA public keys may take a long time...", + installed_ver='pkg:apk/alpine/openssl@3.1.1-r1?arch=x86_64&distro=3.18.2', + fixed_ver='3.1.4-r4', + pkg_path='null', + epss_score='0.00045', + exploit_available='null', + exploit_last_seen='null', + cwes='null') + ] + scan_result = exporter.InspectorScanResult(vulnerabilities=vulns) + got = exporter.to_markdown(scan_result) + got = got.rstrip() + got = got.replace('\r\n', '\n') - scan_result = exporter.InspectorScanResult(vulnerabilities=vulns) - as_csv = exporter.to_csv(scan_result) + want = '''# Amazon Inspector Scan Results +Artifact Name: null - expected_data_path = os.path.join(self.TEST_RESULT_DIR, file) + self.CSV_EXTENSION - with open(expected_data_path, "r", newline="\n") as f: - expected_csv = f.read() - self.assertEqual(expected_csv, as_csv, file) +Artifact Type: null - def test_json_to_markdown(self): - file_list = os.listdir(self.TEST_DIR) - for file in file_list: - with self.subTest(msg=file): +## Vulnerability Counts by Severity + +| Severity | Count | +|----------|-------| +| Critical | null| +| High | null| +| Medium | null| +| Low | null| +| Other | null| - path = os.path.join(self.TEST_DIR, file) - inspector_scan_json = {} - with open(path, "r") as f: - inspector_scan_json = json.load(f) - vulns = pkg_vuln.parse_inspector_scan_result(inspector_scan_json) +## Vulnerability Findings - scan_result = exporter.InspectorScanResult(vulnerabilities=vulns) - as_markdown = exporter.to_markdown(scan_result) - exporter.post_github_step_summary(as_markdown) +| ID | Severity | Source | [CVSS](https://www.first.org/cvss/) | Installed Package ([PURL](https://github.com/package-url/purl-spec/tree/master?tab=readme-ov-file#purl)) | Fixed Package | Path | [EPSS](https://www.first.org/epss/) | Exploit Available | Exploit Last Seen | CWEs | +| ------- | ------- | ------- | ------- | ------- | ------- | ------- | ------- | ------- | ------- | ------- | +| CVE-2023-6237 | other | other | 0.0 | `pkg:apk/alpine/openssl@3.1.1-r1?arch=x86_64&distro=3.18.2` | `3.1.4-r4` | | 0.00045 | | | | - expected_data_path = os.path.join(self.TEST_RESULT_DIR, file) + self.MARKDOWN_EXTENSION - with open(expected_data_path, "r", newline="\n") as f: - expected_markdown = f.read() - self.assertEqual(expected_markdown, as_markdown, file) +''' + want = want.rstrip() + want = want.replace('\r\n', '\n') + self.assertEqual(want, got) + return if __name__ == "__main__": diff --git a/entrypoint/tests/test_pkg_vuln.py b/entrypoint/tests/test_pkg_vuln.py index 72817f5..ed8e66a 100644 --- a/entrypoint/tests/test_pkg_vuln.py +++ b/entrypoint/tests/test_pkg_vuln.py @@ -34,15 +34,15 @@ def test_parse_inspector_scan_result(self): "expected": [ pkg_vuln.Vulnerability( vuln_id="CVE-2015-20109", - severity="medium", - severity_provider="NVD", - cvss_score="5.5", + severity="high", + severity_provider="MITRE", + cvss_score="9.1", published="2023-06-25T17:15:14Z", modified="2023-07-31T19:15:15Z", description="TEST_DESCRIPTION", installed_ver=( - "pkg:golang/github.com/test-namespace/pkg-1@v0.4.0;" - + "pkg:golang/github.com/test-namespace/pkg-2@v2.4.0" + "pkg:golang/github.com/test-namespace/pkg-1@v0.4.0;" + + "pkg:golang/github.com/test-namespace/pkg-2@v2.4.0" ), fixed_ver="pkg-1;pkg-2", pkg_path="/user/local/bin/pkg-1;/tmp/pkg-2", @@ -69,9 +69,9 @@ def test_parse_inspector_scan_result(self): ), pkg_vuln.Vulnerability( vuln_id="CVE-2024-30000", - severity="untriaged", + severity="other", severity_provider="AMAZON_INSPECTOR", - cvss_score="null", + cvss_score="0.0", published="2023-06-25T17:15:14Z", modified="2023-07-31T19:15:15Z", description="TEST_DESCRIPTION", @@ -87,16 +87,16 @@ def test_parse_inspector_scan_result(self): }, { "name": ( - "parse_inspector_scan_result should return vulnerability with 'null' values" - + "when fields don't exist in the scan result" + "parse_inspector_scan_result should return vulnerability with 'null' values" + + " when fields don't exist in the scan result" ), "test_file": os.path.join(test_file_base_dir, "inspector-scan-cdx-empty-vulnerability.json"), "expected": [ pkg_vuln.Vulnerability( vuln_id="vulnerability-id", - severity=pkg_vuln.CvssSeverity.UNTRIAGED, - severity_provider=pkg_vuln.CvssSourceProvider.DEFAULT_PROVIDER, - cvss_score=pkg_vuln.NULL_STR, + severity="medium", + severity_provider="other", + cvss_score='5.5', published=pkg_vuln.NULL_STR, modified=pkg_vuln.NULL_STR, description=pkg_vuln.NULL_STR, @@ -123,8 +123,8 @@ def test_parse_inspector_scan_result(self): modified="2023-07-31T19:15:15Z", description="TEST_DESCRIPTION", installed_ver=( - "pkg:golang/github.com/test-namespace/pkg-2@v2.0.0;" - + "pkg:golang/github.com/test-namespace/pkg-3@v3.0.0" + "pkg:golang/github.com/test-namespace/pkg-2@v2.0.0;" + + "pkg:golang/github.com/test-namespace/pkg-3@v3.0.0" ), fixed_ver="pkg-2;pkg-3", pkg_path="/tmp/pkg-2;/tmp/pkg-3", @@ -156,71 +156,215 @@ def test_parse_inspector_scan_result(self): with open(test["test_file"], "r") as f: inspector_scan = json.load(f) vulns = pkg_vuln.parse_inspector_scan_result(inspector_scan) - self.assertEqual(test["expected"], vulns) + expected = test["expected"] + self.assertEqual(expected, vulns) - def test_get_cvss_rating(self): - tests = [ + def test_get_preferred_vuln_rating_method(self): + test_cases = [ + { + "name": "Test multiple rating methods", + "input": [ + {"method": "CVSSv4", }, + {"method": "CVSSv31", }, + {"method": "CVSSv3", }, + {"method": "CVSSv2", }, + {"method": "OWASP", }, + {"method": "SSVC", }, + {"method": "other", }, + ], + "want": pkg_vuln.VulnRatingMethod.CVSSv4 + }, + { + "name": "Test multiple rating methods - CVSSv4 last", + "input": [ + {"method": "CVSSv31"}, + {"method": "CVSSv3"}, + {"method": "CVSSv4"} + ], + "want": pkg_vuln.VulnRatingMethod.CVSSv4 + }, + { + "name": "Test empty dictionary", + "input": [{}], + "want": pkg_vuln.VulnRatingMethod.OTHER + }, + { + "name": "Test empty list", + "input": [], + "want": pkg_vuln.VulnRatingMethod.OTHER + }, + { + "name": "Test None input", + "input": None, + "want": pkg_vuln.VulnRatingMethod.OTHER + }, + { + "name": "Test list with None value", + "input": [None], + "want": pkg_vuln.VulnRatingMethod.OTHER + }, + { + "name": "Test malformed input", + "input": [ + {"method": "garbage"} + ], + "want": pkg_vuln.VulnRatingMethod.OTHER + }, + ] + + for test_case in test_cases: + got = pkg_vuln.get_preferred_vuln_rating_method(test_case["input"]) + self.assertEqual( + got, + test_case["want"], + "Test '{}' failed. Got: {}, Want: {}".format( + test_case["name"], + got, + test_case["want"] + ) + ) + + def test_get_highest_rating_by_method(self): + test_cases = [ + { + "name": "Select highest score from vulns with same severity method", + "input_method": pkg_vuln.VulnRatingMethod.CVSSv31, + "input_ratings": [ + {"method": "CVSSv31", "score": 9.8, "severity": "high"}, + {"method": "CVSSv31", "score": 8.8, "severity": "high"}, + {"method": "CVSSv31", "score": 7.8, "severity": "high"}, + {"method": "other", "score": 7.7E-4, "severity": "none"}, + ], + "want": {"method": "CVSSv31", "score": 9.8, "severity": "high"} + }, + ] + + for test_case in test_cases: + with self.subTest(test_case["name"]): + got = pkg_vuln.get_highest_rating_by_method( + test_case["input_method"], + test_case["input_ratings"] + ) + self.assertEqual(got, test_case["want"]) + + def test_get_highest_severity_rating(self): + test_cases = [ { - "name": "NVD should take a priority over other providers", - "ratings": [ + "name": "Get highest severity rating", + "input": [ { "method": "CVSSv31", - "score": 7.2, + "score": 9.8, "severity": "high", - "source": {"name": "MITRE"}, + "source": { + "name": "Ubuntu Linux", + "url": "https://people.canonical.com/~ubuntu-security/cve/2022/CVE-2022-2571.html" + }, + "vector": "CVSS:3.1/AV:L/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H" }, { "method": "CVSSv31", - "score": 5.3, - "severity": "medium", - "source": {"name": "NVD"}, + "score": 7.8, + "severity": "high", + "source": { + "name": "MITRE", + "url": "https://cve.org/CVERecord?id=CVE-2022-2571" + }, + "vector": "CVSS:3.0/AV:L/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H" }, { "method": "other", - "score": 0.00051, + "score": 7.7E-4, "severity": "none", - "source": {"name": "EPSS"}, + "source": { + "name": "EPSS", + "url": "https://api.first.org/data/v1/epss?cve=CVE-2022-2571" + }, + "vector": "model:v2023.03.01,date:2025-03-03T00:00:00+0000" + }, + { + "method": "CVSSv31", + "score": 8.8, + "severity": "high", + "source": { + "name": "NVD", + "url": "https://nvd.nist.gov/vuln/detail/CVE-2022-2571" + }, + "vector": "CVSS:3.1/AV:L/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H" }, ], - "expected": pkg_vuln.CvssRating("medium", "NVD", str(5.3)), + "want": pkg_vuln.CvssRating( + provider="Ubuntu Linux", + severity="high", + cvss_score="9.8" + ) }, { - "name": ( - 'score should be "null" and severity should be "untriaged" ' - + 'when method is "other" and severity is "unknown" ' - ), - "ratings": [ + "name": "Should ignore EPSS", + "input": [ { + "severity": "none", + "score": 0.00022, "method": "other", - "severity": "unknown", - "source": {"name": "AMAZON_INSPECTOR"}, + "vector": "model:v2023.03.01,date:2024-05-21T00:00:00+0000", + "source": { + "name": "EPSS", + "url": "https://api.first.org/" + } }, { + "severity": "unknown", "method": "other", - "score": 0.00051, - "severity": "none", - "source": {"name": "EPSS"}, - }, + "source": { + "name": "AMAZON_INSPECTOR" + } + } ], - "expected": pkg_vuln.CvssRating("untriaged", "AMAZON_INSPECTOR", "null"), + "want": pkg_vuln.CvssRating( + provider="AMAZON_INSPECTOR", + severity="other", + cvss_score="0.0" + ) }, { - "name": 'rating should be "null" and severity is not provided when no CVSS rating is provided', - "ratings": [ - { - "method": "other", - "score": 0.00051, - "severity": "none", - "source": {"name": "EPSS"}, - }, - ], - "expected": pkg_vuln.CvssRating("untriaged", "NVD", "null"), - }, + "name": "No severity rating present", + "input": [{}], + "want": pkg_vuln.CvssRating( + provider="in triage", + severity="other", + cvss_score="0.0" + ) + } ] - for test in tests: - with self.subTest(msg=test["name"]): - rating = pkg_vuln.get_cvss_rating(test["ratings"], pkg_vuln.Vulnerability()) - self.assertEqual(test["expected"], rating) + + for test_case in test_cases: + with self.subTest(test_case["name"]): + got = pkg_vuln.get_highest_severity_rating(test_case["input"]) + + # Test equality stubs + self.assertEqual(got.provider, test_case["want"].provider) + self.assertEqual(got.severity, test_case["want"].severity) + self.assertEqual(got.cvss_score, test_case["want"].cvss_score) + + def test_is_epss(self): + test_cases = [ + # method, rating, expected_result + ("other", {"source": {"name": "EPSS"}}, True), + ("other", {"source": {"name": "CVSS"}}, False), + ("CVSSv2", {"source": {"name": "EPSS"}}, False), + ("CVSSv3", {"source": {"name": "EPSS"}}, False), + ("CVSSv31", {"source": {"name": "EPSS"}}, False), + ("CVSSv4", {"source": {"name": "EPSS"}}, False), + ("OWASP", {"source": {"name": "EPSS"}}, False), + ("SSVC", {"source": {"name": "EPSS"}}, False), + ("other", {}, False), + ("other", {"source": {}}, False), + ("other", None, False), + ] + + for method, rating, expected_result in test_cases: + with self.subTest(method=method, rating=rating): + self.assertEqual(pkg_vuln.is_epss(method, rating), expected_result) def get_scan_body(test_file): From 4e74c0428eb3178778d186bf97daa347852858d1 Mon Sep 17 00:00:00 2001 From: Michael Long <31821088+bluesentinelsec@users.noreply.github.com> Date: Tue, 1 Jul 2025 10:31:50 -0400 Subject: [PATCH 15/19] v1.3.0 (#123) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Feature request 91 (#115) * FR-91: Add cli arg only fixable vulnerability; use the variable in get_vuln_counts * Revert "FR-91: Add cli arg only fixable vulnerability; use the variable in get_vuln_counts" This reverts commit bc532d46d1e17d7df18a645e0f712aee0a40d118. * FR-91: Add cli arg only fixable vulnerability; use the variable in get_vuln_counts * FR-91: Fix unit tests * FR-91: Fix typo in unit tests * Revert "FR-91: Fix typo in unit tests" This reverts commit e645542475ef7efda682ec4163c8b1b98fe2d7a8. * Revert "FR-91: Fix unit tests" This reverts commit f9157c9edf3b1a0c4a9024ad613240dd2c26390a. * Revert "FR-91: Add cli arg only fixable vulnerability; use the variable in get_vuln_counts" This reverts commit 812c68509d0fad67b968c4a11830c0cce314a20a. * FR-91: Change orchestrator to only find fixed vulnerabilities if flag show-only-fixed-vulnerabilities is present * FR-91: Fixed missing variable * FR-91: Fixed typo * FR-91: Fixed typo * FR-91: Another fix * FR-91: Another fix * FR-91: Another fix * FR-91: Another fix * FR-91: Another fix * FR-91: Another fix * FR-91: Another fix * Add unit test for get_vuln_count * Fix unit test for get_vuln_count --------- Co-authored-by: Maria Carolina Conceição * Clarify license of inspector-sbomgen dependency (#121) Co-authored-by: Michael Long * [v1.3.0] Only trigger vuln threshold on fixable vulns (#122) * Add --threshold-fixable-only to CLI * implemented business logic * changed 'threshold_fixable_only' from str to bool * Added more test coverage and CLI refinements * debugging failing unit test * test threshold-fixable-only in workflow * test threshold-fixable-only in workflow * debugging CI/CD * debugging CI/CD * debugging * debugging * debugging * debugging * removed debug log showing CLI arguments * add missing argument, fixed_vuln_counts * simplify get_fixed_vuln_counts() return values * refactor return types in get_scan_result() * refactor * refine get_fixed_vuln_counts() * update test_get_fixed_vuln_counts() * testing case sensitivity * revert 'TRUE' to 'true' * use debug log when vuln doesnt have rating * integrate --show-only-fixable-vulns (part 1) * integrate only show fixable vulns * test example workflows * fix CLI input arguments * remove leading '-' character for conditional inclusion * add a no-op CLI arg (workaround) * enable new arguments in workflows * fix failing test * update workflows for prod --------- Co-authored-by: Michael Long * set workflows to v1.3.0 for burn-in --------- Co-authored-by: CarolMebiom <59604360+CarolMebiom@users.noreply.github.com> Co-authored-by: Maria Carolina Conceição Co-authored-by: Michael Long --- .github/workflows/build_scan_container.yml | 2 +- .../workflows/example_display_findings.yml | 2 +- ...ample_vulnerability_threshold_exceeded.yml | 2 +- .github/workflows/test_archive.yml | 2 +- .github/workflows/test_binary.yml | 2 +- .github/workflows/test_containers.yml | 2 +- .github/workflows/test_dockerfile_vulns.yml | 2 +- .github/workflows/test_installation.yml | 2 +- .github/workflows/test_no_vulns.yml | 2 +- .github/workflows/test_reports_no_vulns.yml | 2 +- .github/workflows/test_repository.yml | 2 +- .github/workflows/test_vuln_thresholds.yml | 6 +- README.md | 4 +- action.yml | 14 ++ entrypoint/entrypoint/cli.py | 6 + entrypoint/entrypoint/fixed_vulns.py | 10 ++ entrypoint/entrypoint/orchestrator.py | 153 +++++++++++++++--- entrypoint/tests/test_orchestrator.py | 97 ++++++++++- 18 files changed, 269 insertions(+), 43 deletions(-) create mode 100644 entrypoint/entrypoint/fixed_vulns.py diff --git a/.github/workflows/build_scan_container.yml b/.github/workflows/build_scan_container.yml index a46258b..458f142 100644 --- a/.github/workflows/build_scan_container.yml +++ b/.github/workflows/build_scan_container.yml @@ -47,7 +47,7 @@ jobs: role-to-assume: ${{ secrets.AWS_IAM_ROLE }} - name: Scan built image with Inspector - uses: aws-actions/vulnerability-scan-github-action-for-amazon-inspector@v1 + uses: aws-actions/vulnerability-scan-github-action-for-amazon-inspector@v1.3.0 id: inspector with: artifact_type: 'container' diff --git a/.github/workflows/example_display_findings.yml b/.github/workflows/example_display_findings.yml index 0f87384..cc745a1 100644 --- a/.github/workflows/example_display_findings.yml +++ b/.github/workflows/example_display_findings.yml @@ -29,7 +29,7 @@ jobs: # modify this block to scan your intended artifact - name: Inspector Scan id: inspector - uses: aws-actions/vulnerability-scan-github-action-for-amazon-inspector@v1 + uses: aws-actions/vulnerability-scan-github-action-for-amazon-inspector@v1.3.0 with: # change artifact_type to either 'repository', 'container', 'binary', or 'archive'. # this example scans a container image diff --git a/.github/workflows/example_vulnerability_threshold_exceeded.yml b/.github/workflows/example_vulnerability_threshold_exceeded.yml index d34df37..b88fe1b 100644 --- a/.github/workflows/example_vulnerability_threshold_exceeded.yml +++ b/.github/workflows/example_vulnerability_threshold_exceeded.yml @@ -48,7 +48,7 @@ jobs: # Inspector scan - name: Scan container with Inspector - uses: aws-actions/vulnerability-scan-github-action-for-amazon-inspector@v1 + uses: aws-actions/vulnerability-scan-github-action-for-amazon-inspector@v1.3.0 id: inspector with: artifact_type: 'container' # configure Inspector for scanning a container diff --git a/.github/workflows/test_archive.yml b/.github/workflows/test_archive.yml index 5253d92..203007b 100644 --- a/.github/workflows/test_archive.yml +++ b/.github/workflows/test_archive.yml @@ -32,7 +32,7 @@ jobs: - name: Test archive scan id: inspector - uses: aws-actions/vulnerability-scan-github-action-for-amazon-inspector@v1 + uses: aws-actions/vulnerability-scan-github-action-for-amazon-inspector@v1.3.0 with: artifact_type: 'archive' artifact_path: 'entrypoint/tests/test_data/artifacts/archives/testData.zip' diff --git a/.github/workflows/test_binary.yml b/.github/workflows/test_binary.yml index dbb1efd..23ab702 100644 --- a/.github/workflows/test_binary.yml +++ b/.github/workflows/test_binary.yml @@ -32,7 +32,7 @@ jobs: - name: Test binary scan id: inspector - uses: aws-actions/vulnerability-scan-github-action-for-amazon-inspector@v1 + uses: aws-actions/vulnerability-scan-github-action-for-amazon-inspector@v1.3.0 with: artifact_type: 'binary' artifact_path: 'entrypoint/tests/test_data/artifacts/binaries/inspector-sbomgen' diff --git a/.github/workflows/test_containers.yml b/.github/workflows/test_containers.yml index c8d2514..ae48569 100644 --- a/.github/workflows/test_containers.yml +++ b/.github/workflows/test_containers.yml @@ -32,7 +32,7 @@ jobs: - name: Test container scan id: inspector - uses: aws-actions/vulnerability-scan-github-action-for-amazon-inspector@v1 + uses: aws-actions/vulnerability-scan-github-action-for-amazon-inspector@v1.3.0 with: artifact_type: 'container' artifact_path: 'ubuntu:14.04' diff --git a/.github/workflows/test_dockerfile_vulns.yml b/.github/workflows/test_dockerfile_vulns.yml index 7a39532..258e544 100644 --- a/.github/workflows/test_dockerfile_vulns.yml +++ b/.github/workflows/test_dockerfile_vulns.yml @@ -31,7 +31,7 @@ jobs: - name: Scan Dockerfiles id: inspector - uses: aws-actions/vulnerability-scan-github-action-for-amazon-inspector@v1 + uses: aws-actions/vulnerability-scan-github-action-for-amazon-inspector@v1.3.0 with: artifact_type: 'repository' artifact_path: './' diff --git a/.github/workflows/test_installation.yml b/.github/workflows/test_installation.yml index 0269c02..b43a5a7 100644 --- a/.github/workflows/test_installation.yml +++ b/.github/workflows/test_installation.yml @@ -28,7 +28,7 @@ jobs: role-to-assume: ${{ secrets.AWS_IAM_ROLE }} - name: Test Amazon Inspector GitHub Actions plugin - uses: aws-actions/vulnerability-scan-github-action-for-amazon-inspector@v1 + uses: aws-actions/vulnerability-scan-github-action-for-amazon-inspector@v1.3.0 with: artifact_type: 'container' artifact_path: 'alpine:latest' diff --git a/.github/workflows/test_no_vulns.yml b/.github/workflows/test_no_vulns.yml index 380bb53..b5277bb 100644 --- a/.github/workflows/test_no_vulns.yml +++ b/.github/workflows/test_no_vulns.yml @@ -28,7 +28,7 @@ jobs: - name: Test binary scan id: inspector - uses: aws-actions/vulnerability-scan-github-action-for-amazon-inspector@v1 + uses: aws-actions/vulnerability-scan-github-action-for-amazon-inspector@v1.3.0 with: artifact_type: 'binary' artifact_path: 'entrypoint/tests/test_data/artifacts/binaries/test_go_binary' diff --git a/.github/workflows/test_reports_no_vulns.yml b/.github/workflows/test_reports_no_vulns.yml index f5a1e55..bb99415 100644 --- a/.github/workflows/test_reports_no_vulns.yml +++ b/.github/workflows/test_reports_no_vulns.yml @@ -26,7 +26,7 @@ jobs: - name: Test container scan id: inspector - uses: aws-actions/vulnerability-scan-github-action-for-amazon-inspector@v1 + uses: aws-actions/vulnerability-scan-github-action-for-amazon-inspector@v1.3.0 with: artifact_type: 'container' artifact_path: 'alpine:latest' diff --git a/.github/workflows/test_repository.yml b/.github/workflows/test_repository.yml index 004f0f4..ffb5a34 100644 --- a/.github/workflows/test_repository.yml +++ b/.github/workflows/test_repository.yml @@ -31,7 +31,7 @@ jobs: - name: Test repository scan id: inspector - uses: aws-actions/vulnerability-scan-github-action-for-amazon-inspector@v1 + uses: aws-actions/vulnerability-scan-github-action-for-amazon-inspector@v1.3.0 with: artifact_type: 'repository' artifact_path: './' diff --git a/.github/workflows/test_vuln_thresholds.yml b/.github/workflows/test_vuln_thresholds.yml index d64155c..0ae050b 100644 --- a/.github/workflows/test_vuln_thresholds.yml +++ b/.github/workflows/test_vuln_thresholds.yml @@ -30,7 +30,7 @@ jobs: role-to-assume: ${{ secrets.AWS_IAM_ROLE }} - name: Scan artifact with Inspector - uses: aws-actions/vulnerability-scan-github-action-for-amazon-inspector@v1 + uses: aws-actions/vulnerability-scan-github-action-for-amazon-inspector@v1.3.0 id: inspector with: artifact_type: 'archive' @@ -45,8 +45,8 @@ jobs: low_threshold: 1 other_threshold: 1 sbomgen_version: "latest" + threshold_fixable_only: true + show_only_fixable_vulns: true - name: Fail if vulnerability threshold is exceeded run: if [[ ${{ steps.inspector.outputs.vulnerability_threshold_exceeded }} != "1" ]]; then echo "test failed"; else echo "test passed"; fi - - # TODO: handle failure case diff --git a/README.md b/README.md index ead5875..472d303 100644 --- a/README.md +++ b/README.md @@ -372,4 +372,6 @@ See [CONTRIBUTING](CONTRIBUTING.md#security-issue-notifications) for more inform This project is licensed under the MIT license. -Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved +This project leverages the [Amazon Inspector SBOM Generator](https://docs.aws.amazon.com/inspector/latest/user/sbom-generator.html), which is distributed under the [AWS Intellectual Property License](https://aws.amazon.com/legal/aws-ip-license-terms/). + +Copyright 2025 Amazon.com, Inc. or its affiliates. All Rights Reserved diff --git a/action.yml b/action.yml index d986748..cc9a20f 100644 --- a/action.yml +++ b/action.yml @@ -110,6 +110,18 @@ inputs: description: "Specifies the OS and CPU arch of the container image you wish to scan. Valid inputs are of the form 'os/cpu/variant' for example, 'linux/amd64', 'linux/arm64/v8', etc. If no platform is specified, the system will use the same platform as the host that is performing the scan. This argument only affects container image scans. Requires inspector-sbomgen 1.5.1 or later." required: False + threshold_fixable_only: + description: 'If set to true, only count vulnerabilities with a fix towards threshold exceeded condition.' + required: False + default: false + type: boolean + + show_only_fixable_vulns: + description: "If set to true, this action will show only fixed vulnerabilities in the GitHub Actions step summary page. All vulnerability metadata is still retained in the raw Inspector scan files." + required: False + default: false + type: boolean + outputs: artifact_sbom: description: "The filepath to the artifact's software bill of materials." @@ -148,6 +160,8 @@ runs: - --out-dockerfile-scan-md=${{ inputs.output_inspector_dockerfile_scan_path_markdown }} - --sbomgen-version=${{ inputs.sbomgen_version }} - --thresholds + - ${{ inputs.threshold_fixable_only == 'true' && '--threshold-fixable-only' || '--no-op' }} + - ${{ inputs.show_only_fixable_vulns == 'true' && '--show-only-fixable-vulns'|| '--no-op' }} - --critical=${{ inputs.critical_threshold }} - --high=${{ inputs.high_threshold }} - --medium=${{ inputs.medium_threshold }} diff --git a/entrypoint/entrypoint/cli.py b/entrypoint/entrypoint/cli.py index 133d380..9af7ccb 100644 --- a/entrypoint/entrypoint/cli.py +++ b/entrypoint/entrypoint/cli.py @@ -50,6 +50,10 @@ def init(sys_argv=None) -> argparse.Namespace: help="Specifies one or more files and/or directories that should NOT be inventoried.") parser.add_argument("--timeout", type=str, default="600", help="The amount of time in seconds that inspector-sbomgne will run. When this timeout is exceeded, sbomgen will gracefully conclude and present any findings discovered up to that point.") + parser.add_argument("--show-only-fixable-vulns", action="store_true", default=False, + help="Only show fixed vulnerabilities in the GitHub Actions job summary page.") + parser.add_argument("--threshold-fixable-only", action="store_true", default=False, + help="Only count vulnerabilities with a fix towards threshold exceeded condition.") parser.add_argument("--platform", type=str, help="Specifies the OS and CPU arch of the container image you wish to scan. Valid inputs are " @@ -57,6 +61,8 @@ def init(sys_argv=None) -> argparse.Namespace: "specified, the system will use the same platform as the host that is performing the " "scan. This argument only affects container image scans. Requires inspector-sbomgen " "1.5.1 or later.") + parser.add_argument("--no-op", action="store_true", default=False, + help="A no operation argument, used as the default from the GitHub Actions caller when boolean arguments are not set. This is a workaround because GitHub Actions doesn't have a clean way to invoke or not invoke action='store_true' arguments") args = "" if sys_argv: diff --git a/entrypoint/entrypoint/fixed_vulns.py b/entrypoint/entrypoint/fixed_vulns.py new file mode 100644 index 0000000..365261f --- /dev/null +++ b/entrypoint/entrypoint/fixed_vulns.py @@ -0,0 +1,10 @@ +from dataclasses import dataclass + + +@dataclass +class FixedVulns: + criticals: int + highs: int + mediums: int + lows: int + others: int diff --git a/entrypoint/entrypoint/orchestrator.py b/entrypoint/entrypoint/orchestrator.py index 57f2a4f..aeeeab4 100644 --- a/entrypoint/entrypoint/orchestrator.py +++ b/entrypoint/entrypoint/orchestrator.py @@ -3,12 +3,13 @@ import logging import os import platform +import re import shutil import sys import tempfile -import re +import typing -from entrypoint import dockerfile, executor, exporter, installer, pkg_vuln +from entrypoint import dockerfile, executor, exporter, installer, pkg_vuln, fixed_vulns def execute(args) -> int: @@ -26,12 +27,13 @@ def execute(args) -> int: set_github_actions_output('inspector_scan_results', args.out_scan) logging.info("tallying vulnerabilities") - succeeded, scan_result = get_scan_result(args) + succeeded, scan_result, fixed_vuln_counts = get_scan_result(args) require_true(succeeded, "unable to tally vulnerabilities") print_vuln_count_summary(scan_result) - set_flag_if_vuln_threshold_exceeded(args, scan_result) + vuln_counts = fixed_vuln_counts if args.threshold_fixable_only else scan_result + set_env_var_if_vuln_threshold_exceeded(args, vuln_counts) write_pkg_vuln_report_csv(args.out_scan_csv, scan_result) set_github_actions_output('inspector_scan_results_csv', args.out_scan_csv) @@ -201,7 +203,8 @@ def invoke_sbomgen(args) -> int: if args.platform: platform_arg = args.platform.lower() if not is_valid_container_platform(platform_arg): - logging.fatal(f"received invalid container image platform: '{args.platform}'. Platform should be of the form 'os/cpu/variant' such as 'linux/amd64' or 'linux/arm64/v8'") + logging.fatal( + f"received invalid container image platform: '{args.platform}'. Platform should be of the form 'os/cpu/variant' such as 'linux/amd64' or 'linux/arm64/v8'") sbomgen_args.append("--platform") sbomgen_args.append(platform_arg) @@ -232,31 +235,45 @@ def invoke_inspector_scan(src_sbom, dst_scan): return ret -def get_scan_result(args) -> tuple[bool, exporter.InspectorScanResult]: +def get_scan_result(args) -> tuple[bool, exporter.InspectorScanResult, fixed_vulns.FixedVulns]: + scan_result = exporter.InspectorScanResult(vulnerabilities=[pkg_vuln.Vulnerability()]) + fixed_vulns_counts = fixed_vulns.FixedVulns(criticals=0, highs=0, mediums=0, lows=0, others=0) + + succeeded, fixed_vulns_counts = get_fixed_vuln_counts( + args.out_scan) + if succeeded is False: + return False, scan_result, fixed_vulns_counts + succeeded, criticals, highs, mediums, lows, others = get_vuln_counts(args.out_scan) if succeeded is False: - return False, None + return False, scan_result, fixed_vulns_counts try: with open(args.out_scan, "r") as f: inspector_scan = json.load(f) vulns = pkg_vuln.parse_inspector_scan_result(inspector_scan) + except Exception as e: logging.error(e) - return False, None + return False, scan_result, fixed_vulns_counts + + if args.show_only_fixable_vulns: + for vuln in vulns: + if vuln.fixed_ver == "null": + vulns.remove(vuln) scan_result = exporter.InspectorScanResult( vulnerabilities=vulns, artifact_name=args.artifact_path, artifact_type=args.artifact_type, - criticals=criticals, - highs=highs, - mediums=mediums, - lows=lows, - others=others + criticals=str(criticals), + highs=str(highs), + mediums=str(mediums), + lows=str(lows), + others=str(others) ) - return succeeded, scan_result + return succeeded, scan_result, fixed_vulns_counts def set_github_actions_output(key, value): @@ -267,10 +284,14 @@ def set_github_actions_output(key, value): logging.info(f"setting github actions output: {key}:{value}") os.system(f'echo "{key}={value}" >> "$GITHUB_OUTPUT"') + # set an ENV VAR so that outputs + # look the same locally or on GitHub Actions + os.environ[key] = str(value) + return -def get_vuln_counts(inspector_scan_path: str): +def get_vuln_counts(inspector_scan_path: str) -> tuple[bool, int, int, int, int, int]: # vuln severities criticals = 0 highs = 0 @@ -332,6 +353,83 @@ def get_vuln_counts(inspector_scan_path: str): return True, criticals, highs, mediums, lows, others +def get_fixed_vuln_counts(inspector_scan_path: str) -> tuple[bool, fixed_vulns.FixedVulns]: + fixed_vulns_counts = fixed_vulns.FixedVulns(criticals=0, highs=0, mediums=0, lows=0, others=0) + + scan_contents = "" + try: + with open(inspector_scan_path, 'r') as f: + scan_contents = json.load(f) + except Exception as e: + logging.error(e) + return False, fixed_vulns_counts + + # find the sbom->metadata->properties object + scan_contents = scan_contents.get("sbom") + if scan_contents is None: + logging.error( + f"expected Inspector scan results with 'sbom' as root object, but it was not found in file {inspector_scan_path}") + return False, fixed_vulns_counts + + vulnerabilities = scan_contents.get("vulnerabilities") + + if vulnerabilities is None: + # no vulnerabilities found + return True, fixed_vulns_counts + + is_fix_available_key = "amazon:inspector:sbom_scanner:fixed_version:comp-" + for vuln in vulnerabilities: + props = vuln.get("properties") + if props is None: + logging.debug( + f"expected vulnerability with 'properties' key but none was found in file {inspector_scan_path}") + continue + + for prop in props: + name = prop.get("name") + if name is None: + continue + if is_fix_available_key not in name: + continue + + # vuln has an available fix + rating = vuln.get("ratings") + if rating is None: + logging.error( + f"expected vulnerability with 'rating' key but none was found in file {inspector_scan_path}") + continue + + previous_score = 0 + previous_severity = "" + for each_rating in rating: + severity = each_rating.get("severity") + if severity is None: + logging.error( + f"expected vulnerability with 'severity' key but none was found in file {inspector_scan_path}") + continue + score = each_rating.get("score") + if score is None: + logging.debug( + f"expected vulnerability with 'score' key but none was found in file {inspector_scan_path}") + continue + if previous_score < score: + previous_score = score + previous_severity = severity + + if previous_severity == "critical": + fixed_vulns_counts.criticals += 1 + elif previous_severity == "high": + fixed_vulns_counts.highs += 1 + elif previous_severity == "medium": + fixed_vulns_counts.mediums += 1 + elif previous_severity == "low": + fixed_vulns_counts.lows += 1 + else: + fixed_vulns_counts.others += 1 + + return True, fixed_vulns_counts + + def install_sbomgen(args): os_name = platform.system() if "Linux" in os_name: @@ -371,12 +469,14 @@ def write_pkg_vuln_report_markdown(out_scan_markdown, scan_result: exporter.Insp return markdown -def set_flag_if_vuln_threshold_exceeded(args, scan_result: exporter.InspectorScanResult): - is_exceeded = exceeds_threshold(scan_result.criticals, args.critical, - scan_result.highs, args.high, - scan_result.mediums, args.medium, - scan_result.lows, args.low, - scan_result.others, args.other) +def set_env_var_if_vuln_threshold_exceeded(args, + vuln_counts: typing.Union[ + exporter.InspectorScanResult, fixed_vulns.FixedVulns]): + is_exceeded = exceeds_threshold(vuln_counts.criticals, args.critical, + vuln_counts.highs, args.high, + vuln_counts.mediums, args.medium, + vuln_counts.lows, args.low, + vuln_counts.others, args.other) if is_exceeded and args.thresholds: set_github_actions_output('vulnerability_threshold_exceeded', 1) @@ -390,19 +490,19 @@ def exceeds_threshold(criticals, critical_threshold, lows, low_threshold, others, other_threshold) -> bool: is_threshold_exceed = False - if 0 < critical_threshold <= criticals: + if 0 < critical_threshold <= int(criticals): is_threshold_exceed = True - if 0 < high_threshold <= highs: + if 0 < high_threshold <= int(highs): is_threshold_exceed = True - if 0 < medium_threshold <= mediums: + if 0 < medium_threshold <= int(mediums): is_threshold_exceed = True - if 0 < low_threshold <= lows: + if 0 < low_threshold <= int(lows): is_threshold_exceed = True - if 0 < other_threshold <= others: + if 0 < other_threshold <= int(others): is_threshold_exceed = True return is_threshold_exceed @@ -452,6 +552,7 @@ def require_true(expr: bool, msg: str): logging.error(msg) exit(1) + def is_valid_container_platform(img_platform): # regex for detecting 'os/cpu/variant' # os/cpu are required whereas variant is optional diff --git a/entrypoint/tests/test_orchestrator.py b/entrypoint/tests/test_orchestrator.py index 792abc0..c822649 100644 --- a/entrypoint/tests/test_orchestrator.py +++ b/entrypoint/tests/test_orchestrator.py @@ -2,7 +2,7 @@ import unittest from collections import namedtuple -from entrypoint import dockerfile, exporter, orchestrator +from entrypoint import dockerfile, exporter, orchestrator, fixed_vulns class TestOrchestrator(unittest.TestCase): @@ -125,6 +125,7 @@ def test_system_against_dockerfile_findings(self): "out_scan_markdown", "out_dockerfile_scan_csv", "out_dockerfile_scan_md", + "show_only_fixable_vulns", ], ) args = ArgMock( @@ -135,9 +136,10 @@ def test_system_against_dockerfile_findings(self): out_scan_markdown="/tmp/out_scan.md", out_dockerfile_scan_csv="/tmp/out_dockerfile_scan.csv", out_dockerfile_scan_md="/tmp/out_dockerfile_scan.md", + show_only_fixable_vulns=False, ) - succeeded, scan_result = orchestrator.get_scan_result(args) + succeeded, scan_result, fixed_vuln_counts = orchestrator.get_scan_result(args) self.assertTrue(succeeded) orchestrator.write_pkg_vuln_report_csv(args.out_scan_csv, scan_result) @@ -202,5 +204,96 @@ def test_is_valid_container_platform(self): result = orchestrator.is_valid_container_platform(each_test["input"]) self.assertEqual(result, each_test["expected"]) + def test_get_fixed_vuln_counts(self): + # verify we can successfully parse all known-valid Inspector scans + test_dir = "tests/test_data/scans/" + file_list = os.listdir(test_dir) + for file in file_list: + path = os.path.join(test_dir, file) + succeeded, _ = orchestrator.get_fixed_vuln_counts(path) + self.assertTrue(succeeded) + + # verify our fixed vulnerability counts are correct for alpine:3.18.2.json.scan + succeeded, fixed_vulns_counts = orchestrator.get_fixed_vuln_counts( + "tests/test_data/scans/alpine:3.18.2.json.scan" + ) + self.assertTrue(succeeded) + self.assertEqual(fixed_vulns_counts.criticals, 1) + self.assertEqual(fixed_vulns_counts.highs, 1) + self.assertEqual(fixed_vulns_counts.mediums, 7) + self.assertEqual(fixed_vulns_counts.lows, 0) + self.assertEqual(fixed_vulns_counts.others, 3) + + succeeded, fixed_vulns_counts = orchestrator.get_fixed_vuln_counts( + "tests/test_data/scans/debian:latest.json.scan" + ) + self.assertTrue(succeeded) + self.assertEqual(fixed_vulns_counts.criticals, 0) + self.assertEqual(fixed_vulns_counts.highs, 0) + self.assertEqual(fixed_vulns_counts.mediums, 0) + self.assertEqual(fixed_vulns_counts.lows, 0) + self.assertEqual(fixed_vulns_counts.others, 6) + + def test_threshold_exceeded_on_fixable_vulns(self): + + mock_cli_args = namedtuple( + "args", + [ + "critical", + "high", + "medium", + "low", + "other", + "thresholds", + "threshold_fixable_only" + ], + ) + + threshold_args = mock_cli_args( + critical=1, + high=1, + medium=1, + low=1, + other=1, + thresholds=True, + threshold_fixable_only=True + ) + + # Given a scan containing fixable and unfixable vulns, + # threshold should be exceeded + vulns_with_fixes = fixed_vulns.FixedVulns(criticals=10, highs=0, mediums=0, lows=0, others=0) + orchestrator.set_env_var_if_vuln_threshold_exceeded(threshold_args, vulns_with_fixes) + want = "1" + got = os.environ.get("vulnerability_threshold_exceeded") + self.assertEqual(want, got) + + # Given a scan containing NO fixable vulns, + # threshold exceeded should NOT be set + no_vulns_with_fix = fixed_vulns.FixedVulns(criticals=0, highs=0, mediums=0, lows=0, others=0) + orchestrator.set_env_var_if_vuln_threshold_exceeded(threshold_args, no_vulns_with_fix) + want = "0" + got = os.environ.get("vulnerability_threshold_exceeded") + self.assertEqual(want, got) + + # Given a user who has not opted into threshold + # detection, do not set the threshold exceeded flag + disable_threshold_args = mock_cli_args( + critical=1, + high=1, + medium=1, + low=1, + other=1, + thresholds=False, + threshold_fixable_only=True + ) + vulns_with_fixes = fixed_vulns.FixedVulns(criticals=10, highs=10, mediums=10, lows=10, others=10) + orchestrator.set_env_var_if_vuln_threshold_exceeded(disable_threshold_args, vulns_with_fixes) + want = "0" + got = os.environ.get("vulnerability_threshold_exceeded") + self.assertEqual(want, got) + + return + + if __name__ == "__main__": unittest.main() From cb0e49f066833d24ca54865350e5fb3de2ea4b63 Mon Sep 17 00:00:00 2001 From: Michael Long <31821088+bluesentinelsec@users.noreply.github.com> Date: Tue, 1 Jul 2025 10:56:55 -0400 Subject: [PATCH 16/19] Sync main to v1.3.0 (#126) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Feature request 91 (#115) * FR-91: Add cli arg only fixable vulnerability; use the variable in get_vuln_counts * Revert "FR-91: Add cli arg only fixable vulnerability; use the variable in get_vuln_counts" This reverts commit bc532d46d1e17d7df18a645e0f712aee0a40d118. * FR-91: Add cli arg only fixable vulnerability; use the variable in get_vuln_counts * FR-91: Fix unit tests * FR-91: Fix typo in unit tests * Revert "FR-91: Fix typo in unit tests" This reverts commit e645542475ef7efda682ec4163c8b1b98fe2d7a8. * Revert "FR-91: Fix unit tests" This reverts commit f9157c9edf3b1a0c4a9024ad613240dd2c26390a. * Revert "FR-91: Add cli arg only fixable vulnerability; use the variable in get_vuln_counts" This reverts commit 812c68509d0fad67b968c4a11830c0cce314a20a. * FR-91: Change orchestrator to only find fixed vulnerabilities if flag show-only-fixed-vulnerabilities is present * FR-91: Fixed missing variable * FR-91: Fixed typo * FR-91: Fixed typo * FR-91: Another fix * FR-91: Another fix * FR-91: Another fix * FR-91: Another fix * FR-91: Another fix * FR-91: Another fix * FR-91: Another fix * Add unit test for get_vuln_count * Fix unit test for get_vuln_count --------- Co-authored-by: Maria Carolina Conceição * Clarify license of inspector-sbomgen dependency (#121) Co-authored-by: Michael Long * [v1.3.0] Only trigger vuln threshold on fixable vulns (#122) * Add --threshold-fixable-only to CLI * implemented business logic * changed 'threshold_fixable_only' from str to bool * Added more test coverage and CLI refinements * debugging failing unit test * test threshold-fixable-only in workflow * test threshold-fixable-only in workflow * debugging CI/CD * debugging CI/CD * debugging * debugging * debugging * debugging * removed debug log showing CLI arguments * add missing argument, fixed_vuln_counts * simplify get_fixed_vuln_counts() return values * refactor return types in get_scan_result() * refactor * refine get_fixed_vuln_counts() * update test_get_fixed_vuln_counts() * testing case sensitivity * revert 'TRUE' to 'true' * use debug log when vuln doesnt have rating * integrate --show-only-fixable-vulns (part 1) * integrate only show fixable vulns * test example workflows * fix CLI input arguments * remove leading '-' character for conditional inclusion * add a no-op CLI arg (workaround) * enable new arguments in workflows * fix failing test * update workflows for prod --------- Co-authored-by: Michael Long * set workflows to v1.3.0 for burn-in --------- Co-authored-by: CarolMebiom <59604360+CarolMebiom@users.noreply.github.com> Co-authored-by: Maria Carolina Conceição Co-authored-by: Michael Long From 53b7369ed318bcb708c3add9502126f5b3161492 Mon Sep 17 00:00:00 2001 From: Michael Long Date: Tue, 1 Jul 2025 11:00:08 -0400 Subject: [PATCH 17/19] Verify v1 tag works --- .github/workflows/test_installation.yml | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/.github/workflows/test_installation.yml b/.github/workflows/test_installation.yml index b43a5a7..0eb0a23 100644 --- a/.github/workflows/test_installation.yml +++ b/.github/workflows/test_installation.yml @@ -28,7 +28,7 @@ jobs: role-to-assume: ${{ secrets.AWS_IAM_ROLE }} - name: Test Amazon Inspector GitHub Actions plugin - uses: aws-actions/vulnerability-scan-github-action-for-amazon-inspector@v1.3.0 + uses: aws-actions/vulnerability-scan-github-action-for-amazon-inspector@v1 with: artifact_type: 'container' artifact_path: 'alpine:latest' @@ -40,7 +40,4 @@ jobs: if: ${{ failure() }} run: echo "this feature is not implemented" -# TODO: update this to point to public v1.0.0 release -# TODO: add steps to send notification to a Lambda to cut a ticket on job failure -# TODO: delete on push condition when finished with development -# TODO: use an IAM role + From b8917ac7b10df7627a198a441d6430f2347be502 Mon Sep 17 00:00:00 2001 From: Michael Long Date: Tue, 1 Jul 2025 11:02:02 -0400 Subject: [PATCH 18/19] Verify action against 1.x --- .github/workflows/test_installation.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test_installation.yml b/.github/workflows/test_installation.yml index 0eb0a23..32b1d0f 100644 --- a/.github/workflows/test_installation.yml +++ b/.github/workflows/test_installation.yml @@ -28,7 +28,7 @@ jobs: role-to-assume: ${{ secrets.AWS_IAM_ROLE }} - name: Test Amazon Inspector GitHub Actions plugin - uses: aws-actions/vulnerability-scan-github-action-for-amazon-inspector@v1 + uses: aws-actions/vulnerability-scan-github-action-for-amazon-inspector@1.x with: artifact_type: 'container' artifact_path: 'alpine:latest' From 24d856e04e653702cba656b11852c8291b40c41a Mon Sep 17 00:00:00 2001 From: Michael Long <31821088+bluesentinelsec@users.noreply.github.com> Date: Tue, 16 Sep 2025 10:47:48 -0400 Subject: [PATCH 19/19] v1.4.0 (#133) * Use aws-cli instead of amazonlinux to speed up container build time (#128) * Change Dockerfile source image to aws-cli * Set WORKDIR back to default value --------- Co-authored-by: Joshua-Grisham_SSCSpace * set workflows to develop for aws-cli runtime tests * add explicit permissions to GitHub Actions workflows (#130) * Measuring installation time (#131) (#132) * measuring installation time * Change workflows to point to v1.4.0 branch --------- Co-authored-by: Joshua Grisham Co-authored-by: Joshua-Grisham_SSCSpace --- .github/workflows/build_scan_container.yml | 7 ++++++- .github/workflows/example_display_findings.yml | 6 +++++- .../example_vulnerability_threshold_exceeded.yml | 2 +- .github/workflows/run_unit_tests.yml | 4 ++++ .github/workflows/scan_repo_with_semgrep.yml | 3 +++ .github/workflows/test_archive.yml | 6 +++++- .github/workflows/test_binary.yml | 6 +++++- .github/workflows/test_containers.yml | 6 +++++- .github/workflows/test_dockerfile_vulns.yml | 6 +++++- .github/workflows/test_installation.yml | 6 +++++- .github/workflows/test_no_vulns.yml | 6 +++++- .github/workflows/test_reports_no_vulns.yml | 6 +++++- .github/workflows/test_repository.yml | 6 +++++- .github/workflows/test_vuln_thresholds.yml | 6 +++++- Dockerfile | 10 +++------- 15 files changed, 67 insertions(+), 19 deletions(-) diff --git a/.github/workflows/build_scan_container.yml b/.github/workflows/build_scan_container.yml index 458f142..6b5bf92 100644 --- a/.github/workflows/build_scan_container.yml +++ b/.github/workflows/build_scan_container.yml @@ -12,6 +12,11 @@ on: branches: # - '*' +permissions: + contents: read + id-token: write + actions: write # For uploading artifacts + jobs: build: name: Build docker image @@ -47,7 +52,7 @@ jobs: role-to-assume: ${{ secrets.AWS_IAM_ROLE }} - name: Scan built image with Inspector - uses: aws-actions/vulnerability-scan-github-action-for-amazon-inspector@v1.3.0 + uses: aws-actions/vulnerability-scan-github-action-for-amazon-inspector@v1.4.0 id: inspector with: artifact_type: 'container' diff --git a/.github/workflows/example_display_findings.yml b/.github/workflows/example_display_findings.yml index cc745a1..8ad4b4e 100644 --- a/.github/workflows/example_display_findings.yml +++ b/.github/workflows/example_display_findings.yml @@ -8,6 +8,10 @@ on: branches: # - '*' +permissions: + contents: read + id-token: write + jobs: daily_job: runs-on: ubuntu-latest @@ -29,7 +33,7 @@ jobs: # modify this block to scan your intended artifact - name: Inspector Scan id: inspector - uses: aws-actions/vulnerability-scan-github-action-for-amazon-inspector@v1.3.0 + uses: aws-actions/vulnerability-scan-github-action-for-amazon-inspector@v1.4.0 with: # change artifact_type to either 'repository', 'container', 'binary', or 'archive'. # this example scans a container image diff --git a/.github/workflows/example_vulnerability_threshold_exceeded.yml b/.github/workflows/example_vulnerability_threshold_exceeded.yml index b88fe1b..248ecca 100644 --- a/.github/workflows/example_vulnerability_threshold_exceeded.yml +++ b/.github/workflows/example_vulnerability_threshold_exceeded.yml @@ -48,7 +48,7 @@ jobs: # Inspector scan - name: Scan container with Inspector - uses: aws-actions/vulnerability-scan-github-action-for-amazon-inspector@v1.3.0 + uses: aws-actions/vulnerability-scan-github-action-for-amazon-inspector@v1.4.0 id: inspector with: artifact_type: 'container' # configure Inspector for scanning a container diff --git a/.github/workflows/run_unit_tests.yml b/.github/workflows/run_unit_tests.yml index 454e94a..2c1e0f2 100644 --- a/.github/workflows/run_unit_tests.yml +++ b/.github/workflows/run_unit_tests.yml @@ -7,6 +7,10 @@ on: branches: # - '*' +permissions: + contents: read + id-token: write + jobs: build: runs-on: ubuntu-latest diff --git a/.github/workflows/scan_repo_with_semgrep.yml b/.github/workflows/scan_repo_with_semgrep.yml index 91dcae6..1d2e8a5 100644 --- a/.github/workflows/scan_repo_with_semgrep.yml +++ b/.github/workflows/scan_repo_with_semgrep.yml @@ -2,6 +2,9 @@ name: Semgrep Scan on: [push] +permissions: + contents: read + jobs: semgrep: runs-on: ubuntu-latest diff --git a/.github/workflows/test_archive.yml b/.github/workflows/test_archive.yml index 203007b..c4afb81 100644 --- a/.github/workflows/test_archive.yml +++ b/.github/workflows/test_archive.yml @@ -11,6 +11,10 @@ on: branches: # - '*' +permissions: + contents: read + id-token: write + jobs: daily_job: runs-on: ubuntu-latest @@ -32,7 +36,7 @@ jobs: - name: Test archive scan id: inspector - uses: aws-actions/vulnerability-scan-github-action-for-amazon-inspector@v1.3.0 + uses: aws-actions/vulnerability-scan-github-action-for-amazon-inspector@v1.4.0 with: artifact_type: 'archive' artifact_path: 'entrypoint/tests/test_data/artifacts/archives/testData.zip' diff --git a/.github/workflows/test_binary.yml b/.github/workflows/test_binary.yml index 23ab702..3f86f61 100644 --- a/.github/workflows/test_binary.yml +++ b/.github/workflows/test_binary.yml @@ -11,6 +11,10 @@ on: branches: # - '*' +permissions: + contents: read + id-token: write + jobs: daily_job: runs-on: ubuntu-latest @@ -32,7 +36,7 @@ jobs: - name: Test binary scan id: inspector - uses: aws-actions/vulnerability-scan-github-action-for-amazon-inspector@v1.3.0 + uses: aws-actions/vulnerability-scan-github-action-for-amazon-inspector@v1.4.0 with: artifact_type: 'binary' artifact_path: 'entrypoint/tests/test_data/artifacts/binaries/inspector-sbomgen' diff --git a/.github/workflows/test_containers.yml b/.github/workflows/test_containers.yml index ae48569..d49bb1b 100644 --- a/.github/workflows/test_containers.yml +++ b/.github/workflows/test_containers.yml @@ -11,6 +11,10 @@ on: branches: # - '*' +permissions: + contents: read + id-token: write + jobs: daily_job: runs-on: ubuntu-latest @@ -32,7 +36,7 @@ jobs: - name: Test container scan id: inspector - uses: aws-actions/vulnerability-scan-github-action-for-amazon-inspector@v1.3.0 + uses: aws-actions/vulnerability-scan-github-action-for-amazon-inspector@v1.4.0 with: artifact_type: 'container' artifact_path: 'ubuntu:14.04' diff --git a/.github/workflows/test_dockerfile_vulns.yml b/.github/workflows/test_dockerfile_vulns.yml index 258e544..4cd1c1c 100644 --- a/.github/workflows/test_dockerfile_vulns.yml +++ b/.github/workflows/test_dockerfile_vulns.yml @@ -11,6 +11,10 @@ on: branches: # - '*' +permissions: + contents: read + id-token: write + jobs: daily_job: runs-on: ubuntu-latest @@ -31,7 +35,7 @@ jobs: - name: Scan Dockerfiles id: inspector - uses: aws-actions/vulnerability-scan-github-action-for-amazon-inspector@v1.3.0 + uses: aws-actions/vulnerability-scan-github-action-for-amazon-inspector@v1.4.0 with: artifact_type: 'repository' artifact_path: './' diff --git a/.github/workflows/test_installation.yml b/.github/workflows/test_installation.yml index 32b1d0f..c4459c2 100644 --- a/.github/workflows/test_installation.yml +++ b/.github/workflows/test_installation.yml @@ -11,6 +11,10 @@ on: branches: - '*' +permissions: + contents: read + id-token: write + jobs: daily_job: runs-on: ubuntu-latest @@ -28,7 +32,7 @@ jobs: role-to-assume: ${{ secrets.AWS_IAM_ROLE }} - name: Test Amazon Inspector GitHub Actions plugin - uses: aws-actions/vulnerability-scan-github-action-for-amazon-inspector@1.x + uses: aws-actions/vulnerability-scan-github-action-for-amazon-inspector@v1.4.0 with: artifact_type: 'container' artifact_path: 'alpine:latest' diff --git a/.github/workflows/test_no_vulns.yml b/.github/workflows/test_no_vulns.yml index b5277bb..c5bbb79 100644 --- a/.github/workflows/test_no_vulns.yml +++ b/.github/workflows/test_no_vulns.yml @@ -7,6 +7,10 @@ on: branches: # - '*' +permissions: + contents: read + id-token: write + jobs: daily_job: runs-on: ubuntu-latest @@ -28,7 +32,7 @@ jobs: - name: Test binary scan id: inspector - uses: aws-actions/vulnerability-scan-github-action-for-amazon-inspector@v1.3.0 + uses: aws-actions/vulnerability-scan-github-action-for-amazon-inspector@v1.4.0 with: artifact_type: 'binary' artifact_path: 'entrypoint/tests/test_data/artifacts/binaries/test_go_binary' diff --git a/.github/workflows/test_reports_no_vulns.yml b/.github/workflows/test_reports_no_vulns.yml index bb99415..5f7ca24 100644 --- a/.github/workflows/test_reports_no_vulns.yml +++ b/.github/workflows/test_reports_no_vulns.yml @@ -5,6 +5,10 @@ on: branches: # - '*' +permissions: + contents: read + id-token: write + jobs: daily_job: runs-on: ubuntu-latest @@ -26,7 +30,7 @@ jobs: - name: Test container scan id: inspector - uses: aws-actions/vulnerability-scan-github-action-for-amazon-inspector@v1.3.0 + uses: aws-actions/vulnerability-scan-github-action-for-amazon-inspector@v1.4.0 with: artifact_type: 'container' artifact_path: 'alpine:latest' diff --git a/.github/workflows/test_repository.yml b/.github/workflows/test_repository.yml index ffb5a34..3091846 100644 --- a/.github/workflows/test_repository.yml +++ b/.github/workflows/test_repository.yml @@ -11,6 +11,10 @@ on: branches: # - '*' +permissions: + contents: read + id-token: write + jobs: daily_job: runs-on: ubuntu-latest @@ -31,7 +35,7 @@ jobs: - name: Test repository scan id: inspector - uses: aws-actions/vulnerability-scan-github-action-for-amazon-inspector@v1.3.0 + uses: aws-actions/vulnerability-scan-github-action-for-amazon-inspector@v1.4.0 with: artifact_type: 'repository' artifact_path: './' diff --git a/.github/workflows/test_vuln_thresholds.yml b/.github/workflows/test_vuln_thresholds.yml index 0ae050b..31503cd 100644 --- a/.github/workflows/test_vuln_thresholds.yml +++ b/.github/workflows/test_vuln_thresholds.yml @@ -10,6 +10,10 @@ on: branches: # - '*' +permissions: + contents: read + id-token: write + jobs: build: name: Build docker image @@ -30,7 +34,7 @@ jobs: role-to-assume: ${{ secrets.AWS_IAM_ROLE }} - name: Scan artifact with Inspector - uses: aws-actions/vulnerability-scan-github-action-for-amazon-inspector@v1.3.0 + uses: aws-actions/vulnerability-scan-github-action-for-amazon-inspector@v1.4.0 id: inspector with: artifact_type: 'archive' diff --git a/Dockerfile b/Dockerfile index 82c1641..07d1935 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,11 +1,7 @@ -FROM public.ecr.aws/amazonlinux/amazonlinux:latest - -RUN dnf install python3 aws-cli -y +FROM public.ecr.aws/aws-cli/aws-cli:latest -COPY ./entrypoint . +WORKDIR / +COPY ./entrypoint . RUN chmod 0500 /main.py ENTRYPOINT ["/main.py"] - -# note: don't set a WORKDIR in this image, it conflicts with github actions: -# https://docs.github.com/en/actions/creating-actions/dockerfile-support-for-github-actions#workdir