Rules Deep Dive
Rules are the second phase of the pipeline. Each rule examines evidence from collectors and emits findings — structured observations about non-functional properties. nfr-review ships with 125 rules covering security, reliability, observability, performance, maintainability, and structure.
The Band system
Section titled “The Band system”Rules are organised into three bands by the type of analysis they perform:
| Band | Analysis type | Example |
|---|---|---|
| Band 1 | Single-evidence field checks via FieldRule |
Star import detection, missing health probe |
| Band 2 | Multi-evidence correlation via the Rule protocol |
Cross-language error handling consistency |
| Band 3 | Dynamic / runtime analysis | Latency threshold violations from OTel traces |
Most rules are Band 1 — they check one payload at a time using the FieldRule
base class. Band 2 and 3 rules implement the Rule protocol directly for access
to the full evidence list.
FieldRule anatomy
Section titled “FieldRule anatomy”The FieldRule generic base class is the standard way to write rules:
class FieldRule(Generic[P]):
"""Declarative single-evidence-kind rule with typed payload access.
Subclasses set class attributes and implement ``check()``.
The base handles selection, skip-if-empty, payload coercion,
the green all-clear finding, and Finding construction.
"""
id: str
band: Band = 1
collector_name: str
evidence_kind: str
payload_type: type[P]
pattern_tag: str
required_tech: list[str] = []
default_confidence: float = 0.9
all_clear_summary: str = "No issues detected."
all_clear_recommendation: str = "No action required."
all_clear_tag: str = ""
skip_evidence_kind: str = ""
required_collectors: list[str] = []
def __init_subclass__(cls, **kw: object) -> None:
super().__init_subclass__(**kw)
if not cls.__dict__.get("required_collectors") and hasattr(cls, "collector_name"):
cls.required_collectors = [cls.collector_name]
if "id" in cls.__dict__ and cls.id not in rule_registry:
rule_registry.register(cls.id, cls())
def check(self, payload: P, ev: Evidence) -> Iterable[Hit]:
"""Yield ``Hit`` objects for one typed payload. Yield nothing when clean."""
raise NotImplementedErrorKey attributes:
id— unique rule identifier (e.g.python-star-import)band— always1for FieldRulescollector_name— which collector’s evidence this rule consumesevidence_kind— the specific evidence kind to filter forpayload_type— the Pydantic model the payload is validated againstdefault_confidence— how confident the rule is in its findings (0.0–1.0)
Subclasses implement a single check(payload, evidence) method that yields Hit
objects for each finding.
The Hit dataclass
Section titled “The Hit dataclass”When a rule detects an issue (or confirms a good practice), it yields a Hit:
@dataclass(frozen=True, slots=True)
class Hit:
"""What a rule author yields from ``check()``.
Everything else (rule_id, collector info, default severity, the green
all-clear) is filled by the base class or ``make_finding``.
"""
rag: RAG
summary: str
recommendation: str
locator: str
severity: Severity | None = None
confidence: float | None = None
pattern_tag: str | None = None
content_hash: str = ""Each Hit carries:
rag— Red, Amber, or Green statussummary— what was found, in one sentencerecommendation— what to do about itlocator— precise source location (file:line)severity— optional override (otherwise derived from RAG)confidence— optional per-finding override
The RAG model
Section titled “The RAG model”Every finding has a RAG (Red / Amber / Green) status:
| RAG | Meaning | Default severity |
|---|---|---|
| 🔴 Red | Definite problem requiring action | high |
| 🟡 Amber | Potential concern or missing best practice | medium |
| 🟢 Green | Good practice confirmed present | info |
| ⏭️ Skipped | Rule could not run (missing collector data) | — |
Green findings are positive signals — they confirm that a best practice is in place. Reports include green findings so reviewers can see what’s working, not just what’s broken.
Example findings
Section titled “Example findings”dockerfile-secret-leakagek8s-resource-limitsjava-health-checkSeverity and confidence
Section titled “Severity and confidence”Severity uses five levels aligned with common vulnerability scoring:
| Level | Meaning | Score deduction |
|---|---|---|
critical |
Exploitable vulnerability or data loss risk | −15 points |
high |
Significant gap requiring near-term fix | −8 points |
medium |
Best practice violation worth addressing | −3 points |
low |
Minor improvement opportunity | −1 point |
info |
Informational or positive confirmation | 0 points |
Confidence (0.0–1.0) indicates how sure the rule is about the finding.
Rules set a default_confidence (typically 0.85–0.95 for AST-based checks),
and individual Hits can override it. Low-confidence findings can be filtered
out in reports.
Example: a complete rule
Section titled “Example: a complete rule”Here’s a real rule that detects Python star imports:
class PythonStarImportRule(FieldRule[PythonAstFilePayload]):
"""Flag wildcard imports that obscure dependencies."""
id = "python-star-import"
collector_name = "python-ast"
evidence_kind = "python-ast-file"
payload_type = PythonAstFilePayload
pattern_tag = "star-import"
default_confidence = 0.95
all_clear_summary = "No wildcard imports detected."
def check(self, payload: PythonAstFilePayload, ev: Evidence) -> Iterable[Hit]:
for imp in payload.imports:
if imp.is_star:
yield Hit(
rag="amber",
summary=f"Star import from {imp.module}",
recommendation=(
"Use explicit imports to make dependencies"
" visible and avoid namespace pollution."
),
locator=f"{payload.file_path}:{imp.line}",
)The pattern is consistent across all FieldRules:
- Declare the collector, evidence kind, and payload type
- Implement
check()to iterate over the payload - Yield
Hitobjects with RAG status, summary, and recommendation
Rule filtering
Section titled “Rule filtering”You can control which rules run via configuration:
rules: skip: - python-star-import - python-mutable-defaultrules: include_only: - dockerfile-secret-leakage - k8s-resource-limits - java-health-checkWhen include_only is set, only those rules run — everything else is skipped.
When both skip and include_only are set, include_only takes precedence.
Inline suppressions
Section titled “Inline suppressions”You can suppress specific rules on individual lines using magic comments:
import os # nfr-review:skip(python-star-import)Or suppress multiple rules:
import os # nfr-review:skip(python-star-import, python-mutable-default)Suppressed findings are excluded from the report but logged in the JSONL output
with a suppression_reason field for auditability.
Now that you understand how findings are produced, the next chapter covers the output formats available for consuming them.