mirror of
https://github.com/PX4/PX4-Autopilot.git
synced 2026-05-27 10:17:45 +08:00
feat(build): add SPDX 2.3 SBOM generation for builds (#26731)
This commit is contained in:
@@ -268,4 +268,5 @@ jobs:
|
|||||||
files: |
|
files: |
|
||||||
artifacts/*.px4
|
artifacts/*.px4
|
||||||
artifacts/*.deb
|
artifacts/*.deb
|
||||||
|
artifacts/**/*.sbom.spdx.json
|
||||||
name: ${{ steps.upload-location.outputs.uploadlocation }}
|
name: ${{ steps.upload-location.outputs.uploadlocation }}
|
||||||
|
|||||||
@@ -46,6 +46,8 @@ jobs:
|
|||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
- name: Building [${{ matrix.check }}]
|
- name: Building [${{ matrix.check }}]
|
||||||
|
env:
|
||||||
|
PX4_SBOM_DISABLE: 1
|
||||||
run: |
|
run: |
|
||||||
cd "$GITHUB_WORKSPACE"
|
cd "$GITHUB_WORKSPACE"
|
||||||
git config --global --add safe.directory "$GITHUB_WORKSPACE"
|
git config --global --add safe.directory "$GITHUB_WORKSPACE"
|
||||||
|
|||||||
@@ -36,8 +36,8 @@ jobs:
|
|||||||
px4io/px4-dev-ros-melodic:2021-09-08 \
|
px4io/px4-dev-ros-melodic:2021-09-08 \
|
||||||
bash -c '
|
bash -c '
|
||||||
git config --global --add safe.directory /workspace
|
git config --global --add safe.directory /workspace
|
||||||
make px4_sitl_default
|
PX4_SBOM_DISABLE=1 make px4_sitl_default
|
||||||
make px4_sitl_default sitl_gazebo-classic
|
PX4_SBOM_DISABLE=1 make px4_sitl_default sitl_gazebo-classic
|
||||||
./test/rostest_px4_run.sh \
|
./test/rostest_px4_run.sh \
|
||||||
mavros_posix_test_mission.test \
|
mavros_posix_test_mission.test \
|
||||||
mission:=MC_mission_box \
|
mission:=MC_mission_box \
|
||||||
|
|||||||
@@ -36,8 +36,8 @@ jobs:
|
|||||||
px4io/px4-dev-ros-melodic:2021-09-08 \
|
px4io/px4-dev-ros-melodic:2021-09-08 \
|
||||||
bash -c '
|
bash -c '
|
||||||
git config --global --add safe.directory /workspace
|
git config --global --add safe.directory /workspace
|
||||||
make px4_sitl_default
|
PX4_SBOM_DISABLE=1 make px4_sitl_default
|
||||||
make px4_sitl_default sitl_gazebo-classic
|
PX4_SBOM_DISABLE=1 make px4_sitl_default sitl_gazebo-classic
|
||||||
./test/rostest_px4_run.sh \
|
./test/rostest_px4_run.sh \
|
||||||
mavros_posix_tests_offboard_posctl.test \
|
mavros_posix_tests_offboard_posctl.test \
|
||||||
vehicle:=iris
|
vehicle:=iris
|
||||||
|
|||||||
@@ -110,6 +110,8 @@ jobs:
|
|||||||
run: ccache -s
|
run: ccache -s
|
||||||
|
|
||||||
- name: Build PX4
|
- name: Build PX4
|
||||||
|
env:
|
||||||
|
PX4_SBOM_DISABLE: 1
|
||||||
run: make px4_sitl_default
|
run: make px4_sitl_default
|
||||||
- name: ccache post-run px4/firmware
|
- name: ccache post-run px4/firmware
|
||||||
run: ccache -s
|
run: ccache -s
|
||||||
|
|||||||
@@ -0,0 +1,42 @@
|
|||||||
|
name: SBOM License Check
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- 'main'
|
||||||
|
- 'release/**'
|
||||||
|
- 'stable'
|
||||||
|
paths:
|
||||||
|
- '.gitmodules'
|
||||||
|
- 'Tools/ci/license-overrides.yaml'
|
||||||
|
- 'Tools/ci/generate_sbom.py'
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- '**'
|
||||||
|
paths:
|
||||||
|
- '.gitmodules'
|
||||||
|
- 'Tools/ci/license-overrides.yaml'
|
||||||
|
- 'Tools/ci/generate_sbom.py'
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: ${{ github.workflow }}-${{ github.ref }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
verify-licenses:
|
||||||
|
runs-on: ubuntu-24.04
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 1
|
||||||
|
submodules: false
|
||||||
|
|
||||||
|
- name: Install PyYAML
|
||||||
|
run: pip install pyyaml --break-system-packages
|
||||||
|
|
||||||
|
- name: Verify submodule licenses
|
||||||
|
run: python3 Tools/ci/generate_sbom.py --verify-licenses --source-dir .
|
||||||
@@ -0,0 +1,132 @@
|
|||||||
|
name: SBOM Monthly Audit
|
||||||
|
|
||||||
|
on:
|
||||||
|
schedule:
|
||||||
|
# First Monday of each month at 09:00 UTC
|
||||||
|
- cron: '0 9 1-7 * 1'
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
branch:
|
||||||
|
description: 'Branch to audit (leave empty for current)'
|
||||||
|
required: false
|
||||||
|
type: string
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
issues: write
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
audit:
|
||||||
|
runs-on: ubuntu-24.04
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
ref: ${{ inputs.branch || github.ref }}
|
||||||
|
fetch-depth: 1
|
||||||
|
submodules: recursive
|
||||||
|
|
||||||
|
- name: Install PyYAML
|
||||||
|
run: pip install pyyaml --break-system-packages
|
||||||
|
|
||||||
|
- name: Run license verification
|
||||||
|
id: verify
|
||||||
|
continue-on-error: true
|
||||||
|
run: |
|
||||||
|
python3 Tools/ci/generate_sbom.py --verify-licenses --source-dir . 2>&1 | tee /tmp/sbom-verify.txt
|
||||||
|
echo "exit_code=$?" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
|
- name: Check for issues
|
||||||
|
id: check
|
||||||
|
run: |
|
||||||
|
if grep -q "NOASSERTION" /tmp/sbom-verify.txt; then
|
||||||
|
echo "has_issues=true" >> "$GITHUB_OUTPUT"
|
||||||
|
# Extract NOASSERTION lines
|
||||||
|
grep "NOASSERTION" /tmp/sbom-verify.txt | grep -v "skipped" > /tmp/sbom-issues.txt || true
|
||||||
|
# Extract copyleft lines
|
||||||
|
sed -n '/Copyleft licenses detected/,/^$/p' /tmp/sbom-verify.txt > /tmp/sbom-copyleft.txt || true
|
||||||
|
else
|
||||||
|
echo "has_issues=false" >> "$GITHUB_OUTPUT"
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Create issue if problems found
|
||||||
|
if: steps.check.outputs.has_issues == 'true'
|
||||||
|
uses: actions/github-script@v7
|
||||||
|
with:
|
||||||
|
script: |
|
||||||
|
const fs = require('fs');
|
||||||
|
|
||||||
|
const fullOutput = fs.readFileSync('/tmp/sbom-verify.txt', 'utf8');
|
||||||
|
let issueLines = '';
|
||||||
|
try {
|
||||||
|
issueLines = fs.readFileSync('/tmp/sbom-issues.txt', 'utf8');
|
||||||
|
} catch (e) {
|
||||||
|
issueLines = 'No specific NOASSERTION lines captured.';
|
||||||
|
}
|
||||||
|
let copyleftLines = '';
|
||||||
|
try {
|
||||||
|
copyleftLines = fs.readFileSync('/tmp/sbom-copyleft.txt', 'utf8');
|
||||||
|
} catch (e) {
|
||||||
|
copyleftLines = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
const date = new Date().toISOString().split('T')[0];
|
||||||
|
const branch = '${{ inputs.branch || github.ref_name }}';
|
||||||
|
|
||||||
|
// Check for existing open issue to avoid duplicates
|
||||||
|
const existing = await github.rest.issues.listForRepo({
|
||||||
|
owner: context.repo.owner,
|
||||||
|
repo: context.repo.repo,
|
||||||
|
labels: 'sbom-audit',
|
||||||
|
state: 'open',
|
||||||
|
});
|
||||||
|
|
||||||
|
if (existing.data.length > 0) {
|
||||||
|
// Update existing issue with new findings
|
||||||
|
await github.rest.issues.createComment({
|
||||||
|
owner: context.repo.owner,
|
||||||
|
repo: context.repo.repo,
|
||||||
|
issue_number: existing.data[0].number,
|
||||||
|
body: `## Monthly audit update (${date})\n\nIssues still present:\n\n\`\`\`\n${issueLines}\n\`\`\`\n${copyleftLines ? `\n### Copyleft warnings\n\`\`\`\n${copyleftLines}\n\`\`\`` : ''}`,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await github.rest.issues.create({
|
||||||
|
owner: context.repo.owner,
|
||||||
|
repo: context.repo.repo,
|
||||||
|
title: `chore(sbom): license audit found NOASSERTION entries on ${branch} (${date})`,
|
||||||
|
labels: ['sbom-audit'],
|
||||||
|
assignees: ['mrpollo'],
|
||||||
|
body: [
|
||||||
|
`## SBOM Monthly Audit -- ${branch} -- ${date}`,
|
||||||
|
'',
|
||||||
|
'The automated SBOM license audit found submodules with unresolved licenses.',
|
||||||
|
'',
|
||||||
|
'### NOASSERTION entries',
|
||||||
|
'',
|
||||||
|
'```',
|
||||||
|
issueLines,
|
||||||
|
'```',
|
||||||
|
'',
|
||||||
|
copyleftLines ? `### Copyleft warnings\n\n\`\`\`\n${copyleftLines}\n\`\`\`\n` : '',
|
||||||
|
'### How to fix',
|
||||||
|
'',
|
||||||
|
'1. Check the submodule repo for a LICENSE file',
|
||||||
|
'2. Add an override to `Tools/ci/license-overrides.yaml`',
|
||||||
|
'3. Run `python3 Tools/ci/generate_sbom.py --verify-licenses --source-dir .` to confirm',
|
||||||
|
'',
|
||||||
|
'### Full output',
|
||||||
|
'',
|
||||||
|
'<details>',
|
||||||
|
'<summary>Click to expand</summary>',
|
||||||
|
'',
|
||||||
|
'```',
|
||||||
|
fullOutput,
|
||||||
|
'```',
|
||||||
|
'',
|
||||||
|
'</details>',
|
||||||
|
'',
|
||||||
|
'cc @mrpollo',
|
||||||
|
].join('\n'),
|
||||||
|
});
|
||||||
@@ -71,6 +71,7 @@ jobs:
|
|||||||
- name: Build PX4
|
- name: Build PX4
|
||||||
env:
|
env:
|
||||||
PX4_CMAKE_BUILD_TYPE: ${{matrix.config.build_type}}
|
PX4_CMAKE_BUILD_TYPE: ${{matrix.config.build_type}}
|
||||||
|
PX4_SBOM_DISABLE: 1
|
||||||
run: make px4_sitl_default
|
run: make px4_sitl_default
|
||||||
|
|
||||||
- name: Cache Post-Run [px4_sitl_default]
|
- name: Cache Post-Run [px4_sitl_default]
|
||||||
|
|||||||
@@ -484,6 +484,7 @@ include(bloaty)
|
|||||||
|
|
||||||
include(metadata)
|
include(metadata)
|
||||||
include(package)
|
include(package)
|
||||||
|
include(sbom)
|
||||||
|
|
||||||
# install python requirements using configured python
|
# install python requirements using configured python
|
||||||
add_custom_target(install_python_requirements
|
add_custom_target(install_python_requirements
|
||||||
|
|||||||
Executable
+603
File diff suppressed because it is too large
Load Diff
Executable
+163
@@ -0,0 +1,163 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Inspect a PX4 SPDX SBOM file.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
inspect_sbom.py <sbom.spdx.json> # full summary
|
||||||
|
inspect_sbom.py <sbom.spdx.json> search <term> # search packages by name
|
||||||
|
inspect_sbom.py <sbom.spdx.json> ntia # NTIA minimum elements check
|
||||||
|
inspect_sbom.py <sbom.spdx.json> licenses # license summary
|
||||||
|
inspect_sbom.py <sbom.spdx.json> list <type> # list packages (Submodule|PyDep|Module|all)
|
||||||
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
import sys
|
||||||
|
from collections import Counter
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
def load(path):
|
||||||
|
return json.loads(Path(path).read_text())
|
||||||
|
|
||||||
|
|
||||||
|
def pkg_type(pkg):
|
||||||
|
spdx_id = pkg["SPDXID"]
|
||||||
|
for prefix in ("Submodule", "PyDep", "Module", "Compiler", "PX4"):
|
||||||
|
if f"-{prefix}-" in spdx_id or spdx_id.startswith(f"SPDXRef-{prefix}"):
|
||||||
|
return prefix
|
||||||
|
return "Other"
|
||||||
|
|
||||||
|
|
||||||
|
def summary(doc):
|
||||||
|
print(f"spdxVersion: {doc['spdxVersion']}")
|
||||||
|
print(f"name: {doc['name']}")
|
||||||
|
print(f"namespace: {doc['documentNamespace']}")
|
||||||
|
print(f"created: {doc['creationInfo']['created']}")
|
||||||
|
print(f"creators: {', '.join(doc['creationInfo']['creators'])}")
|
||||||
|
print()
|
||||||
|
|
||||||
|
types = Counter(pkg_type(p) for p in doc["packages"])
|
||||||
|
print(f"Packages: {len(doc['packages'])}")
|
||||||
|
for t, c in types.most_common():
|
||||||
|
print(f" {t}: {c}")
|
||||||
|
print()
|
||||||
|
|
||||||
|
rc = Counter(r["relationshipType"] for r in doc["relationships"])
|
||||||
|
print(f"Relationships: {len(doc['relationships'])}")
|
||||||
|
for t, n in rc.most_common():
|
||||||
|
print(f" {t}: {n}")
|
||||||
|
print()
|
||||||
|
|
||||||
|
primary = doc["packages"][0]
|
||||||
|
print(f"Primary package:")
|
||||||
|
print(f" name: {primary['name']}")
|
||||||
|
print(f" version: {primary['versionInfo']}")
|
||||||
|
print(f" purpose: {primary.get('primaryPackagePurpose', 'N/A')}")
|
||||||
|
print(f" license: {primary['licenseDeclared']}")
|
||||||
|
print()
|
||||||
|
|
||||||
|
noassert = [
|
||||||
|
p["name"]
|
||||||
|
for p in doc["packages"]
|
||||||
|
if pkg_type(p) == "Submodule" and p["licenseDeclared"] == "NOASSERTION"
|
||||||
|
]
|
||||||
|
if noassert:
|
||||||
|
print(f"WARNING: {len(noassert)} submodules with NOASSERTION license:")
|
||||||
|
for n in noassert:
|
||||||
|
print(f" - {n}")
|
||||||
|
else:
|
||||||
|
print("All submodule licenses mapped")
|
||||||
|
|
||||||
|
print(f"\nFile size: {Path(sys.argv[1]).stat().st_size // 1024}KB")
|
||||||
|
|
||||||
|
|
||||||
|
def search(doc, term):
|
||||||
|
term = term.lower()
|
||||||
|
found = [p for p in doc["packages"] if term in p["name"].lower()]
|
||||||
|
if not found:
|
||||||
|
print(f"No packages matching '{term}'")
|
||||||
|
return
|
||||||
|
print(f"Found {len(found)} packages matching '{term}':\n")
|
||||||
|
for p in found:
|
||||||
|
print(json.dumps(p, indent=2))
|
||||||
|
print()
|
||||||
|
|
||||||
|
|
||||||
|
def ntia_check(doc):
|
||||||
|
required = ["SPDXID", "name", "versionInfo", "supplier", "downloadLocation"]
|
||||||
|
missing = []
|
||||||
|
for p in doc["packages"]:
|
||||||
|
for f in required:
|
||||||
|
if f not in p or p[f] in ("", None):
|
||||||
|
missing.append((p["name"], f))
|
||||||
|
|
||||||
|
if missing:
|
||||||
|
print(f"FAIL: {len(missing)} missing fields:")
|
||||||
|
for name, field in missing:
|
||||||
|
print(f" {name}: missing {field}")
|
||||||
|
else:
|
||||||
|
print(f"PASS: All {len(doc['packages'])} packages have required fields")
|
||||||
|
|
||||||
|
print(f"\nCreators: {doc['creationInfo']['creators']}")
|
||||||
|
print(f"Timestamp: {doc['creationInfo']['created']}")
|
||||||
|
|
||||||
|
rels = [r for r in doc["relationships"] if r["relationshipType"] == "DESCRIBES"]
|
||||||
|
print(f"DESCRIBES relationships: {len(rels)}")
|
||||||
|
|
||||||
|
return len(missing) == 0
|
||||||
|
|
||||||
|
|
||||||
|
def licenses(doc):
|
||||||
|
by_license = {}
|
||||||
|
for p in doc["packages"]:
|
||||||
|
lic = p.get("licenseDeclared", "NOASSERTION")
|
||||||
|
by_license.setdefault(lic, []).append(p["name"])
|
||||||
|
|
||||||
|
for lic in sorted(by_license.keys()):
|
||||||
|
names = by_license[lic]
|
||||||
|
print(f"\n{lic} ({len(names)}):")
|
||||||
|
for n in sorted(names):
|
||||||
|
print(f" {n}")
|
||||||
|
|
||||||
|
|
||||||
|
def list_packages(doc, filter_type):
|
||||||
|
filter_type = filter_type.lower()
|
||||||
|
for p in sorted(doc["packages"], key=lambda x: x["name"]):
|
||||||
|
t = pkg_type(p)
|
||||||
|
if filter_type != "all" and t.lower() != filter_type:
|
||||||
|
continue
|
||||||
|
lic = p.get("licenseDeclared", "?")
|
||||||
|
ver = p["versionInfo"][:20] if len(p["versionInfo"]) > 20 else p["versionInfo"]
|
||||||
|
print(f" {t:10s} {p['name']:50s} {ver:20s} {lic}")
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
if len(sys.argv) < 2:
|
||||||
|
print(__doc__)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
doc = load(sys.argv[1])
|
||||||
|
cmd = sys.argv[2] if len(sys.argv) > 2 else "summary"
|
||||||
|
|
||||||
|
if cmd == "summary":
|
||||||
|
summary(doc)
|
||||||
|
elif cmd == "search":
|
||||||
|
if len(sys.argv) < 4:
|
||||||
|
print("Usage: inspect_sbom.py <file> search <term>")
|
||||||
|
sys.exit(1)
|
||||||
|
search(doc, sys.argv[3])
|
||||||
|
elif cmd == "ntia":
|
||||||
|
if not ntia_check(doc):
|
||||||
|
sys.exit(1)
|
||||||
|
elif cmd == "licenses":
|
||||||
|
licenses(doc)
|
||||||
|
elif cmd == "list":
|
||||||
|
filter_type = sys.argv[3] if len(sys.argv) > 3 else "all"
|
||||||
|
list_packages(doc, filter_type)
|
||||||
|
else:
|
||||||
|
print(f"Unknown command: {cmd}")
|
||||||
|
print(__doc__)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
# SPDX license overrides for submodules where auto-detection fails or is wrong.
|
||||||
|
# Each entry maps a submodule path to its SPDX license identifier and an
|
||||||
|
# optional comment explaining why the override exists.
|
||||||
|
#
|
||||||
|
# Run `python3 Tools/ci/generate_sbom.py --verify-licenses` to validate.
|
||||||
|
|
||||||
|
overrides:
|
||||||
|
src/modules/mavlink/mavlink:
|
||||||
|
license: "LGPL-3.0-only AND MIT"
|
||||||
|
comment: "Generator is LGPL-3.0; PX4 ships only MIT-licensed generated headers."
|
||||||
|
|
||||||
|
src/lib/cdrstream/cyclonedds:
|
||||||
|
license: "EPL-2.0 OR BSD-3-Clause"
|
||||||
|
comment: >-
|
||||||
|
Dual-licensed. PX4 elects BSD-3-Clause.
|
||||||
|
No board currently enables CONFIG_LIB_CDRSTREAM.
|
||||||
|
|
||||||
|
src/lib/cdrstream/rosidl:
|
||||||
|
license: "Apache-2.0"
|
||||||
|
|
||||||
|
src/lib/crypto/monocypher:
|
||||||
|
license: "BSD-2-Clause OR CC0-1.0"
|
||||||
|
comment: >-
|
||||||
|
Dual-licensed. LICENCE.md offers BSD-2-Clause with CC0-1.0 as
|
||||||
|
public domain fallback.
|
||||||
|
|
||||||
|
src/lib/crypto/libtomcrypt:
|
||||||
|
license: "Unlicense"
|
||||||
|
comment: "Public domain dedication. Functionally equivalent to Unlicense."
|
||||||
|
|
||||||
|
src/lib/crypto/libtommath:
|
||||||
|
license: "Unlicense"
|
||||||
|
comment: "Public domain dedication. Functionally equivalent to Unlicense."
|
||||||
|
|
||||||
|
platforms/nuttx/NuttX/nuttx:
|
||||||
|
license: "Apache-2.0"
|
||||||
|
comment: >-
|
||||||
|
Composite LICENSE (6652 lines) includes BSD/MIT/ISC sub-components.
|
||||||
|
Primary license is Apache-2.0. NOTICE file contains FAT LFN patent warnings.
|
||||||
|
|
||||||
|
platforms/nuttx/NuttX/apps:
|
||||||
|
license: "Apache-2.0"
|
||||||
|
|
||||||
|
boards/modalai/voxl2/libfc-sensor-api:
|
||||||
|
license: "NOASSERTION"
|
||||||
|
comment: >-
|
||||||
|
No LICENSE file in repo. README describes it as public interface
|
||||||
|
for proprietary sensor library.
|
||||||
|
|
||||||
|
boards/modalai/voxl2/src/lib/mpa/libmodal-json:
|
||||||
|
license: "LGPL-3.0-only"
|
||||||
|
comment: "LGPL-3.0 weak copyleft. Used via header includes in VOXL2 mpa library."
|
||||||
|
|
||||||
|
boards/modalai/voxl2/src/lib/mpa/libmodal-pipe:
|
||||||
|
license: "LGPL-3.0-only"
|
||||||
|
comment: "LGPL-3.0 weak copyleft. Used via header includes in VOXL2 mpa library."
|
||||||
@@ -29,6 +29,8 @@ for build_dir_path in build/*/ ; do
|
|||||||
# Events
|
# Events
|
||||||
mkdir -p artifacts/$build_dir/events/
|
mkdir -p artifacts/$build_dir/events/
|
||||||
cp $build_dir_path/events/all_events.json.xz artifacts/$build_dir/events/ 2>/dev/null || true
|
cp $build_dir_path/events/all_events.json.xz artifacts/$build_dir/events/ 2>/dev/null || true
|
||||||
|
# SBOM
|
||||||
|
cp $build_dir_path/*.sbom.spdx.json artifacts/$build_dir/ 2>/dev/null || true
|
||||||
ls -la artifacts/$build_dir
|
ls -la artifacts/$build_dir
|
||||||
echo "----------"
|
echo "----------"
|
||||||
done
|
done
|
||||||
|
|||||||
@@ -0,0 +1,72 @@
|
|||||||
|
############################################################################
|
||||||
|
#
|
||||||
|
# Copyright (c) 2026 PX4 Development Team. All rights reserved.
|
||||||
|
#
|
||||||
|
# Redistribution and use in source and binary forms, with or without
|
||||||
|
# modification, are permitted provided that the following conditions
|
||||||
|
# are met:
|
||||||
|
#
|
||||||
|
# 1. Redistributions of source code must retain the above copyright
|
||||||
|
# notice, this list of conditions and the following disclaimer.
|
||||||
|
# 2. Redistributions in binary form must reproduce the above copyright
|
||||||
|
# notice, this list of conditions and the following disclaimer in
|
||||||
|
# the documentation and/or other materials provided with the
|
||||||
|
# distribution.
|
||||||
|
# 3. Neither the name PX4 nor the names of its contributors may be
|
||||||
|
# used to endorse or promote products derived from this software
|
||||||
|
# without specific prior written permission.
|
||||||
|
#
|
||||||
|
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||||
|
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||||
|
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
|
||||||
|
# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
|
||||||
|
# COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
|
||||||
|
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
|
||||||
|
# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS
|
||||||
|
# OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
|
||||||
|
# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
|
||||||
|
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
|
||||||
|
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||||
|
# POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
#
|
||||||
|
############################################################################
|
||||||
|
|
||||||
|
# SBOM - SPDX 2.3 JSON Software Bill of Materials generation
|
||||||
|
|
||||||
|
option(GENERATE_SBOM "Generate SPDX 2.3 SBOM" ON)
|
||||||
|
|
||||||
|
if(DEFINED ENV{PX4_SBOM_DISABLE})
|
||||||
|
set(GENERATE_SBOM OFF)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
if(GENERATE_SBOM)
|
||||||
|
|
||||||
|
# Write board-specific module list for the SBOM generator
|
||||||
|
set(sbom_module_list_file "${PX4_BINARY_DIR}/config_module_list.txt")
|
||||||
|
get_property(module_list GLOBAL PROPERTY PX4_MODULE_PATHS)
|
||||||
|
string(REPLACE ";" "\n" module_list_content "${module_list}")
|
||||||
|
file(GENERATE OUTPUT ${sbom_module_list_file} CONTENT "${module_list_content}\n")
|
||||||
|
|
||||||
|
set(sbom_output "${PX4_BINARY_DIR}/${PX4_CONFIG}.sbom.spdx.json")
|
||||||
|
|
||||||
|
add_custom_command(
|
||||||
|
OUTPUT ${sbom_output}
|
||||||
|
COMMAND ${PYTHON_EXECUTABLE} ${PX4_SOURCE_DIR}/Tools/ci/generate_sbom.py
|
||||||
|
--source-dir ${PX4_SOURCE_DIR}
|
||||||
|
--board ${PX4_CONFIG}
|
||||||
|
--modules-file ${sbom_module_list_file}
|
||||||
|
--compiler ${CMAKE_C_COMPILER}
|
||||||
|
--platform ${PX4_PLATFORM}
|
||||||
|
--output ${sbom_output}
|
||||||
|
DEPENDS
|
||||||
|
${PX4_SOURCE_DIR}/Tools/ci/generate_sbom.py
|
||||||
|
${PX4_SOURCE_DIR}/Tools/ci/license-overrides.yaml
|
||||||
|
${PX4_SOURCE_DIR}/.gitmodules
|
||||||
|
${PX4_SOURCE_DIR}/Tools/setup/requirements.txt
|
||||||
|
${sbom_module_list_file}
|
||||||
|
COMMENT "Generating SPDX SBOM for ${PX4_CONFIG}"
|
||||||
|
)
|
||||||
|
|
||||||
|
add_custom_target(sbom ALL DEPENDS ${sbom_output})
|
||||||
|
|
||||||
|
endif()
|
||||||
@@ -906,6 +906,7 @@
|
|||||||
- [Translation](contribute/translation.md)
|
- [Translation](contribute/translation.md)
|
||||||
- [Terminology/Notation](contribute/notation.md)
|
- [Terminology/Notation](contribute/notation.md)
|
||||||
- [Licenses](contribute/licenses.md)
|
- [Licenses](contribute/licenses.md)
|
||||||
|
- [SBOM](contribute/sbom.md)
|
||||||
- [Releases](releases/index.md)
|
- [Releases](releases/index.md)
|
||||||
- [Release Process](releases/release_process.md)
|
- [Release Process](releases/release_process.md)
|
||||||
- [main (alpha)](releases/main.md)
|
- [main (alpha)](releases/main.md)
|
||||||
|
|||||||
@@ -0,0 +1,226 @@
|
|||||||
|
# Software Bill of Materials (SBOM)
|
||||||
|
|
||||||
|
PX4 generates a [Software Bill of Materials](https://ntia.gov/SBOM) for every firmware build in [SPDX 2.3](https://spdx.github.io/spdx-spec/v2.3/) JSON format.
|
||||||
|
|
||||||
|
## Why SBOM?
|
||||||
|
|
||||||
|
- **Regulatory compliance**: The EU Cyber Resilience Act (CRA) requires SBOMs for products with digital elements (reporting obligations begin in September 2026).
|
||||||
|
- **Supply chain transparency**: SBOMs enumerate every component compiled into firmware, enabling users and integrators to audit dependencies.
|
||||||
|
- **NTIA minimum elements**: Each SBOM satisfies all seven [NTIA required fields](https://www.ntia.gov/report/2021/minimum-elements-software-bill-materials-sbom): supplier, component name, version, unique identifier, dependency relationship, author, and timestamp.
|
||||||
|
|
||||||
|
## Format
|
||||||
|
|
||||||
|
PX4 uses SPDX 2.3 JSON.
|
||||||
|
SPDX is the Linux Foundation's own standard (ISO/IEC 5962), aligning with PX4's position as a Dronecode/LF project.
|
||||||
|
Zephyr RTOS also uses SPDX.
|
||||||
|
|
||||||
|
Each SBOM contains:
|
||||||
|
|
||||||
|
- **Primary package**: The PX4 firmware for a specific board target, marked with `primaryPackagePurpose: FIRMWARE`.
|
||||||
|
- **Git submodules**: All third-party libraries included via git submodules (~33 packages), with SPDX license identifiers and commit hashes.
|
||||||
|
- **Python build dependencies**: Packages from `Tools/setup/requirements.txt` marked as `BUILD_DEPENDENCY_OF` the firmware.
|
||||||
|
- **Board-specific modules**: Internal PX4 modules compiled for the target board.
|
||||||
|
- **Compiler**: The C compiler used for the build.
|
||||||
|
|
||||||
|
Typical SBOM size: 70-100 packages, ~500 lines, ~20 KB JSON.
|
||||||
|
|
||||||
|
## Generation
|
||||||
|
|
||||||
|
SBOMs are generated automatically as part of every CMake build.
|
||||||
|
The output file is:
|
||||||
|
|
||||||
|
```txt
|
||||||
|
build/<target>/<target>.sbom.spdx.json
|
||||||
|
```
|
||||||
|
|
||||||
|
For example:
|
||||||
|
|
||||||
|
```txt
|
||||||
|
build/px4_fmu-v6x_default/px4_fmu-v6x_default.sbom.spdx.json
|
||||||
|
```
|
||||||
|
|
||||||
|
The generator script is `Tools/ci/generate_sbom.py`.
|
||||||
|
It requires PyYAML (`pyyaml`) for loading license overrides.
|
||||||
|
|
||||||
|
### CMake Integration
|
||||||
|
|
||||||
|
The `sbom` CMake target is included in the default `ALL` target.
|
||||||
|
The relevant CMake module is `cmake/sbom.cmake`.
|
||||||
|
|
||||||
|
### Disabling SBOM Generation
|
||||||
|
|
||||||
|
Set the environment variable before building.
|
||||||
|
This is checked at CMake configure time, so a clean build or reconfigure is required:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
PX4_SBOM_DISABLE=1 make px4_fmu-v6x_default
|
||||||
|
```
|
||||||
|
|
||||||
|
If the build directory already exists, force a reconfigure:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
PX4_SBOM_DISABLE=1 cmake -B build/px4_fmu-v6x_default .
|
||||||
|
```
|
||||||
|
|
||||||
|
### Manual Generation
|
||||||
|
|
||||||
|
You can also run the generator directly:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
python3 Tools/ci/generate_sbom.py \
|
||||||
|
--source-dir . \
|
||||||
|
--board px4_fmu-v6x_default \
|
||||||
|
--modules-file build/px4_fmu-v6x_default/config_module_list.txt \
|
||||||
|
--compiler arm-none-eabi-gcc \
|
||||||
|
--output build/px4_fmu-v6x_default/px4_fmu-v6x_default.sbom.spdx.json
|
||||||
|
```
|
||||||
|
|
||||||
|
## Artifacts
|
||||||
|
|
||||||
|
SBOMs are available in:
|
||||||
|
|
||||||
|
| Location | Path |
|
||||||
|
| --------------- | ---------------------------------------- |
|
||||||
|
| Build directory | `build/<target>/<target>.sbom.spdx.json` |
|
||||||
|
| GitHub Releases | Alongside `.px4` firmware files |
|
||||||
|
| S3 | Same directory as firmware artifacts |
|
||||||
|
|
||||||
|
## Validation
|
||||||
|
|
||||||
|
Validate an SBOM against the SPDX JSON schema:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
python3 -c "
|
||||||
|
import json
|
||||||
|
doc = json.load(open('build/px4_sitl_default/px4_sitl_default.sbom.spdx.json'))
|
||||||
|
assert doc['spdxVersion'] == 'SPDX-2.3'
|
||||||
|
assert doc['dataLicense'] == 'CC0-1.0'
|
||||||
|
assert len(doc['packages']) > 0
|
||||||
|
print(f'Valid: {len(doc[\"packages\"])} packages')
|
||||||
|
"
|
||||||
|
```
|
||||||
|
|
||||||
|
For full schema validation, use the [SPDX online validator](https://tools.spdx.org/app/validate/) or the `spdx-tools` CLI.
|
||||||
|
|
||||||
|
## License Detection
|
||||||
|
|
||||||
|
Submodule licenses are identified through a combination of auto-detection and manual overrides.
|
||||||
|
|
||||||
|
### Auto-Detection
|
||||||
|
|
||||||
|
The generator reads the first 100 lines of each submodule's LICENSE or COPYING file
|
||||||
|
and matches keywords against known patterns.
|
||||||
|
Copyleft licenses (GPL, LGPL, AGPL) are checked before permissive ones
|
||||||
|
to prevent false positives.
|
||||||
|
|
||||||
|
Supported patterns include:
|
||||||
|
|
||||||
|
| SPDX Identifier | Matched Keywords |
|
||||||
|
| --------------- | ----------------------------------------------------- |
|
||||||
|
| GPL-3.0-only | "GNU GENERAL PUBLIC LICENSE", "Version 3" |
|
||||||
|
| GPL-2.0-only | "GNU GENERAL PUBLIC LICENSE", "Version 2" |
|
||||||
|
| LGPL-3.0-only | "GNU LESSER GENERAL PUBLIC LICENSE", "Version 3" |
|
||||||
|
| LGPL-2.1-only | "GNU Lesser General Public License", "Version 2.1" |
|
||||||
|
| AGPL-3.0-only | "GNU AFFERO GENERAL PUBLIC LICENSE", "Version 3" |
|
||||||
|
| Apache-2.0 | "Apache License", "Version 2.0" |
|
||||||
|
| MIT | "Permission is hereby granted" |
|
||||||
|
| BSD-3-Clause | "Redistribution and use", "Neither the name" |
|
||||||
|
| BSD-2-Clause | "Redistribution and use", "THIS SOFTWARE IS PROVIDED" |
|
||||||
|
| ISC | "Permission to use, copy, modify, and/or distribute" |
|
||||||
|
| EPL-2.0 | "Eclipse Public License", "2.0" |
|
||||||
|
| Unlicense | "The Unlicense", "unlicense.org" |
|
||||||
|
|
||||||
|
If no pattern matches, the license is set to `NOASSERTION`.
|
||||||
|
|
||||||
|
### Override File
|
||||||
|
|
||||||
|
When auto-detection fails or returns the wrong result,
|
||||||
|
add an entry to `Tools/ci/license-overrides.yaml`:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
overrides:
|
||||||
|
src/lib/crypto/libtomcrypt:
|
||||||
|
license: "Unlicense"
|
||||||
|
comment: "Public domain dedication. Functionally equivalent to Unlicense."
|
||||||
|
```
|
||||||
|
|
||||||
|
Each entry maps a submodule path to its correct SPDX license identifier.
|
||||||
|
The optional `comment` field is emitted as `licenseComments` in the SBOM,
|
||||||
|
providing context for auditors reviewing complex licensing situations
|
||||||
|
(dual licenses, composite LICENSE files, public domain dedications).
|
||||||
|
|
||||||
|
### Copyleft Guardrail
|
||||||
|
|
||||||
|
The `--verify-licenses` command flags submodules with copyleft licenses
|
||||||
|
(GPL, LGPL, AGPL) in a dedicated warning section.
|
||||||
|
This is informational only and does not cause a failure.
|
||||||
|
It helps maintainers track copyleft obligations when adding new submodules.
|
||||||
|
|
||||||
|
### Platform Filtering
|
||||||
|
|
||||||
|
Submodules under `platforms/nuttx/` are excluded from POSIX and QURT SBOMs.
|
||||||
|
The `--platform` argument (set automatically by CMake via `${PX4_PLATFORM}`)
|
||||||
|
controls which platform-specific submodules are included.
|
||||||
|
This ensures SITL builds do not list NuttX RTOS packages.
|
||||||
|
|
||||||
|
### Verification
|
||||||
|
|
||||||
|
Run the verify command to check detection for all submodules:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
python3 Tools/ci/generate_sbom.py --verify-licenses --source-dir .
|
||||||
|
```
|
||||||
|
|
||||||
|
This prints each submodule with its detected license, any override, and the final value.
|
||||||
|
It exits non-zero if any checked-out submodule resolves to `NOASSERTION` without an override.
|
||||||
|
Copyleft warnings are printed after the main table.
|
||||||
|
|
||||||
|
### Adding a New Submodule
|
||||||
|
|
||||||
|
1. Add the submodule normally.
|
||||||
|
2. Run `--verify-licenses` to confirm the license is detected.
|
||||||
|
3. If detection fails, add an override to `Tools/ci/license-overrides.yaml`.
|
||||||
|
4. If the license is not in the SPDX list, use `LicenseRef-<name>`.
|
||||||
|
|
||||||
|
### EU CRA Compliance
|
||||||
|
|
||||||
|
The EU Cyber Resilience Act requires SBOMs for products with digital elements.
|
||||||
|
The goal is zero `NOASSERTION` licenses in shipped firmware SBOMs.
|
||||||
|
Every submodule should have either a detected or overridden license.
|
||||||
|
The `--verify-licenses` check enforces this in CI.
|
||||||
|
|
||||||
|
## What's in an SBOM
|
||||||
|
|
||||||
|
This section is for integrators, compliance teams, and anyone reviewing SBOM artifacts.
|
||||||
|
|
||||||
|
### Where to Find SBOMs
|
||||||
|
|
||||||
|
| Location | Path |
|
||||||
|
| --------------- | ---------------------------------------- |
|
||||||
|
| Build directory | `build/<target>/<target>.sbom.spdx.json` |
|
||||||
|
| GitHub Releases | Alongside `.px4` firmware files |
|
||||||
|
| S3 | Same directory as firmware artifacts |
|
||||||
|
|
||||||
|
### Reading the JSON
|
||||||
|
|
||||||
|
Each SBOM is a single JSON document following SPDX 2.3.
|
||||||
|
Key fields:
|
||||||
|
|
||||||
|
- **`packages`**: Array of all components. Each has `name`, `versionInfo`, `licenseConcluded`, and `SPDXID`.
|
||||||
|
- **`relationships`**: How packages relate. `CONTAINS` means a submodule is compiled into firmware. `BUILD_DEPENDENCY_OF` means a tool used only during build.
|
||||||
|
- **`licenseConcluded`**: The SPDX license identifier determined for that package.
|
||||||
|
- **`licenseComments`**: Free-text explanation for complex cases (dual licenses, composite files, public domain).
|
||||||
|
- **`externalRefs`**: Package URLs (purls) linking to GitHub repos or PyPI.
|
||||||
|
|
||||||
|
### Understanding NOASSERTION
|
||||||
|
|
||||||
|
`NOASSERTION` means no license could be determined.
|
||||||
|
For submodules, this happens when:
|
||||||
|
|
||||||
|
- The submodule is not checked out (common in CI shallow clones).
|
||||||
|
- No LICENSE/COPYING file exists.
|
||||||
|
- The LICENSE file does not match any known pattern and no override is configured.
|
||||||
|
|
||||||
|
For shipped firmware, `NOASSERTION` should be resolved by adding an override.
|
||||||
|
For build-only dependencies (Python packages), `NOASSERTION` is acceptable
|
||||||
|
since these are not compiled into the firmware binary.
|
||||||
Reference in New Issue
Block a user