Evidence Tiers¶
TrustSight groups scoring signals into four evidence tiers. Each tier represents a fundamentally different kind of information with different availability and confidence characteristics.
Tier A : Structural (strongest, always available)
Tier B : Priors / Context (domain reputation, always available)
Tier C : History / Novelty (first-seen tracking, maturity-gated)
Tier D : Verification (declared, weight 0, end-state only)
Tier A : Structural¶
Pattern-matched from the PKGBUILD diff. Direct, observable facts about what the build script does.
Sources¶
- R001-R131 rules firing against resolved command strings or raw diff lines.
- R004/R005 checksum integrity rules (hard-coded, not TOML).
- C001-C007 structural anomaly rules (checksum/source integrity heuristics).
- D001-D004 dependency-graph rules.
Availability¶
Available cold. No database, no prior observations, no network; Tier A signals fire on the diff content alone. They are always scored at full weight.
Examples¶
| Signal | Rule | Severity |
|---|---|---|
curl ... \| bash |
R001 | CRITICAL |
sha256sums=SKIP |
R004 | HIGH / INFO |
sha256sums=() |
R005 | HIGH |
sudo in function body |
R009 | CRITICAL |
| Unicode bidi override | R013 | FATAL |
Benchmark¶
CRITICAL recall: 100%; every CRITICAL-class sample in the benchmark corpus (267 tests) is detected.
Tier B : Priors / Context¶
Domain reputation classification for every new source URL in the diff.
Sources¶
- Source bucket assignment via
classify_url()insrc/trustsight/buckets.py. - Domain allowlists in
~/.config/trustsight/trusted_domains.toml.
Buckets¶
| Bucket | Modifier | Examples |
|---|---|---|
trusted_forge |
0 | github.com, gitlab.com, codeberg.org, bitbucket.org |
official |
0 | kernel.org, python.org, nginx.org, archlinux.org |
self_hosted |
+10 | Custom domain under maintainer control |
raw_hosting |
+15 | raw.githubusercontent.com, pastebin.com, gist.github.com |
unknown |
+20 | Domain not in any allowlist |
homograph_attack |
+30 | Domain with Cyrillic homoglyphs (e.g. githab.com) |
A trusted forge is neutral, not a credit. Routing through github.com costs an
attacker nothing, so it is reported as the weight-0 finding P007 instead
(src/trustsight/scoring.py).
Availability¶
Available cold. Domain classification depends only on the built-in allowlists and tldextract. No database or history needed. Always scored at full weight.
Scope¶
The bucket prior is scored once per diff, at the modifier of the least-trusted
single added URL (the maximum), not summed per URL. A package adding thirty
unknown-host URLs is one diff whose provenance is unknown, not thirty separate
facts; summing stacked the modifier until legitimate multi-source packages
(electron, fonts) outscored CRITICAL findings, which the §10 separation gate
(benign_p95 < malicious_p5) exists to catch. Only added URLs are
classified; removed URLs are not scored.
Tier C : History / Novelty¶
First-seen tracking for URLs and maintainers, backed by the local SQLite database.
Sources¶
build_novelty_context()insrc/trustsight/analysis/novelty.py.source_urlsandmaintainerstables in the local database at~/.local/share/trustsight/.
Signals¶
| Signal | Raw weight | Description |
|---|---|---|
url_first_seen_globally |
+10 | Normalised URL never seen in any package. |
url_first_seen_in_this_package |
+5 | Normalised URL never seen for this package. |
maintainer_first_seen_for_this_package |
+15 | Maintainer never recorded for this package. |
URLs are normalised before novelty checking: version numbers are replaced with 0, hashes with HASH, dates with DATE. This prevents routine bumps from generating false novelty signals. See normalize_url() in src/trustsight/novelty.py.
Maturity gate¶
All Tier C signals are scaled by the maturity multiplier:
| Observations | Multiplier | Effective weight (url_first_globally) |
|---|---|---|
| 0 | 0.0 | 0 |
| 10 | 0.2 | 3 |
| 25 | 0.5 | 7 |
| 49 | 0.98 | 14 |
| 50+ | 1.0 | 15 |
Availability¶
Cold DB → zero contribution. With no prior observations, all novelty weights multiply by 0. As the database warms up, novelty signals ramp linearly to full weight.
INCONCLUSIVE verdict¶
When the final score is in the Medium range (21-50) and maturity is below 0.5 (fewer than ~25 observations) and no entry in the breakdown is HIGH, CRITICAL or FATAL, the verdict is downgraded from "Medium" to "Inconclusive". This signals insufficient data rather than actual risk. Logic at src/trustsight/scoring.py.
The same downgrade is applied, at any maturity, when the analysis recorded a coverage gap: see the security model. Logic at src/trustsight/coverage.py.
Tier D : Verification¶
Structural integrity protections declared in the resolved (post-diff) PKGBUILD.
They are reported, never scored: each is emitted as a weight-0 finding in
the P namespace. See
B10 for why
a signal an attacker can assert for free must not lower a score.
Sources¶
detect_verification_evidence()insrc/trustsight/differ.py.
Evidence¶
| Evidence | Finding | Weight | Condition |
|---|---|---|---|
checksum_present |
P001 |
0 | Post-diff PKGBUILD has a non-empty sha256/sha512/b2/md5 checksum array. |
validpgpkeys_declared |
P002 |
0 | Post-diff PKGBUILD declares PGP key fingerprints (16+ hex chars). |
gpg_verify_present |
P003 |
0 | Post-diff PKGBUILD runs gpg --verify, gpgv, or openpgp --check-signatures. |
| source pinning | P005 / P006 |
0 | Pinned to a commit hash, or to a tag (the weaker form: tags can be repointed). |
End-state, not delta¶
Verification evidence is computed over the **resolved end-state of the PKGBUILD (what the file looks like after the diff is applied), not over the diff delta. A checksum that was already present before the diff and unchanged still counts. This reflects the actual protection in place when the package is installed.
Checksum evidence is suppressed when checksum_behavior is "changed_from_sha256_to_skip" or "checksum_array_emptied"; an intentionally disabled checksum does not count as present even if the array declaration remains.
Availability¶
Available cold. Computed from the diff text alone. Contributes nothing to the score in either direction, so it is available and reported from the first run, with no maturity gate and no cold-start caveat.
Tier summary¶
| Tier | Name | Cold? | Maturity-gated? | Direction | Max contribution per signal |
|---|---|---|---|---|---|
| A | Structural | Yes | No | Positive | 40 (CRITICAL) or 100 (FATAL) |
| B | Priors/Context | Yes | No | Positive only | +30 (homograph); trusted forge is 0 |
| C | History/Novelty | No : zero | Yes (×0→1) | Positive only | +20 (maintainer) |
| D | Verification | Yes | No | Reported, never scored | 0 (P001-P007) |