bompage 1.2.2__tar.gz → 1.3.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {bompage-1.2.2 → bompage-1.3.0}/.gitignore +1 -0
- {bompage-1.2.2 → bompage-1.3.0}/.gitlab-ci.yml +75 -57
- {bompage-1.2.2 → bompage-1.3.0}/PKG-INFO +10 -8
- {bompage-1.2.2 → bompage-1.3.0}/README.md +9 -7
- bompage-1.3.0/deploy/.gitlab-ci.yml +55 -0
- {bompage-1.2.2 → bompage-1.3.0}/deploy/README.md +26 -30
- {bompage-1.2.2 → bompage-1.3.0}/deploy/trust/allowed_signers +5 -3
- {bompage-1.2.2 → bompage-1.3.0}/docs/index.md +1 -1
- {bompage-1.2.2 → bompage-1.3.0}/pyproject.toml +4 -4
- {bompage-1.2.2 → bompage-1.3.0}/renovate.json +2 -2
- bompage-1.3.0/src/bompage/_paths.py +17 -0
- {bompage-1.2.2 → bompage-1.3.0}/src/bompage/analysis/drift.py +10 -9
- {bompage-1.2.2 → bompage-1.3.0}/src/bompage/analysis/inventory.py +14 -2
- {bompage-1.2.2 → bompage-1.3.0}/src/bompage/analysis/timeline.py +7 -5
- {bompage-1.2.2 → bompage-1.3.0}/src/bompage/attest.py +34 -30
- {bompage-1.2.2 → bompage-1.3.0}/src/bompage/bompage.py +5 -4
- {bompage-1.2.2 → bompage-1.3.0}/src/bompage/build.py +31 -15
- {bompage-1.2.2 → bompage-1.3.0}/src/bompage/metadata.py +2 -1
- {bompage-1.2.2 → bompage-1.3.0}/src/bompage/models.py +2 -1
- {bompage-1.2.2 → bompage-1.3.0}/src/bompage/parsers/base.py +29 -1
- {bompage-1.2.2 → bompage-1.3.0}/src/bompage/parsers/cyclonedx.py +29 -3
- {bompage-1.2.2 → bompage-1.3.0}/src/bompage/push.py +112 -40
- bompage-1.2.2/src/bompage/web/assets/index-D_clWxBW.js → bompage-1.3.0/src/bompage/web/assets/index-BQwfQt1A.js +71 -71
- {bompage-1.2.2 → bompage-1.3.0}/src/bompage/web/index.html +2 -1
- {bompage-1.2.2 → bompage-1.3.0}/tests/analysis/test_inventory.py +31 -0
- {bompage-1.2.2 → bompage-1.3.0}/tests/parsers/test_base.py +48 -0
- {bompage-1.2.2 → bompage-1.3.0}/tests/parsers/test_cyclonedx.py +19 -0
- {bompage-1.2.2 → bompage-1.3.0}/tests/test_attest.py +15 -0
- {bompage-1.2.2 → bompage-1.3.0}/tests/test_build.py +14 -0
- {bompage-1.2.2 → bompage-1.3.0}/tests/test_prune_report.py +7 -4
- {bompage-1.2.2 → bompage-1.3.0}/tests/test_push_report.py +109 -45
- {bompage-1.2.2 → bompage-1.3.0}/web-src/app.test.ts +42 -0
- {bompage-1.2.2 → bompage-1.3.0}/web-src/app.ts +25 -9
- {bompage-1.2.2 → bompage-1.3.0}/web-src/index.html +1 -0
- {bompage-1.2.2 → bompage-1.3.0}/web-src/lib.test.ts +121 -2
- {bompage-1.2.2 → bompage-1.3.0}/web-src/lib.ts +59 -4
- bompage-1.2.2/deploy/.gitlab-ci.yml +0 -99
- bompage-1.2.2/templates/push-report.yml +0 -158
- {bompage-1.2.2 → bompage-1.3.0}/.gitleaks.toml +0 -0
- {bompage-1.2.2 → bompage-1.3.0}/.markdownlint.yaml +0 -0
- {bompage-1.2.2 → bompage-1.3.0}/.node-version +0 -0
- {bompage-1.2.2 → bompage-1.3.0}/.pre-commit-config.yaml +0 -0
- {bompage-1.2.2 → bompage-1.3.0}/.python-version +0 -0
- {bompage-1.2.2 → bompage-1.3.0}/.releaserc +0 -0
- {bompage-1.2.2 → bompage-1.3.0}/.yamllint +0 -0
- {bompage-1.2.2 → bompage-1.3.0}/CONTRIBUTING.md +0 -0
- {bompage-1.2.2 → bompage-1.3.0}/LICENSE +0 -0
- {bompage-1.2.2 → bompage-1.3.0}/SECURITY.md +0 -0
- {bompage-1.2.2 → bompage-1.3.0}/Taskfile.project.yml +0 -0
- {bompage-1.2.2 → bompage-1.3.0}/Taskfile.yml +0 -0
- {bompage-1.2.2 → bompage-1.3.0}/biome.json +0 -0
- {bompage-1.2.2 → bompage-1.3.0}/deploy/reports/example-component/.gitkeep +0 -0
- {bompage-1.2.2 → bompage-1.3.0}/deploy/web/.gitkeep +0 -0
- {bompage-1.2.2 → bompage-1.3.0}/docs/analysis/drift.md +0 -0
- {bompage-1.2.2 → bompage-1.3.0}/docs/analysis/freshness.md +0 -0
- {bompage-1.2.2 → bompage-1.3.0}/docs/analysis/inventory.md +0 -0
- {bompage-1.2.2 → bompage-1.3.0}/docs/analysis/timeline.md +0 -0
- {bompage-1.2.2 → bompage-1.3.0}/docs/bompage.md +0 -0
- {bompage-1.2.2 → bompage-1.3.0}/docs/build.md +0 -0
- {bompage-1.2.2 → bompage-1.3.0}/docs/cli.md +0 -0
- {bompage-1.2.2 → bompage-1.3.0}/docs/dashboard.md +0 -0
- {bompage-1.2.2 → bompage-1.3.0}/docs/discovery.md +0 -0
- {bompage-1.2.2 → bompage-1.3.0}/docs/metadata.md +0 -0
- {bompage-1.2.2 → bompage-1.3.0}/docs/models.md +0 -0
- {bompage-1.2.2 → bompage-1.3.0}/docs/parsers/base.md +0 -0
- {bompage-1.2.2 → bompage-1.3.0}/docs/parsers/cyclonedx.md +0 -0
- {bompage-1.2.2 → bompage-1.3.0}/docs/parsers/spdx.md +0 -0
- {bompage-1.2.2 → bompage-1.3.0}/docs/prune.md +0 -0
- {bompage-1.2.2 → bompage-1.3.0}/docs/push.md +0 -0
- {bompage-1.2.2 → bompage-1.3.0}/docs/render/api.md +0 -0
- {bompage-1.2.2 → bompage-1.3.0}/docs/render/data.md +0 -0
- {bompage-1.2.2 → bompage-1.3.0}/docs/render/web.md +0 -0
- {bompage-1.2.2 → bompage-1.3.0}/docs/verify.md +0 -0
- {bompage-1.2.2 → bompage-1.3.0}/lychee.toml +0 -0
- {bompage-1.2.2 → bompage-1.3.0}/mkdocs.yml +0 -0
- {bompage-1.2.2 → bompage-1.3.0}/package.json +0 -0
- {bompage-1.2.2 → bompage-1.3.0}/pnpm-lock.yaml +0 -0
- {bompage-1.2.2 → bompage-1.3.0}/pnpm-workspace.yaml +0 -0
- {bompage-1.2.2 → bompage-1.3.0}/scripts/gen-demo-data.py +0 -0
- {bompage-1.2.2 → bompage-1.3.0}/scripts/merge_sbom_licenses.py +0 -0
- {bompage-1.2.2 → bompage-1.3.0}/scripts/subset-icons.mjs +0 -0
- {bompage-1.2.2 → bompage-1.3.0}/src/bompage/__init__.py +0 -0
- {bompage-1.2.2 → bompage-1.3.0}/src/bompage/_git.py +0 -0
- {bompage-1.2.2 → bompage-1.3.0}/src/bompage/_time.py +0 -0
- {bompage-1.2.2 → bompage-1.3.0}/src/bompage/_version.py +0 -0
- {bompage-1.2.2 → bompage-1.3.0}/src/bompage/analysis/__init__.py +0 -0
- {bompage-1.2.2 → bompage-1.3.0}/src/bompage/analysis/freshness.py +0 -0
- {bompage-1.2.2 → bompage-1.3.0}/src/bompage/discovery.py +0 -0
- {bompage-1.2.2 → bompage-1.3.0}/src/bompage/parsers/__init__.py +0 -0
- {bompage-1.2.2 → bompage-1.3.0}/src/bompage/parsers/spdx.py +0 -0
- {bompage-1.2.2 → bompage-1.3.0}/src/bompage/prune.py +0 -0
- {bompage-1.2.2 → bompage-1.3.0}/src/bompage/py.typed +0 -0
- {bompage-1.2.2 → bompage-1.3.0}/src/bompage/render/__init__.py +0 -0
- {bompage-1.2.2 → bompage-1.3.0}/src/bompage/render/api.py +0 -0
- {bompage-1.2.2 → bompage-1.3.0}/src/bompage/render/data.py +0 -0
- {bompage-1.2.2 → bompage-1.3.0}/src/bompage/render/web.py +0 -0
- {bompage-1.2.2 → bompage-1.3.0}/src/bompage/web/android-chrome-192x192.png +0 -0
- {bompage-1.2.2 → bompage-1.3.0}/src/bompage/web/android-chrome-512x512.png +0 -0
- {bompage-1.2.2 → bompage-1.3.0}/src/bompage/web/apple-touch-icon.png +0 -0
- {bompage-1.2.2 → bompage-1.3.0}/src/bompage/web/assets/bompage-D8Wv-XTb.png +0 -0
- {bompage-1.2.2 → bompage-1.3.0}/src/bompage/web/assets/index-CBZgukme.css +0 -0
- {bompage-1.2.2 → bompage-1.3.0}/src/bompage/web/assets/material-symbols-outlined-CQbRTMbb.woff2 +0 -0
- {bompage-1.2.2 → bompage-1.3.0}/src/bompage/web/assets/roboto-latin-ext-wght-normal-DYIxWhlt.woff2 +0 -0
- {bompage-1.2.2 → bompage-1.3.0}/src/bompage/web/assets/roboto-latin-wght-normal-ccAYIvAh.woff2 +0 -0
- {bompage-1.2.2 → bompage-1.3.0}/src/bompage/web/favicon-16x16.png +0 -0
- {bompage-1.2.2 → bompage-1.3.0}/src/bompage/web/favicon-32x32.png +0 -0
- {bompage-1.2.2 → bompage-1.3.0}/src/bompage/web/favicon.ico +0 -0
- {bompage-1.2.2 → bompage-1.3.0}/src/bompage/web/site.webmanifest +0 -0
- {bompage-1.2.2 → bompage-1.3.0}/tests/__init__.py +0 -0
- {bompage-1.2.2 → bompage-1.3.0}/tests/analysis/__init__.py +0 -0
- {bompage-1.2.2 → bompage-1.3.0}/tests/analysis/test_drift.py +0 -0
- {bompage-1.2.2 → bompage-1.3.0}/tests/analysis/test_freshness.py +0 -0
- {bompage-1.2.2 → bompage-1.3.0}/tests/analysis/test_timeline.py +0 -0
- {bompage-1.2.2 → bompage-1.3.0}/tests/conftest.py +0 -0
- {bompage-1.2.2 → bompage-1.3.0}/tests/files/cyclonedx/simple.cdx.json +0 -0
- {bompage-1.2.2 → bompage-1.3.0}/tests/files/reports/app-api/sbom-20260115.json +0 -0
- {bompage-1.2.2 → bompage-1.3.0}/tests/files/spdx/simple.spdx.json +0 -0
- {bompage-1.2.2 → bompage-1.3.0}/tests/files/spdx/web-frontend.spdx.json +0 -0
- {bompage-1.2.2 → bompage-1.3.0}/tests/parsers/__init__.py +0 -0
- {bompage-1.2.2 → bompage-1.3.0}/tests/parsers/test_spdx.py +0 -0
- {bompage-1.2.2 → bompage-1.3.0}/tests/render/__init__.py +0 -0
- {bompage-1.2.2 → bompage-1.3.0}/tests/render/test_data.py +0 -0
- {bompage-1.2.2 → bompage-1.3.0}/tests/render/test_web.py +0 -0
- {bompage-1.2.2 → bompage-1.3.0}/tests/scripts/test_merge_sbom_licenses.py +0 -0
- {bompage-1.2.2 → bompage-1.3.0}/tests/test_bompage.py +0 -0
- {bompage-1.2.2 → bompage-1.3.0}/tests/test_dashboard_e2e.py +0 -0
- {bompage-1.2.2 → bompage-1.3.0}/tests/test_discovery.py +0 -0
- {bompage-1.2.2 → bompage-1.3.0}/tests/test_metadata.py +0 -0
- {bompage-1.2.2 → bompage-1.3.0}/tests/test_models.py +0 -0
- {bompage-1.2.2 → bompage-1.3.0}/tests/test_prune.py +0 -0
- {bompage-1.2.2 → bompage-1.3.0}/tests/test_time.py +0 -0
- {bompage-1.2.2 → bompage-1.3.0}/tsconfig.json +0 -0
- {bompage-1.2.2 → bompage-1.3.0}/uv.lock +0 -0
- {bompage-1.2.2 → bompage-1.3.0}/vite.config.ts +0 -0
- {bompage-1.2.2 → bompage-1.3.0}/vitest.config.ts +0 -0
- {bompage-1.2.2 → bompage-1.3.0}/web-src/bompage.png +0 -0
- {bompage-1.2.2 → bompage-1.3.0}/web-src/public/android-chrome-192x192.png +0 -0
- {bompage-1.2.2 → bompage-1.3.0}/web-src/public/android-chrome-512x512.png +0 -0
- {bompage-1.2.2 → bompage-1.3.0}/web-src/public/apple-touch-icon.png +0 -0
- {bompage-1.2.2 → bompage-1.3.0}/web-src/public/favicon-16x16.png +0 -0
- {bompage-1.2.2 → bompage-1.3.0}/web-src/public/favicon-32x32.png +0 -0
- {bompage-1.2.2 → bompage-1.3.0}/web-src/public/favicon.ico +0 -0
- {bompage-1.2.2 → bompage-1.3.0}/web-src/public/site.webmanifest +0 -0
- {bompage-1.2.2 → bompage-1.3.0}/web-src/theme.css +0 -0
|
@@ -12,9 +12,9 @@ workflow:
|
|
|
12
12
|
# semantic-release created the tag. On the tag pipeline only the release
|
|
13
13
|
# jobs matter, and none of them gate on lint/test anyway (each has an
|
|
14
14
|
# explicit `needs:`). DISABLE_JOBS switches off every op_so lint/test
|
|
15
|
-
# job; the
|
|
16
|
-
# (prepare-uv:sync, publish-pypi-uv, pages,
|
|
17
|
-
# DISABLE_JOBS guard, so it survives as-is.
|
|
15
|
+
# job; the jobs the release needs re-enable themselves below
|
|
16
|
+
# (prepare-uv:sync, publish-pypi-uv, pages, generate-sbom, bompage-push).
|
|
17
|
+
# build-web has no DISABLE_JOBS guard, so it survives as-is.
|
|
18
18
|
DISABLE_JOBS: "true"
|
|
19
19
|
- if: $CI_COMMIT_BRANCH
|
|
20
20
|
# Superseded pipelines abandon their still-running interruptible jobs when a
|
|
@@ -45,11 +45,11 @@ variables:
|
|
|
45
45
|
TASK_VERSION: "v3.53.1"
|
|
46
46
|
IMAGE_UV: ghcr.io/astral-sh/uv:python${PYTHON_VERSION}-trixie-slim
|
|
47
47
|
IMAGE_NODE: node:24-trixie-slim
|
|
48
|
-
# Trivy generates bompage's own release SBOM in the `
|
|
49
|
-
# re-running an old tag is reproducible; Renovate bumps it (custom manager
|
|
50
|
-
# renovate.json).
|
|
48
|
+
# Trivy generates bompage's own release SBOM in the `generate-sbom` job. Pinned
|
|
49
|
+
# so re-running an old tag is reproducible; Renovate bumps it (custom manager
|
|
50
|
+
# in renovate.json).
|
|
51
51
|
TRIVY_VERSION: "0.74.0"
|
|
52
|
-
# Node binary installed directly in the `
|
|
52
|
+
# Node binary installed directly in the `generate-sbom` job (a Python-image job):
|
|
53
53
|
# Trivy's pnpm analyzer only reads license data from an installed
|
|
54
54
|
# `node_modules`, not from `pnpm-lock.yaml` alone. Pinned like TRIVY_VERSION
|
|
55
55
|
# above; Renovate bumps it (custom manager in renovate.json). pnpm itself
|
|
@@ -94,6 +94,26 @@ include:
|
|
|
94
94
|
inputs:
|
|
95
95
|
publish_pypi_uv_python_version: "3.14"
|
|
96
96
|
publish_pypi_uv_stage: pypi-release
|
|
97
|
+
# Only the `bompage-push` job is used here: `bompage_pages_enabled`/
|
|
98
|
+
# `bompage_prune_enabled` are turned off because the component unconditionally
|
|
99
|
+
# also defines a `pages` job (this repo already has its own, for the mkdocs
|
|
100
|
+
# docs site -- a same-name collision) and a `bompage-prune` job (which would
|
|
101
|
+
# otherwise fire on this repo's own scheduled Renovate pipeline and try to
|
|
102
|
+
# prune "$CI_PROJECT_URL.git", the wrong repository). Both belong only to the
|
|
103
|
+
# central sbom-bompage repo's own inclusion (deploy/.gitlab-ci.yml).
|
|
104
|
+
#
|
|
105
|
+
# `bompage_push_repo` is a literal, not `${VARIABLE}`: like the
|
|
106
|
+
# `*_python_version` inputs above, `include: inputs:` cannot reference this
|
|
107
|
+
# file's own `variables:` block (see the note on `include:` above).
|
|
108
|
+
- component: gitlab.com/op_so/components/pages/bompage@v1.0.0
|
|
109
|
+
inputs:
|
|
110
|
+
bompage_push_stage: sbom-release
|
|
111
|
+
bompage_push_repo: "https://gitlab.com/op_so/pages/sbom-bompage.git"
|
|
112
|
+
bompage_push_component: bompage
|
|
113
|
+
bompage_push_sbom: sbom.cdx.json
|
|
114
|
+
bompage_push_sbom_format: cyclonedx
|
|
115
|
+
bompage_pages_enabled: false
|
|
116
|
+
bompage_prune_enabled: false
|
|
97
117
|
|
|
98
118
|
stages:
|
|
99
119
|
- prepare-uv
|
|
@@ -104,6 +124,10 @@ stages:
|
|
|
104
124
|
- pypi-release
|
|
105
125
|
- pages-release
|
|
106
126
|
- sbom-release
|
|
127
|
+
# The bompage component always declares `bompage-prune` in this stage, even
|
|
128
|
+
# with `bompage_prune_enabled: false`; the stage must exist or the whole
|
|
129
|
+
# pipeline is rejected. The job itself never runs here.
|
|
130
|
+
- prune
|
|
107
131
|
- renovate
|
|
108
132
|
|
|
109
133
|
# Shared pnpm setup for the dashboard sources (web-src/). corepack reads the
|
|
@@ -239,7 +263,7 @@ test-e2e:
|
|
|
239
263
|
rules: *skip-when-jobs-disabled
|
|
240
264
|
before_script:
|
|
241
265
|
# Pinned uv release tarball, same reasoning and shape as the Trivy / Node
|
|
242
|
-
# installs in `
|
|
266
|
+
# installs in `generate-sbom`: `curl … | sh` returns 0 when the download fails
|
|
243
267
|
# (the installer just gets an empty script) and the job then dies later on a
|
|
244
268
|
# confusing "uv: not found".
|
|
245
269
|
- |
|
|
@@ -303,7 +327,7 @@ test-e2e:
|
|
|
303
327
|
policy: pull
|
|
304
328
|
interruptible: true
|
|
305
329
|
|
|
306
|
-
# Required by publish-pypi-uv / pages /
|
|
330
|
+
# Required by publish-pypi-uv / pages / generate-sbom, so it must survive the
|
|
307
331
|
# DISABLE_JOBS switch the tag pipeline sets in `workflow:` above (the component
|
|
308
332
|
# ships `- if: $DISABLE_JOBS == "true" when: never` as its first rule).
|
|
309
333
|
prepare-uv:sync:
|
|
@@ -400,47 +424,23 @@ pages:
|
|
|
400
424
|
rules:
|
|
401
425
|
- if: $CI_COMMIT_TAG
|
|
402
426
|
|
|
403
|
-
# Dogfooding: on a release tag, generate bompage's own SBOM with Trivy
|
|
404
|
-
#
|
|
405
|
-
#
|
|
406
|
-
#
|
|
407
|
-
#
|
|
408
|
-
# (reports/bompage/sbom-YYYYMMDD-<version>.json) and the commit message
|
|
409
|
-
# `push_report`'s anti-noise filter skips the commit
|
|
410
|
-
# byte-identical to the previous snapshot.
|
|
411
|
-
|
|
412
|
-
# Requires a masked CI/CD variable BOMPAGE_TOKEN: a project/group access token
|
|
413
|
-
# scoped to write_repository on gitlab.com/op_so/pages/sbom-bompage only (spec
|
|
414
|
-
# section 5.3). `bompage push` reads it from that variable.
|
|
415
|
-
#
|
|
416
|
-
# Signed (attestation spec section 6.3): a masked, protected CI/CD variable
|
|
417
|
-
# BOMPAGE_SIGN_KEY on this project holds the private ed25519 key (see
|
|
418
|
-
# deploy/README.md's "Attestation" section for how it was generated and how
|
|
419
|
-
# its public half was added to `trust/allowed_signers` in the central
|
|
420
|
-
# gitlab.com/op_so/pages/sbom-bompage repository). `bompage push` picks it up
|
|
421
|
-
# on its own (`--sign-key` reads the BOMPAGE_SIGN_KEY env var by default, same
|
|
422
|
-
# mechanism as BOMPAGE_TOKEN above) -- but it shells out to `ssh-keygen` to do
|
|
423
|
-
# the signing (attest.py), so `openssh-client` must be installed below like
|
|
424
|
-
# every other job in this file that touches git/signing.
|
|
425
|
-
push-sbom:
|
|
427
|
+
# Dogfooding: on a release tag, generate bompage's own SBOM with Trivy (spec
|
|
428
|
+
# sections 5 and 9). The released version is carried two ways: `uv lock`
|
|
429
|
+
# restamps it into uv.lock so Trivy records it as the SBOM's root component
|
|
430
|
+
# version, and the `bompage-push` job below (from the
|
|
431
|
+
# gitlab.com/op_so/components/pages/bompage component) puts it in the snapshot
|
|
432
|
+
# name (reports/bompage/sbom-YYYYMMDD-<version>.json) and the commit message via
|
|
433
|
+
# its own `--sbom-version`. `push_report`'s anti-noise filter skips the commit
|
|
434
|
+
# when the dependency set is byte-identical to the previous snapshot.
|
|
435
|
+
generate-sbom:
|
|
426
436
|
extends: .abstract-uv
|
|
427
437
|
stage: sbom-release
|
|
428
|
-
# Pushes to an external repository: not interruptible.
|
|
429
|
-
interruptible: false
|
|
430
438
|
needs:
|
|
431
439
|
- job: "prepare-uv:sync"
|
|
432
440
|
artifacts: true
|
|
433
|
-
# Only publish the SBOM once the release itself has shipped to PyPI.
|
|
434
|
-
# `optional` keeps non-tag pipelines (where neither job runs) valid.
|
|
435
|
-
- job: publish-pypi-uv
|
|
436
|
-
artifacts: false
|
|
437
|
-
optional: true
|
|
438
|
-
variables:
|
|
439
|
-
# Central repository that hosts bompage's SBOM history.
|
|
440
|
-
BOMPAGE_REPO: "https://gitlab.com/op_so/pages/sbom-bompage.git"
|
|
441
441
|
before_script:
|
|
442
442
|
- *install-task # curl + ca-certificates + task
|
|
443
|
-
- DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends
|
|
443
|
+
- DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends xz-utils
|
|
444
444
|
# Pinned Trivy release tarball (no `curl | sh`): reproducible, fails loudly.
|
|
445
445
|
# Asset suffix is Linux-64bit (amd64) or Linux-ARM64 depending on the runner.
|
|
446
446
|
- |
|
|
@@ -484,7 +484,6 @@ push-sbom:
|
|
|
484
484
|
- PNPM_VERSION="$(node -e "console.log(require('./package.json').packageManager.split('@')[1])")"
|
|
485
485
|
- npm install --global "pnpm@${PNPM_VERSION}"
|
|
486
486
|
- pnpm install --frozen-lockfile --prod
|
|
487
|
-
- 'test -n "$BOMPAGE_TOKEN" || { echo "ERROR: CI/CD variable BOMPAGE_TOKEN is unset or empty" >&2; exit 1; }'
|
|
488
487
|
# `.venv` (bompage installed editable) matches the committed 0.0.0 lock here;
|
|
489
488
|
# stamp the real version afterwards and relock so uv.lock carries it before
|
|
490
489
|
# Trivy reads it as the SBOM's root component version.
|
|
@@ -509,19 +508,38 @@ push-sbom:
|
|
|
509
508
|
# `licenses` fields, by purl, for packages the `fs` scan above already kept.
|
|
510
509
|
- trivy rootfs --quiet --format cyclonedx --scanners license --offline-scan --output sbom-rootfs.cdx.json .
|
|
511
510
|
- uv run --no-sync python scripts/merge_sbom_licenses.py sbom-fs.cdx.json sbom-rootfs.cdx.json --output sbom.cdx.json
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
--repo "$BOMPAGE_REPO"
|
|
517
|
-
--branch main
|
|
518
|
-
--sbom-version "${CI_COMMIT_TAG#v}"
|
|
519
|
-
--sbom-format cyclonedx
|
|
520
|
-
--source-project "$CI_PROJECT_URL"
|
|
521
|
-
--source-commit "$CI_COMMIT_SHA"
|
|
522
|
-
--source-ref "$CI_COMMIT_TAG"
|
|
523
|
-
--ci-job-url "$CI_JOB_URL"
|
|
511
|
+
artifacts:
|
|
512
|
+
paths:
|
|
513
|
+
- sbom.cdx.json
|
|
514
|
+
expire_in: 1 day
|
|
524
515
|
# No $DISABLE_JOBS guard: the tag pipeline sets DISABLE_JOBS to switch off
|
|
525
|
-
# lint/test (see `workflow:`), and the SBOM
|
|
516
|
+
# lint/test (see `workflow:`), and the SBOM generation must still run so
|
|
517
|
+
# `bompage-push` below has something to push.
|
|
518
|
+
rules:
|
|
519
|
+
- if: $CI_COMMIT_TAG
|
|
520
|
+
|
|
521
|
+
# `bompage-push` is defined by the gitlab.com/op_so/components/pages/bompage
|
|
522
|
+
# component included above. Its default `rules:` skip the job when
|
|
523
|
+
# $DISABLE_JOBS == "true" -- but that variable is set to "true" exactly on the
|
|
524
|
+
# tag pipeline (see `workflow:` at the top of this file), i.e. exactly when the
|
|
525
|
+
# push must run. Overridden here the same way `publish-pypi-uv`/`pages` already
|
|
526
|
+
# override the same component-supplied guard elsewhere in this file.
|
|
527
|
+
#
|
|
528
|
+
# Signing (attestation spec section 6.3): a masked, protected CI/CD variable
|
|
529
|
+
# BOMPAGE_SIGN_KEY on this project holds the private ed25519 key (see
|
|
530
|
+
# deploy/README.md's "Attestation" section for how it was generated and how
|
|
531
|
+
# its public half was added to `trust/allowed_signers` in the central
|
|
532
|
+
# gitlab.com/op_so/pages/sbom-bompage repository). `bompage push` picks it up
|
|
533
|
+
# on its own from the environment -- the component does not need to know about
|
|
534
|
+
# it, same as BOMPAGE_TOKEN.
|
|
535
|
+
bompage-push:
|
|
536
|
+
needs:
|
|
537
|
+
- job: generate-sbom
|
|
538
|
+
artifacts: true
|
|
539
|
+
# Only publish the SBOM once the release itself has shipped to PyPI.
|
|
540
|
+
# `optional` keeps non-tag pipelines (where neither job runs) valid.
|
|
541
|
+
- job: publish-pypi-uv
|
|
542
|
+
artifacts: false
|
|
543
|
+
optional: true
|
|
526
544
|
rules:
|
|
527
545
|
- if: $CI_COMMIT_TAG
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: bompage
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.3.0
|
|
4
4
|
Summary: A generic, server-less system to track SBOM history of N components and publish a static dashboard
|
|
5
5
|
Project-URL: Homepage, https://gitlab.com/op_so/pages/bompage
|
|
6
6
|
Project-URL: Documentation, https://op_so.gitlab.io/pages/bompage/
|
|
@@ -163,17 +163,19 @@ a signed attestation sidecar (`.att` / `.att.sig`) beside the SBOM, which
|
|
|
163
163
|
changes — signing is entirely opt-in.
|
|
164
164
|
|
|
165
165
|
On GitLab you usually do not call it by hand — include the ready-made
|
|
166
|
-
|
|
166
|
+
`bompage-push` job instead (signing, if `BOMPAGE_SIGN_KEY` is set in the
|
|
167
|
+
project, is picked up automatically — see `deploy/README.md`):
|
|
167
168
|
|
|
168
169
|
```yaml
|
|
170
|
+
stages:
|
|
171
|
+
- sbom-release # bompage_push_stage default; declare it or override the input
|
|
172
|
+
|
|
169
173
|
include:
|
|
170
|
-
- component: gitlab.com/op_so/pages/bompage
|
|
174
|
+
- component: gitlab.com/op_so/components/pages/bompage@v1.0.0
|
|
171
175
|
inputs:
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
bompage_version: "1.2.3"
|
|
176
|
-
bompage_sign_key_var: BOMPAGE_SIGN_KEY # optional, see deploy/README.md
|
|
176
|
+
bompage_push_repo: https://gitlab.com/acme/bompage.git
|
|
177
|
+
bompage_push_component: app-api
|
|
178
|
+
bompage_push_sbom: build/sbom.cdx.json
|
|
177
179
|
```
|
|
178
180
|
|
|
179
181
|
### `bompage prune` — keep the repository small
|
|
@@ -134,17 +134,19 @@ a signed attestation sidecar (`.att` / `.att.sig`) beside the SBOM, which
|
|
|
134
134
|
changes — signing is entirely opt-in.
|
|
135
135
|
|
|
136
136
|
On GitLab you usually do not call it by hand — include the ready-made
|
|
137
|
-
|
|
137
|
+
`bompage-push` job instead (signing, if `BOMPAGE_SIGN_KEY` is set in the
|
|
138
|
+
project, is picked up automatically — see `deploy/README.md`):
|
|
138
139
|
|
|
139
140
|
```yaml
|
|
141
|
+
stages:
|
|
142
|
+
- sbom-release # bompage_push_stage default; declare it or override the input
|
|
143
|
+
|
|
140
144
|
include:
|
|
141
|
-
- component: gitlab.com/op_so/pages/bompage
|
|
145
|
+
- component: gitlab.com/op_so/components/pages/bompage@v1.0.0
|
|
142
146
|
inputs:
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
bompage_version: "1.2.3"
|
|
147
|
-
bompage_sign_key_var: BOMPAGE_SIGN_KEY # optional, see deploy/README.md
|
|
147
|
+
bompage_push_repo: https://gitlab.com/acme/bompage.git
|
|
148
|
+
bompage_push_component: app-api
|
|
149
|
+
bompage_push_sbom: build/sbom.cdx.json
|
|
148
150
|
```
|
|
149
151
|
|
|
150
152
|
### `bompage prune` — keep the repository small
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
---
|
|
2
|
+
# CI for the CENTRAL bompage repository (not the Python package repo).
|
|
3
|
+
#
|
|
4
|
+
# - On a push to the default branch: rebuild and republish the static dashboard
|
|
5
|
+
# (spec-bompage.md sections 3 and 8).
|
|
6
|
+
# - On a *scheduled* pipeline: run `bompage prune` to drop SBOM snapshots older
|
|
7
|
+
# than the retention depth (spec section 5.4). Its commit lands on the default
|
|
8
|
+
# branch and triggers a fresh pipeline whose `pages` job republishes.
|
|
9
|
+
#
|
|
10
|
+
# Both jobs come from the gitlab.com/op_so/components/pages/bompage CI/CD
|
|
11
|
+
# component (its image bundles the bompage CLI, git and ssh-keygen -- no more
|
|
12
|
+
# per-job `pip install bompage==...`).
|
|
13
|
+
#
|
|
14
|
+
# CI/CD variables:
|
|
15
|
+
# BOMPAGE_TOKEN required by both jobs; scoped token with write_repository on
|
|
16
|
+
# THIS repository. Must be a project/group access token or PAT
|
|
17
|
+
# -- the prune commit is pushed with it, and only a real token
|
|
18
|
+
# (not CI_JOB_TOKEN) makes that push start the follow-up
|
|
19
|
+
# pipeline whose `pages` job republishes the dashboard.
|
|
20
|
+
|
|
21
|
+
# Interruptible by default so superseded pipelines free their runners; the prune
|
|
22
|
+
# job opts out because it commits and pushes. Retries cover only causes that are
|
|
23
|
+
# never the code itself: infra hiccups plus transient GitLab API failures.
|
|
24
|
+
default:
|
|
25
|
+
interruptible: true
|
|
26
|
+
retry:
|
|
27
|
+
max: 2
|
|
28
|
+
when:
|
|
29
|
+
- api_failure
|
|
30
|
+
- runner_system_failure
|
|
31
|
+
- stuck_or_timeout_failure
|
|
32
|
+
- scheduler_failure
|
|
33
|
+
|
|
34
|
+
include:
|
|
35
|
+
- component: gitlab.com/op_so/components/pages/bompage@v1.0.0
|
|
36
|
+
inputs:
|
|
37
|
+
# `--metadata` rows fill the dashboard header. A value that is an http(s)
|
|
38
|
+
# URL is rendered as a link, so point readers at where the data comes
|
|
39
|
+
# from: this repository, the pipeline that built the page, and the tool
|
|
40
|
+
# itself. Drop or replace any of them -- they are an example, not a
|
|
41
|
+
# requirement. `--allowed-signers` is this repository's trust root
|
|
42
|
+
# (attestation spec section 5); passed explicitly even though it matches
|
|
43
|
+
# the default (`<reports>/../trust/allowed_signers`) so a reader does not
|
|
44
|
+
# have to know that default to see where verification gets its keys
|
|
45
|
+
# from. An empty or placeholder trust/allowed_signers (see that file)
|
|
46
|
+
# simply makes every verdict read `unknown` -- it never fails the build.
|
|
47
|
+
bompage_pages_metadata: >-
|
|
48
|
+
--allowed-signers trust/allowed_signers
|
|
49
|
+
--metadata "Repository=$CI_PROJECT_URL"
|
|
50
|
+
--metadata "Pipeline=$CI_PIPELINE_URL"
|
|
51
|
+
--metadata "Built with=https://gitlab.com/op_so/pages/bompage"
|
|
52
|
+
|
|
53
|
+
stages:
|
|
54
|
+
- prune
|
|
55
|
+
- pages
|
|
@@ -17,7 +17,7 @@ trust/
|
|
|
17
17
|
allowed_signers # trust root for SBOM attestation verification (optional, see below)
|
|
18
18
|
web/ # optional: a full built page (index.html + assets/) that
|
|
19
19
|
# replaces the packaged dashboard, via `bompage build --web-dir web`
|
|
20
|
-
.gitlab-ci.yml #
|
|
20
|
+
.gitlab-ci.yml # includes the bompage CI/CD component: pages + prune jobs
|
|
21
21
|
```
|
|
22
22
|
|
|
23
23
|
The dashboard page carries **no project data**: `bompage build` writes the
|
|
@@ -48,22 +48,28 @@ component's; reusing one is refused rather than shown twice.
|
|
|
48
48
|
|
|
49
49
|
## Ingesting SBOMs
|
|
50
50
|
|
|
51
|
-
Each source pipeline pushes with the `push
|
|
52
|
-
|
|
51
|
+
Each source pipeline pushes with the `bompage-push` job from the
|
|
52
|
+
`gitlab.com/op_so/components/pages/bompage` GitLab CI/CD component:
|
|
53
53
|
|
|
54
54
|
```yaml
|
|
55
|
+
stages:
|
|
56
|
+
- sbom-release # bompage_push_stage default; declare it or override the input
|
|
57
|
+
|
|
55
58
|
include:
|
|
56
|
-
- component: gitlab.com/op_so/pages/bompage
|
|
59
|
+
- component: gitlab.com/op_so/components/pages/bompage@v1.0.0
|
|
57
60
|
inputs:
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
61
|
+
bompage_push_repo: https://gitlab.com/acme/bompage.git # this repo
|
|
62
|
+
bompage_push_component: my-service
|
|
63
|
+
bompage_push_sbom: artifacts/sbom.spdx.json
|
|
64
|
+
bompage_push_sbom_format: spdx
|
|
62
65
|
```
|
|
63
66
|
|
|
64
|
-
The job
|
|
65
|
-
this repo with the scoped token in `$BOMPAGE_TOKEN`, writes
|
|
66
|
-
`reports/my-service/sbom-YYYYMMDD.json` and commits only if the content
|
|
67
|
+
The job's image bundles a pinned `bompage` release and runs `bompage push`: it
|
|
68
|
+
clones this repo with the scoped token in `$BOMPAGE_TOKEN`, writes
|
|
69
|
+
`reports/my-service/sbom-YYYYMMDD.json` and commits only if the content
|
|
70
|
+
changed. By default it runs only on the tag pipeline, versioning the snapshot
|
|
71
|
+
from the tag; set `bompage_push_on: branch` to instead run from the consumer's
|
|
72
|
+
default branch, versioned `0.0.0-$CI_COMMIT_SHORT_SHA`.
|
|
67
73
|
|
|
68
74
|
## Attestation (optional signing)
|
|
69
75
|
|
|
@@ -88,8 +94,10 @@ this section for a deployment that does not need it, nothing else changes.
|
|
|
88
94
|
```
|
|
89
95
|
|
|
90
96
|
3. Store the **private** key (`bompage-push`, no extension — never the `.pub`
|
|
91
|
-
file) as a **masked, protected** CI/CD variable
|
|
92
|
-
every source project that should sign its pushes.
|
|
97
|
+
file) as a **masked, protected** CI/CD variable named exactly
|
|
98
|
+
`BOMPAGE_SIGN_KEY`, in every source project that should sign its pushes.
|
|
99
|
+
`bompage push` reads that variable from the job environment on its own —
|
|
100
|
+
the `bompage-push` component job needs no extra input to enable signing.
|
|
93
101
|
|
|
94
102
|
Because GitLab CI masked variables strictly forbid whitespace and newlines,
|
|
95
103
|
encode the private key as a single-line Base64 string before saving it
|
|
@@ -98,21 +106,9 @@ this section for a deployment that does not need it, nothing else changes.
|
|
|
98
106
|
- **Linux:** `base64 -w 0 bompage-push`
|
|
99
107
|
- **macOS:** `base64 -i bompage-push | tr -d '\n'`
|
|
100
108
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
include:
|
|
105
|
-
- component: gitlab.com/op_so/pages/bompage/push-report@v1
|
|
106
|
-
inputs:
|
|
107
|
-
component: my-service
|
|
108
|
-
sbom_path: artifacts/sbom.spdx.json
|
|
109
|
-
bompage_repo: https://gitlab.com/acme/bompage.git
|
|
110
|
-
bompage_version: "1.2.3"
|
|
111
|
-
bompage_sign_key_var: BOMPAGE_SIGN_KEY
|
|
112
|
-
```
|
|
113
|
-
|
|
114
|
-
A source pipeline that omits `bompage_sign_key_var` keeps pushing unsigned —
|
|
115
|
-
its snapshots simply verify as `unknown` here, never as a build failure.
|
|
109
|
+
A source pipeline whose project never sets `BOMPAGE_SIGN_KEY` keeps pushing
|
|
110
|
+
unsigned — its snapshots simply verify as `unknown` here, never as a build
|
|
111
|
+
failure.
|
|
116
112
|
|
|
117
113
|
### Protecting `trust/`
|
|
118
114
|
|
|
@@ -175,8 +171,8 @@ bompage prune --repo <url> --token <token> --dry-run
|
|
|
175
171
|
lexicographically. Every source pipeline must respect this, including those
|
|
176
172
|
driven by a third-party provisioning tool.
|
|
177
173
|
- Source pipelines push with a **scoped token** limited to this repository, and
|
|
178
|
-
commit only when the SBOM content actually changed — the `push
|
|
179
|
-
component centralises this (anti-noise filter, spec sections 5.1 / 5.2 / 10).
|
|
174
|
+
commit only when the SBOM content actually changed — the `bompage-push`
|
|
175
|
+
component job centralises this (anti-noise filter, spec sections 5.1 / 5.2 / 10).
|
|
180
176
|
- Hosts provisioned via IaC generate their SBOM on the target, fetch it back to
|
|
181
177
|
the controller/runner, and the **controller** commits and pushes — never the
|
|
182
178
|
host itself (spec section 3).
|
|
@@ -16,9 +16,11 @@
|
|
|
16
16
|
# 1. ssh-keygen -q -t ed25519 -N '' -C bompage-push -f bompage-push
|
|
17
17
|
# 2. Add the line above with the contents of bompage-push.pub.
|
|
18
18
|
# 3. Store the PRIVATE key (bompage-push, no extension) as a masked,
|
|
19
|
-
# protected CI/CD variable
|
|
20
|
-
#
|
|
21
|
-
#
|
|
19
|
+
# protected CI/CD variable named exactly BOMPAGE_SIGN_KEY, in every
|
|
20
|
+
# source pipeline that should sign its pushes -- never commit it.
|
|
21
|
+
# `bompage push` (the `bompage-push` job of the
|
|
22
|
+
# gitlab.com/op_so/components/pages/bompage component) reads that
|
|
23
|
+
# variable from the environment on its own; no extra wiring needed.
|
|
22
24
|
#
|
|
23
25
|
# This file is the REAL trust root -- unlike its read-only copy the published
|
|
24
26
|
# site may serve for convenience, it lives in the central repository and is
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "bompage"
|
|
3
|
-
version = "1.
|
|
3
|
+
version = "1.3.0"
|
|
4
4
|
description = "A generic, server-less system to track SBOM history of N components and publish a static dashboard"
|
|
5
5
|
authors = [
|
|
6
6
|
{ name = "FX Soubirou", email = "soubirou@yahoo.fr" }
|
|
@@ -91,9 +91,9 @@ show_missing = true
|
|
|
91
91
|
fail_under = 100
|
|
92
92
|
|
|
93
93
|
[tool.coverage.run]
|
|
94
|
-
# `scripts/merge_sbom_licenses.py` runs in the release pipeline (the
|
|
95
|
-
# job), so it is held to the same bar as `src/`. The demo-data
|
|
96
|
-
# local convenience with no CI role and is left out.
|
|
94
|
+
# `scripts/merge_sbom_licenses.py` runs in the release pipeline (the
|
|
95
|
+
# `generate-sbom` job), so it is held to the same bar as `src/`. The demo-data
|
|
96
|
+
# generator is a local convenience with no CI role and is left out.
|
|
97
97
|
source = ["src", "scripts"]
|
|
98
98
|
omit = ["scripts/gen-demo-data.py"]
|
|
99
99
|
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
"customManagers": [
|
|
11
11
|
{
|
|
12
12
|
"customType": "regex",
|
|
13
|
-
"description": "Trivy version pinned in .gitlab-ci.yml (
|
|
13
|
+
"description": "Trivy version pinned in .gitlab-ci.yml (generate-sbom job)",
|
|
14
14
|
"managerFilePatterns": ["/^\\.gitlab-ci\\.yml$/"],
|
|
15
15
|
"matchStrings": ["TRIVY_VERSION:\\s*\"(?<currentValue>[^\"]+)\""],
|
|
16
16
|
"datasourceTemplate": "github-releases",
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
},
|
|
20
20
|
{
|
|
21
21
|
"customType": "regex",
|
|
22
|
-
"description": "Node version pinned in .gitlab-ci.yml (
|
|
22
|
+
"description": "Node version pinned in .gitlab-ci.yml (generate-sbom job)",
|
|
23
23
|
"managerFilePatterns": ["/^\\.gitlab-ci\\.yml$/"],
|
|
24
24
|
"matchStrings": ["NODE_VERSION:\\s*\"(?<currentValue>[^\"]+)\""],
|
|
25
25
|
"datasourceTemplate": "github-tags",
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""Path-segment safety shared by every module that turns untrusted input into
|
|
2
|
+
a file name under ``reports/<component>/``.
|
|
3
|
+
|
|
4
|
+
``push`` validates ``component`` / ``sbom_version`` before writing there
|
|
5
|
+
(spec §4: one directory = one component); ``attest`` validates an
|
|
6
|
+
attestation's own ``prev.file`` before joining it onto the same tree
|
|
7
|
+
(SECURITY.md: an input that escapes ``reports/<component>/`` is a
|
|
8
|
+
vulnerability regardless of what leaks). Both need the identical rule --
|
|
9
|
+
reject separators and ``.`` / ``..`` -- so it lives here once rather than as
|
|
10
|
+
two copies that could silently drift apart.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import re
|
|
16
|
+
|
|
17
|
+
SAFE_SEGMENT = re.compile(r"[A-Za-z0-9][A-Za-z0-9._-]*")
|
|
@@ -12,6 +12,7 @@ from __future__ import annotations
|
|
|
12
12
|
|
|
13
13
|
from datetime import date
|
|
14
14
|
|
|
15
|
+
from bompage.analysis.timeline import highest_by_name
|
|
15
16
|
from bompage.models import ComponentReport, Drift, PackageOccurrence
|
|
16
17
|
|
|
17
18
|
# A drift needs the package on at least this many components, at this many
|
|
@@ -25,22 +26,22 @@ def build_package_index(
|
|
|
25
26
|
"""Map each package name to one occurrence per component that carries it.
|
|
26
27
|
|
|
27
28
|
A component may list the same name at several versions (spec section 7.3
|
|
28
|
-
speaks of "the version detected on each", singular); keep the highest
|
|
29
|
-
|
|
29
|
+
speaks of "the version detected on each", singular); keep the highest --
|
|
30
|
+
via the same tie-break :func:`bompage.analysis.timeline.highest_by_name`
|
|
31
|
+
applies -- so a within-component duplicate is not mistaken for a
|
|
32
|
+
cross-component drift, and the drift list can never disagree with the
|
|
33
|
+
timeline about which version a component is "really" on.
|
|
30
34
|
"""
|
|
31
35
|
index: dict[str, list[PackageOccurrence]] = {}
|
|
32
36
|
for component in sorted(components, key=lambda c: c.name):
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
if current is None or (pkg.version or "") > (current.version or ""):
|
|
37
|
-
best[pkg.name] = PackageOccurrence(
|
|
37
|
+
for name, pkg in highest_by_name(component.packages).items():
|
|
38
|
+
index.setdefault(name, []).append(
|
|
39
|
+
PackageOccurrence(
|
|
38
40
|
component=component.name,
|
|
39
41
|
version=pkg.version,
|
|
40
42
|
ecosystem=pkg.ecosystem,
|
|
41
43
|
)
|
|
42
|
-
|
|
43
|
-
index.setdefault(name, []).append(occurrence)
|
|
44
|
+
)
|
|
44
45
|
return index
|
|
45
46
|
|
|
46
47
|
|
|
@@ -70,11 +70,23 @@ def _is_strong_copyleft(license_id: str | None) -> bool:
|
|
|
70
70
|
|
|
71
71
|
|
|
72
72
|
def build_inventory(packages: list[Package]) -> list[Package]:
|
|
73
|
-
"""Return the deduplicated, ecosystem-enriched inventory, sorted by name.
|
|
73
|
+
"""Return the deduplicated, ecosystem-enriched inventory, sorted by name.
|
|
74
|
+
|
|
75
|
+
A duplicate (name, version, purl) is merged, not dropped: one occurrence
|
|
76
|
+
of the same package can legitimately carry a license the other lacks
|
|
77
|
+
(e.g. a manifest with no license metadata vs. a lockfile-resolved entry
|
|
78
|
+
for the exact same package), and keeping only whichever happened to be
|
|
79
|
+
seen first would silently lose that license -- and with it, a copyleft
|
|
80
|
+
flag the dashboard depends on.
|
|
81
|
+
"""
|
|
74
82
|
seen: dict[tuple[str, str | None, str | None], Package] = {}
|
|
75
83
|
for pkg in packages:
|
|
76
84
|
key = (pkg.name, pkg.version, pkg.purl)
|
|
77
|
-
|
|
85
|
+
existing = seen.get(key)
|
|
86
|
+
if existing is not None and (existing.license or not pkg.license):
|
|
87
|
+
# Already captured a license (skip a duplicate lacking one), or
|
|
88
|
+
# this duplicate has none either (nothing would change) -- either
|
|
89
|
+
# way, no need to re-enrich and overwrite the entry already held.
|
|
78
90
|
continue
|
|
79
91
|
seen[key] = pkg.model_copy(
|
|
80
92
|
update={
|
|
@@ -24,7 +24,7 @@ from bompage.models import Change, Package, Snapshot, TimelineEntry
|
|
|
24
24
|
_MIN_SNAPSHOTS_FOR_DIFF = 2
|
|
25
25
|
|
|
26
26
|
|
|
27
|
-
def
|
|
27
|
+
def highest_by_name(packages: list[Package]) -> dict[str, Package]:
|
|
28
28
|
"""One representative package per name: the highest ``version or ""``.
|
|
29
29
|
|
|
30
30
|
A name may legitimately appear more than once in one snapshot (e.g. an npm
|
|
@@ -34,7 +34,9 @@ def _highest_by_name(packages: list[Package]) -> dict[str, Package]:
|
|
|
34
34
|
``diffSnapshots`` apply -- or the timeline, the deltas and the drift list
|
|
35
35
|
quietly disagree about which version a component is on. Building the map by
|
|
36
36
|
plain assignment would instead be last-wins, i.e. dependent on the order the
|
|
37
|
-
scanner happened to emit.
|
|
37
|
+
scanner happened to emit. Public (not `_`-prefixed): shared with
|
|
38
|
+
``drift.build_package_index``, which applies the exact same tie-break to a
|
|
39
|
+
component's packages -- see that module's docstring.
|
|
38
40
|
"""
|
|
39
41
|
best: dict[str, Package] = {}
|
|
40
42
|
for pkg in packages:
|
|
@@ -49,10 +51,10 @@ def snapshot_versions(snapshot: Snapshot) -> dict[str, str | None]:
|
|
|
49
51
|
|
|
50
52
|
Keeps the highest version of a name listed more than once, so the timeline
|
|
51
53
|
and the added/removed deltas stay deterministic instead of depending on
|
|
52
|
-
package order (see :func:`
|
|
54
|
+
package order (see :func:`highest_by_name`).
|
|
53
55
|
"""
|
|
54
56
|
return {
|
|
55
|
-
name: pkg.version for name, pkg in
|
|
57
|
+
name: pkg.version for name, pkg in highest_by_name(snapshot.packages).items()
|
|
56
58
|
}
|
|
57
59
|
|
|
58
60
|
|
|
@@ -99,7 +101,7 @@ def diff_last_two(
|
|
|
99
101
|
return [], []
|
|
100
102
|
per_snapshot = _resolve(snapshots, versions)
|
|
101
103
|
previous = per_snapshot[-2]
|
|
102
|
-
current =
|
|
104
|
+
current = highest_by_name(snapshots[-1].packages)
|
|
103
105
|
added = [pkg for name, pkg in current.items() if name not in previous]
|
|
104
106
|
removed = [name for name in previous if name not in current]
|
|
105
107
|
return added, sorted(removed)
|