Skip to content

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.

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.

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 NotImplementedError
FieldRule[P] — generic base class with typed payload access

Key attributes:

  • id — unique rule identifier (e.g. python-star-import)
  • band — always 1 for FieldRules
  • collector_name — which collector’s evidence this rule consumes
  • evidence_kind — the specific evidence kind to filter for
  • payload_type — the Pydantic model the payload is validated against
  • default_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.

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 = ""
Hit — what rules yield for each finding

Each Hit carries:

  • rag — Red, Amber, or Green status
  • summary — what was found, in one sentence
  • recommendation — what to do about it
  • locator — precise source location (file:line)
  • severity — optional override (otherwise derived from RAG)
  • confidence — optional per-finding override

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.

REDdockerfile-secret-leakage
Secret leaked via COPY instruction
Dockerfile:14
The file .env is copied into the image. Secrets in environment files will be baked into the image layer and visible to anyone with access to the image.
AMBERk8s-resource-limits
Container missing memory limit
k8s/deployment.yaml:28
No resources.limits.memory set on container 'api'. Without memory limits, a single pod can exhaust node memory and trigger OOM kills across workloads.
GREENjava-health-check
Health endpoint configured
src/main/resources/application.yml:12
Spring Boot Actuator health endpoint is enabled and exposed, supporting liveness and readiness probes.

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.

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}",
                )
A complete FieldRule — checks one payload field, yields Hits

The pattern is consistent across all FieldRules:

  1. Declare the collector, evidence kind, and payload type
  2. Implement check() to iterate over the payload
  3. Yield Hit objects with RAG status, summary, and recommendation

You can control which rules run via configuration:

nfr-review.yaml
rules:
skip:
- python-star-import
- python-mutable-default

When include_only is set, only those rules run — everything else is skipped. When both skip and include_only are set, include_only takes precedence.

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.