Skip to content

feat: persist PASS/FAIL/ERROR/NOT_APPLICABLE per rule per resource, fix compliance score #263

Description

@parthrohit22

What problem does this solve?

Two coupled problems, one fix:

  1. The scanner only persists failing findings. scanner/engine.py::run_scan() collects a flat
    findings list — there is no record of which rules ran clean, which errored, or which
    resources were never evaluated.
  2. As a direct consequence, api/models/finding.py::get_compliance_score() computes control
    status as status = "FAIL" if rule_id in failed_rule_ids else "PASS" — any rule that errored,
    whose resource was never observed, or that never ran is counted as PASS by default. This
    inflates every reported compliance score.

Both block every downstream reasoning-layer feature (drift history, campaign detection,
attack-path confidence) and the eight already-open "enterprise rules" issues (#253, #255, #257,
#258, #259, #261, #262, plus closed #256/#260) that already assume a PASS/FAIL/UNKNOWN/
NOT_APPLICABLE contract exists, with no shared engine support for it.

PR #250 ("Azure Resource Graph inventory foundation") already lands Phase 1 of the underlying
evidence model — normalised, tenant-scoped resource snapshots with complete/partial/failed
collection state — but explicitly does not persist rule-outcome data. This issue is Phase 2: the
rule-outcome side, built on top of #250's snapshots.

Describe the solution

  • New table (e.g. rule_evaluations) keyed on (scan_id, rule_id, resource_id) with a status
    enum: PASS / FAIL / ERROR / NOT_APPLICABLE, plus a free-text reason for ERROR/NOT_APPLICABLE.
  • ScanEngine.run_scan() writes an evaluation row for every resource a rule was applicable to,
    not just the ones that failed.
  • Update get_compliance_score() to compute PASS only from persisted successful evaluations in
    this table — remove the absence-implies-PASS default entirely.
  • Document the contract in docs/adding-a-rule.md so new rules (including the 8 open enterprise
    issues) target it directly instead of inventing ad hoc indeterminate handling (see
    scanner/rules/az_cmp_002.py's own LOW-severity indeterminate workaround for what happens
    without a shared model).

Alternatives considered

Ship the schema and the compliance-score fix as two separate PRs — rejected; the fix has no
correct implementation without the schema existing first, and reviewing them apart just means
merging a known-broken intermediate state.

Additional context

Depends on: PR #250 (merge first). Blocks: issue 2 below, and all eight open enterprise-rules
issues. Exit test: a forced API error or unseen resource never appears as PASS.

Coordination

This issue is the shared evaluation-contract foundation for the open enterprise-rule packs. It
does not duplicate them — it provides the engine support (per-resource PASS / FAIL / ERROR /
NOT_APPLICABLE) they assume but do not themselves build. It explicitly blocks and should be
linked from: #253, #254, #255, #257, #258, #259, #261, #262. Enterprise-rule PRs should consume
this persisted evaluation contract instead of re-implementing ad hoc indeterminate handling.

Metadata

Metadata

Assignees

Labels

bugSomething isn't workingcoreCore team ownership not for studentsenhancementNew feature or requestpriority: criticalMust be fixed immediately, breaks core functionality

Type

No type

Projects

Status
📋 Backlog

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions