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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

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

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions