Rule Nature¶
Every rule in TrustSight belongs to a series (R, H, C, D, S, X, P, or W) that determines how it detects, not what it detects. The series is the mechanism; the category is the subject. A rule can be category = "integrity" and still be an R-series regex, an H-series heuristic, or a C-series structural check.
Understanding the nature of a rule tells you what you can tune, what you cannot, and what the finding means.
R-series: Regex Pattern Matching¶
Count: 37 rules (R001-R003, R007-R008, R010-R013, R017, R039-R059, R078, R091, R099, R104, R144, R152)
Mechanism: A Python regex is applied to either the resolved command text or the raw diff line. The pattern lives in ~/.config/trustsight/rules.toml and is loaded at runtime by load_rules().
What you can tune:
- The pattern itself (edit
rules.toml) match_target:resolved(post-variable-expansion) orraw_line(literal diff line)scope: restrict tofunction_body,message,other, or a named functionseverityandweight: adjust viaconfig.tomloverridesenabled: switch the rule off entirely
Rule definitions in rules.toml control matching behavior (pattern, target, scope). config.toml controls runtime overrides (severity, weight, enablement). When both set the same field, config.toml wins.
What you cannot tune:
- The rule ID (immutable)
- The series assignment (R means regex, always)
Example: R001 matches curl ... | bash on resolved text. Changing the pattern to wget ... | sh would narrow the detection. Changing match_target to raw_line would miss payloads assembled from variables.
When it fires: On every diff line (or resolved line) that matches the pattern. No diff context needed; no history needed.
Coupling to the tokenizer. match_target = resolved rules detect only what the tokenizer can read; an assignment it refuses is not matched against, and the refusal routes suspicion to the X-series instead. A shrinking R fire rate beside a non-zero unresolved count is the tokenizer's boundary showing through, not silence from the recipe.
H-series: Heuristic Behavioral Analysis¶
Count: 103 rules (H001-H103)
Mechanism: Engine-defined logic in analysis/*.py examines behavioral signals: what changed relative to, what the corpus has seen before, what files exist outside the diff hunk, or what the observation database records. H-series rules need context a single regex cannot see, but unlike C-series and D-series, they reason about behavioral suspicion rather than structural invariants or graph properties.
What you can tune:
severityandweight: adjust viaconfig.tomloverridesenabled: switch the rule off entirely
What you cannot tune:
- The detection logic (engine-defined, not a pattern)
- The rule ID
- The match target (always
programmatic)
Example: H015 fires when build() was modified between commits. It needs the old and new commit trees, not just the diff. H022 fires when a package gap exceeds 365 days. It needs the observation database.
Internal taxonomy. H carries two kinds that share a series anyway: shape heuristics (recipe-form reasoning, single-commit, deterministic) and observational heuristics (they need old trees, the observation database, or the seed: H015, H022, H029's floors). Both reason about behavioral suspicion rather than invariants, and both are engine-defined; the doc note exists so the series does not imply every H rule costs the same to replay.
Known misfile. H091 (checksum array shorter than the source array) is a two-field consistency invariant - the definition of the C-series - filed in H for ID-history reasons. Rule IDs are immutable citation targets, so the divergence is recorded here rather than migrated.
When it fires: When the behavioral condition is met. The finding carries a line number and body extracted from the diff, but the decision required multi-line or cross-commit context.
C-series: Check Structural Invariants¶
Count: 23 rules (C001-C025)
Mechanism: Deterministic structural checks across the PKGBUILD. C-series rules enforce invariants that cannot be expressed as a single regex match: two conditions must hold simultaneously, or a relationship between fields must be consistent. Unlike H-series (which reasons about behavioral suspicion), C-series checks are purely mechanical: if condition A and condition B co-occur, the invariant is violated.
What you can tune:
severityandweight: adjust viaconfig.tomloverridesenabled: switch the rule off entirely
What you cannot tune:
- The structural invariant (engine-defined logic)
- The rule ID
Example: C003 fires when source URLs changed without a version bump. It needs both the URL diff and the version diff to exist (or not exist) in the same change.
When it fires: When two or more structural conditions co-occur in a way that violates an invariant.
Filing rule. New invariant-shaped detections default to C-series even when the ID sequence has to skip: a C rule with a fresh ID beats another H misfile (Addendum 3's intake freeze). C014-C025 arrived this way.
D-series: Dependency Graph Analysis¶
Count: 4 rules (D001-D004)
Mechanism: Graph algorithms walk the AUR dependency tree to detect naming anomalies, typosquatting, or suspicious dependency additions. D-series rules compare package names against known-good lists or edit-distance thresholds, and flag new network-using makedepends. Unlike H-series (which examines behavioral context) and C-series (which checks structural invariants), D-series operates specifically on the dependency graph topology.
What you can tune:
severityandweight: adjust viaconfig.tomloverridesenabled: switch the rule off entirely
What you cannot tune:
- The graph-walking logic (engine-defined)
- The rule ID
Example: D002 fires when a dependency name is an edit-distance neighbor of a popular package (typosquatting). D003 fires when a new makedepends entry uses a network client.
H029 versus D002. H029 (package-name typosquat) and D002 (dependency typosquat) are the same mechanism on different graph positions: H029 scans the package's own name against observed popularity; D002 walks the dependency closure. The series split is positional, not mechanical, and that is the intended reading of "series = mechanism."
Cold start. D is weakest for new, low-observation packages - exactly where novel supply-chain attacks live. A D-rule that declines to fire for insufficient observations reports that abstention as a weight-0 boundary finding naming the rule and the observation count; the absence of a D finding is never read as evidence of safety.
When it fires: During the dependency closure walk, when a naming or composition anomaly is detected.
S-series: Sabotage Hostile Impact Detection¶
Count: 8 rules (S001-S008)
Mechanism: Detects payloads intended to harm, abuse, compromise, or exploit the operator environment. These rules are distinguished from general payload detection because they specifically identify actions intended to negatively impact the host: resource exhaustion (fork bombs, disk fill), file deletion, privilege abuse, service disruption, data exfiltration, and resource theft.
What you can tune:
severityandweight: adjust viaconfig.tomloverridesenabled: switch the rule off entirely
What you cannot tune:
- The detection logic (engine-defined)
- The rule ID
Example: S001 fires on fork bombs (:(){ :|:& };:). S004 fires on outbound data exfiltration patterns (the payload exfiltrates data from the operator's machine).
The evidence vacuum. Sabotage patterns are vanishingly rare in benign corpora, so S rules have near-zero measurable fire rates and are the least validated by calibration. The absence of an S finding is weak evidence: the series' value is severity-when-it-fires, not frequency. Every S rule ships with a mandatory fixture pair (one malicious that must fire, one benign lookalike that must not).
When it fires: When a sabotage pattern is found in the diff or the committed file tree.
X-series: Crossfire Evasion Detection¶
Count: 31 rules (X001-X031)
Mechanism: Detects evasion techniques, not the payloads they hide. X-series rules fire on how a thing was written rather than what it does. The tokenizer could not resolve the command, so the evasion technique itself is the signal.
The key distinction
A payload rule (R001, H075) fires on what the command does. A crossfire rule (X017) fires on how the command was hidden. Both can fire on the same line; they score independently. This is why a single diff can legitimately produce findings from both families.
What you can tune:
severityandweight: adjust viaconfig.tomloverridesenabled: switch the rule off entirely
What you cannot tune:
- The detection logic (engine-defined)
- The rule ID
Example: X007 fires when the tokenizer found multiple evasion techniques in one diff. X017 fires when enable -f loads an arbitrary ELF into bash as a builtin. X024 fires when a sensitive variable (DLAGENTS, COMPRESS*, PACMAN_AUTH) is assigned an indirect value.
X is a function of refusal. Any tokenizer improvement that resolves more constructs shrinks X's detection surface. That is the intended way this family shrinks, and every shrinkage is a measured event: the calibration replay records X-series aggregate fire rates per release, and a tokenizer change that moves any X rule's rate beyond the published epsilon carries a changelog adjudication naming the technique the family can no longer see and the rule that covers it now, if any.
When it fires: When the tokenizer reports that it could not resolve a command, and the evasion technique is identifiable.
P-series: Practice Declared Practices¶
Count: 7 rules (P001-P008; P004 is skipped)
Mechanism: Reports practices the recipe declares, not risks that were found. P-series findings are emitted at weight 0 and never contribute to the score. They exist so a reviewer can see what the recipe claims (checksums, PGP keys, pinned sources) without those claims being able to lower the score.
What you can tune:
- Which P findings render (default vs
--verbose) enabled: switch off entirely
What you cannot tune:
- The weight (always 0, by design: B10)
- The rule ID
Example: P001 reports "checksums declared for all non-VCS sources." P007 reports "source hosted on a trusted forge over HTTPS." Neither lowers the score; both are information for the reader.
Absence is signal. P findings are present-or-absent. No P001 means the recipe declares no checksums. Read P findings as a claims ledger - the recipe's own account of what it declares - and read the gaps between entries as what it does not. Not scored, per B10, forever.
When it fires: On every analysis, for every package that declares the practice. P findings are present or absent, not triggered by a diff.
W-series: Warning Analysis Boundaries¶
Count: 6 rules (W001-W006)
Mechanism: Reports what the analysis could not read. W-series findings mark visibility boundaries in the analysis: a package will run code the examination did not cover, and silence about that would be dishonest. They warn the reviewer where the analysis stopped.
What you can tune:
enabled: switch off entirely (not recommended)
What you cannot tune:
- The weight (always 0)
- The rule ID
- The fact that the gap exists (it is a property of the input, not the rule)
Example: W001 reports "a script invoked from inside the source tree was not read." W002 reports "npm install resolving dependencies from a registry." Neither changes the score; both tell the reviewer where the analysis stopped.
When it fires: When the analysis encounters a boundary it cannot cross. Always shown, never scored.
Known fuzzy edges¶
The taxonomy is a reading aid, not a partition, and a divergence found later is documented here rather than migrated (the W-series honesty principle applied to the taxonomy itself):
- H091 is C-natured (a two-field consistency invariant) but keeps its H ID: citation stability outranks the taxonomy, the same reason rule IDs are immutable.
- H029 and D002 are the same mechanism on different graph positions (package name versus dependency closure); the series split is positional.
- H carries an internal seam between shape heuristics and observational heuristics; both reason about behavioral suspicion, but their replay costs differ.
- R and X are both functions of the tokenizer: R-on-resolved detects only what resolution can read, and X detects what it refuses. A tokenizer change moves both, so both carry measured-shrinkage/growth gates.
- A rule whose mechanism later proves to belong to another series is documented here, not renumbered.
Summary Table¶
| Series | Full Name | Mechanism | Configurable | Weight | When it fires |
|---|---|---|---|---|---|
| R | Regex | Pattern match | Pattern, target, scope, severity | Severity-based | On every matching line |
| H | Heuristic | Behavioral analysis | Severity only | Severity-based | When behavioral condition is met |
| C | Check | Structural invariant | Severity only | Severity-based | When invariant is violated |
| D | Dependency | Graph algorithm | Severity only | Severity-based | During closure walk |
| S | Sabotage | Hostile impact detection | Severity only | Severity-based | When sabotage pattern found |
| X | Crossfire | Evasion detection | Severity only | Severity-based | When tokenizer reports evasion |
| P | Practice | Declared practice | Visibility only | 0 (always) | On every analysis |
| W | Warning | Analysis boundary | Visibility only | 0 (always) | When boundary encountered |
See Also¶
- Rules Reference; complete catalog by category
- Rule System Reference; engine internals, severity weights, field definitions
- Configuring Rules and Weights; how to tune R-series rules