feat(action): upload VEX documents with the SBOM when a platform token is set - #105
Conversation
…n is set
Exploitability statements never left the runner. `aisbom scan --vex` writes
them next to the SBOM, but the Action uploaded only the SBOM itself, so a
connected repo's hosted inventory could show what the repo contains and
never whether a finding was actually exploitable — the question the EU
Cyber Resilience Act and FDA §524B ask about most directly.
Setting `token` now also runs the scan with `--vex`, and the resulting
OpenVEX and CycloneDX VEX documents are uploaded in the same request as
`{"sbom": ..., "vex": [...]}`. The receiver sniffs the body shape, so an
upload carrying no VEX is still posted as the raw SBOM bytes, verbatim —
byte-identical to what every previous release sent.
Generation is tied to the platform opt-in rather than made unconditional.
A user who has not connected a repo gets no extra files in their
workspace and no extra work in their scan; a user who has gets the data
without editing their workflow, which is the only way an inventory
feature reaches the people who already enabled it.
Failure handling matches the existing best-effort posture: an unreadable
or non-object VEX sibling is skipped with a log line rather than failing
the upload, because the SBOM is what the user actually needs in their
inventory. An SBOM that cannot be parsed is posted raw so the receiver
returns its own specific rejection reason instead of one invented here.
`vex_paths_for` mirrors `aisbom.cli._vex_paths` and must stay in step: if
the two drift, a plain `scan --vex` writes documents this helper never
looks for and the upload silently stops carrying them.
Verified end to end against real output, not stubs — scanning the mock
artifacts with --vex and building the actual request body produced a
15.7 KB envelope with both documents bound to the SBOM's serial number
and states the receiver accepts.
The README privacy section said the SBOM was "the entire payload", which
this change would have made false; it now describes the VEX documents,
what they do and do not contain, and that no token means no VEX at all.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: e160f29824
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| if [ -n "${INPUT_TOKEN}" ]; then | ||
| VEX_ARGS=(--vex) |
There was a problem hiding this comment.
Upload results before enforcing the risk exit
When a token is set and the scan finds a CRITICAL artifact under the default fail-on-risk: true, the CLI still writes the SBOM and VEX files but exits 2 (aisbom/cli.py:637-642). The entrypoint then exits at action/entrypoint.sh:124-127, before the platform uploader at lines 133-146, so the affected VEX statements this change is intended to deliver never reach the dashboard. Move the upload ahead of the risk gate while preserving the final exit status.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Fixed in 5c7cd85. This was the best catch of the review — and worse than described.
The bug predates this PR and applied to the SBOM upload as a whole, not just VEX: with fail-on-risk: true (the default), any connected repo whose scan found a CRITICAL artifact exited before the uploader and never appeared in the hosted inventory at all. The inventory has been silently omitting exactly the repos that most needed to be in it.
It also defeats this PR specifically, which is what makes it belong here rather than in a follow-up: affected VEX statements only exist when there IS a critical finding, so the data this change exists to deliver was unreachable in precisely the case that produces it.
The upload now runs before both exit gates. Exit status is unchanged for every previously reachable case. Where the two gates can now both apply — a CRITICAL scan whose upload also failed — exit 2 wins over exit 3, on the reasoning that the required check should report the dangerous model rather than the plumbing; the header exit-code table records that precedence.
Asserted by driving the real script, not by reading it: the entrypoint harness gained scan_exit / python_exit knobs and an ordered record of helper invocations, and eight tests now cover upload-on-critical, the preserved exit 2, fail-on-risk off, helper ordering, both precedence cases, and the untouched no-token path.
| **Dashboard upload — off by default, enabled by setting `token`.** The Action POSTs the generated CycloneDX SBOM JSON to `https://app.aisbom.io/v1/scan-result`, along with the branch/tag name (`GITHUB_REF_NAME`) so the dashboard can attribute results to the right ref. That's the entire payload. Data is stored in the EU (Cloudflare R2/D1, EU jurisdiction). Every upload is announced in a loud log group in your CI output. Remove the `token` input to stop. | ||
| **Dashboard upload — off by default, enabled by setting `token`.** The Action POSTs the generated CycloneDX SBOM JSON to `https://app.aisbom.io/v1/scan-result`, along with the branch/tag name (`GITHUB_REF_NAME`) so the dashboard can attribute results to the right ref. | ||
|
|
||
| Setting `token` also makes the Action run the scan with `--vex`, and the two generated VEX documents (OpenVEX and CycloneDX VEX) are uploaded in the same request as `{"sbom": …, "vex": [...]}`. They are derived entirely from findings already described in the SBOM — per finding, whether each scanned artifact is actually affected — and add no new information about your files; they let the dashboard show whether a finding is *exploitable* rather than merely present. The CI log reports how many were sent (`vex-documents=N`). That is the entire payload: SBOM, VEX documents, ref, and the trigger/run identifiers. |
There was a problem hiding this comment.
Update the action privacy reference for VEX uploads
The new disclosure conflicts with the linked full Action documentation: action/README_ACTION.md:120-123 still says the dashboard receives the same CycloneDX JSON and that both upload payloads contain only the SBOM. Users relying on that privacy reference are therefore not told that setting token now generates and transmits two additional documents; update that document alongside this disclosure.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Fixed in 5c7cd85 — correct, and I had missed that file entirely.
action/README_ACTION.md is the linked full Action reference, so leaving it saying the dashboard receives "the same CycloneDX JSON" and that both upload paths carry only the SBOM would have meant the more detailed of the two privacy documents was the wrong one. Updated to match the root README: what the VEX documents are, that they are derived from findings already in the SBOM and add no new information about the user files, the vex-documents=N log line, and that no token means no --vex and no files written into the workspace. The payload-summary sentence at the end of that section is corrected too.
… Action README Both findings from the Codex review on PR #105. A CRITICAL scan never reached the dashboard. `aisbom scan` exits 2 on a CRITICAL finding but still writes its SBOM and VEX documents, and the entrypoint honoured `fail-on-risk` — true by default — before the platform upload. So a repo containing a genuinely dangerous artifact exited the script and uploaded nothing: the hosted inventory silently omitted exactly the repos that most needed to be in it. The bug predates this change and applied to the SBOM upload as a whole, but it defeats this PR completely: `affected` VEX statements only exist when there IS a critical finding, so the data this change exists to deliver was unreachable in precisely the case that produces it. The upload now runs before both exit gates. Exit status is unchanged for every previously reachable case, and where the two new gates can now both apply — a CRITICAL scan whose upload also failed — exit 2 wins over exit 3, because the required check should report the dangerous model rather than the plumbing. The header's exit-code table records that precedence. The entrypoint test harness gained `scan_exit` / `python_exit` knobs and an ordered record of helper invocations, so this is asserted by driving the real script rather than by reading it. Also: action/README_ACTION.md still said the dashboard receives the same CycloneDX JSON and that both upload paths carry only the SBOM. Users reading that privacy reference would not have been told that setting `token` now transmits two additional documents. It now matches the root README.
Exploitability statements never left the runner.
aisbom scan --vexwrites them next to the SBOM, but the Action uploaded only the SBOM itself — so a connected repo's hosted inventory could show what the repo contains and never whether a finding was actually exploitable, which is the question the EU Cyber Resilience Act and FDA §524B ask about most directly.What changes
Setting
tokennow also runs the scan with--vex, and the resulting OpenVEX and CycloneDX VEX documents are uploaded in the same request as{"sbom": …, "vex": [...]}.The receiver sniffs the body shape, so an upload carrying no VEX is still posted as the raw SBOM bytes, verbatim — byte-identical to what every previous release sent. A test pins that.
Generation is tied to the platform opt-in, not made unconditional:
token→ no--vex, no VEX files in the workspace, no request. Zero change for the broad user base.tokenset → the data arrives without anyone editing their workflow, which is the only way an inventory feature reaches people who already enabled it.Failure handling
Matches the existing best-effort posture:
vex_paths_formirrorsaisbom.cli._vex_pathsand must stay in step: if the two drift, a plainscan --vexwrites documents this helper never looks for and the upload silently stops carrying them — a failure with no error message anywhere. Two tests pin the naming.Verification
Full suite: 1134 passed, coverage 92.8% (floor 85).
Verified end to end against real output rather than stubs — scanned the mock artifacts with
--vex, then built the actual request body from the files the CLI really wrote:Both documents bind to the SBOM's serial number, and every emitted state is one the receiver accepts — the check that matters, since a mismatch would score zero silently rather than erroring.
New tests cover: sibling naming (both suffix forms), discovery with none / one / both present, an unparseable sibling, a non-object sibling, verbatim raw body without VEX, envelope with VEX, raw fallback on an unparseable SBOM, the posted body in both shapes, the
vex-documents=Nlog line, and — driving the realentrypoint.shwith a recording stub — that--vexappears with a token, is absent without one, and doesn't drag in--share.Docs
The README privacy section stated the SBOM was "the entire payload", which this change would have made false. It now describes what the VEX documents are, that they are derived from findings already in the SBOM and add no new information about your files, that the CI log reports how many were sent, and that no token means no VEX at all.
action.yml'stokeninput says the same.