Skip to content

Commit 8706897

Browse files
committed
Merge branch 'main' into 1730-sca-integrations-osv
2 parents 6726ff5 + b571c17 commit 8706897

23 files changed

+5851
-46
lines changed
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
name: Generate SBOM with CycloneDX cdxgen and load into ScanCode.io
2+
3+
# This workflow:
4+
# 1. Generates a CycloneDX SBOM for a container image using CycloneDX cdxgen.
5+
# 2. Uploads the SBOM as a GitHub artifact for future inspection.
6+
# 3. Loads the SBOM into ScanCode.io for further analysis.
7+
# 4. Runs assertions to verify that the SBOM was properly processed in ScanCode.io.
8+
#
9+
# It runs on demand, and once a week (scheduled).
10+
11+
on:
12+
workflow_dispatch:
13+
schedule:
14+
# Run once a week (every 7 days) at 00:00 UTC on Sunday
15+
- cron: "0 0 * * 0"
16+
17+
permissions:
18+
contents: read
19+
20+
env:
21+
IMAGE_REFERENCE: "python:3.13.0-slim"
22+
23+
jobs:
24+
generate-and-load-sbom:
25+
runs-on: ubuntu-24.04
26+
steps:
27+
- name: Install CycloneDX cdxgen
28+
run: npm install @cyclonedx/cdxgen
29+
30+
- name: Generate SBOM with CycloneDX cdxgen
31+
run: |
32+
npx cdxgen ${{ env.IMAGE_REFERENCE }} \
33+
--type docker \
34+
--output cdxgen-sbom.cdx.json \
35+
--spec-version 1.6 \
36+
--json-pretty
37+
38+
- name: Upload SBOM as GitHub Artifact
39+
uses: actions/upload-artifact@v4
40+
with:
41+
name: cdxgen-sbom
42+
path: "cdxgen-sbom.cdx.json"
43+
retention-days: 20
44+
45+
- name: Import SBOM into ScanCode.io
46+
uses: aboutcode-org/scancode-action@main
47+
with:
48+
pipelines: "load_sbom"
49+
inputs-path: "cdxgen-sbom.cdx.json"
50+
51+
- name: Verify SBOM Analysis Results in ScanCode.io
52+
shell: bash
53+
run: |
54+
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"
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
name: Generate SBOM with OWASP dep-scan and load into ScanCode.io
2+
3+
# This workflow:
4+
# 1. Generates a CycloneDX SBOM for a container image using OWASP dep-scan.
5+
# 2. Uploads the SBOM as a GitHub artifact for future inspection.
6+
# 3. Loads the SBOM into ScanCode.io for further analysis.
7+
# 4. Runs assertions to verify that the SBOM was properly processed in ScanCode.io.
8+
#
9+
# It runs on demand, and once a week (scheduled).
10+
11+
on:
12+
workflow_dispatch:
13+
schedule:
14+
# Run once a week (every 7 days) at 00:00 UTC on Sunday
15+
- cron: "0 0 * * 0"
16+
17+
permissions:
18+
contents: read
19+
20+
env:
21+
IMAGE_REFERENCE: "python:3.13.0-slim"
22+
23+
jobs:
24+
generate-and-load-sbom:
25+
runs-on: ubuntu-24.04
26+
steps:
27+
- name: Install OWASP dep-scan
28+
run: |
29+
sudo npm install -g @cyclonedx/cdxgen
30+
pip install owasp-depscan
31+
32+
- name: Generate SBOM with OWASP dep-scan
33+
run: |
34+
depscan \
35+
--src ${{ env.IMAGE_REFERENCE }} \
36+
--type docker \
37+
--reports-dir reports \
38+
--explain
39+
40+
- name: Upload SBOM as GitHub Artifact
41+
uses: actions/upload-artifact@v4
42+
with:
43+
name: depscan-sbom
44+
path: reports/
45+
retention-days: 20
46+
47+
- name: Uninstall dep-scan to avoid conflicts in the Python env
48+
run: pip uninstall --yes owasp-depscan
49+
50+
- name: Import SBOM into ScanCode.io
51+
uses: aboutcode-org/scancode-action@main
52+
with:
53+
pipelines: "load_sbom"
54+
inputs-path: "reports/sbom-docker.vdr.json"
55+
56+
- name: Verify SBOM Analysis Results in ScanCode.io
57+
shell: bash
58+
run: |
59+
scanpipe shell --command "from scanpipe.models import DiscoveredPackage, DiscoveredDependency; package_manager = DiscoveredPackage.objects; assert package_manager.count() > 220; assert package_manager.vulnerable().count() > 10; assert DiscoveredDependency.objects.count() > 150"
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
name: Generate SBOM with SBOM tool and load into ScanCode.io
2+
3+
# This workflow:
4+
# 1. Generates a CycloneDX SBOM for a container image using SBOM tool.
5+
# 2. Uploads the SBOM as a GitHub artifact for future inspection.
6+
# 3. Loads the SBOM into ScanCode.io for further analysis.
7+
# 4. Runs assertions to verify that the SBOM was properly processed in ScanCode.io.
8+
#
9+
# It runs on demand, and once a week (scheduled).
10+
11+
on:
12+
workflow_dispatch:
13+
schedule:
14+
# Run once a week (every 7 days) at 00:00 UTC on Sunday
15+
- cron: "0 0 * * 0"
16+
17+
permissions:
18+
contents: read
19+
20+
env:
21+
IMAGE_REFERENCE: "python:3.13.0-slim"
22+
23+
jobs:
24+
generate-and-load-sbom:
25+
runs-on: ubuntu-24.04
26+
steps:
27+
- name: Download SBOM tool
28+
run: |
29+
curl -Lo $RUNNER_TEMP/sbom-tool https://github.com/microsoft/sbom-tool/releases/latest/download/sbom-tool-linux-x64
30+
chmod +x $RUNNER_TEMP/sbom-tool
31+
32+
- name: Generate SBOM with SBOM tool
33+
run: |
34+
mkdir -p sbom-output
35+
$RUNNER_TEMP/sbom-tool generate \
36+
-di ${{ env.IMAGE_REFERENCE }} \
37+
-pn DockerImage \
38+
-pv 1.0.0 \
39+
-ps Company \
40+
-nsb https://sbom.company.com \
41+
-m sbom-output \
42+
-V Verbose
43+
44+
- name: Upload SBOM artifact
45+
uses: actions/upload-artifact@v4
46+
with:
47+
name: sbom-output
48+
path: sbom-output
49+
50+
- name: Import SBOM into ScanCode.io
51+
uses: aboutcode-org/scancode-action@main
52+
with:
53+
pipelines: "load_sbom"
54+
inputs-path: "sbom-output/_manifest/spdx_2.2/manifest.spdx.json"
55+
scancodeio-repo-branch: "main"
56+
57+
- name: Verify SBOM Analysis Results in ScanCode.io
58+
shell: bash
59+
run: |
60+
scanpipe shell --command "from scanpipe.models import DiscoveredPackage, DiscoveredDependency; package_manager = DiscoveredPackage.objects; assert package_manager.count() > 90; assert package_manager.vulnerable().count() == 0; assert DiscoveredDependency.objects.count() > 90"

CHANGELOG.rst

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,12 @@
11
Changelog
22
=========
33

4+
v35.4.0 (unreleased)
5+
--------------------
6+
7+
- Resolve and load dependencies from SPDX SBOMs.
8+
https://github.com/aboutcode-org/scancode.io/issues/1145
9+
410
v35.3.0 (2025-08-20)
511
--------------------
612

docs/faq.rst

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -363,3 +363,21 @@ The input to ScanCode is a local saved image: Docker or OCI.
363363
Docker in Docker support will demand to have access to the saved images
364364
(either extracted from the Docker images in Docker, or mounted in a volume or saved
365365
from the Docker in the Docker image). Once saved we can analyze these alright.
366+
367+
Can I import SBOM from other SCA tools?
368+
---------------------------------------
369+
370+
Yes! You can load SBOMs generated by other tools for further review and run
371+
pipelines to enrich or validate the data directly in ScanCode.io.
372+
373+
While most valid SBOMs should work out of the box, SBOMs from the following tools
374+
are actively supported and tested::
375+
376+
- Anchore: https://anchore.com/sbom/
377+
- CycloneDX cdxgen: https://cyclonedx.github.io/cdxgen/
378+
- OWASP dep-scan: https://owasp.org/www-project-dep-scan/
379+
- Trivy: https://trivy.dev/latest/
380+
381+
.. note:: Imported SBOMs must follow the SPDX or CycloneDX standards, in JSON format.
382+
You can use the ``load-sbom`` pipeline to process and enhance these SBOMs in your
383+
ScanCode.io projects.

scanpipe/pipelines/load_sbom.py

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
# ScanCode.io is a free software code scanning tool from nexB Inc. and others.
2121
# Visit https://github.com/aboutcode-org/scancode.io for support and download.
2222

23+
from scanpipe.models import DiscoveredDependency
2324
from scanpipe.pipelines.scan_codebase import ScanCodebase
2425
from scanpipe.pipes import resolve
2526

@@ -44,7 +45,7 @@ def steps(cls):
4445
cls.flag_empty_files,
4546
cls.flag_ignored_resources,
4647
cls.get_sbom_inputs,
47-
cls.get_packages_from_sboms,
48+
cls.get_data_from_sboms,
4849
cls.create_packages_from_sboms,
4950
cls.create_dependencies_from_sboms,
5051
)
@@ -53,13 +54,13 @@ def get_sbom_inputs(self):
5354
"""Locate all the SBOMs among the codebase resources."""
5455
self.manifest_resources = resolve.get_manifest_resources(self.project)
5556

56-
def get_packages_from_sboms(self):
57-
"""Get packages data from SBOMs."""
58-
self.packages = resolve.get_packages(
57+
def get_data_from_sboms(self):
58+
"""Get data from SBOMs."""
59+
self.packages, self.dependencies = resolve.get_data_from_manifests(
5960
project=self.project,
6061
package_registry=resolve.sbom_registry,
6162
manifest_resources=self.manifest_resources,
62-
model="get_packages_from_sboms",
63+
model="get_data_from_sboms",
6364
)
6465

6566
def create_packages_from_sboms(self):
@@ -71,4 +72,12 @@ def create_packages_from_sboms(self):
7172

7273
def create_dependencies_from_sboms(self):
7374
"""Create the dependency relationship declared in the SBOMs."""
75+
# CycloneDX support: the dependency data is stored in ``extra_data``.
7476
resolve.create_dependencies_from_packages_extra_data(project=self.project)
77+
78+
# SPDX support: the dependency data is loaded from ``self.dependencies``.
79+
for dependency_data in self.dependencies:
80+
DiscoveredDependency.create_from_data(
81+
project=self.project,
82+
dependency_data=dependency_data,
83+
)

scanpipe/pipelines/resolve_dependencies.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ def get_packages_from_manifest(self):
8484
Resolve package data from lockfiles/requirement files with package
8585
requirements/dependencies.
8686
"""
87-
self.resolved_packages = resolve.get_packages(
87+
self.packages, self.dependencies = resolve.get_data_from_manifests(
8888
project=self.project,
8989
package_registry=resolve.resolver_registry,
9090
manifest_resources=self.manifest_resources,
@@ -99,6 +99,6 @@ def create_resolved_packages(self):
9999
"""
100100
resolve.create_packages_and_dependencies(
101101
project=self.project,
102-
packages=self.resolved_packages,
102+
packages=self.packages,
103103
resolved=True,
104104
)

scanpipe/pipes/cyclonedx.py

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,9 @@ def get_external_references(component):
7979

8080
references = defaultdict(list)
8181
for reference in external_references:
82-
references[reference.type.value].append(reference.url.uri)
82+
reference_url = reference.url
83+
if reference_url and reference_url.uri:
84+
references[reference.type.value].append(reference_url.uri)
8385

8486
return dict(references)
8587

@@ -158,12 +160,9 @@ def cyclonedx_component_to_package_data(
158160
vulnerabilities = vulnerabilities or {}
159161
extra_data = {}
160162

161-
# Store the original bom_ref and dependencies for future processing.
162163
bom_ref = str(cdx_component.bom_ref)
163-
if bom_ref:
164-
extra_data["bom_ref"] = bom_ref
165-
if depends_on := dependencies.get(bom_ref):
166-
extra_data["depends_on"] = depends_on
164+
if depends_on := dependencies.get(bom_ref):
165+
extra_data["depends_on"] = depends_on
167166

168167
package_url_dict = {}
169168
if cdx_component.purl:
@@ -189,6 +188,8 @@ def cyclonedx_component_to_package_data(
189188
)
190189

191190
package_data = {
191+
# Store the original "bom_ref" as package_uid for dependencies resolution.
192+
"package_uid": bom_ref,
192193
"name": cdx_component.name,
193194
"extracted_license_statement": declared_license,
194195
"copyright": cdx_component.copyright,

scanpipe/pipes/output.py

Lines changed: 30 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -671,6 +671,28 @@ def _get_spdx_extracted_licenses(license_expressions):
671671
return extracted_licenses
672672

673673

674+
def get_dependency_as_spdx_relationship(dependency, document_spdx_id, packages_as_spdx):
675+
"""Return a spdx.Relationship crafted from the provided ``dependency`` instance."""
676+
if dependency.for_package: # Package dependency
677+
parent_id = dependency.for_package.spdx_id
678+
else: # Project dependency
679+
parent_id = document_spdx_id
680+
681+
if dependency.is_resolved_to_package: # Resolved to a Package
682+
child_id = dependency.resolved_to_package.spdx_id
683+
else: # Not resolved to a Package (only package_url value is available)
684+
dependency_as_package = dependency.as_spdx_package()
685+
packages_as_spdx.append(dependency_as_package)
686+
child_id = dependency_as_package.spdx_id
687+
688+
spdx_relationship = spdx.Relationship(
689+
spdx_id=child_id,
690+
related_spdx_id=parent_id,
691+
relationship="DEPENDENCY_OF",
692+
)
693+
return spdx_relationship
694+
695+
674696
def to_spdx(project, include_files=False):
675697
"""
676698
Generate output for the provided ``project`` in SPDX document format.
@@ -682,6 +704,7 @@ def to_spdx(project, include_files=False):
682704
discoveredpackage_qs = get_queryset(project, "discoveredpackage")
683705
discovereddependency_qs = get_queryset(project, "discovereddependency")
684706

707+
document_spdx_id = f"SPDXRef-DOCUMENT-{project.uuid}"
685708
packages_as_spdx = []
686709
license_expressions = []
687710
relationships = []
@@ -692,15 +715,12 @@ def to_spdx(project, include_files=False):
692715
license_expressions.append(license_expression)
693716

694717
for dependency in discovereddependency_qs:
695-
packages_as_spdx.append(dependency.as_spdx_package())
696-
if dependency.for_package:
697-
relationships.append(
698-
spdx.Relationship(
699-
spdx_id=dependency.spdx_id,
700-
related_spdx_id=dependency.for_package.spdx_id,
701-
relationship="DEPENDENCY_OF",
702-
)
703-
)
718+
spdx_relationship = get_dependency_as_spdx_relationship(
719+
dependency,
720+
document_spdx_id,
721+
packages_as_spdx,
722+
)
723+
relationships.append(spdx_relationship)
704724

705725
files_as_spdx = []
706726
if include_files:
@@ -710,6 +730,7 @@ def to_spdx(project, include_files=False):
710730
]
711731

712732
document = spdx.Document(
733+
spdx_id=document_spdx_id,
713734
name=f"scancodeio_{project.name}",
714735
namespace=f"https://scancode.io/spdxdocs/{project.uuid}",
715736
creation_info=spdx.CreationInfo(tool=f"ScanCode.io-{scancodeio_version}"),

0 commit comments

Comments
 (0)