Skip to content

v0.12.0

Choose a tag to compare

@emiliano-go emiliano-go released this 11 Aug 01:50
· 34 commits to master since this release
91a5659

TL;DR

Fifty-six commits since v0.11.0 move every baseline the tool consumes onto a signed release channel, state and enforce the security model as 45 measurable gates, add a signed, multi-curator IOC federation system, hash all user-facing seed data, stop shipping the seed inside the package, detect the split fetch-then-execute and committed-file execution forms, stop crediting declared verification, and make coverage gaps fail closed.

Changed

  • Every baseline now ships as a signed GitHub release asset. The seed, IOC baselines and corpus are no longer downloaded from an undocumented location: baseline-seed.tar.gz, baseline-ioc-<source>-<incident>-manifest.json / -iocs.jsonl, baseline-corpus.tar.zst and baseline-manifest.json (per-asset SHA-256, size and signature) each carry a detached Ed25519 .sig under the pinned distribution key. A download that does not verify is refused, never imported.

  • The novelty seed no longer ships inside the package. The 20 MB src/trustsight/data/seed.db.gz is gone from the repo, wheel and package; the v2 hashed seed is distributed as the signed baseline-seed.tar.gz asset. First-run auto-import keeps working by fetching and verifying the channel asset (silently skipping on failure or offline), and trustsight seed fetch imports it on demand.

  • The security model's network doctrine now names two declared hosts: aur.archlinux.org everywhere, and github.com confined to the new fetch module release.py (seed fetch, ioc update, first-run import). Downloads are bounded (512 MiB cap, 60 s timeout, TRUSTSIGHT_OFFLINE switch), and the network confined to the fetch modules and one network host, declared gates match.

  • docs/security.md is the canonical security page. Part A describes the program consuming hostile input, Part B what a verdict claims and does not claim, Part C the enforcement map, Part D the disclosure policy. Forty-five scripts/security_gates.py gates enforce one invariant each, covering the new subsystems and guarding the docs themselves (the maturity numbers must be derived, every cross-page link must resolve, and the doc and gate list must describe the same set).

  • Declared verification is no longer credited (B10). Checksums, validpgpkeys, GPG signature sources, source pinning and trusted-forge hosting moved from up to 25 points of discount to weight-0 findings in a new P namespace (P001-P003, P005-P007), reported under the line "TrustSight does not verify these claims. It reports that the recipe makes them." [verification_evidence] and [pinning_weights] are removed from the shipped config. A signal an attacker can assert for free must not lower a score. Measured: benign p95 moved 35 to 45; separation still holds.

  • Coverage accounting is fail-closed. Five gaps are first-class on PackageFact and in the JSON: diff_truncated, line_truncated, tree_not_analyzed, unresolved_source (now including multi-line source=() arrays whose $(...) rides a continuation line) and unresolved_parse_time (a top-level command substitution that runs while makepkg sources the PKGBUILD). A gap never adds points, forbids an UNFLAGGED verdict, and travels with the band wherever it is rendered, so an incomplete run reads High (incomplete analysis) instead of High. Reported as a weight-0 COVERAGE entry with coverage_gaps and risk_label in machine output.

  • trustsight full-aur is safe by default. A missing snapshot no longer silently triggers a from-scratch scrape of every PKGBUILD in the AUR (~120k): it refuses unless --bootstrap is passed. Every cycle is capped at limits.corpus_max_per_cycle (default 2000) and resumes automatically; a capped cycle does not advance the snapshot or export a half-built corpus. It is also faster and polite: fetches run a window ahead with corpus_fetch_workers (default 5), a global aggregate rate cap (~5 requests/second) regardless of worker count, retry with exponential backoff on 429/5xx/connection resets honouring Retry-After, and a live progress bar on a TTY.

  • Maintainer identities hash through one chokepoint. db._hash_maintainer_value delegates to seed_build._hash_value; both normalise strip().lower(), so a maintainer whose name or email differs only in case or whitespace is one identity rather than a fresh novelty signal.

  • The tokenizer normalises partial quoting. c"u"rl and ba"sh" reconstruct to curl and bash before rules match, so intra-word quoting no longer hides a literal from the resolved-line rules.

  • Every page describing the subtractive model was rewritten after B10: the scoring formula and tier map in rules.md, the Tier D tables, the "Why verification subtracts" section, the worked examples, plus the configuration, explanation, corpus-priors, cold-start, auditing and index pages. docs/security.md no longer claims 20 is the benign 95th percentile; it is the 83.7th.

  • Analysis is about 42% faster (14.9 ms to 8.7 ms per diff), detection bit-identical: memoised tokenizer.resolve_added_lines and rules._classify_enclosing_function, and copy_result=False for the read-only config accessors.

Added

  • A release channel for every baseline. trustsight seed fetch (download, verify, import), release-channel ioc update (per-curator verification preserved on top of the distribution signature), first-run auto-import of a missing seed, and scripts/build_release_baselines.py (build, sign, self-verify, manifest). Baselines ship on dedicated baseline-<date> channel releases, published after the software release they serve so the tool's default latest channel resolves to them. The .github/workflows/baselines.yml workflow rebuilds and uploads the seed, IOC and manifest assets on channel releases, signing with the BASELINE_SIGNING_KEY Actions secret; the corpus baseline is exported by the maintainer per the publishing guide. Software releases (this one included) never carry baseline assets.

  • IOC Federation baseline system. A signed, multi-curator, time-bounded inventory of known-bad artifacts (domains, file hashes, package names) that sits outside the heuristic score: Ed25519-signed manifest.json + iocs.jsonl directories, imported per source and replaced idempotently. Each match names the curator that flagged it (attribution, not aggregation), carries its incident and evidence URL, and reports expiry rather than silently lapsing. A new IOC Match stage attaches PackageFact.ioc_matches; matches never enter score_breakdown and never move the number. [baselines.ioc] config section, ioc_entries table, and trustsight ioc {sources,import,update,list,export} instructions.

  • docs/security.md Part A to D and scripts/security_gates.py: 45 gates, one per invariant, enforced by the security CI job. No interpreter or shell execution, version arguments shape-checked, every request timed out, bounded and never-indirect expansion, data-driven rendering, no archive extraction, parameterised SQL, inert terminal output, FATAL integrity, seed and baseline containment, reserved names refused by every writer, IOC matches carrying their source and never contributing to the score, expiry reported, no plaintext identity in the seed, and deterministic seed hashing.

  • User-data hashing for the novelty seed. The seed's ~36k maintainer names and emails are stored as salted SHA-256 hashes; a per-seed 32-byte salt travels in seed_meta and defeats precomputed tables. An old plaintext seed migrates on first run, with the original table renamed to maintainers_deprecated_backup. New maintainers_hashed / package_maintainers_hashed tables and trustsight seed {info,stats,migrate} commands.

  • R137 (Fetch Then Execute, CRITICAL). A downloader writes a file and the same function later executes it. R001/R002 own the single-line pipe; R137 owns the split download-then-run form. R136 (Committed File Executed Without Declaration, HIGH) flags a build step that executes a repo-committed file not declared in source=(), closing the write_then_exec family.

  • Committed-file scanning (differ.companion_source_hunks). A payload that ships as a file inside the AUR repo (declared in source=() or merely named by the recipe) is now read with the same rules as the PKGBUILD. The whole current content of every companion the recipe names is scanned, so a payload committed earlier and referenced later is still seen. Unreferenced committed files are left alone.

  • src/trustsight/safe_text.py. clean() and safe_markup() strip ANSI and OSC sequences, C0/C1 control bytes and DEL, and neutralise Rich markup, applied at every render boundary in cli/. Stored evidence and JSON output stay byte-exact.

  • PackageFact.risk. The verdict band is carried on the fact and read through scoring.verdict_level() (bare band, for machines) or scoring.verdict_label() (qualified, for people).

  • B7, a change summary on every result, so "nothing fired" cannot read as "nothing happened"; B8, findings are checkable (content rules carry file/line, the rest declare an evidence class in findings.NON_CONTENT_RULES); B9, no output grants permission to skip review (every verdict ends with a direction to review, structurally required and gated by AST); config_fingerprint on every report so determinism is checkable; B1 restated as algorithmic, not configurational determinism; A14, the overarching resource guarantee; scoring.FLAG_THRESHOLD, so the 20-point threshold is read rather than repeated; and A3/A10 addenda (cloning executes nothing; sanitisation is not transliteration).

Fixed

  • The truncation bypass. Padding a diff past max_diff_bytes and appending the payload turned a High into a Low. diff_truncated is now a coverage gap consumed by the verdict, and scan_diff applies the same cap the git path always did.
  • Inconclusive was computed and then discarded. Every CLI path re-derived the band with risk_level(final_score), which cannot express it; the downgrade never reached the output.
  • AUR-controlled text reached the terminal raw. Package names, maintainer names, file paths and quoted evidence could clear the screen, forge a verdict, recolour a row, or abort the render of a whole review batch with an unbalanced Rich tag.
  • A seed could rewrite the database it was merged into. import_seed copied seed.metadata wholesale; it is now limited to the two keys a seed owns, cannot raise a locally learned maintainer count, and records the imported artifact's SHA-256 and origin.
  • A FATAL rule could be deleted from rules.toml. config.enforce_fatal_rules() re-asserts the shipped FATAL set in memory at load.
  • The AUR metadata fetch had no timeout and no response cap, unlike every other fetch path, on the default review route.
  • Rule patterns ran on unbounded lines. Input is clamped to 8 KiB per logical line before matching (via rules.clamp_text at all three call sites, including the ~88 code-emitted patterns that previously escaped A5's bound); an over-length line records line_truncated.
  • A heading rename could silently break every link to it. The doc cross-references resolve gate walks every docs/** link, resolves the file and the anchor, and fails the build on a dangling one.
  • Three more instances of one recurring failure documented in contributing/security-review.md: a control applied at one of several equivalent call sites, with the gate pointed at a covered one (terminal output is inert now renders through four paths, every result declares its coverage walks the AST for every PackageFact construction, pacman -Sl gained its -- separator).
  • The reserved-name guard covered one writer of three. upsert_package, save_package_profile and save_pkgbuild_snapshot all refuse __seed__ and any __-prefixed name.
  • Resolved rules lost their line numbers (a line_map keyed by compacted index); the tokenizer now records the raw diff-line index of each resolved string.
  • forget --prune, history and list surfaces fixed in the audit pass: raw stored names cleaned, bands re-derived via stored_band() from saved fact_json, depend changes reported, and DECLARED_DEFAULT applied so declared-practice findings no longer duplicate.
  • The line clamp only covered half the rule engine, _MAX_EXPANSION_DEPTH was declared and never applied (removed), config lists became regex (properly escaped), corpus pivot reads a snapshot from one location, and the AUR metadata fetch is bounded.

Removed

  • src/trustsight/data/seed.db.gz and everything that depended on the bundled seed: the 20 MB seed leaves the repo, wheel and package for the signed baseline-seed.tar.gz channel asset.
  • [verification_evidence] and [pinning_weights] from the shipped config, so a local config.toml cannot reintroduce a credit; trusted_forge is 0.

Stats

  • 56 commits since v0.11.0
  • 160 files changed, +28450 / -1651
  • 1521 tests (42 files), all passing
  • 45/45 security gates, 10/10 calibration gates
  • Package version 0.12.0