v0.12.0
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.zstandbaseline-manifest.json(per-asset SHA-256, size and signature) each carry a detached Ed25519.sigunder 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.gzis gone from the repo, wheel and package; the v2 hashed seed is distributed as the signedbaseline-seed.tar.gzasset. First-run auto-import keeps working by fetching and verifying the channel asset (silently skipping on failure or offline), andtrustsight seed fetchimports it on demand. -
The security model's network doctrine now names two declared hosts:
aur.archlinux.orgeverywhere, andgithub.comconfined to the new fetch modulerelease.py(seed fetch,ioc update, first-run import). Downloads are bounded (512 MiB cap, 60 s timeout,TRUSTSIGHT_OFFLINEswitch), and thenetwork confined to the fetch modulesandone network host, declaredgates match. -
docs/security.mdis 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-fivescripts/security_gates.pygates 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 newPnamespace (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
PackageFactand in the JSON:diff_truncated,line_truncated,tree_not_analyzed,unresolved_source(now including multi-linesource=()arrays whose$(...)rides a continuation line) andunresolved_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 readsHigh (incomplete analysis)instead ofHigh. Reported as a weight-0COVERAGEentry withcoverage_gapsandrisk_labelin machine output. -
trustsight full-auris safe by default. A missing snapshot no longer silently triggers a from-scratch scrape of every PKGBUILD in the AUR (~120k): it refuses unless--bootstrapis passed. Every cycle is capped atlimits.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 withcorpus_fetch_workers(default 5), a global aggregate rate cap (~5 requests/second) regardless of worker count, retry with exponential backoff on429/5xx/connection resets honouringRetry-After, and a live progress bar on a TTY. -
Maintainer identities hash through one chokepoint.
db._hash_maintainer_valuedelegates toseed_build._hash_value; both normalisestrip().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"rlandba"sh"reconstruct tocurlandbashbefore 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.mdno 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_linesandrules._classify_enclosing_function, andcopy_result=Falsefor the read-only config accessors.
Added
-
A release channel for every baseline.
trustsight seed fetch(download, verify, import), release-channelioc update(per-curator verification preserved on top of the distribution signature), first-run auto-import of a missing seed, andscripts/build_release_baselines.py(build, sign, self-verify, manifest). Baselines ship on dedicatedbaseline-<date>channel releases, published after the software release they serve so the tool's defaultlatestchannel resolves to them. The.github/workflows/baselines.ymlworkflow rebuilds and uploads the seed, IOC and manifest assets on channel releases, signing with theBASELINE_SIGNING_KEYActions 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.jsonldirectories, 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 newIOC Matchstage attachesPackageFact.ioc_matches; matches never enterscore_breakdownand never move the number.[baselines.ioc]config section,ioc_entriestable, andtrustsight ioc {sources,import,update,list,export}instructions. -
docs/security.mdPart A to D andscripts/security_gates.py: 45 gates, one per invariant, enforced by thesecurityCI 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_metaand defeats precomputed tables. An old plaintext seed migrates on first run, with the original table renamed tomaintainers_deprecated_backup. Newmaintainers_hashed/package_maintainers_hashedtables andtrustsight 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 thewrite_then_execfamily. -
Committed-file scanning (
differ.companion_source_hunks). A payload that ships as a file inside the AUR repo (declared insource=()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()andsafe_markup()strip ANSI and OSC sequences, C0/C1 control bytes and DEL, and neutralise Rich markup, applied at every render boundary incli/. Stored evidence and JSON output stay byte-exact. -
PackageFact.risk. The verdict band is carried on the fact and read throughscoring.verdict_level()(bare band, for machines) orscoring.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 infindings.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_fingerprinton 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_bytesand appending the payload turned a High into a Low.diff_truncatedis now a coverage gap consumed by the verdict, andscan_diffapplies the same cap the git path always did. Inconclusivewas computed and then discarded. Every CLI path re-derived the band withrisk_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_seedcopiedseed.metadatawholesale; 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
reviewroute. - Rule patterns ran on unbounded lines. Input is clamped to 8 KiB per logical line before matching (via
rules.clamp_textat all three call sites, including the ~88 code-emitted patterns that previously escaped A5's bound); an over-length line recordsline_truncated. - A heading rename could silently break every link to it. The
doc cross-references resolvegate walks everydocs/**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 inertnow renders through four paths,every result declares its coveragewalks the AST for everyPackageFactconstruction,pacman -Slgained its--separator). - The reserved-name guard covered one writer of three.
upsert_package,save_package_profileandsave_pkgbuild_snapshotall refuse__seed__and any__-prefixed name. - Resolved rules lost their line numbers (a
line_mapkeyed by compacted index); the tokenizer now records the raw diff-line index of each resolved string. forget --prune,historyandlistsurfaces fixed in the audit pass: raw stored names cleaned, bands re-derived viastored_band()from savedfact_json, depend changes reported, andDECLARED_DEFAULTapplied so declared-practice findings no longer duplicate.- The line clamp only covered half the rule engine,
_MAX_EXPANSION_DEPTHwas declared and never applied (removed), config lists became regex (properly escaped),corpus pivotreads a snapshot from one location, and the AUR metadata fetch is bounded.
Removed
src/trustsight/data/seed.db.gzand everything that depended on the bundled seed: the 20 MB seed leaves the repo, wheel and package for the signedbaseline-seed.tar.gzchannel asset.[verification_evidence]and[pinning_weights]from the shipped config, so a localconfig.tomlcannot reintroduce a credit;trusted_forgeis 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