How TrustSight Works¶
TrustSight is a deterministic AUR PKGBUILD audit tool. It computes a score from 0 to 100 over the end-state of a diff: the post-patch PKGBUILD, not the delta. Every decision is reproducible: same input always produces the same score and the same evidence record.
These explanation pages describe why the tool makes the decisions it does. If you are looking for how to use it, start with the getting-started guide. If you want the reference, see the reference section.
The pipeline¶
The analysis runs in five sequential stages. Each stage has its own failure mode and its own integrity guarantee.
1. Parse¶
The PKGBUILD is a shell script with named variables, arrays, function calls, and conditional expressions. The parser resolves variable references in source, sha256sums, pkgver, pkgrel, and the package() function to produce a structured representation.
Resolution is partial by design. PKGBUILDs are not executed, so the parser can only resolve what is statically determinable:
- Simple variable references (
$pkgname,${pkgver}) are resolved. - Function calls (
pkgver() { ... }) are parsed for structure but not executed. - Conditional branches (
if [[ ... ]]) are noted but not taken. - Dynamically constructed strings (command substitution, arithmetic expansion) are marked as unresolvable.
When a source= entry is computed at build time (a command substitution, not a variable the tokenizer can expand), the URL the build will actually fetch is not in the analysed text. The pipeline records this as the unresolved_source coverage gap, and a coverage gap forbids an UNFLAGGED verdict: the result is reported as Inconclusive unless a HIGH or worse finding already stands on its own. The rationale: a package whose source URL cannot be determined statically cannot be audited with confidence. Reporting "could not verify" is more honest than guessing and potentially missing a swapped URL. See the security model for the other gaps and how they are enforced.
This is the same principle that drives the rest of the scoring system: when the signal is uncertain, surface the uncertainty; do not hide it with a default.
2. Analyze¶
The analysis stage extracts four categories of signal from the parsed PKGBUILD:
Structural signals (Tier A) come from rule matching. Two match targets exist because PKGBUILDs have two surfaces:
- Resolved strings are the post-resolution values of variables and function bodies. Rules matched against resolved strings (R001, R002, R003, R006, R008, R012) catch patterns that survive variable resolution. For example,
curl $url | bashis detected in the resolved string after$urlis expanded, not in the raw diff line where the actual URL is hidden behind a variable. - Raw diff lines are the literal lines changed in the diff, with the
+/-prefix stripped. Rules matched against raw lines (R004, R005, R007, R009, R010, R011, R013) catch patterns in the PKGBUILD text itself: asha256sums=('SKIP')declaration, asudocommand, a unicode bidi override character.
Scope constraints further refine matching. R010 (curl) and R011 (wget) are restricted to function_body context to avoid firing on top-level variable assignments or informational messages. This was a direct result of corpus analysis: these patterns in comments or messages were high-frequency false positives, while the uses worth reporting occur inside build functions. R009 (sudo) was later moved out of the TOML ruleset entirely, because "inside a function body" still admitted optdepends names and echo strings; it is now a code rule that requires sudo at a command position.
The top-level position is not ignored, it is a separate claim: R129 reports a network client invoked outside every function, because that line runs when makepkg merely sources the recipe rather than when it builds.
Context signals (Tier B) classify every new source URL by domain. Classification is deterministic: a bundled domain list assigns each URL to a bucket (trusted_forge, official, self_hosted, raw_hosting, unknown, or homograph). No network calls are made at analysis time; the domain list is pre-computed from the corpus.
History signals (Tier C) compare new URLs and maintainers against the local database. A URL that has never been observed before in any package is globally novel; one never seen for this specific package is locally novel. Novelty is definitionally meaningless on first run, so its contribution is maturity-gated (see step 3).
Verification signals (Tier D) inspect the end-state PKGBUILD for cryptographic metadata: checksum arrays, PGP key declarations, and GPG verify calls. These are computed over the resolved end-state, not the diff delta, because what matters is the protection in place when the package is installed, not whether that protection was added or removed in this particular update.
3. Score¶
The score is a single integer from 0 to 100 computed from all signals. The calculation is purely additive: nothing lowers a score.
Base score = sum of severity weights of all fired rules, plus source bucket modifiers, plus novelty weights scaled by maturity.
Each severity level carries a weight that reflects its information value:
| Severity | Weight | Meaning |
|---|---|---|
| CRITICAL | 40 | Almost certainly malicious if triggered |
| HIGH | 25 | Strong signal |
| MEDIUM | 15 | Notable but not definitive |
| LOW | 5 | Weak signal; context-dependent |
| INFO | 0 | Recorded for audit only |
FATAL rules (R012, R013) short-circuit scoring. When a FATAL rule fires, the score is immediately set to 100 regardless of any other signal. FATAL rules contribute 0 to the additive sum because their weight would be irrelevant; the hard stop at 100 is their entire effect.
Declared verification is reported, never credited. Checksums, PGP keys, GPG
verification and source pinning are emitted as weight-0 findings in the P
namespace and shown to the reader:
| Evidence | Finding | Weight |
|---|---|---|
| checksums declared | P001 |
0 |
validpgpkeys declared |
P002 |
0 |
| signature source declared | P003 |
0 |
| pinned to a commit | P005 |
0 |
| pinned to a tag | P006 |
0 |
| trusted-forge source | P007 |
0 |
Earlier versions subtracted for these. They no longer do. Everything TrustSight
sees is attacker-declared, and TrustSight never fetches, so it never confirms
that a declared key signs anything or that a pinned commit holds what it claims.
Adding validpgpkeys=(...) costs an attacker nothing, so a credit for it is a
mechanism whose only reliable effect is buying points back for whoever reads the
rules. The reader can check these claims in ways the tool cannot, which is why
they are still reported. See
B10.
The calibration problem the subtractions solved, a package doing GPG
verification scoring worse than one doing nothing because a SKIP on a .asc
file added points, is fixed at source instead: R004 does not fire on a SKIP
that is mandatory, structurally uncheckable, or covered by declared PGP keys.
Source bucket modifiers adjust for the trustworthiness of the domain:
| Bucket | Modifier | Rationale |
|---|---|---|
trusted_forge |
0 | A forge is neutral: reported as P007, never credited |
official |
0 | Known upstream domains are neutral |
unknown |
+20 | Never-before-seen domain requires scrutiny |
homograph |
+30 | Visually confusable domain is high risk |
Novelty weights add to the score when maturity allows:
| Signal | Full weight | Scaled by maturity |
|---|---|---|
url_first_globally |
10 | x min(1, observations/50) |
url_first_in_package |
5 | x min(1, observations/50) |
maintainer_first |
15 | x min(1, observations/50) |
The maturity gate exists because novelty is meaningless in a cold database. On first run, every URL is first-seen, every maintainer is first-seen. Full-weight novelty from a cold DB would flag every package, producing zero information. The gate phases in novelty weight linearly as observations accumulate, reaching full weight at 50 observations.
The final score is clamped to 0 to 100. A package with checksums, a trusted forge source and no rule firings scores 0, and its three declared practices are reported beside that 0 rather than folded into it.
4. Classify¶
The score maps to a verdict class:
| Score range | Verdict | Meaning |
|---|---|---|
| 0 to 20 | UNFLAGGED | No actionable signals detected, and the analysis was complete |
| 21+ | FLAGGED | Signals warrant review before updating |
| Any | INCONCLUSIVE | A cold database, or an analysis that could not examine the whole change; requires manual review |
The 20-point threshold is calibrated against corpus benchmarks. The benign p95 (95th percentile of benign package scores) is 45; the CRITICAL p5 (5th percentile of CRITICAL-class malicious packages) is 60. The 15-point gap between these two distributions is the operational separation, and the published threshold stays at 20: moving it is a calibration decision with its own evidence, not a bookkeeping fix.
INCONCLUSIVE is not a score range but a state. It signals that the tool could not complete its analysis, not that the package is clean or dirty, and it is produced in exactly two situations:
- Cold database. The score is in the Medium band (21 to 50), maturity is below 0.5 (fewer than 25 recorded analyses; novelty reaches full weight at 50), and no HIGH, CRITICAL or FATAL finding fired. Novelty is the only thing holding the score up, and novelty is not trustworthy on a cold database.
- Incomplete coverage. The run recorded a coverage gap (
diff_truncated,line_truncated,tree_not_analyzedorunresolved_source) and no HIGH or worse finding fired. When a HIGH or worse did fire, the band survives but is shown qualified, asHigh (incomplete analysis).
In both cases a HIGH, CRITICAL or FATAL finding keeps its own band: an analysis that found something does not get to hide it behind "inconclusive".
5. Translate¶
The score, evidence breakdown, and verification metadata are rendered into a structured report. All output is deterministic and generated locally from the computed data.
Key numbers¶
- 1,535 tests, 69.1% zero-rate on the 3,246-diff locked corpus, 100% malicious recall (all labelled fixtures).
- CRITICAL p5 = 60, benign p95 = 45: the gap that matters.
- Enabling the full R039 to R059 set costs 0.5 percentage points of zero-rate and leaves p95 unchanged; 14 of 21 fire on zero benign diffs.
- R013 recall 88%, R012 recall 17% (R012 is a tripwire).
Start here¶
| Page | What it covers |
|---|---|
| Security Model | Why the score is deterministic and reproducible; the security model |
| Scoring Philosophy | Evidence tiers, why verification is declared rather than scored, corpus-derived weights |
| Rules Reference | Complete rule catalog with severity, weight, target, and scoring formula |
| Cold Start and Maturity | Why novelty is meaningless on run one; maturity gating |
| Corpus and Priors | AUR-wide snapshot, global priors, local novelty weighting |
| Fire Rates | Per-rule false-positive rates on the benign corpus and the 30 % gate |
| What TrustSight Cannot See | The reasoned ceiling of the tool |
| Seed Provenance | How the novelty seed is built, signed on the release channel, and audited |
| Benchmarks and Methodology | Per-class separation, CI gates, reproducible eval |