Skip to content

fix(action): make SBOM sharing opt-in and correct the privacy docs - #104

Merged
lab700xdev merged 3 commits into
mainfrom
slice-134-action-share-optin
Sep 6, 2026
Merged

fix(action): make SBOM sharing opt-in and correct the privacy docs#104
lab700xdev merged 3 commits into
mainfrom
slice-134-action-share-optin

Conversation

@lab700xdev

@lab700xdev lab700xdev commented Sep 6, 2026

Copy link
Copy Markdown
Contributor

The problem

action/entrypoint.sh invoked the scan with --share --share-yes hardcoded, with no conditional and no input to disable it. Every run of the Action, for every user, uploaded the full CycloneDX SBOM to aisbom.io/api/sbom-share and minted a publicly-readable 30-day link. --share-yes exists to bypass the CLI's confirmation prompt, and the Action passed it on the user's behalf.

The shipped docs said the opposite. From the Action's own Data flow & privacy section:

Leave token unset and the Action stays purely local — nothing is sent to the dashboard.

To stop uploading, remove the token input — there is no background or implicit sending.

Removing token stopped the dashboard upload and nothing else. The share upload was unconditional, undocumented as optional, and could not be turned off at all.

Exposure shape, stated accurately

  • The share id is 9 random bytes base64url-encoded (72 bits) — not enumerable.
  • The payload is the SBOM: file names, SHA-256 hashes, licenses, risk/legal findings, aisbom:* properties. Never weights or file contents.
  • Retention is 30 days via KV expirationTtl, after which it is deleted.
  • On public repos the Action prints the viewer URL into the workflow log, which is public — so anyone reading the log could fetch the SBOM. Arguably moot for an already-public repo, but not what the docs promised.
  • On private repos the logs are private, so the URL is not exposed — but a document describing the repo's dependency and model inventory was sitting on our infrastructure without the user opting in, while the docs told them nothing had been sent.

The exposure is modest. The documentation defect is the serious part: a security product making a privacy claim its own code contradicted.

The fix

Sharing becomes an explicit share input, default false.

  • The flags are passed only when share is exactly true. Otherwise no request reaches aisbom.io, and the entrypoint says so in the log rather than going quiet.
  • share-url is now always written to GITHUB_OUTPUT, empty when sharing is off, so consumers read an empty string instead of an unset output.
  • The SBOM artifact, the PR comment, fail-on-risk and the platform upload are untouched — all four render from the local sbom.json, which is the artifact they should have been using all along.

Two implementation details worth a reviewer's eye:

  • The input is appended as argv $10, not inserted, so positions 1–9 stay put.
  • The conditional flags expand as ${SHARE_ARGS[@]+"${SHARE_ARGS[@]}"} — expanding an empty array under set -u is an unbound-variable error on bash < 4.4.

Docs

Both READMEs now state exactly which network call each input enables, and the "no background or implicit sending" claim is gone.

While checking that claim I found a second one of the same kind: action/README_ACTION.md documented AISBOM_NO_TELEMETRY=1 as disabling the share upload. It does not — aisbom/cli.py gates the upload on share and has_content alone; the env var only withholds the cli_share_created event. Corrected in both READMEs.

Tests

tests/test_action_entrypoint.py (new, 17 tests) executes the real entrypoint.sh with a stubbed aisbom on PATH that records its argv, then asserts on that argv and on the scan log — not on the entrypoint source. A future edit that re-introduces publishing by another route still fails.

Covered: default off; share omitted entirely (an old caller passing nine args) still off; only the literal "true" enables it (TRUE/True/yes/1 do not); scan target and --output survive the conditional; share-url empty vs populated; sbom-path and the PR comment unaffected either way.

Verification

  • poetry run pytest --cov=aisbom --cov-fail-under=85 — 1111 passed, coverage 92.80%.
  • Restoring main's entrypoint and re-running the new tests fails 11 of 17, so the guard is real. One test passed vacuously on an empty log; it now asserts the log is non-empty first.
  • End-to-end with the real CLI, sharing off: no Share Link Created, zero viewer?h= in the scan log, GITHUB_OUTPUT = sbom-path=…/sbom.json plus share-url=, and sbom.json written.

Compatibility

This is a behaviour change for every existing Action user. Anyone consuming the share-url output, or expecting the viewer link in their PR comment, loses it until they set share: true. It ships as v1.4.0 and is called out in the README and the release notes as a deliberate privacy-improving change rather than going out quietly.

Left deliberately out of scope: whether the hosted share feature earns its place in the Action at all. That's a product call, not a fix.

`action/entrypoint.sh` invoked the scan with `--share --share-yes`
hardcoded, so every Action run — for every user, with or without a
`token` — uploaded the full CycloneDX SBOM to aisbom.io and minted a
publicly-readable 30-day link. The shipped docs said the opposite:
"Leave `token` unset and the Action stays purely local — nothing is
sent" and "there is no background or implicit sending".

Sharing is now an explicit `share` input, default `false`. The flags are
passed only when it is exactly `true`; otherwise no request reaches
aisbom.io and the entrypoint says so in the log. `share-url` is always
written to GITHUB_OUTPUT, empty when sharing is off, so consumers read an
empty string rather than an unset output. The SBOM artifact, the PR
comment, fail-on-risk and the platform upload are untouched — they render
from the local sbom.json, which is what they should have used all along.

The input is appended as argv $10 so positions 1-9 do not shift, and the
empty-array expansion is written `${SHARE_ARGS[@]+"${SHARE_ARGS[@]}"}` so
it stays safe under `set -u` on bash < 4.4.

Docs corrected in both READMEs, including a second claim of the same kind
found while checking the first: AISBOM_NO_TELEMETRY=1 was documented as
disabling the share upload, but it only withholds the cli_share_created
event — the upload itself is gated solely on `--share`.

tests/test_action_entrypoint.py executes the real script with a stubbed
aisbom on PATH and asserts on the recorded argv and scan log, not on the
entrypoint source, so a future edit cannot silently re-enable publishing.

BREAKING CHANGE: anyone relying on the `share-url` output or on the
viewer link in the PR comment must now set `share: true`.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 959fb8067f

ℹ️ 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".

Comment thread action/entrypoint.sh
Comment on lines +85 to 87
if [ "${INPUT_SHARE}" = "true" ]; then
SHARE_URL=$(grep -oE 'https://aisbom\.io/viewer\?h=[A-Za-z0-9_-]+' "${SCAN_LOG}" | head -n1 || true)
fi

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Gate the comment viewer link on share

When share is false, this guard only keeps share-url empty; post_comment.py still searches the entire scan log for any viewer-shaped URL and inserts the first match into the PR comment. For example, scanning a remote target such as https://aisbom.io/viewer?h=Unrelated.pt prints that target in the CLI banner, causing the comment to link to h=Unrelated despite sharing being disabled. Pass the share setting or a separately validated URL to the comment renderer so the promised opt-out also governs comments.

Useful? React with 👍 / 👎.

Comment thread action/entrypoint.sh Outdated
if [ "${INPUT_SHARE}" = "true" ]; then
SHARE_ARGS=(--share --share-yes)
else
echo "[aisbom-action] Sharing is off (share: false, the default): nothing is sent to aisbom.io and the share-url output will be empty. Set share: true to publish a hosted viewer link."

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Narrow the no-network assurance to SBOM sharing

On a default run without AISBOM_NO_TELEMETRY, the CLI emits scan telemetry and post_comment.py posts Action telemetry to api.aisbom.io, so the statement that “nothing is sent to aisbom.io” is false. The newly added Action README likewise says the default “sends nothing anywhere” before later documenting default-on telemetry. Qualify these messages as saying that no SBOM/share request is sent; otherwise privacy-conscious users receive an incorrect runtime assurance.

Useful? React with 👍 / 👎.

Comment thread action/README_ACTION.md Outdated
1. **SBOM upload (`--share`):** the rendered CycloneDX JSON is POSTed to `aisbom.io/api/sbom-share` so the comment can link to a hosted viewer. The SBOM is publicly viewable to anyone with the URL and expires after 30 days. The unguessable 12-char URL token is the only access control.
2. **Hosted dashboard upload (opt-in via `token`):** when you set the `token` input, the same CycloneDX JSON is POSTed to `https://app.aisbom.io/v1/scan-result` (or your `platform-url` override) along with the branch/tag name (`GITHUB_REF_NAME`), so your dashboard at [app.aisbom.io](https://app.aisbom.io) can track the repo's SBOM history. Data is stored in the EU. The upload is logged loudly in your CI output every time it happens. Remove the token (or the input) to stop. Without a token, nothing is sent to the dashboard.
3. **Anonymous telemetry:** two events (`github_action_run` and `github_action_comment_posted`) are POSTed to `api.aisbom.io/v1/telemetry`. No repo identifier, no file paths, no findings content — just severity buckets and whether the comment was created vs updated.
In every case the payload is the SBOM — file names, SHA-256 hashes, licenses, risk and legal findings — never model weights or file contents.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Describe the telemetry payload separately

This says that the payload is the SBOM “in every case,” including anonymous telemetry, contradicting both the preceding paragraph and the implementation: telemetry sends event names and low-cardinality parameters, not file names, hashes, licenses, or the SBOM document. Users evaluating data exposure are therefore told that default-on telemetry transmits substantially more information than it actually does; restrict this sentence to the two SBOM upload paths.

Useful? React with 👍 / 👎.

…laims

Addresses three findings from Codex review on #104.

1. The opt-out governed only the `share-url` output. post_comment.py
   independently re-derived the link by scraping the scan log with a
   URL-shaped regex, so a URL-shaped scan target or filename could put a
   viewer link in the PR comment on a run that shared nothing. The scrape
   is now gated on a `--share-enabled` argument passed from the
   entrypoint, defaulting to false, so `share: false` means no viewer link
   by construction rather than by the log happening not to match.

2. The runtime notice said "nothing is sent to aisbom.io" when sharing is
   off. Anonymous telemetry is default-on and goes to api.aisbom.io, so
   that was false — the same class of overclaim this PR exists to correct.
   Narrowed to the SBOM, with telemetry named as separate and still on.
   The Action README's quick start had the matching contradiction: it
   promised the default "sends nothing anywhere" and then documented
   default-on telemetry three paragraphs later.

3. Both READMEs said the payload is the SBOM "in every case", sweeping
   telemetry in with the two upload paths and overstating what it carries.
   Telemetry sends event names and low-cardinality parameters — no SBOM,
   no file names, no hashes, no repo identifier. Described separately.

Six new tests: four drive post_comment.main() with a decoy URL-shaped
scan log to prove the gate holds and that suppressing the link does not
suppress the findings table, and two assert the entrypoint forwards the
setting.
@lab700xdev
lab700xdev merged commit f731273 into main Sep 6, 2026
2 checks passed
@lab700xdev
lab700xdev deleted the slice-134-action-share-optin branch September 6, 2026 05:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant