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
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: docker://rhysd/actionlint:1.7.7
- uses: docker://rhysd/actionlint:1.7.12
with:
# GitHub's expression and schema grammar only. The image also bundles
# shellcheck and pyflakes, which flag pre-existing quoting warnings in
Expand Down
55 changes: 48 additions & 7 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -1399,6 +1399,46 @@ Targeted at 0.4.0. This release is a behaviour change for anyone who set
- `references/` link integrity: `error.md` and `wheel_extract.md` existed but
were unreachable from the map index.

### Fixed

- **A file stamped text whose bytes do not decode no longer diffs as "everything
deleted".** `diff_single_file` coerced either side's invalid UTF-8 to `""`,
so a real change scored as all-add or all-delete depending on which side it
was on. Both sides are refused now, with an error naming the path and the
side. The path is unreachable through `compute_delta` — `classify_bytes`
already routes non-UTF-8 to `Binary` — so this could only fire where the
classification and the bytes disagree, which is the one state a coercion
cannot answer correctly.

### Removed

- `heuristic::evaluate`, a five-argument wrapper around `evaluate_with_policy`
with `Policy::default()` baked in. Production had no callers; only its tests
did, and they now call `evaluate_with_policy` directly, so the default-policy
path is pinned at its one real call site rather than in a second copy.

- `SimpleFile::size` and `SimpleFile::provenance` in the PyPI `/simple/`
response model. Both were `#[serde(default)]` members nothing read — the same
shape as the `SimpleMeta::api-version` marker the struct's own doc comment
already refuses to carry.

### Documentation corrections

- CHANGELOG 0.3.1 said "all six managers are reviewed through one grammar".
The CLI enumerates eleven (`npm npx pnpm yarn bun bunx pip pip3 cargo yay
paru`, the same list as `SHIM_MANAGERS`), `--help` is authoritative, and the
entry now says eleven. Resolves drift entry 6.
- CHANGELOG 0.3.0 claimed `-`/`_` canonicalization of crate names
(`serde_json` → `serde-json`). The adapter lowercases only, deliberately:
a configured index may serve both spellings, so folding would risk answering
one spelling with the other's bytes. A hyphen-spelled crates.io request
fails closed at the index fetch (the live index has no hyphen file), which
was checked rather than assumed. The claim is removed and the lane records
the stance. Resolves drift entry 4.
- CHANGELOG 0.3.0 described `blueline install` as shipping "sandboxed
extraction". It never did; the entry now says what shipped and points at
drift entry 1, which stays High and unfixed.

## [0.3.1] - 2026-09-21

### Added
Expand All @@ -1410,8 +1450,8 @@ Targeted at 0.4.0. This release is a behaviour change for anyone who set
Fail closed everywhere — blueline missing, errored, or refusing means the
install does not run, and a missing real binary refuses shim creation.
The scanner gained `cargo install`, and `yay`/`paru -S` operands
(AUR-grammar specs with `name=version` pinning), so all six managers are
reviewed through one grammar; pip flags that name non-registry sources
(AUR-grammar specs with `name=version` pinning), so all eleven scanned
managers are reviewed through one grammar; pip flags that name non-registry sources
(`-r`, `-e`, `--constraint`, …) are refused with a pointer to
`blueline ci`. `BLUELINE_REGISTRY` and `BLUELINE_POLICY` environment
variables scope a shimmed shell to a mirror and a project policy.
Expand Down Expand Up @@ -1634,9 +1674,9 @@ Targeted at 0.4.0. This release is a behaviour change for anyone who set
sparse-index NDJSON client with fail-closed parsing (bad `vers` on a
recognized row aborts; unknown schema `v > 2` rows are skipped with a note;
missing `yanked` reads as false), `config.json` handling that refuses
authenticated registries, canonical crate names (`serde_json` → `serde-json`),
and `.crate` downloads verified by sha256 against the index checksum before
extraction. Extracted archives must unpack to exactly one top-level
authenticated registries, and `.crate` downloads verified by sha256 against
the index checksum before extraction. Extracted archives must unpack to
exactly one top-level
`{name}-{version}` directory.
- Packed `Cargo.toml` reader: `[package] build`/`links`, `[[bin]]` targets,
dependency maps, and `[features]`; dependencies project onto the existing
Expand Down Expand Up @@ -1732,8 +1772,9 @@ Targeted at 0.4.0. This release is a behaviour change for anyone who set

- `blueline review <pkg@ver>` command with text and JSON output, `--registry`
override, and a fail-closed `--yes` flag.
- `blueline install` command with sandboxed extraction and fail-closed script
execution.
- `blueline install` command with fail-closed script execution (extraction was
parser-bounded; the OS-level sandbox this line once claimed was never
shipped, see drift entry 1).
- `blueline ci --output-file <path>` parameter to save formatted CI review
reports directly to disk.
- npm registry client with SHA-512 integrity verification before extraction.
Expand Down
47 changes: 27 additions & 20 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

64 changes: 50 additions & 14 deletions references/drift.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,12 @@ Severity is this file's own judgement, not the project's.
| 8 | ~~SSRF guard fails **open** on DNS failure, port 443 hard-coded~~ **resolved** — `ValidatingResolver` is the guard now, not the pre-flight check | High — see below |
| 11 | ~~PyPI `METADATA` parse fails **open**~~ **resolved** — every unreadable or undetermined-metadata case is a refusal | High — see below |
| 3 | Integrity mismatch surfaces as `Err` + exit 1, not `Verdict::Block` | Medium — a consumer keying on band never sees BLOCK |
| 4 | No cargo `-`/`_` name canonicalization, contrary to `CHANGELOG` 0.3.0 | Medium — `serde_json` and `serde-json` are distinct crates |
| 10 | `diff_single_file` coerces invalid UTF-8 to `""`, so a real change reads as all-add or all-delete | Medium — undocumented, silently mis-scores |
| 4 | ~~No cargo `-`/`_` name canonicalization, contrary to `CHANGELOG` 0.3.0~~ — **resolved as a doc correction:** the claim was wrong; the distinctness is deliberate and fails closed | Medium — see below |
| 10 | ~~`diff_single_file` coerces invalid UTF-8 to `""`, so a real change reads as all-add or all-delete~~ — **resolved:** both sides are refused with a naming error | Medium — was silent mis-scoring; see below |
| 12 | ~~`store.rs` discards the `0o600` pre-create error (`let _ =`)~~ **resolved** — all three discarded errors propagate. See below | Low |
| 9 | ~~`hex_to_bytes` uses `.unwrap_or(0)`, mapping invalid hex to byte `0`~~ **resolved** — the helper is fallible and `to_sri` returns `Result` | Low |
| 5 | ~~`registry/mod.rs` doc comments are stale~~ **resolved** — both were deleted; the four adapters are all real. See below | Low |
| 6 | Manager count is six in `CHANGELOG` 0.3.1, eleven in `ARCHITECTURE.md` §5; `--help` says eleven | Low — `--help` is authoritative |
| 6 | ~~Manager count is six in `CHANGELOG` 0.3.1, eleven in `ARCHITECTURE.md` §5; `--help` says eleven~~ — **resolved:** the 0.3.1 entry now says eleven, matching `--help` and `SHIM_MANAGERS` | Low — `--help` was authoritative |
| 7 | ~~Dead code: `Policy::is_maintainer_blocked`, `blocklist.maintainers`, `allowlist.packages[].max_risk` / `.integrity`~~ **resolved** — the three policy keys are now refused at load. Still dead: `BaselineStore::clear_advisory_cache` | Low — test-only helper, `#[allow(dead_code)]` |

## Lane verification basis
Expand All @@ -37,7 +37,7 @@ the prose docs.
| [verdict](features/verdict.md) | `src/verdict.rs` full read |
| [error](features/error.md) | `src/error.rs` full read + cross-referenced callers |
| [heuristic](features/heuristic.md) | `src/heuristic.rs` production + all 52 test names |
| [diff](features/diff.md) | `src/diff.rs` production + all 7 test names |
| [diff](features/diff.md) | `src/diff.rs` production + all 8 test names |
| [render](features/render.md) | `src/render.rs` production + all 13 test names + 2 proptests |
| [version](features/version.md) | `src/version.rs` production + all 23 test names |
| [manifest](features/manifest.md) | `src/manifest.rs` production + all 32 test names |
Expand Down Expand Up @@ -73,6 +73,12 @@ Each finding is cited in its lane file with the exact source location.
(Landlock on Linux, `cap-std`, or seccomp), which is a dependency decision
and therefore not something to do silently. See [extract](features/extract.md)
for what the bounds actually are.

One more prose-level claim was corrected while the capability was still
absent: the 0.3.0 entry described `blueline install` as shipping "sandboxed
extraction". It never did, and the entry now says fail-closed script
execution and points here, so the history does not assert the capability
either. Entry stays **High, unfixed**; the fix is code.
2. **The npm registry signature is read for presence and never verified. Partly
resolved; the verification gap itself is untouched.** `release_signatures`
still returns the block as raw JSON, nothing compares the bytes to it, and
Expand All @@ -96,19 +102,40 @@ Each finding is cited in its lane file with the exact source location.
3. **Integrity mismatch produces an `Err`, not `Verdict::Block`.** A CI report
contains an error and exit 1, not a BLOCK verdict. See
[extract](features/extract.md).
4. **No cargo `-`/`_` canonicalization.** `canonical_crate_name` only lowercases,
so `serde_json` and `serde-json` are distinct crates — contrary to CHANGELOG
0.3.0. See [registry](features/registry.md).
4. **~~No cargo `-`/`_` canonicalization, contrary to CHANGELOG 0.3.0~~ —
resolved as a doc correction; the code stance was right and stays.**
`canonical_crate_name` only lowercases, so `serde_json` and `serde-json`
were distinct crates — which CHANGELOG 0.3.0 claimed was not the case. The
claim was wrong about this tree, and it is now corrected. The code is right
to stay as it is, for two reasons that survived checking the claim against
the live registry:

- The real crates.io index does store only the folded file
(`index.crates.io/se/rd/serde_json` is 200; `/se/rd/serde-json` is 404;
`static.crates.io/crates/rocket-db-pools/0.2.0/download` is 403 where
`/rocket_db_pools/` is 200), so a hyphen-spelled review of a crates.io
crate fails closed at the index fetch today. That is a named, loud
refusal, not a wrong answer.
- This tool is not restricted to crates.io: `--index` and the `dl` in
`config.json` point at whatever registry is configured, and a mirror may
genuinely serve both spellings. Folding would answer one spelling with
the other's bytes — the ci lane pins that invariant from its side
(`lockfile_diff_keeps_pypi_alias_off_cargo_pins`).

The ergonomic cost (a hyphen-spelled crates.io request refuses) is a
judgement call for the project, not a defect: the failure is closed and
names the package. See [registry](features/registry.md).
5. **~~`registry/mod.rs` doc comments were stale~~ — resolved.** Two comments
claimed npm was the only wired adapter ("npm is fully wired; cargo, PyPI,
and AUR adapters build on these seams in later PRs", and "npm is the only
full impl for now") while `NpmRegistry`, `CratesIoRegistry`, `PyPIRegistry`,
and `AurRegistry` all implement the trait. Both comments are deleted.
6. **CHANGELOG 0.3.1 says "all six managers are reviewed through one grammar"
and "eleven scanned managers" in ARCHITECTURE.md §5 — the CLI enumerates
eleven** (`npm npx pnpm yarn bun bunx pip pip3 cargo yay paru`) while the
0.3.1 entry lists six. Both counts appear in the project's own docs; the
`--help` text is authoritative and says eleven.
6. **~~CHANGELOG 0.3.1 says "all six managers are reviewed through one grammar"~~
— resolved.** "eleven scanned managers" in ARCHITECTURE.md §5 and the CLI's
eleven (`npm npx pnpm yarn bun bunx pip pip3 cargo yay paru`, the same list
as `SHIM_MANAGERS`) were right; the 0.3.1 entry listed six. Both counts
appeared in the project's own docs and `--help` was authoritative, so the
entry now says eleven.
7. **~~`BaselineStore::clear_advisory_cache` and
`BaselineResolution::display_summary` are dead~~ — `display_summary` is
deleted; `impl BaselineResolution` carries only `package()`. The one that
Expand Down Expand Up @@ -147,8 +174,17 @@ Each finding is cited in its lane file with the exact source location.
`to_sri`, so an undecodable digest is a refusal that names the offending
input. Tests `a_malformed_digest_fails_loud_instead_of_zeroing` and
`a_well_formed_digest_still_renders`. See [registry](features/registry.md).
10. **`diff_single_file` silently converts invalid UTF-8 to `""`**, so a real
change reads as all-add or all-delete. `ambiguous`, not documented. See
10. **~~`diff_single_file` silently converts invalid UTF-8 to `""`~~ —
resolved.** A real change read as all-add or all-delete, because one side
decoded to the empty string and every line then counted as inserted or
deleted. `diff_single_file` refuses with `BluelineError::Extraction` naming
the path and the side. The path is unreachable through `compute_delta`
(`classify_bytes` routes non-UTF-8 to `Binary` and `diff_single_file`
returns early for non-text), so this only ever fired on a
classification-vs-bytes disagreement — which is exactly the state a
coercion cannot answer correctly. Test
`a_text_classified_file_that_does_not_decode_is_refused_not_coerced`,
verified failing on the pre-fix coercion. See
[diff](features/diff.md).
11. **~~PyPI `METADATA` parsing fails OPEN~~ — resolved.** The old
`if let Ok(raw) = read_to_string(root.join("METADATA"))` is gone. A PyPI
Expand Down
11 changes: 8 additions & 3 deletions references/features/diff.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,9 +54,14 @@ Separately, `is_executable_extension` is a platform-independent 11-entry list
`base_iter.next()` / `target_iter.next()` immediately after a `peek()`
returned `Some`, with `#[allow(clippy::unwrap_used)]` and a comment. Do not
"clean them up"; and do not add more elsewhere.
- **`diff_single_file`'s `from_utf8(...).unwrap_or("")` means a misclassified
non-UTF-8 file diffs as "everything deleted."** It is unreachable in practice
because `classify_bytes` already routed it to `Binary`.
- **A file stamped `Text` whose bytes do not decode is refused, not coerced.**
`diff_single_file` used to run `from_utf8(...).unwrap_or("")`, so a
misclassified non-UTF-8 file diffed as "everything deleted" (or "everything
added" on the other side) and scored accordingly. It is unreachable through
`compute_delta` because `classify_bytes` already routed it to `Binary`, so
the coercion was defense that could only ever answer wrong: both sides now
fail the review with `BluelineError::Extraction` naming the path and side.
Test `a_text_classified_file_that_does_not_decode_is_refused_not_coerced`.
- **`collect_all_dependencies` uses `or_insert`, so `dependencies` shadows
`optionalDependencies` and `peerDependencies`.** A peer-range flip on an
existing dep is invisible. Switching to `insert` would make it a *modified*
Expand Down
Loading
Loading