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
10 changes: 10 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -84,3 +84,13 @@ jobs:
if poetry run aisbom score sbom.json --fail-under 101; then
echo "::error::--fail-under did not gate on a failing score"; exit 1
fi

# --offline must still produce an SBOM, and must refuse a target that
# can only be reached over the network rather than quietly fetching it.
- name: Smoke Test #13 - Offline Mode
run: |
poetry run aisbom scan demo_data --offline --output offline-sbom.json --no-fail-on-risk
test -s offline-sbom.json
if poetry run aisbom scan hf://google-bert/bert-base-uncased --offline --no-fail-on-risk; then
echo "::error::--offline scanned a remote target"; exit 1
fi
58 changes: 55 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -318,8 +318,9 @@ the other ids listed as `aliases`.
unexpected, the run prints a warning and the VEX documents simply carry no
CVE statements. Exit codes and model findings are unchanged.

A plain `aisbom scan` without `--vex` never contacts OSV. To keep `--vex` fully
offline, pass `--no-osv` or set `AISBOM_NO_OSV=1`.
A plain `aisbom scan` without `--vex` never contacts OSV. To skip only the OSV
lookup, pass `--no-osv` or set `AISBOM_NO_OSV=1`; to make no network access at
all, use [`--offline`](#offline-and-air-gapped-scans).

#### Remediation evidence (`fixed`)

Expand All @@ -334,6 +335,49 @@ aisbom scan . --vex --vex-baseline last-release-sbom.json --output sbom.json
Baselines predating structured findings still work: the finding is recovered
from the component description rather than reported as a spurious `fixed`.

### Dependency licenses from PyPI

Library components parsed from `requirements.txt` carry the license their
package declares on PyPI. For each exact pin (`transformers==5.13.1`) the scan
reads that release's metadata from `https://pypi.org/pypi/<name>/<version>/json`
and writes the result into the component's standard `licenses[]` field, with an
`aisbom:license:source` property of `pypi` so you can tell a registry
declaration from something read out of the file itself.

- **Exact pins only.** `torch>=2.0` doesn't say which release is installed, and
licenses occasionally change between releases, so ranges are skipped and
counted in the scan summary.
- **Nothing is guessed.** The PEP 639 `License-Expression` is used when present,
then a valid SPDX identifier or expression in the `license` field, then a
classifier that names exactly one license. A license *text* pasted into the
field, or a classifier such as `BSD License` that doesn't say which BSD,
produces no license rather than a wrong one. Other short declarations
(`Proprietary`) are kept as a license name.
- **A declaration, not a verdict.** The license never feeds the legal-risk
status shown for model files.
- **SPDX 2.3** output carries SPDX-valid values as `licenseDeclared`;
`licenseConcluded` stays `NOASSERTION`.
- **Cached** in `~/.aisbom/pypi_license_cache.json` — 30 days for a resolved
license, 24 hours for a package that declares none — so repeat CI scans of the
same pins make no requests.
- **Never breaks a scan.** If PyPI is unreachable or rate-limits the lookup, the
affected dependencies carry no license, the run prints a warning, and exit
codes are unchanged.

### Offline and air-gapped scans

```bash
aisbom scan ./models --offline
```

`--offline` (or `AISBOM_OFFLINE=1`) makes no network access of any kind: no
PyPI license lookup, no OSV lookup, no telemetry and no update check. A remote
target (`hf://`, `https://`) or `--share` is refused with exit `1` rather than
fetched, since each needs the network. The SBOM is otherwise the same, minus
the dependency licenses and CVE statements those lookups would have added.
`aisbom score <target> --offline` works the same way. See the
[air-gapped guide](docs/air-gapped-guide.md).

### Completeness score (`aisbom score`)

A scan can come back perfectly clean and still produce an SBOM that names no
Expand Down Expand Up @@ -491,6 +535,8 @@ Without a `token` the scan runs exactly as before — no `--vex`, no VEX files w

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

**Package registry lookups — on by default.** Exact `requirements.txt` pins (package name and version only) are looked up on `pypi.org` from the runner to fill in [dependency licenses](#dependency-licenses-from-pypi), and — when `token` is set, which turns on `--vex` — in the OSV database. No SBOM content is sent. Set `AISBOM_OFFLINE=1` in the step's `env:` to skip both; it also disables the CLI's telemetry, and it cannot be combined with `share: true`, which needs the network.

For the two upload paths the payload is the SBOM — names, hashes, licenses, risk levels — plus, on the dashboard path only, the VEX documents derived from those same findings. All of it describes the *structure and findings* of your model files, never the weights or file contents. Telemetry carries none of that: no SBOM, no VEX, 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`.
Expand Down Expand Up @@ -642,7 +688,11 @@ Each event carries an anonymous `user_id` — a SHA-256 of your machine's MAC ad

### OSV lookups (`--vex` only)

When you pass `--vex` and the scan finds exact `requirements.txt` pins, AIsbom sends each pinned **package name and version** to the public OSV API at `https://api.osv.dev`, and fetches the advisories it names. Nothing else is sent: no file paths, model names, hashes, findings, or identifiers. This is a request to a third-party service, not telemetry, so `AISBOM_NO_TELEMETRY` does not affect it; `--no-osv` or `AISBOM_NO_OSV=1` does. Responses are cached locally in `~/.aisbom/osv_cache.json` for 24 hours, and deleting that file is always safe.
When you pass `--vex` and the scan finds exact `requirements.txt` pins, AIsbom sends each pinned **package name and version** to the public OSV API at `https://api.osv.dev`, and fetches the advisories it names. Nothing else is sent: no file paths, model names, hashes, findings, or identifiers. This is a request to a third-party service, not telemetry, so `AISBOM_NO_TELEMETRY` does not affect it; `--no-osv`, `AISBOM_NO_OSV=1` or `--offline` does. Responses are cached locally in `~/.aisbom/osv_cache.json` for 24 hours, and deleting that file is always safe.

### PyPI license lookups (on by default)

When a scan finds exact `requirements.txt` pins and writes CycloneDX or SPDX 2.3 output, AIsbom requests each pinned **package name and version** from the public PyPI JSON API at `https://pypi.org/pypi/<name>/<version>/json` to read its declared license. Nothing else is sent: no file paths, model names, hashes, findings, or identifiers. Like the OSV lookup this is a request to a third-party service, not telemetry, so `AISBOM_NO_TELEMETRY` does not affect it; `--offline` or `AISBOM_OFFLINE=1` does. Answers are cached locally in `~/.aisbom/pypi_license_cache.json`, and deleting that file is always safe.

### What's never collected

Expand All @@ -660,6 +710,8 @@ export AISBOM_NO_TELEMETRY=1
AISBOM_NO_TELEMETRY=1 aisbom scan ./my-project
```

`AISBOM_OFFLINE=1` (or `--offline` on `scan` and `score`) goes further: it disables telemetry and also every other network request — the update check and the PyPI and OSV lookups.

### Where the data goes

Events POST to `https://api.aisbom.io/v1/telemetry` (a Cloudflare Worker we operate), which sanitizes the payload and forwards to Google Analytics 4 on the dedicated `cli.aisbom.io` data stream. We don't share, sell, or use this data for ad targeting.
Expand Down
2 changes: 1 addition & 1 deletion action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ branding:

inputs:
directory:
description: 'Directory to scan for AI model artifacts.'
description: 'Directory to scan for AI model artifacts. Exact requirements.txt pins found in it (package name and version only) are looked up on pypi.org from the runner to fill in their declared licenses; set AISBOM_OFFLINE=1 in the step env to skip network lookups.'

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Propagate offline/share refusal from the Action

When an Action user follows this description by setting AISBOM_OFFLINE=1 while share: true is configured, the new CLI rejects the combination with exit 1, but action/entrypoint.sh only propagates SCAN_EXIT == 2 and ultimately exits 0. The workflow therefore reports success despite producing no new SBOM; worse, if the configured output path already exists, the wrapper proceeds to comment on or upload that stale file. Handle the CLI's exit 1 in the Action wrapper before processing the output.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in 3d5b314. action/entrypoint.sh now rejects AISBOM_OFFLINE together with share: true before the scan runs and exits 1, so the job fails and no pre-existing sbom.json is commented on or uploaded. Exit 1 is added to the entrypoint's documented exit codes. Tests cover the refused pair (no scan, no helper invocations, exit 1) and offline without share (scans normally). I kept this narrow on purpose: the wrapper not re-raising a CLI exit 1 in general (e.g. a missing directory) predates this PR, and changing it would flip the job result for existing users, so it is tracked separately.

required: true
default: '.'
output-file:
Expand Down
4 changes: 3 additions & 1 deletion action/README_ACTION.md
Original file line number Diff line number Diff line change
Expand Up @@ -114,14 +114,16 @@ The Action embeds a hidden `<!-- aisbom-action -->` marker in the comment body.

## 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. Four things can be sent over the wire, each with its own switch:

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.

Setting `token` also runs the scan with `--vex`, and the two resulting VEX documents (OpenVEX and CycloneDX VEX) are uploaded in that same request as `{"sbom": …, "vex": [...]}`. They are derived entirely from findings already present in the SBOM — per finding, whether each scanned artifact is actually affected — and add no new information about your files; they are what lets the dashboard show whether a finding is *exploitable* rather than merely present. The log group reports how many were sent (`vex-documents=N`). With no `token`, `--vex` is not passed, no VEX files are written into your workspace, and no request is made.
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.

4. **Package registry lookups — on by default.** Exact `requirements.txt` pins found in `directory` (package name and version only) are requested from `pypi.org` to fill in each dependency's declared license, and, when `token` is set (which turns on `--vex`), looked up in the OSV database at `api.osv.dev` for CVE statements. No SBOM content, file names or repo identifiers are sent. Set `AISBOM_OFFLINE=1` in your workflow's `env:` block to skip both; it also disables the CLI's own telemetry events, and it cannot be combined with `share: true`, which needs the network.

For the two upload paths (1 and 2) the payload is the SBOM — file names, SHA-256 hashes, licenses, risk and legal findings — plus, on path 2 only, the VEX documents derived from those same findings. Never model weights or file contents. Telemetry (3) carries none of that: no SBOM, no VEX, no file names, no hashes, no repo identifier — just event names and low-cardinality parameters such as severity counts.

`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.
Expand Down
10 changes: 10 additions & 0 deletions action/entrypoint.sh
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
#
# Exit codes:
# 0 — Scan succeeded OR scan reported risks but fail-on-risk is false.
# 1 — share: true was combined with AISBOM_OFFLINE (refused before scanning).
# 2 — Scan reported CRITICAL findings AND fail-on-risk is true.
# 3 — Platform upload failed AND fail-on-platform-error is true.
#
Expand Down Expand Up @@ -72,6 +73,15 @@ if [ -n "${INPUT_TOKEN}" ]; then
VEX_ARGS=(--vex)
fi

# `AISBOM_OFFLINE=1` in the step env forbids network access and `share: true`
# requires it, so the CLI refuses the pair with exit 1. Step 4 only re-raises
# exit 2, so without this the job would pass — and an `sbom.json` already in
# the workspace would be commented on and uploaded as if freshly scanned.
if [ -n "${AISBOM_OFFLINE:-}" ] && [ "${INPUT_SHARE}" = "true" ]; then
echo "[aisbom-action] share: true cannot be combined with AISBOM_OFFLINE: sharing uploads the SBOM to aisbom.io. Remove one of them."
exit 1
fi

SHARE_ARGS=()
if [ "${INPUT_SHARE}" = "true" ]; then
SHARE_ARGS=(--share --share-yes)
Expand Down
Loading