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 (subtractions, end-state only)
Tier A : Structural¶
Pattern-matched from the PKGBUILD diff. Direct, observable facts about what the build script does.
Sources¶
- R001–R013 rules firing against resolved command strings or raw diff lines.
- R004/R005 checksum integrity rules (hard-coded, not TOML).
- C001–C003 structural anomaly rules (checksum/source integrity heuristics).
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:22. - Domain allowlists in
~/.config/trustsight/trusted_domains.toml.
Buckets¶
| Bucket | Modifier | Examples |
|---|---|---|
trusted_forge |
–10 | 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) |
The trusted forge modifier is capped at –20 total across all URLs (src/trustsight/scoring.py:108).
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¶
Evaluated per-URL. Each added URL contributes its bucket modifier independently. 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/novelty.py:81.source_urlsandmaintainerstables in the local database at~/.local/share/trustsight/.
Signals¶
| Signal | Raw weight | Description |
|---|---|---|
url_first_seen_globally |
+15 | Normalised URL never seen in any package. |
url_first_seen_in_this_package |
+10 | Normalised URL never seen for this package. |
maintainer_first_seen_for_this_package |
+20 | 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:12.
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 all contributing signals are Tier C novelty and maturity is below 0.5 (fewer than ~25 observations), the verdict is downgraded from "Medium" to "Inconclusive". This signals insufficient data rather than actual risk. Logic at src/trustsight/scoring.py:192.
Tier D : Verification¶
Structural integrity protections in the resolved (post-diff) PKGBUILD. These subtract from the score; they can never increase it.
Sources¶
detect_verification_evidence()insrc/trustsight/differ.py:149.
Evidence¶
| Evidence | Modifier | Condition |
|---|---|---|
checksum_present |
–10 | Post-diff PKGBUILD has a non-empty sha256/sha512/b2/md5 checksum array. |
validpgpkeys_declared |
–10 | Post-diff PKGBUILD declares PGP key fingerprints (16+ hex chars). |
gpg_verify_present |
–5 | Post-diff PKGBUILD runs gpg --verify, gpgv, or openpgp --check-signatures. |
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. Always scored at full weight. Negative modifiers can bring the total score below zero (the final score is then floored at 0).
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/negative | +30 (homograph) / –10 (trusted forge, capped –20) |
| C | History/Novelty | No : zero | Yes (×0→1) | Positive only | +20 (maintainer) |
| D | Verification | Yes | No | Negative only | –10 (checksum or PGP) |