diff --git a/.github/workflows/build_and_test_ebclfsa.yml b/.github/workflows/build_and_test_ebclfsa.yml index afb4f554e1e..86ff50df68d 100644 --- a/.github/workflows/build_and_test_ebclfsa.yml +++ b/.github/workflows/build_and_test_ebclfsa.yml @@ -45,7 +45,7 @@ jobs: run: | bazel build --lockfile_mode=error --config=eb-aarch64 //images/ebclfsa_aarch64:run - name: Upload test logs - uses: actions/upload-artifact@v5 + uses: actions/upload-artifact@604875ba63842e1d9f9aee9c783174be8b5afc23 # v5.0.0 with: name: test-logs path: | diff --git a/.github/workflows/build_and_test_linux.yml b/.github/workflows/build_and_test_linux.yml index 39f9fa2527f..6c559e1fb95 100644 --- a/.github/workflows/build_and_test_linux.yml +++ b/.github/workflows/build_and_test_linux.yml @@ -32,9 +32,11 @@ jobs: - name: Clean disk space uses: eclipse-score/more-disk-space@v1 - name: Checkout repository - uses: actions/checkout@v4.2.2 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3.8.0 + uses: docker/setup-buildx-action@d707e0a9f13ec3f37a07b96fc8d8ce2ae8bb7557 # v3.8.0 + - name: Setup Bazel uses: bazel-contrib/setup-bazel@0.18.0 with: @@ -42,9 +44,21 @@ jobs: disk-cache: ${{ github.workflow }} repository-cache: true cache-save: ${{ github.event_name == 'push' }} - - name: Build image + + - name: Install uv (required for SBOM license scanning) + uses: astral-sh/setup-uv@v5 + with: + enable-cache: false + - name: Build image and product SBOM run: | - bazel build --lockfile_mode=error --config=linux-x86_64 //images/linux_x86_64:image + bazel build --lockfile_mode=error --config=linux-x86_64 \ + //images/linux_x86_64:image \ + //:sbom - name: Integration tests run: | bazel test --lockfile_mode=error --config=linux-x86_64 //feature_integration_tests/itf + - name: Upload product SBOM + uses: actions/upload-artifact@b4b15b6c7981bb8d8f533a89aa19f67a4b7b679e # v4.4.1 + with: + name: sbom-spdx + path: bazel-bin/sbom.spdx.json diff --git a/.github/workflows/test_and_docs.yml b/.github/workflows/test_and_docs.yml index e643406fc02..3f1dfbd11a5 100644 --- a/.github/workflows/test_and_docs.yml +++ b/.github/workflows/test_and_docs.yml @@ -73,6 +73,13 @@ jobs: - name: Execute Feature Integration Tests run: | bazel test --config=linux-x86_64 //feature_integration_tests/test_cases:fit + - name: Install uv (required for SBOM license scanning) + uses: astral-sh/setup-uv@v5 + with: + enable-cache: false + - name: Build build tools SBOM + run: | + bazel build --lockfile_mode=error //:build_tools_sbom - name: Publish build summary if: always() run: | @@ -102,6 +109,10 @@ jobs: files: ${{ github.event.repository.name }}_test_reports.zip env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + - name: Upload build tools SBOM to release + uses: softprops/action-gh-release@v1 + with: + files: bazel-bin/build_tools_sbom.spdx.json - name: Install Graphviz uses: eclipse-score/apt-install@main with: @@ -122,7 +133,7 @@ jobs: bazel run //scripts/tooling -- misc html_report --output ${CURRENT}/_build/status_dashboard.html tar -cf github-pages.tar _build - name: Upload documentation artifact - uses: actions/upload-artifact@v4.4.0 + uses: actions/upload-artifact@b4b15b6c7981bb8d8f533a89aa19f67a4b7b679e # v4.4.1 with: name: github-pages-${{ github.event.pull_request.head.sha || github.sha }} path: github-pages.tar diff --git a/.gitignore b/.gitignore index b719861b67c..678f4cddfac 100644 --- a/.gitignore +++ b/.gitignore @@ -28,3 +28,6 @@ __pycache__/ /.gita-workspace.csv /build artifacts + +#Macos +.DS_Store diff --git a/BUILD b/BUILD index 24fe9e51ef7..f883ae13ed5 100644 --- a/BUILD +++ b/BUILD @@ -12,6 +12,7 @@ # ******************************************************************************* load("@score_docs_as_code//:docs.bzl", "docs") +load("@score_sbom//:defs.bzl", "sbom") load("@score_tooling//:defs.bzl", "setup_starpls", "use_format_targets") # Docs-as-code @@ -45,6 +46,72 @@ use_format_targets() exports_files([ "MODULE.bazel", + "MODULE.bazel.lock", "pyproject.toml", "known_good.json", ]) + +sbom( + name = "sbom", + auto_cdxgen = False, + auto_crates_cache = True, + component_name = "score_reference_integration", + generation_context = "build", + module_lockfiles = [":MODULE.bazel.lock"], + output_formats = [ + "spdx", + ], + targets = [ + "//feature_integration_tests/test_scenarios/cpp:cpp_test_scenarios", + "//feature_integration_tests/test_scenarios/rust:rust_test_scenarios", + "//showcases/cli:cli", + "//showcases/orchestration_persistency:orch_per_example", + "@score_communication//score/mw/com/example/com-api-example:com-api-example", + "@score_kyron//examples:main_macro", + "@score_kyron//examples:safety_task", + "@score_kyron//examples:select", + "@score_logging//score/test/component/dlt_generator_app:dlt_generator", + "@score_logging//score/test/component/logging_app:logging_app", + "@score_time//examples/time/high_res_steady_time", + "@score_time//examples/time/steady_time", + "@score_time//examples/time/system_time", + "@score_time//examples/time/vehicle_time", + ], +) + +# Product SBOM alias with an explicit lifecycle-oriented name. +alias( + name = "product_sbom", + actual = ":sbom", + visibility = ["//visibility:public"], +) + +# Qualification inventory for Python-based build and test tools. This is kept +# separate from the product SBOM because build-time dependencies are not +# product/runtime dependencies. +sbom( + name = "build_tools_sbom", + testonly = True, + auto_cdxgen = False, + auto_crates_cache = False, + component_name = "score_reference_integration_build_tools", + # Pip repositories are represented authoritatively by python_lockfiles; + # exclude their generated Bazel aliases to avoid duplicate components. + exclude_patterns = ["rules_python++pip+"], + generation_context = "build", + java_files = ["@score_docs_as_code//src:plantuml.jar"], + output_formats = ["spdx"], + python_lockfiles = [ + "//feature_integration_tests/test_cases:requirements.txt.lock", + "//scripts/tooling:requirements.txt", + "@score_docs_as_code//src:requirements_lock", + ], + targets = [ + "//:docs_combo_experimental", + "//feature_integration_tests/test_scenarios/cpp:cpp_test_scenarios", + "//scripts/tooling:checkout_repos", + "//scripts/tooling:recategorize_guidelines", + "//scripts/tooling:tooling", + "@score_docs_as_code//src:plantuml", + ], +) diff --git a/MODULE.bazel b/MODULE.bazel index df2e3a8995e..30e598deee7 100644 --- a/MODULE.bazel +++ b/MODULE.bazel @@ -81,3 +81,27 @@ git_override( commit = "2792e2daee2cf524fdc7b1545fd3537791ebc36c", remote = "https://github.com/bmw-software-engineering/lobster.git", ) + +bazel_dep(name = "score_sbom", version = "0.0.2") + +sbom_ext = use_extension("@score_sbom//:extensions.bzl", "sbom_metadata") +sbom_ext.track_module(name = "score_sbom") +sbom_ext.track_module(name = "score_baselibs") +sbom_ext.track_module(name = "score_communication") +sbom_ext.track_module(name = "score_persistency") +sbom_ext.track_module(name = "score_orchestrator") +sbom_ext.track_module(name = "score_kyron") +sbom_ext.track_module(name = "score_lifecycle_health") +sbom_ext.track_module(name = "score_logging") +sbom_ext.track_module(name = "score_time") +sbom_ext.track_module(name = "score_crates") +sbom_ext.track_module(name = "score_itf") +sbom_ext.track_module(name = "score_tooling") +sbom_ext.track_module(name = "score_platform") +sbom_ext.track_module(name = "score_bazel_platforms") +sbom_ext.track_module(name = "score_test_scenarios") +sbom_ext.track_module(name = "score_docs_as_code") +sbom_ext.track_module(name = "score_process") +sbom_ext.track_module(name = "trlc") +sbom_ext.track_module(name = "lobster") +use_repo(sbom_ext, "sbom_metadata") diff --git a/MODULE.bazel.lock b/MODULE.bazel.lock index c9eb12e0be6..69b01cc688c 100644 --- a/MODULE.bazel.lock +++ b/MODULE.bazel.lock @@ -1025,6 +1025,8 @@ "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/score_rust_policies/0.0.3/MODULE.bazel": "9e8310a75c13ccebc49fb9cbf7acc6c1b75654292b2ca907fb5d513133dbf6f3", "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/score_rust_policies/0.0.5/MODULE.bazel": "7de02547bdf121d3dedf5141b97f0fd9a545bd255ff5c7b699056b35816ffad9", "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/score_rust_policies/0.0.5/source.json": "22c8bf0a5cbf7c7b06f774f3f66498e0bc14346a8b2208f7427a8fbb78a42547", + "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/score_sbom/0.0.2/MODULE.bazel": "eaddaa984143fc540c48d59988976db82a5ff40b6264df41ad097b520c213201", + "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/score_sbom/0.0.2/source.json": "757694093b76ab165663f67987abe7c84a049fb4a5489ee98eccaf5a3fd8da5c", "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/score_toolchains_rust/0.8.0/MODULE.bazel": "ea57a9a4dcb8ad49f4556f824500eb559365f413ccbb39d70d0b363685aacec5", "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/score_toolchains_rust/0.8.0/source.json": "394a615e03ad722bc27bd4a6f098c6ff2fe7120b69cdf3925d47e39d30ada8a4", "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/stardoc/0.5.0/MODULE.bazel": "not found", @@ -9892,6 +9894,45 @@ ] } }, + "@@score_sbom+//:extensions.bzl%sbom_metadata": { + "general": { + "bzlTransitiveDigest": "6MZmMjhsKcOnMq12Rbna7aYgF4ZAbEtH3kjZYu4M5nI=", + "usagesDigest": "9i7n8SjRs3isUDHbGTLmCZHFqJDV0njGZIidi0RDBkc=", + "recordedFileInputs": {}, + "recordedDirentsInputs": {}, + "envVariables": {}, + "generatedRepoSpecs": { + "sbom_metadata": { + "repoRuleId": "@@score_sbom+//:extensions.bzl%_sbom_metadata_repo", + "attributes": { + "metadata_content": "{\"crates\":{},\"git_repositories\":{},\"http_archives\":{},\"licenses\":{},\"modules\":{}}", + "tracked_modules": [ + "score_sbom", + "score_baselibs", + "score_communication", + "score_persistency", + "score_orchestrator", + "score_kyron", + "score_lifecycle_health", + "score_logging", + "score_time", + "score_crates", + "score_itf", + "score_tooling", + "score_platform", + "score_bazel_platforms", + "score_test_scenarios", + "score_docs_as_code", + "score_process", + "trlc", + "lobster" + ] + } + } + }, + "recordedRepoMappingEntries": [] + } + }, "@@score_toolchains_rust+//extensions:ferrocene_toolchain_ext.bzl%ferrocene_toolchain_ext": { "general": { "bzlTransitiveDigest": "XEgifqjEEdSGw80RccoJ/aUy9smsRXQJ9jO4RDOf2vk=", diff --git a/README.md b/README.md index 24643df8390..be07e870078 100644 --- a/README.md +++ b/README.md @@ -65,6 +65,72 @@ Cross-compile all showcases and assemble the IFS image: bazel build --config=qnx-aarch64 //images/qnx_aarch64:image ``` +### Software Bill of Materials + +The root `//:sbom` target generates SPDX 2.3 and CycloneDX 1.6 documents for +the integrated showcase and feature-test binaries. It consumes `sbom-tool` +from the local git checkout at `../sbom-tool` in this workspace until the +module is available in the Bazel registry. + +Install Node.js/npm and `@cyclonedx/cdxgen` before building the C++ dependency +metadata, then run: + +```bash +npm install -g @cyclonedx/cdxgen +bazel build //:sbom +``` + +The generated documents are written below `bazel-bin/` as `sbom.spdx.json` and +`sbom.cdx.json`. Rust crate metadata is collected automatically from the +workspace lockfile and the configured `score_crates` module. + +### Selecting an SBOM mode + +Choose the mode based on what is being qualified: + +- **Product mode** is for the software delivered by the integration. It follows + the configured showcase and feature-test binaries and includes their runtime + dependencies. Use `//:product_sbom` (an alias of `//:sbom`) when you need both + SPDX and CycloneDX output for the product scope. +- **Build-tool qualification mode** is for tools used to build, test, or + generate documentation. It is separate from the product scope because these + tools are not shipped as product runtime dependencies. Use + `//:build_tools_sbom` when collecting ISO 26262 qualification evidence for + the development toolchain. + +Build-tool mode includes the Python lockfiles used by the workspace tooling and +docs-as-code, the Sphinx documentation toolchain, the PlantUML integration, and +the PlantUML JAR itself with a SHA-256 checksum. It emits SPDX only and does +not run cdxgen or the Rust crate cache collector. + +Build the selected mode as follows: + +```bash +# Product/runtime dependencies +bazel build //:product_sbom +# Outputs: bazel-bin/product_sbom.spdx.json and bazel-bin/product_sbom.cdx.json + +# Build-tool qualification inventory (SPDX JSON) +bazel build //:build_tools_sbom +# Output: bazel-bin/build_tools_sbom.spdx.json +``` + +While testing the unpublished Python collector branch of `sbom-tool`, resolve +it with the module override below. Apply the override to whichever mode you are +building: + +```bash +bazel build //:product_sbom \ + --override_module=score_sbom=/workspaces/sbom-tool + +bazel build //:build_tools_sbom \ + --override_module=score_sbom=/workspaces/sbom-tool +``` + +Once the collector branch is merged, replace this command-line override with +the merged commit in the root `MODULE.bazel` git override. + +## Operating system integrations The built IFS image is written to: ``` diff --git a/feature_integration_tests/test_cases/BUILD b/feature_integration_tests/test_cases/BUILD index dcf8ab0542b..18d0b24212f 100644 --- a/feature_integration_tests/test_cases/BUILD +++ b/feature_integration_tests/test_cases/BUILD @@ -14,6 +14,8 @@ load("@pip_score_venv_test//:requirements.bzl", "all_requirements") load("@rules_python//python:pip.bzl", "compile_pip_requirements") load("@score_tooling//python_basics:defs.bzl", "score_py_pytest") +exports_files(["requirements.txt.lock"]) + # In order to update the requirements, change the `requirements.txt` file and run: # `bazel run //feature_integration_tests/test_cases:requirements.update`. # This will update the `requirements.txt.lock` file. diff --git a/scripts/tooling/BUILD b/scripts/tooling/BUILD index c2088414897..8f843625a0a 100644 --- a/scripts/tooling/BUILD +++ b/scripts/tooling/BUILD @@ -15,6 +15,8 @@ load("@rules_python//python:defs.bzl", "py_binary", "py_library") load("@rules_python//python:pip.bzl", "compile_pip_requirements") load("@score_tooling//python_basics:defs.bzl", "score_py_pytest") +exports_files(["requirements.txt"]) + # In order to update the requirements, change the `requirements.in` file and run # `bazel run //scripts/tooling:requirements.update` # This will update the `requirements.txt` file diff --git a/scripts/tooling/cli/misc/assets/report_template.html b/scripts/tooling/cli/misc/assets/report_template.html index f3b501af52c..384d0fc60be 100644 --- a/scripts/tooling/cli/misc/assets/report_template.html +++ b/scripts/tooling/cli/misc/assets/report_template.html @@ -256,13 +256,91 @@ border-top: 1px solid var(--border); padding-top: 1rem; } +
-

Known Good Status

+

Integration Dashboard

Snapshot: {{ timestamp }}

+
+ + + +
+
@@ -272,25 +350,165 @@

Known Good Status

-
- 🔒 Add a GitHub personal access token (PAT) for live status — exact commit counts fetched directly from GitHub. - Enter it in the token field above, or - create one here. - Without a token, up-to-date / behind status is still shown from data embedded at report generation time. - Your PAT is not sent anywhere — it is only kept in the local cache of this page. -
+ +
+
+ 🔒 Add a GitHub personal access token (PAT) for live status — exact commit counts fetched directly from GitHub. + Enter it in the token field above, or + create one here. + Without a token, up-to-date / behind status is still shown from data embedded at report generation time. + Your PAT is not sent anywhere — it is only kept in the local cache of this page. +
-
-
+
+
+ + +
+ + +
+
+

Traceability Overview

+

+ This tab provides an overview of requirements, architecture, and test traceability for the S-CORE reference integration. + Detailed models are maintained in Sphinx with sphinx-needs and linked below. +

+ +
+

Requirements & Architecture

+

+ Central documentation and needs-based traceability are published via Sphinx. Use the links below to inspect coverage between requirements, design, tests, and components. +

+ +
+ +
+

ISO 26262 Alignment

+

+ For ISO 26262, this view will be extended to expose: +

+
    +
  • Traceability from system requirements → software components → tests.
  • +
  • Links to Safety Sentinel–based checks in the Bazel build.
  • +
  • Automated coverage metrics derived from Sphinx-Needs exports (future).
  • +
+
+
+
+ Central documentation and needs-based traceability are published via Sphinx. Use the links below to inspect coverage between requirements, design, tests, and components. +

+ + + +
+

ISO 26262 Alignment

+

+ For ISO 26262, this view will be extended to expose: +

+ +
+ + + --> + + +
+
+

SBOM & Tool Qualification

+

+ Product SBOM (ISO 26262) and tool qualification overview for the S-CORE reference integration. +

+
- + +
+

Product SBOM

+

+ +
+ +
+ +
+ + + + + + + + + + +
PackageVersionSupplierLicenses
+
+
+ + +
+

Tool Qualification (ISO 26262)

+

+ Tools used in the build, analysis, and verification pipeline are documented separately and linked here. + Detailed qualification information is maintained in the S-CORE tooling documentation. +

+ +
+

Static Analysis & Code Quality

+ +
+ +
+

SBOM & Dependency Tools

+

+ SBOM generation and dependency analysis tools (e.g., Syft, Grype) are configured in CI and documented as part of the tooling landscape. + A dedicated tool-qualification table will be added here once a central manifest is available. +

+
+
+
diff --git a/scripts/tooling/cli/misc/html_report.py b/scripts/tooling/cli/misc/html_report.py index 00af22e2cf1..04d8a94c8a6 100644 --- a/scripts/tooling/cli/misc/html_report.py +++ b/scripts/tooling/cli/misc/html_report.py @@ -97,10 +97,40 @@ def _enrich_with_compare_data(entries: list[dict[str, Any]], token: str) -> None _LOG.warning("Could not fetch compare data for %s@%s", entry["owner_repo"], entry["branch"]) -def generate_report(known_good: KnownGood, token: Optional[str] = None) -> str: +def _parse_sbom_packages(sbom_path: Path) -> list[dict[str, Any]]: + """Minimal SPDX JSON parser for SBOM dashboard.""" + data = json.loads(sbom_path.read_text(encoding="utf-8")) + packages = [] + for p in data.get("packages", []): + name = (p.get("name") or "").strip() + version = (p.get("versionInfo") or "").strip() + supplier_raw = p.get("supplier") or {} + supplier = "" + if isinstance(supplier_raw, dict): + supplier = supplier_raw.get("name", "") + elif isinstance(supplier_raw, str): + supplier = supplier_raw + + licenses = [] + for lic in (p.get("licenseConcluded") or "").split(" OR "): + lic = lic.strip() + if lic and lic != "NOASSERTION": + licenses.append(lic) + + packages.append({ + "name": name, + "version": version or "", + "supplier": supplier or "", + "licenses": licenses, + }) + return packages + + +def generate_report(known_good: KnownGood, token: Optional[str] = None, sbom_packages: Optional[list[dict[str, Any]]] = None) -> str: entries = _collect_entries(known_good) if token: _enrich_with_compare_data(entries, token) + env = Environment( loader=FileSystemLoader(TEMPLATE_DIR), autoescape=select_autoescape(["html"]), @@ -108,12 +138,13 @@ def generate_report(known_good: KnownGood, token: Optional[str] = None) -> str: tmpl = env.get_template("report_template.html") return tmpl.render( modules_json=json.dumps(entries, indent=2), + sbom_packages_json=json.dumps(sbom_packages or [], indent=2), timestamp=known_good.timestamp, ) -def write_report(known_good: KnownGood, output_path: Path, token: Optional[str] = None) -> None: - Path(output_path).write_text(generate_report(known_good, token), encoding="utf-8") +def write_report(known_good: KnownGood, output_path: Path, token: Optional[str] = None, sbom_packages: Optional[list[dict[str, Any]]] = None) -> None: + Path(output_path).write_text(generate_report(known_good, token, sbom_packages), encoding="utf-8") def register(subparsers: argparse._SubParsersAction) -> None: @@ -130,6 +161,12 @@ def register(subparsers: argparse._SubParsersAction) -> None: default="report.html", help="Output HTML file path (default: report.html)", ) + parser.add_argument( + "--sbom", + metavar="PATH", + default=None, + help="Optional SPDX JSON SBOM to include in the SBOM & Tools tab.", + ) parser.set_defaults(func=_run) @@ -143,7 +180,17 @@ def _run(args: argparse.Namespace) -> int: token = os.environ.get("GITHUB_TOKEN") output = _resolve_path_from_bazel(Path(args.output)) - write_report(known_good, output, token=token) + + sbom_packages = None + if args.sbom: + sbom_path = _resolve_path_from_bazel(Path(args.sbom)) + try: + sbom_packages = _parse_sbom_packages(sbom_path) + except Exception as e: + print(f"warning: failed to parse SBOM {sbom_path}: {e}", file=sys.stderr) + + write_report(known_good, output, token=token, sbom_packages=sbom_packages) + if token: print(f"Report written to {output} (current hashes fetched from GitHub)") else: