Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 20 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -443,11 +443,28 @@ The PR comment shows you each scan; the hosted dashboard at [app.aisbom.io](http
token: ${{ secrets.AISBOM_TOKEN }}
```

Get a per-repo token at <https://app.aisbom.io/connect> (sign in with GitHub). Leave `token` unset and the Action stays purely local — nothing is sent to the dashboard.
Get a per-repo token at <https://app.aisbom.io/connect> (sign in with GitHub). Leave `token` unset and nothing is sent to the dashboard.

#### Data flow & privacy

When (and only when) `token` is set, 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: the SBOM describes the *structure and findings* of your model files (names, hashes, licenses, risk levels) — never the weights or file contents, which don't leave the GitHub runner. Data is stored in the EU (Cloudflare R2/D1, EU jurisdiction). Every upload is announced in a loud log group in your CI output, so your logs always show when a network call happened and where the data went. To stop uploading, remove the `token` input — there is no background or implicit sending.
The model files themselves never leave the GitHub runner in any configuration — the scan, the SBOM and the PR comment are all produced on the runner. Three things can go over the wire, each with its own switch:

**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.

**Share upload — off by default, enabled by `share: true`.** The same SBOM is POSTed to `aisbom.io/api/sbom-share`, which mints a **publicly-readable** viewer link retained for 30 days and adds it to the PR comment and the `share-url` output. The unguessable URL token is the only access control, and on a public repository the Action prints that URL into the workflow log, which is itself public. With `share` unset, no request reaches `aisbom.io` and `share-url` is empty.

```yaml
- uses: Lab700xOrg/aisbom@v1
with:
directory: models/
share: true # opt in to the public hosted viewer link
```

**Anonymous telemetry — on by default,** as described in [Telemetry & Privacy](#telemetry--privacy). `AISBOM_NO_TELEMETRY=1` disables telemetry only; it does not suppress either upload above.

For the two upload paths the payload is the SBOM — names, hashes, licenses, risk levels — describing the *structure and findings* of your model files, never the weights or file contents. Telemetry carries none of that: no SBOM, no file names, no hashes, no repo identifier.

> **Changed in v1.4.0.** Sharing used to be unconditional: every Action run published its SBOM to a public 30-day link whether or not `token` was set, which contradicted the paragraph above. It is now opt-in and off by default. If you consume the `share-url` output or want the viewer link in your PR comments, set `share: true`.

See [`action/README_ACTION.md`](action/README_ACTION.md) for the full inputs/outputs reference, permissions block, and troubleshooting.

Expand Down Expand Up @@ -579,7 +596,7 @@ AIsbom collects a small amount of anonymous usage telemetry — what model forma

Per `aisbom scan`: `target_type` (the **bucket**: `local` / `huggingface` / `http` / `https` — never the actual path or URL), `model_format` (the file-type bucket), `risk_level_max`, `scan_duration_ms`, `file_count`, `parse_error_count`, `strict_mode`. A `cli_scan_critical_found` event with a count is added when at least one CRITICAL is found.

If you explicitly use `--share`: the generated `sbom.json` document is uploaded to our servers and retained for 30 days to generate the shareable viewer link. That document is the **full CycloneDX SBOM** — for each scanned model it carries the file name, SHA-256 hash, detected license, and structured `aisbom:*` properties describing the file's format and scan findings (such as dangerous pickle opcodes, tensor/header metadata, model architecture details, and the assessed risk and legal status) so the hosted viewer can render per-format detail. For `hf://` scans it additionally carries the `modelCard` block described above — task, architecture, training datasets and the repo's licence/revision, all of which are already public metadata published on the model's Hugging Face page. These describe the *structure and findings* of your model files, never their weights or data. Nothing leaves your machine unless you pass `--share` and confirm the prompt (or pass `--share-yes`). A `cli_share_created` event is fired tracking whether `has_share_yes=true|false`.
If you explicitly use `--share`: the generated `sbom.json` document is uploaded to our servers and retained for 30 days to generate the shareable viewer link. That document is the **full CycloneDX SBOM** — for each scanned model it carries the file name, SHA-256 hash, detected license, and structured `aisbom:*` properties describing the file's format and scan findings (such as dangerous pickle opcodes, tensor/header metadata, model architecture details, and the assessed risk and legal status) so the hosted viewer can render per-format detail. For `hf://` scans it additionally carries the `modelCard` block described above — task, architecture, training datasets and the repo's licence/revision, all of which are already public metadata published on the model's Hugging Face page. These describe the *structure and findings* of your model files, never their weights or data. Nothing leaves your machine unless you pass `--share` and confirm the prompt (or pass `--share-yes`). Note that `AISBOM_NO_TELEMETRY=1` does **not** suppress this upload — it withholds the `cli_share_created` event only; dropping `--share` is what stops the upload. If you use the GitHub Action rather than the CLI directly, the Action passes `--share --share-yes` on your behalf when you set its `share: true` input, and passes neither otherwise. A `cli_share_created` event is fired tracking whether `has_share_yes=true|false`.

Per `aisbom diff`: a `cli_diff` event with `has_drift=true|false`.

Expand Down
8 changes: 7 additions & 1 deletion action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -44,12 +44,16 @@ inputs:
description: 'Default false. When true, a failed upload fails the CI job.'
required: false
default: 'false'
share:
description: 'Default false. When true, the generated SBOM is uploaded to aisbom.io to mint a publicly-readable hosted viewer link, retained 30 days, and that link is added to the PR comment and exposed as the `share-url` output. Leave unset and no request is made to aisbom.io.'
required: false
default: 'false'

outputs:
sbom-path:
description: 'Path to the generated SBOM JSON file (relative to the workspace).'
share-url:
description: 'Hosted viewer URL for the scanned SBOM (empty if --share was rejected or upload failed).'
description: 'Hosted viewer URL for the scanned SBOM. Empty unless `share: true` is set (and empty if that upload failed).'

runs:
using: 'docker'
Expand All @@ -70,3 +74,5 @@ runs:
- ${{ inputs.token }}
- ${{ inputs.platform-url }}
- ${{ inputs.fail-on-platform-error }}
# Appended, not inserted: argv positions 1-9 are load-bearing and must not shift.
- ${{ inputs.share }}
29 changes: 18 additions & 11 deletions action/README_ACTION.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,13 +33,14 @@ jobs:
That's it. The Action will:

1. Scan the `models/` directory in your PR head.
2. Upload the resulting SBOM to `aisbom.io` (via the existing `--share` flow) for a hosted viewer URL.
3. Post or update a markdown comment on the PR summarizing findings and linking to the viewer.
4. Fail the job (exit 2) if any CRITICAL findings were detected, unless you set `fail-on-risk: false`.
2. Post or update a markdown comment on the PR summarizing findings, rendered from the SBOM on the runner.
3. Fail the job (exit 2) if any CRITICAL findings were detected, unless you set `fail-on-risk: false`.

On that default, **your SBOM is not uploaded anywhere** — the scan, the SBOM and the comment are all produced on the runner. Two inputs, both off by default, send it somewhere: `share:` uploads it to `aisbom.io` for a public viewer link, and `token:` posts it to your dashboard at `app.aisbom.io`. Anonymous telemetry — event names and severity counts, never the SBOM — is separate and on by default. See [Data flow & privacy](#data-flow--privacy).

## What the PR comment looks like

A two-section panel with the highest-risk artifacts and a viewer link:
A two-section panel with the highest-risk artifacts. The viewer link shown below appears only when `share: true` is set; by default the comment is identical minus that line:

```
🛡️ AIsbom Security Scan
Expand Down Expand Up @@ -72,13 +73,14 @@ When there are no CRITICAL or HIGH findings, the comment collapses to a one-line
| `token` | _(empty)_ | Optional. Per-repo API token for posting the generated SBOM to your hosted inventory dashboard at app.aisbom.io. Leave unset for purely local PR-comment behavior. Get a token at <https://app.aisbom.io/connect>. |
| `platform-url` | `https://app.aisbom.io` | Override for the platform webhook URL. Only meaningful when `token` is set. |
| `fail-on-platform-error` | `false` | Default false. When true, a failed upload fails the CI job. |
| `share` | `false` | Upload the SBOM to `aisbom.io` for a **publicly-readable** hosted viewer link, retained 30 days, added to the PR comment and exposed as `share-url`. Off by default: leave it unset and no request reaches `aisbom.io`. |

## Outputs

| Name | Description |
|---|---|
| `sbom-path` | Path to the generated SBOM (default `sbom.json`). |
| `share-url` | Hosted viewer URL for the SBOM (empty if upload was skipped or failed). |
| `share-url` | Hosted viewer URL for the SBOM. **Empty unless you set `share: true`** (and empty if that upload failed). |

Example downstream usage:

Expand All @@ -87,10 +89,13 @@ Example downstream usage:
id: aisbom
with:
directory: models/
share: true # required — without it share-url is empty
- name: Echo viewer link
run: echo "SBOM at ${{ steps.aisbom.outputs.share-url }}"
```

The `sbom-path` output needs no opt-in — the SBOM is always written to the workspace, so uploading a build artifact or running your own tooling over it works with sharing off.

## Permissions

The minimum needed by the consuming workflow:
Expand All @@ -107,15 +112,17 @@ If you omit `pull-requests: write`, the scan still runs and the SBOM is still pr

The Action embeds a hidden `<!-- aisbom-action -->` marker in the comment body. On every re-run, it finds the existing comment by that marker and updates it in place — you'll never see stacked AIsbom comments on the same PR. Only the **first** post triggers a PR notification; subsequent updates are silent (GitHub's API treats `edit` differently from `create`).

## Privacy
## Data flow & privacy

Scans run inside the Action container; the model files themselves never leave the GitHub runner. Three things can be sent over the wire, each with its own switch:

Scans run inside the Action container; the model files themselves never leave the GitHub runner. Three things can be sent over the wire:
1. **SBOM share upload — off by default, enabled by `share: true`.** The rendered CycloneDX JSON is POSTed to `aisbom.io/api/sbom-share`, which mints a **publicly-readable** viewer link retained for 30 days; the unguessable URL token is the only access control. With `share` unset — the default — no request is made to `aisbom.io` and the `share-url` output is empty. Note that on a public repository the Action prints that URL into the workflow log, which is itself public.
2. **Hosted dashboard upload — off by default, enabled by setting `token`.** 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 to stop.
3. **Anonymous telemetry — on by default.** Two events (`github_action_run` and `github_action_comment_posted`) are POSTed to `api.aisbom.io/v1/telemetry`, plus the CLI's own scan events. No repo identifier, no file paths, no findings content — just severity buckets and whether the comment was created vs updated. Set `AISBOM_NO_TELEMETRY=1` in your workflow's `env:` block to disable.

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.
For the two upload paths (1 and 2) the payload is the SBOM — file names, SHA-256 hashes, licenses, risk and legal findings — never model weights or file contents. Telemetry (3) carries none of that: no SBOM, no file names, no hashes, no repo identifier — just event names and low-cardinality parameters such as severity counts.

To disable 1 and 3, set `AISBOM_NO_TELEMETRY=1` in your workflow's `env:` block. The Action still posts the comment and produces the SBOM; the share upload and telemetry pings are skipped. The dashboard upload (2) is controlled solely by whether `token` is set.
`AISBOM_NO_TELEMETRY=1` disables (3) only. It does **not** suppress the share upload: with `share: true` the SBOM is still uploaded, and only the `cli_share_created` event is withheld. Leave `share` unset to stop the upload itself.

## Troubleshooting

Expand Down
40 changes: 32 additions & 8 deletions action/entrypoint.sh
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
# $7 token (optional — opt-in for platform upload)
# $8 platform-url (optional override; blank → default in helper)
# $9 fail-on-platform-error (default "false")
# $10 share (default "false" — opt-in hosted share link)
#
# Bash (not POSIX sh) is required for the PIPESTATUS array — we need the
# scan's exit code, not tee's, to honor fail-on-risk correctly.
Expand All @@ -38,34 +39,56 @@ FAIL_ON_RISK="${6:-true}"
INPUT_TOKEN="${7:-}"
INPUT_PLATFORM_URL="${8:-}"
INPUT_FAIL_ON_PLATFORM_ERROR="${9:-false}"
INPUT_SHARE="${10:-false}"

# Pass the token through to post_comment.py via a clean underscore-only env
# var. We never echo $GH_TOKEN — GitHub already masks it in the docker-run
# command log, but using a properly-named env var keeps secret hygiene easy.
export AISBOM_GITHUB_TOKEN="${GH_TOKEN}"

SCAN_LOG="/tmp/aisbom-scan.log"
# Overridable so the regression suite can run this script without racing on a
# fixed /tmp path. Unset in the Docker image, which is the only place it runs
# for real.
SCAN_LOG="${AISBOM_SCAN_LOG:-/tmp/aisbom-scan.log}"

# Step 1 — Run the scan. Sharing is OPT-IN: `--share --share-yes` uploads the
# SBOM to aisbom.io and mints a publicly-readable 30-day viewer link, so it is
# only passed when the user explicitly sets `share: true`. Everything else the
# Action does — the SBOM artifact, the PR comment, fail-on-risk, the platform
# upload — renders from the local SBOM and works identically with sharing off.
SHARE_ARGS=()
if [ "${INPUT_SHARE}" = "true" ]; then
SHARE_ARGS=(--share --share-yes)
else
# Deliberately scoped to the SBOM. Anonymous telemetry is default-on and
# goes to api.aisbom.io, so a blanket "nothing is sent" would be false —
# the exact kind of overclaim this input exists to correct.
echo "[aisbom-action] Sharing is off (share: false, the default): the SBOM is not uploaded to aisbom.io and the share-url output will be empty. Set share: true to publish a hosted viewer link. (Anonymous telemetry is separate and still on; set AISBOM_NO_TELEMETRY=1 to disable it.)"
fi

# Step 1 — Run the scan. `--share --share-yes` uploads the SBOM and emits
# a viewer URL we can grep out for the PR comment.
echo "::group::aisbom scan output"
set -o pipefail
# `${SHARE_ARGS[@]+"${SHARE_ARGS[@]}"}` — expanding an empty array under
# `set -u` is an unbound-variable error on bash < 4.4; this form yields no
# words at all when the array is empty.
aisbom scan "${DIRECTORY}" \
--output "${OUTPUT_FILE}" \
--share \
--share-yes \
${SHARE_ARGS[@]+"${SHARE_ARGS[@]}"} \
2>&1 | tee "${SCAN_LOG}"
SCAN_EXIT=${PIPESTATUS[0]}
set +o pipefail
echo "::endgroup::"

# Echo Action outputs so consumers can reference them in subsequent steps.
# `share-url` is always written — empty when sharing is off — so consumers read
# an empty string rather than an unset output.
if [ -n "${GITHUB_OUTPUT:-}" ]; then
echo "sbom-path=${OUTPUT_FILE}" >> "${GITHUB_OUTPUT}"
SHARE_URL=$(grep -oE 'https://aisbom\.io/viewer\?h=[A-Za-z0-9_-]+' "${SCAN_LOG}" | head -n1 || true)
if [ -n "${SHARE_URL}" ]; then
echo "share-url=${SHARE_URL}" >> "${GITHUB_OUTPUT}"
SHARE_URL=""
if [ "${INPUT_SHARE}" = "true" ]; then
SHARE_URL=$(grep -oE 'https://aisbom\.io/viewer\?h=[A-Za-z0-9_-]+' "${SCAN_LOG}" | head -n1 || true)
fi
Comment on lines +88 to 90

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 👍 / 👎.

echo "share-url=${SHARE_URL}" >> "${GITHUB_OUTPUT}"
fi

# Step 2 — Post the PR comment. Only run if the scan actually produced
Expand All @@ -77,6 +100,7 @@ if [ -f "${OUTPUT_FILE}" ]; then
--max-rows "${MAX_ROWS}" \
--comment-on-clean "${COMMENT_ON_CLEAN}" \
--directory "${DIRECTORY}" \
--share-enabled "${INPUT_SHARE}" \
|| echo "[aisbom-action] post_comment.py errored; SBOM artifact still produced."
else
echo "[aisbom-action] No SBOM file at ${OUTPUT_FILE}; skipping PR comment."
Expand Down
Loading