Skip to content

Add bootintel submit to fetch server-side artifacts - #37

Merged
Zenofex merged 1 commit into
mainfrom
cli-submit-artifacts
Oct 7, 2026
Merged

Zenofex merged 1 commit into
mainfrom
cli-submit-artifacts

Conversation

@Zenofex

@Zenofex Zenofex commented Oct 7, 2026

Copy link
Copy Markdown
Contributor

The CLI could reach server analysis but not anything the server builds from it. scan --api posts to /analysis/scan, which the server documents as "stateless inline log analysis without device_id/database persistence" and which returns a freshly minted uuid as its scan_id, naming no row. The SBOM, evidence pack, PDF and JSON report are all built from a real ScanSession, so there was nothing for the CLI to export from. SBOM-in-CI is the canonical use case for the format, and it was the one place the product could not do it.

bootintel submit <log> creates the persisted scan and downloads whichever artifacts are asked for: --sbom, --evidence, --pdf, --json-report, with --json printing the scan id and written paths for scripting.

Why a separate subcommand

It costs something scan does not: a saved scan consumes the account's monthly quota. The notice prints immediately before that spend rather than at the top of the run — printing it first meant a 403 from the device lookup was preceded by a claim about a charge that never happened.

Requires Pro, not Researcher

The two gates are easy to conflate. The export endpoints are gated at Researcher, but get_current_user refuses any X-API-Key request below Pro, so a Researcher can download these from the dashboard and not from here. The help text first said "Researcher or higher" by quoting the endpoint's own gate, which would have told a Researcher this command works for them.

Device reuse

An existing device with the same name is reused instead of creating one per run. POST /devices/ accepts duplicates, so a nightly CI job would otherwise add a device a day and nothing server-side would have complained.

Verification

End to end against the live API with a real Pro account, through a loopback forwarder because the droplet's /etc/hosts points bootintel.com at a local nginx whose origin certificate the system store does not trust. All four artifacts came back genuine:

  • a 25-page PDF with an intact %PDF-1.4 header
  • a valid zip containing manifest / findings / analysis / raw-log
  • a CycloneDX 1.6 SBOM naming the real board, 6 components and 73 vulnerabilities, validating clean against the vendored upstream schema

A second run produced two scans and one device, confirming reuse. 403, missing-key and plaintext-base all exit 77 with the server's own reason shown.

Seven integration tests drive the compiled binary against a mock server, since the exit code, the request sequence and byte fidelity are properties of the process. The binary-artifact test ships bytes that are not valid UTF-8, because a String round-trip would corrupt a PDF silently and a corrupt PDF still looks like a file on disk.

391 tests pass; cargo fmt and cargo clippy --all-targets clean.

🤖 Generated with Claude Code

The CLI could reach server analysis but not anything the server builds from
it. `scan --api` posts to `/analysis/scan`, which the server documents as
"stateless inline log analysis without device_id/database persistence" and
which returns a freshly minted uuid as its scan_id, naming no row. The SBOM,
evidence pack, PDF and JSON report are all built from a real ScanSession, so
there was nothing for the CLI to export from. SBOM-in-CI is the canonical use
case for the format and it was the one place the product could not do it.

`bootintel submit <log>` creates the persisted scan and downloads whichever
artifacts are asked for: --sbom, --evidence, --pdf, --json-report, with --json
printing the scan id and written paths for scripting.

A separate subcommand rather than a flag on `scan`, because it costs something
`scan` does not: a saved scan consumes the account's monthly quota. The notice
is printed immediately before that spend rather than at the top of the run,
since printing it first meant a 403 from the device lookup was preceded by a
claim about a charge that never happened.

Requires PRO, and the two gates are easy to conflate. The export endpoints are
gated at Researcher, but get_current_user refuses any X-API-Key request below
Pro, so a Researcher can download these from the dashboard and not from here.
The help text first said "Researcher or higher" by quoting the endpoint's own
gate, which would have told a Researcher this command works for them.

An existing device with the same name is reused instead of creating one per
run. POST /devices/ accepts duplicates, so a nightly CI job would otherwise add
a device a day and nothing server-side would have complained.

Verified end to end against the live API with a real Pro account, through a
loopback forwarder because the droplet's /etc/hosts points bootintel.com at a
local nginx whose origin certificate the system store does not trust. All four
artifacts came back genuine: a 25-page PDF with an intact %PDF-1.4 header, a
valid zip containing manifest/findings/analysis/raw-log, and a CycloneDX 1.6
SBOM naming the real board with 6 components and 73 vulnerabilities that
validates clean against the vendored upstream schema. A second run produced two
scans and one device, confirming reuse. 403, missing-key and plaintext-base all
exit 77 with the server's own reason shown.

Seven integration tests drive the compiled binary against a mock server, since
the exit code, the request sequence and byte fidelity are properties of the
process. The binary-artifact test ships bytes that are not valid UTF-8, because
a String round-trip would corrupt a PDF silently and a corrupt PDF still looks
like a file on disk. 391 tests pass; fmt and clippy clean.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@Zenofex
Zenofex merged commit e2b71b1 into main Oct 7, 2026
11 checks passed
@Zenofex
Zenofex deleted the cli-submit-artifacts branch October 7, 2026 09:18
Zenofex added a commit that referenced this pull request Oct 7, 2026
#37 shipped a race, and it blocked #38.

Every test in `submit_cli.rs` wrote the same `tmpdir()/boot.log`. Rust
runs tests in parallel threads, and `fs::write` truncates before it
writes, so one test's spawned process could read the file in the instant
another had emptied it. The binary then did exactly what it should:

```
Error: /tmp/submit_cli/boot.log is empty; nothing to submit
```

It passed 7/7 locally and passed on main's own CI run, then failed `test
(default-features)` on the next branch. That is the shape of flake that
survives: red often enough to erode trust in the suite, green often
enough that nobody can reproduce it.

Each test now writes its own `<tag>.log`.

Verified with 15 consecutive runs of the file — 0 failures — and seven
distinct files on disk.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Zenofex added a commit that referenced this pull request Oct 7, 2026
`bootintel submit` (#37) is merged but sits under [Unreleased], and the
workspace is still 0.13.0, which is also the latest published release. So
nobody can install the command: a `brew install` or a release download today
gets 0.13.0 without it. A merged feature no user can run is not shipped.

The repo's own policy makes this MINOR: "new detector; new subcommand; new
flag; new output format".

Same four files the 0.13.0 release touched: the workspace version, the
detectors pin in crates/cli, Cargo.lock, and the CHANGELOG heading. Nothing
here publishes anything by itself -- cli-release.yml is workflow_dispatch only,
with an explicit version input and a dry_run option -- so the release remains a
deliberate manual act after this lands.

Verified the built binary reports 0.14.0 and still carries `submit`.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Zenofex added a commit that referenced this pull request Oct 7, 2026
`bootintel submit` (#37) is merged but sits under `[Unreleased]`, and
the workspace is still `0.13.0` — which is also the latest published
release. So **nobody can install the command**: a `brew install` or a
release download today gets 0.13.0 without it. A merged feature no user
can run is not shipped.

The repo's own policy makes this MINOR: *"new detector; new subcommand;
new flag; new output format"*.

## What this changes

The same four files the 0.13.0 release touched:

- workspace version `0.13.0` → `0.14.0`
- the `bootintel-detectors` pin in `crates/cli/Cargo.toml`
- `Cargo.lock`
- `CHANGELOG.md` — `[Unreleased]` closed out into a dated heading

## This does not publish anything

`cli-release.yml` is `workflow_dispatch` only, with an explicit
`version` input and a `dry_run` option. Merging this prepares the
release; cutting it stays a deliberate manual dispatch afterwards.

Verified the built binary reports `bootintel 0.14.0` and still carries
`submit`.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
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