From 436a5b449bf42361842bd37b5d1668e96b167ca0 Mon Sep 17 00:00:00 2001 From: tdruez Date: Mon, 25 Aug 2025 10:58:43 +0400 Subject: [PATCH 1/3] Add GitHub workflow to generate SBOM with CycloneDX cdxgen Signed-off-by: tdruez --- .github/workflows/sca-integration-cdxgen.yml | 54 ++++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100644 .github/workflows/sca-integration-cdxgen.yml diff --git a/.github/workflows/sca-integration-cdxgen.yml b/.github/workflows/sca-integration-cdxgen.yml new file mode 100644 index 0000000000..0af013ab99 --- /dev/null +++ b/.github/workflows/sca-integration-cdxgen.yml @@ -0,0 +1,54 @@ +name: Generate SBOM with CycloneDX cdxgen and load into ScanCode.io + +# This workflow: +# 1. Generates a CycloneDX SBOM for a container image using CycloneDX cdxgen. +# 2. Uploads the SBOM as a GitHub artifact for future inspection. +# 3. Loads the SBOM into ScanCode.io for further analysis. +# 4. Runs assertions to verify that the SBOM was properly processed in ScanCode.io. +# +# It runs on demand, and once a week (scheduled). + +on: + workflow_dispatch: + schedule: + # Run once a week (every 7 days) at 00:00 UTC on Sunday + - cron: "0 0 * * 0" + +permissions: + contents: read + +env: + IMAGE_REFERENCE: "python:3.13.0-slim" + +jobs: + generate-and-load-sbom: + runs-on: ubuntu-24.04 + steps: + - name: Install CycloneDX cdxgen + run: npm install @cyclonedx/cdxgen + + - name: Generate SBOM with CycloneDX cdxgen + run: | + npx cdxgen ${{ env.IMAGE_REFERENCE }} \ + --type docker \ + --output cdxgen-sbom.cdx.json \ + --spec-version 1.6 \ + --json-pretty + + - name: Upload SBOM as GitHub Artifact + uses: actions/upload-artifact@v4 + with: + name: cdxgen-sbom + path: "cdxgen-sbom.cdx.json" + retention-days: 20 + + - name: Import SBOM into ScanCode.io + uses: aboutcode-org/scancode-action@main + with: + pipelines: "load_sbom" + inputs-path: "cdxgen-sbom.cdx.json" + + - name: Verify SBOM Analysis Results in ScanCode.io + shell: bash + run: | + scanpipe shell --command "from scanpipe.models import DiscoveredPackage, DiscoveredDependency; package_manager = DiscoveredPackage.objects; assert package_manager.count() > 340; assert package_manager.vulnerable().count() == 0; assert DiscoveredDependency.objects.count() == 0" From 188564b8720e8f07a1acd7fa08ffe702d367c463 Mon Sep 17 00:00:00 2001 From: tdruez Date: Mon, 25 Aug 2025 11:02:09 +0400 Subject: [PATCH 2/3] Add unit test for the CycloneDX cdxgen SBOM support Signed-off-by: tdruez --- .../cdxgen-alpine-3.17-sbom.json | 732 ++++++++++++++++++ scanpipe/tests/test_sca_integrations.py | 26 + 2 files changed, 758 insertions(+) create mode 100644 scanpipe/tests/data/sca-integrations/cdxgen-alpine-3.17-sbom.json diff --git a/scanpipe/tests/data/sca-integrations/cdxgen-alpine-3.17-sbom.json b/scanpipe/tests/data/sca-integrations/cdxgen-alpine-3.17-sbom.json new file mode 100644 index 0000000000..e002ac536a --- /dev/null +++ b/scanpipe/tests/data/sca-integrations/cdxgen-alpine-3.17-sbom.json @@ -0,0 +1,732 @@ +{ + "bomFormat": "CycloneDX", + "specVersion": "1.6", + "serialNumber": "urn:uuid:c134c2dd-4815-4edf-847a-6633874ac74c", + "version": 1, + "metadata": { + "timestamp": "2025-08-25T06:39:25Z", + "tools": { + "components": [ + { + "group": "@cyclonedx", + "name": "cdxgen", + "version": "11.6.0", + "purl": "pkg:npm/%40cyclonedx/cdxgen@11.6.0", + "type": "application", + "bom-ref": "pkg:npm/@cyclonedx/cdxgen@11.6.0", + "publisher": "OWASP Foundation", + "authors": [ + { + "name": "OWASP Foundation" + } + ] + } + ] + }, + "authors": [ + { + "name": "OWASP Foundation" + } + ], + "lifecycles": [ + { + "phase": "pre-build" + }, + { + "phase": "post-build" + } + ], + "component": { + "name": "alpine", + "version": "3.17.0", + "type": "container", + "purl": "pkg:oci/alpine@sha256:8914eb54f968791faf6a8638949e480fef81e697984fba772b3976835194c6d4", + "bom-ref": "pkg:oci/alpine@sha256:8914eb54f968791faf6a8638949e480fef81e697984fba772b3976835194c6d4" + }, + "properties": [ + { + "name": "oci:image:Id", + "value": "sha256:49176f190c7e9cdb51ac85ab6c6d5e4512352218190cd69b08e6fd803ffbf3da" + }, + { + "name": "oci:image:RepoTag", + "value": "alpine:3.17.0" + }, + { + "name": "oci:image:RepoDigest", + "value": "alpine@sha256:8914eb54f968791faf6a8638949e480fef81e697984fba772b3976835194c6d4" + }, + { + "name": "oci:image:Created", + "value": "2022-11-22T22:19:29.008562326Z" + }, + { + "name": "oci:image:Architecture", + "value": "amd64" + }, + { + "name": "oci:image:Os", + "value": "linux" + }, + { + "name": "oci:image:manifest:Config", + "value": "blobs/sha256/49176f190c7e9cdb51ac85ab6c6d5e4512352218190cd69b08e6fd803ffbf3da" + }, + { + "name": "oci:image:manifest:Layers", + "value": "blobs/sha256/ded7a220bb058e28ee3254fbba04ca90b679070424424761a53a043b93b612bf" + }, + { + "name": "oci:image:lastLayer:Env", + "value": "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" + }, + { + "name": "oci:image:env:PATH", + "value": "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" + }, + { + "name": "oci:image:lastLayer:Cmd", + "value": "/bin/sh" + }, + { + "name": "cdx:bom:componentTypes", + "value": "generic" + } + ] + }, + "components": [ + { + "name": "busybox", + "type": "file", + "purl": "pkg:generic/busybox", + "bom-ref": "pkg:generic/busybox", + "hashes": [ + { + "alg": "MD5", + "content": "54c55186b332e6dfe5a75530c4a453bd" + }, + { + "alg": "SHA-1", + "content": "c9534b79e07b568ba108efe4cfe75b7cbddd6387" + } + ], + "properties": [ + { + "name": "SrcFile", + "value": "/bin/busybox" + }, + { + "name": "internal:is_executable", + "value": "true" + } + ], + "evidence": { + "identity": [ + { + "field": "purl", + "confidence": 0, + "methods": [ + { + "technique": "filename", + "confidence": 0, + "value": "/bin/busybox" + } + ], + "concludedValue": "/bin/busybox" + } + ] + } + }, + { + "name": "apk", + "type": "file", + "purl": "pkg:generic/apk", + "bom-ref": "pkg:generic/apk", + "hashes": [ + { + "alg": "MD5", + "content": "ac9ea8f31b1a7d83af65af97f1d32f7b" + }, + { + "alg": "SHA-1", + "content": "ff86e638f7bf1f56211d1ce5ae3dbba2e7d384cc" + } + ], + "properties": [ + { + "name": "SrcFile", + "value": "/sbin/apk" + }, + { + "name": "internal:is_executable", + "value": "true" + } + ], + "evidence": { + "identity": [ + { + "field": "purl", + "confidence": 0, + "methods": [ + { + "technique": "filename", + "confidence": 0, + "value": "/sbin/apk" + } + ], + "concludedValue": "/sbin/apk" + } + ] + }, + "tags": [ + "package" + ] + }, + { + "name": "ldconfig", + "type": "file", + "purl": "pkg:generic/ldconfig", + "bom-ref": "pkg:generic/ldconfig", + "hashes": [ + { + "alg": "MD5", + "content": "830d01f7821b978df770b06db3790921" + }, + { + "alg": "SHA-1", + "content": "2a36b6f8f3992b112450e66ac128c2ea499a103e" + } + ], + "properties": [ + { + "name": "SrcFile", + "value": "/sbin/ldconfig" + }, + { + "name": "internal:is_executable", + "value": "true" + } + ], + "evidence": { + "identity": [ + { + "field": "purl", + "confidence": 0, + "methods": [ + { + "technique": "filename", + "confidence": 0, + "value": "/sbin/ldconfig" + } + ], + "concludedValue": "/sbin/ldconfig" + } + ] + } + }, + { + "name": "getconf", + "type": "file", + "purl": "pkg:generic/getconf", + "bom-ref": "pkg:generic/getconf", + "hashes": [ + { + "alg": "MD5", + "content": "e58b1487fd604f02b3923621b6fd6b64" + }, + { + "alg": "SHA-1", + "content": "514468d67656129d5bc162ca39a16ff4c4641ae6" + } + ], + "properties": [ + { + "name": "SrcFile", + "value": "/usr/bin/getconf" + }, + { + "name": "internal:is_executable", + "value": "true" + } + ], + "evidence": { + "identity": [ + { + "field": "purl", + "confidence": 0, + "methods": [ + { + "technique": "filename", + "confidence": 0, + "value": "/usr/bin/getconf" + } + ], + "concludedValue": "/usr/bin/getconf" + } + ] + } + }, + { + "name": "getent", + "type": "file", + "purl": "pkg:generic/getent", + "bom-ref": "pkg:generic/getent", + "hashes": [ + { + "alg": "MD5", + "content": "448d1bc633b9067eeb8da91188096ba6" + }, + { + "alg": "SHA-1", + "content": "ceddd4e0b7e00ddcc5b85de5205ce4b506f99cbc" + } + ], + "properties": [ + { + "name": "SrcFile", + "value": "/usr/bin/getent" + }, + { + "name": "internal:is_executable", + "value": "true" + } + ], + "evidence": { + "identity": [ + { + "field": "purl", + "confidence": 0, + "methods": [ + { + "technique": "filename", + "confidence": 0, + "value": "/usr/bin/getent" + } + ], + "concludedValue": "/usr/bin/getent" + } + ] + } + }, + { + "name": "iconv", + "type": "file", + "purl": "pkg:generic/iconv", + "bom-ref": "pkg:generic/iconv", + "hashes": [ + { + "alg": "MD5", + "content": "a0d8b1e97322d3192ae21583265c0e7c" + }, + { + "alg": "SHA-1", + "content": "314af66ec460757ec81034fc55e6fcd9ab5badf9" + } + ], + "properties": [ + { + "name": "SrcFile", + "value": "/usr/bin/iconv" + }, + { + "name": "internal:is_executable", + "value": "true" + } + ], + "evidence": { + "identity": [ + { + "field": "purl", + "confidence": 0, + "methods": [ + { + "technique": "filename", + "confidence": 0, + "value": "/usr/bin/iconv" + } + ], + "concludedValue": "/usr/bin/iconv" + } + ] + } + }, + { + "name": "ldd", + "type": "file", + "purl": "pkg:generic/ldd", + "bom-ref": "pkg:generic/ldd", + "hashes": [ + { + "alg": "MD5", + "content": "be6a1717bf61614c6be87c0755a1bc98" + }, + { + "alg": "SHA-1", + "content": "c850211a08262fb11181b200eca431c93cdfde4b" + } + ], + "properties": [ + { + "name": "SrcFile", + "value": "/usr/bin/ldd" + }, + { + "name": "internal:is_executable", + "value": "true" + } + ], + "evidence": { + "identity": [ + { + "field": "purl", + "confidence": 0, + "methods": [ + { + "technique": "filename", + "confidence": 0, + "value": "/usr/bin/ldd" + } + ], + "concludedValue": "/usr/bin/ldd" + } + ] + } + }, + { + "name": "scanelf", + "type": "file", + "purl": "pkg:generic/scanelf", + "bom-ref": "pkg:generic/scanelf", + "hashes": [ + { + "alg": "MD5", + "content": "38402b7639b4e3fec70bc5d3a24416ab" + }, + { + "alg": "SHA-1", + "content": "368022b2c543b9a9d717528e125f2aac50376eca" + } + ], + "properties": [ + { + "name": "SrcFile", + "value": "/usr/bin/scanelf" + }, + { + "name": "internal:is_executable", + "value": "true" + } + ], + "evidence": { + "identity": [ + { + "field": "purl", + "confidence": 0, + "methods": [ + { + "technique": "filename", + "confidence": 0, + "value": "/usr/bin/scanelf" + } + ], + "concludedValue": "/usr/bin/scanelf" + } + ] + } + }, + { + "name": "ssl_client", + "type": "file", + "purl": "pkg:generic/ssl_client", + "bom-ref": "pkg:generic/ssl_client", + "hashes": [ + { + "alg": "MD5", + "content": "38e490a85ffa4b084b67a36f50de71c6" + }, + { + "alg": "SHA-1", + "content": "dd2841de918c0cbf11eecaaf0d578b38167a3f74" + } + ], + "properties": [ + { + "name": "SrcFile", + "value": "/usr/bin/ssl_client" + }, + { + "name": "internal:is_executable", + "value": "true" + } + ], + "evidence": { + "identity": [ + { + "field": "purl", + "confidence": 0, + "methods": [ + { + "technique": "filename", + "confidence": 0, + "value": "/usr/bin/ssl_client" + } + ], + "concludedValue": "/usr/bin/ssl_client" + } + ] + } + }, + { + "name": "ld-musl-x86_64.so.1", + "type": "file", + "purl": "pkg:generic/ld-musl-x86_64.so.1", + "bom-ref": "pkg:generic/ld-musl-x86_64.so.1", + "hashes": [ + { + "alg": "MD5", + "content": "63440a8a226fbb7d118deb67f0435143" + }, + { + "alg": "SHA-1", + "content": "b46c60c7614bac3fb4524d373540706db1224425" + } + ], + "properties": [ + { + "name": "SrcFile", + "value": "/lib/ld-musl-x86_64.so.1" + }, + { + "name": "internal:is_shared_library", + "value": "true" + } + ], + "evidence": { + "identity": [ + { + "field": "purl", + "confidence": 0, + "methods": [ + { + "technique": "filename", + "confidence": 0, + "value": "/lib/ld-musl-x86_64.so.1" + } + ], + "concludedValue": "/lib/ld-musl-x86_64.so.1" + } + ] + }, + "tags": [ + "runtime" + ] + }, + { + "name": "libapk.so.3.12.0", + "type": "file", + "purl": "pkg:generic/libapk.so.3.12.0", + "bom-ref": "pkg:generic/libapk.so.3.12.0", + "hashes": [ + { + "alg": "MD5", + "content": "16a996a811e650948251bd52d712d5a4" + }, + { + "alg": "SHA-1", + "content": "a298e962a5e0ce6395a3b11b35ef25e57a25d3c8" + } + ], + "properties": [ + { + "name": "SrcFile", + "value": "/lib/libapk.so.3.12.0" + }, + { + "name": "internal:is_shared_library", + "value": "true" + } + ], + "evidence": { + "identity": [ + { + "field": "purl", + "confidence": 0, + "methods": [ + { + "technique": "filename", + "confidence": 0, + "value": "/lib/libapk.so.3.12.0" + } + ], + "concludedValue": "/lib/libapk.so.3.12.0" + } + ] + } + }, + { + "name": "libcrypto.so.3", + "type": "file", + "purl": "pkg:generic/libcrypto.so.3", + "bom-ref": "pkg:generic/libcrypto.so.3", + "hashes": [ + { + "alg": "MD5", + "content": "f0adf918b27de9e5be5a515140928dd1" + }, + { + "alg": "SHA-1", + "content": "cb3c6cb6ad39366fb80c04b4811fd749c32d8516" + } + ], + "properties": [ + { + "name": "SrcFile", + "value": "/lib/libcrypto.so.3" + }, + { + "name": "internal:is_shared_library", + "value": "true" + } + ], + "evidence": { + "identity": [ + { + "field": "purl", + "confidence": 0, + "methods": [ + { + "technique": "filename", + "confidence": 0, + "value": "/lib/libcrypto.so.3" + } + ], + "concludedValue": "/lib/libcrypto.so.3" + } + ] + }, + "tags": [ + "crypto" + ] + }, + { + "name": "libssl.so.3", + "type": "file", + "purl": "pkg:generic/libssl.so.3", + "bom-ref": "pkg:generic/libssl.so.3", + "hashes": [ + { + "alg": "MD5", + "content": "55ecd1621ee12862bece282bbd634a7b" + }, + { + "alg": "SHA-1", + "content": "4e1b4e4e9647c4b4f45b7254d4123c6c6da05bf6" + } + ], + "properties": [ + { + "name": "SrcFile", + "value": "/lib/libssl.so.3" + }, + { + "name": "internal:is_shared_library", + "value": "true" + } + ], + "evidence": { + "identity": [ + { + "field": "purl", + "confidence": 0, + "methods": [ + { + "technique": "filename", + "confidence": 0, + "value": "/lib/libssl.so.3" + } + ], + "concludedValue": "/lib/libssl.so.3" + } + ] + }, + "tags": [ + "security" + ] + }, + { + "name": "libz.so.1.2.13", + "type": "file", + "purl": "pkg:generic/libz.so.1.2.13", + "bom-ref": "pkg:generic/libz.so.1.2.13", + "hashes": [ + { + "alg": "MD5", + "content": "c8f4f91b139ae30a3b09f62e17952df6" + }, + { + "alg": "SHA-1", + "content": "7698af4c7e324d69d09d311d28699431a97f0ac2" + } + ], + "properties": [ + { + "name": "SrcFile", + "value": "/lib/libz.so.1.2.13" + }, + { + "name": "internal:is_shared_library", + "value": "true" + } + ], + "evidence": { + "identity": [ + { + "field": "purl", + "confidence": 0, + "methods": [ + { + "technique": "filename", + "confidence": 0, + "value": "/lib/libz.so.1.2.13" + } + ], + "concludedValue": "/lib/libz.so.1.2.13" + } + ] + } + } + ], + "services": [], + "dependencies": [ + { + "ref": "pkg:oci/alpine@sha256:8914eb54f968791faf6a8638949e480fef81e697984fba772b3976835194c6d4", + "dependsOn": [] + } + ], + "annotations": [ + { + "bom-ref": "metadata-annotations", + "subjects": [ + "pkg:oci/alpine@sha256:8914eb54f968791faf6a8638949e480fef81e697984fba772b3976835194c6d4" + ], + "annotator": { + "component": { + "group": "@cyclonedx", + "name": "cdxgen", + "version": "11.6.0", + "purl": "pkg:npm/%40cyclonedx/cdxgen@11.6.0", + "type": "application", + "bom-ref": "pkg:npm/@cyclonedx/cdxgen@11.6.0", + "publisher": "OWASP Foundation", + "authors": [ + { + "name": "OWASP Foundation" + } + ] + } + }, + "timestamp": "2025-08-25T06:39:25Z", + "text": "This Software Bill-of-Materials (SBOM) document was created on Monday, August 25, 2025 with cdxgen. The lifecycles phases represented are: pre-build and post-build. The document describes a container named 'alpine' with version '3.17.0'. There are 14 components. The linux image uses the amd64 architecture and has the registry tag alpine:3.17.0." + } + ] +} \ No newline at end of file diff --git a/scanpipe/tests/test_sca_integrations.py b/scanpipe/tests/test_sca_integrations.py index 26a3114528..1d7a0c9edf 100644 --- a/scanpipe/tests/test_sca_integrations.py +++ b/scanpipe/tests/test_sca_integrations.py @@ -73,3 +73,29 @@ def test_scanpipe_scan_integrations_load_sbom_anchore(self): self.assertEqual(94, project1.discoveredpackages.count()) self.assertEqual(7, project1.discoveredpackages.vulnerable().count()) self.assertEqual(20, project1.discovereddependencies.count()) + + def test_scanpipe_scan_integrations_load_sbom_cdxgen(self): + # Input file generated with: + # $ cdxgen alpine:3.17.0 \ + # --type docker \ + # --output cdxgen-alpine-3.17-sbom.json \ + # --spec-version 1.6 \ + # --json-pretty + input_location = ( + self.data / "sca-integrations" / "cdxgen-alpine-3.17-sbom.json" + ) + + pipeline_name = "load_sbom" + project1 = make_project() + project1.copy_input_from(input_location) + + run = project1.add_pipeline(pipeline_name) + pipeline = run.make_pipeline_instance() + + exitcode, out = pipeline.execute() + self.assertEqual(0, exitcode, msg=out) + + self.assertEqual(1, project1.codebaseresources.count()) + self.assertEqual(14, project1.discoveredpackages.count()) + self.assertEqual(0, project1.discoveredpackages.vulnerable().count()) + self.assertEqual(0, project1.discovereddependencies.count()) From ab8cf9f353b14137f84288868fc113ed014147cc Mon Sep 17 00:00:00 2001 From: tdruez Date: Mon, 25 Aug 2025 11:10:29 +0400 Subject: [PATCH 3/3] Add entry in the FAQ about SCA tools support Signed-off-by: tdruez --- docs/faq.rst | 17 +++++++++++++++++ scanpipe/tests/test_sca_integrations.py | 11 +++-------- 2 files changed, 20 insertions(+), 8 deletions(-) diff --git a/docs/faq.rst b/docs/faq.rst index 3a5530ad8b..91ab03d9e5 100644 --- a/docs/faq.rst +++ b/docs/faq.rst @@ -363,3 +363,20 @@ The input to ScanCode is a local saved image: Docker or OCI. Docker in Docker support will demand to have access to the saved images (either extracted from the Docker images in Docker, or mounted in a volume or saved from the Docker in the Docker image). Once saved we can analyze these alright. + +Can I import SBOM from other SCA tools? +--------------------------------------- + +Yes! You can load SBOMs generated by other tools for further review and run +pipelines to enrich or validate the data directly in ScanCode.io. + +While most valid SBOMs should work out of the box, SBOMs from the following tools +are actively supported and tested:: + + - Anchore: https://anchore.com/sbom/ + - CycloneDX cdxgen: https://cyclonedx.github.io/cdxgen/ + - Trivy: https://trivy.dev/latest/ + +.. note:: Imported SBOMs must follow the SPDX or CycloneDX standards, in JSON format. + You can use the ``load-sbom`` pipeline to process and enhance these SBOMs in your + ScanCode.io projects. diff --git a/scanpipe/tests/test_sca_integrations.py b/scanpipe/tests/test_sca_integrations.py index 1d7a0c9edf..991132ec6b 100644 --- a/scanpipe/tests/test_sca_integrations.py +++ b/scanpipe/tests/test_sca_integrations.py @@ -76,14 +76,9 @@ def test_scanpipe_scan_integrations_load_sbom_anchore(self): def test_scanpipe_scan_integrations_load_sbom_cdxgen(self): # Input file generated with: - # $ cdxgen alpine:3.17.0 \ - # --type docker \ - # --output cdxgen-alpine-3.17-sbom.json \ - # --spec-version 1.6 \ - # --json-pretty - input_location = ( - self.data / "sca-integrations" / "cdxgen-alpine-3.17-sbom.json" - ) + # $ cdxgen alpine:3.17.0 --type docker --spec-version 1.6 --json-pretty \ + # --output cdxgen-alpine-3.17-sbom.json + input_location = self.data / "sca-integrations" / "cdxgen-alpine-3.17-sbom.json" pipeline_name = "load_sbom" project1 = make_project()