Vulnerability Management

The Finding Rule Engine: AND/OR Criteria and Safe Mutations

By PMAP Security Team 21 min read

Every scanner import arrives with a verdict already attached. A DAST tool calls something critical. An SCA scanner flags a transitive dependency as high. A network scanner reports the same exposed service across forty hosts. None of those verdicts know your environment, your compensating controls, or which team owns the asset. So an analyst opens each finding, reads the context, and adjusts. That work is correct and necessary. It is also the single least scalable activity in a vulnerability management program.

The finding rule engine exists to remove that repetition without removing the judgment. You write the triage decision once as a named rule. The engine applies it to every matching finding the moment it is created, and on demand whenever you re-evaluate. The decision is still yours. It just stops being a thing you retype hundreds of times a week.

This article walks through how the PMAP rule engine is built, what a rule can and cannot do, and the controls that keep automated mutation safe at enterprise scale. It is part of our vulnerability management automation coverage, which sits one level above this piece and ties rules together with event-driven runbooks. Here the focus is narrow on purpose. We are looking at inline finding mutation only, the layer that reshapes a finding as it enters the platform.

Why Manual Triage of Every Import Does Not Scale

The math is unforgiving. A mid-sized program ingests findings from several scanners across many engagements. Each import can produce hundreds or thousands of records. If a human has to confirm severity, decide ownership, and tag context on every one, the backlog grows faster than the team can drain it. The analyst is not adding insight at that point. They are applying the same handful of policies over and over.

There is a second cost that is harder to see. When triage lives only in people’s heads, it drifts. Two analysts handle the same class of finding differently. The same finding gets re-triaged inconsistently across quarters. There is no record of why a scanner-reported critical was downgraded, so the next reviewer either trusts it blindly or redoes the analysis. Tribal knowledge is not a control. An auditor cannot read it.

The rule engine attacks both problems at once. It converts a recurring triage judgment into a declarative policy that the platform enforces uniformly. The decision is captured, versioned, and applied identically every time. Analysts stop spending their hours on mechanical adjustment and spend them on the findings that genuinely need a human. The program gets faster time-to-triage, lower analyst toil, and a traceable record of every automated change. Those three outcomes are the reason the feature exists.

The engine is built for the security program manager who owns triage policy, the platform administrator who manages global rules and runs bulk sweeps, the senior analyst or approver who signs off on high-impact rules, and the analyst who needs to understand why a finding looks the way it does. Each of those roles touches a different part of the workflow described below.

A Rule Is a Declarative AND/OR Criteria Tree

A rule has two halves. The first is the IF half, a set of conditions that decide which findings match. The second is the THEN half, the single action applied to every match. This section is about the IF half.

PMAP expresses matching conditions as a nested AND/OR criteria tree rather than a flat list. A condition is a triplet of field, operator, and value. Conditions are grouped, and each group carries a logic mode of and or or. Groups nest with unlimited depth, and a parent group can mix sub-groups freely. That structure lets you write a policy as specific as the real world demands. For example, match findings that are critical AND internet exposed AND either have a known exploit OR carry a CVSS score at or above 9.0. A flat list cannot express that branch. A tree can.

The matchable surface is wide. The engine exposes more than 25 fields spanning the finding itself, the asset it lives on, and group membership. You can match on severity, original_severity, cvss_score, has_known_exploit, sla_breached, vuln_type, title, cve_id, asset_type, asset_class, internet_exposed, tags, endpoint, company_id, project_id, asset_group_id, scanner_source, scanner_category, scanner_ref, and several taxonomy arrays including effects, root_causes, and remediation_techniques. These are the same dimensions an analyst reasons about during manual triage. The rule engine simply lets you state the reasoning formally.

Behind each field sits an operator allowlist. The engine ships 16 operators in total: eq, neq, contains, starts_with, matches_regex, gt, lt, gte, lte, lte_ord, gte_ord, in, not_in, all_in, older_than, is_empty, and is_not_empty. The two ordinal operators deserve a note. Severity is not a number, so a naive numeric comparison would be meaningless. PMAP gives lte_ord and gte_ord an explicit ordering of info < low < medium < high < critical < urgent, so a condition like “severity is at least high” behaves the way a human expects. The matches_regex operator opens pattern matching on titles and similar string fields, and the set operators like all_in and not_in handle tag and taxonomy arrays cleanly.

Not every operator applies to every field. A boolean field like internet_exposed only accepts eq. A number like cvss_score accepts the comparison operators. A string array like tags accepts the set operators. The builder enforces these pairings so you cannot author a nonsensical condition. The next section explains where those pairings come from.

One safety rule governs the whole tree. An empty criteria tree is rejected at create and update time with an ErrEmptyCriteria error and an HTTP 400. The evaluator independently returns false for an empty tree. That is a deliberate fail-closed posture. A rule with no conditions would match every finding, and a rule that matches everything is a mass-mutation accident waiting to happen. The engine refuses to let one exist.

Eight Action Types Applied in Priority Order

Once a finding matches, the rule applies exactly one action. There are eight action types, each mapped to a specific finding mutation.

override_severity sets the finding severity to a value you choose. This is the workhorse for re-rating scanner output against your own risk model. accepted_risk sets the finding status to accepted risk, the policy expression of “we have looked at this class and chosen to accept it.” assign is the unified assignment action that can target multiple users and multiple teams at once by writing into the assignee set. Two legacy single-target variants, auto_assign for a single user and auto_assign_team for a single team, remain available for back-compatibility. append_tag adds one or more tags to the finding. set_vuln_type sets the vulnerability type. set_remediation writes remediation guidance text. normalize_title trims and title-cases the finding title, which is how you tame the inconsistent strings different scanners produce for the same issue.

Those map cleanly to the triage decisions teams actually make. Re-rate severity, accept a known class, route to the right owner, tag for context, normalize a messy import, attach guidance. The action set is intentionally focused on inline finding shaping rather than broad orchestration. If you need to open a ticket, fire a notification, or trigger a scan in response to an event, that is the job of an event-triggered runbook, not an inline rule. We cover that distinction in detail in the runbook automation companion piece linked later, since the two layers are easy to confuse and serve different purposes.

Rules do not fire in isolation. The engine evaluates all active rules for a finding in ascending priority order, so multiple rules can match the same finding in sequence. That ordering matters. If two severity-override rules both match, the last one to run wins, because each writes the same field. Priority is the lever you use to make that deterministic. Author your broad rules at a lower priority and your specific exceptions at a higher one, and the exception lands last. The behavior is predictable as long as you are deliberate about ordering, and the priority value is visible on every rule so the ordering is never a mystery.

No-Code Building From a Schema

A rule builder that hardcodes its field list breaks the moment the platform adds a field. PMAP avoids that by making the builder schema-driven. A single endpoint, GET /schema, returns declarative FieldSpec metadata that describes every criteria field, its type, its enum values where it has them, and its operator allowlist. It also describes the parameters each action type accepts. The frontend reads that metadata and renders the entire criteria builder and action form from it.

The practical effect is that there is zero frontend hardcoding of fields or operators. The form-builder schema is the single source of truth, cached for five minutes on the client. When the platform adds a matchable field or a new operator pairing, the builder picks it up automatically. For you as an author, it means the UI only ever offers valid combinations. You pick a field, the builder offers exactly the operators that field supports, and you supply a value of the right shape. There is no JSON to memorize and no way to author a condition the engine cannot evaluate. Power users who prefer raw structure still get a Form and JSON toggle throughout the editor, so the no-code surface never becomes a ceiling.

PMAP also ships a library of seeded rules. These are platform-provided policies that cover common triage decisions, surfaced under a Suggested tab in the rules list. You enable one with a single click rather than authoring it from scratch. It is a sensible starting point for a new program and a quick reference for how a well-formed rule looks.

Dry-Run Preview Before Anything Changes

The most important moment in working with a rule is the moment before you turn it on. A rule that matches more findings than you expected, or fewer, is a problem you want to discover in advance, not after it has mutated production data. PMAP gives you two ways to see a rule’s effect without touching the database.

The first is dry-run preview. Preview runs the rule against up to 500 active findings for a company and returns, per finding, the before and after values the rule would produce. Crucially, it writes nothing. You see exactly which findings would be touched and what each one would become, with no commit. For a severity-override rule, that means a list of findings with their current severity beside the severity the rule would set. You read it, you confirm it matches your intent, and only then do you activate. For a global rule, preview requires you to pick a company first, since “every finding across every tenant” is not a question the dry-run will answer blindly.

Live Affected-Findings View

The second view answers a slightly different question. The Affected Findings tab live-evaluates the rule’s criteria tree against up to 5,000 of the most recent active findings and returns the matching page along with a change summary for each row. Where preview is a focused before-and-after for validation, the affected view is the broader blast-radius picture. It shows the title, severity pill, asset, and change-summary arrows for each match, and it marks rows where the rule has already been applied so you can tell new matches from settled ones.

The 5,000-row ceiling is a deliberate guard. When a rule matches more findings than that, the UI shows the paginated first page and the total match count rather than trying to load every record into the browser. You still learn the true scale of the rule. You just learn it without freezing the page. Between preview and the affected view, you can quantify a rule’s impact precisely before it ever runs, which is the foundation the approval workflow builds on next.

Four-Eyes Approval for Wide Blast Radius

Some rules are low stakes. A rule that normalizes titles on one scanner’s output is hard to get badly wrong. Other rules carry real weight. A rule that downgrades severity across an entire asset class, or auto-accepts risk for a category of findings, changes how the whole program sees and prioritizes its work. Those rules should not go live on one person’s say-so.

PMAP handles this with an opt-in four-eyes approval workflow. Set requires_approval on a rule and it enters a governed lifecycle. The rule starts in draft. The author submits it for review, moving it to pending. A second person with the rule:approve permission then approves or rejects it. Until the rule reaches the approved state, the evaluator skips it entirely. It exists, it is authored, but it mutates nothing.

The control that makes this real is the self-approval block. The approver’s identity must differ from the rule’s creator. If the author tries to approve their own rule, the request is rejected with an ErrSelfApproval error, and the UI hides the approve button from them as well. That is the four-eyes principle enforced at the server, not just suggested by the interface. A rejection carries a mandatory reason, so a turned-down rule comes back with an explanation the author can act on rather than a silent no. The approver does not have to guess at the rule’s impact either, because the preview and affected views described above give them the exact blast radius before they sign. Approval becomes a quantified decision instead of a leap of faith.

The four-eyes principle here is applied specifically to rule governance. The broader principle, and how it compares to single-approver models elsewhere in the platform, is covered in dedicated pieces; this article keeps it scoped to what approval means for a rule.

Why Every Mutation Is Reversible

Automated mutation is only as safe as your ability to undo it. A policy changes. A rule turns out to be too broad. A scanner taxonomy shifts and a rule starts matching findings it should not. When that happens you need to walk back the rule’s effects cleanly, and you need to do it without trampling the manual work analysts have done since the rule ran. PMAP’s revoke capability is built for exactly that.

Revoke walks the rule’s audit log and restores each affected finding to its before-state. It runs in two modes. Safe mode, the default, restores only findings whose current severity, status, and vuln type still match the rule’s after-summary snapshot. In other words, it restores a finding only if nothing has touched it since the rule did. If an analyst manually edited a finding after the rule fired, safe mode leaves that finding alone and reports it as skipped, because reverting it would clobber a human decision. Force mode does an unconditional restore regardless of current state, for the cases where you genuinely want everything rolled back.

That safe-by-default behavior is the point. Revoke is not a blunt instrument. It respects subsequent edits unless you explicitly tell it not to, so the common case of “undo this rule but keep the work people have done since” is the default, not a special flag you have to remember. The result summary makes the outcome transparent, showing how many findings were reverted, how many were skipped, and any errors.

Scale is handled too. The synchronous revoke path is capped at 5,000 audit rows. When a rule’s blast radius exceeds that, revoke runs as an async job. The request returns a 202 with a job descriptor, and the UI polls for progress so a large rollback never blocks a request or times out. Whether the revoke touches ten findings or fifty thousand, the mechanism and the safety guarantees are the same.

A Full Audit Trail and Override Badge

A rule that changes a finding without explaining itself is worse than no rule at all, because now the finding lies and nobody knows why. PMAP makes every automated mutation legible at two levels.

At the record level, every successful rule application writes a row to the rule_audit_log. Each row captures which rule fired, the target finding, the action snapshot at the time of application, and the before and after summaries of severity, status, and vuln type. That is the structured, queryable history that satisfies an auditor and feeds the revoke walk. You can open any rule’s Audit Log tab and read its full application history, finding by finding, with before-and-after detail.

At the finding level, the change is visible right where an analyst will see it. On every apply, the engine stamps override fields onto the finding: the rule id, the rule name, and a human-readable override message such as Rule 'No Exploit Critical → High': severity critical → high, along with the before and after severity values. That override message surfaces as a badge on the finding detail page. So when an analyst opens a finding and sees a severity that differs from the raw scanner value, they do not have to wonder. The badge tells them which rule changed it and what it changed. The effective verdict and its provenance live side by side.

This is the difference between automation you trust and automation you fear. Every change is attributed, explained, and reversible. The finding never silently disagrees with the scanner. It shows its work.

Idempotent Mutations and Expiry

Two smaller behaviors keep the engine clean over time, and both matter more than they first appear.

The first is idempotency on tag mutation. The append_tag action deduplicates before it commits. If a rule fires on a finding that already carries the target tag, no duplicate tag is added and, just as important, no spurious audit-log entry is written. This is what makes bulk re-evaluation safe. You can re-run all active rules across the whole finding set after a policy refresh, and tags that are already present do not pile up and the audit log does not fill with no-op noise. Re-evaluation becomes a routine sweep rather than a destructive event you hesitate to trigger.

The second is expiry. A rule can carry an optional expires_at timestamp. Once that time passes, the evaluator skips the rule automatically. The rule record is not deleted, so you keep the history and the audit trail, but the rule stops mutating new findings. This fits time-boxed policy perfectly. A temporary downgrade during a known maintenance window, a rule that should only apply for the duration of a specific engagement, a seasonal exception. You set the expiry once and the engine retires the rule on schedule without anyone having to remember to turn it off.

Together these two behaviors mean the engine stays correct under repetition and under time. Rules do not accumulate noise when re-run, and they do not outlive their intended window.

How PMAP Shifts Triage From Per-Finding Toil to Policy

Step back and the through-line is clear. The rule engine takes the triage decision and moves it from an action a person repeats to a policy the platform enforces. The criteria tree captures the judgment with real precision. The action set applies it. Priority ordering makes overlapping policies deterministic. The schema-driven builder keeps authoring honest and no-code. Preview and the affected view quantify impact before anything runs. Four-eye approval governs the rules that carry weight. Revoke makes every change reversible without clobbering manual work. The audit log and override badge make every mutation legible. Idempotency and expiry keep the whole system clean over time.

None of those controls is decorative. Each one answers a specific way that naive automation goes wrong. Mass mutation from an empty rule is blocked by fail-closed validation. Unreviewed high-impact rules are blocked by approval. Irreversible changes are solved by revoke. Silent mutation is solved by the badge and the log. The engine is permissive in what you can express and strict in what it lets you do unsafely, which is exactly the balance an enterprise needs before it trusts software to reshape its findings automatically.

The payoff is the one named at the start. Faster time-to-triage, because findings arrive correctly shaped instead of waiting in a queue for manual adjustment. Lower analyst toil, because the mechanical decisions are encoded once. And a traceable record of every automated mutation, because nothing the engine does is hidden. Triage stops being a backlog you drain by hand and becomes a policy the platform applies for you.

To put this into practice, the authoring a finding rule with AND/OR criteria guide walks through building, previewing, and approving a real rule step by step. Where this article explains why the engine works the way it does, the guide shows you how to drive it.

Frequently Asked Questions

What is a finding rule engine?

A finding rule engine is a no-code policy layer that encodes triage decisions as named, priority-ordered rules and applies them automatically to findings. In PMAP, each rule pairs a declarative AND/OR criteria tree of more than 25 matchable fields and 16 operators with one of 8 action types, such as severity override, risk acceptance, or auto-assignment. Rules fire inline as findings are created and on demand during bulk re-evaluation, so the same triage judgment is applied consistently instead of being repeated by hand on every import.

How is a rule different from a runbook in PMAP?

A rule mutates a finding inline as it enters the platform. Its action set is focused on shaping the finding itself, such as overriding severity, assigning an owner, appending a tag, or normalizing a title. A runbook is an event-triggered playbook that responds to events with a broader action catalog, including opening tickets, sending notifications, and triggering scans. In short, a rule reshapes a finding, and a runbook orchestrates a response. The two layers complement each other and are covered separately in our runbook automation article.

Can I preview a rule before it changes anything?

Yes. PMAP gives you a dry-run preview that runs a rule against up to 500 active findings and returns the before and after values for each one, with no database writes. There is also a live Affected Findings view that evaluates the criteria against up to 5,000 recent findings to show the full blast radius. Both let you quantify a rule’s impact precisely before you ever activate it.

What stops a rule from mutating findings without oversight?

Two controls. First, any rule can require four-eyes approval. When requires_approval is set, the rule moves through draft, pending, and approved states, and the evaluator skips it until it is approved. The approver must be a different person from the creator, enforced at the server, so self-approval is impossible. Second, an empty criteria tree is rejected outright, because a rule with no conditions would match every finding. A rule cannot go live unreviewed if you require approval, and a rule cannot accidentally match everything.

Can I undo a rule after it has changed findings?

Yes. Revoke walks the rule’s audit log and restores each affected finding. Safe mode, the default, only restores findings whose state still matches the rule’s snapshot, so any manual edits made after the rule ran are preserved and reported as skipped. Force mode restores everything unconditionally. For large rollbacks above 5,000 audit rows, revoke runs as an async job with progress polling, so even a wide rollback completes safely.

How do I know which rule changed a finding’s severity?

Every successful application stamps override fields onto the finding, including the rule name and a human-readable message such as “Rule ‘No Exploit Critical → High’: severity critical → high”. That message appears as a badge on the finding detail page, with the before and after severity values. Behind it, the full per-finding history lives in the rule’s audit log, so the change is both visible to analysts and queryable for audit.

Are rule changes idempotent on re-evaluation?

Yes, where it matters. The append_tag action deduplicates before committing, so re-running a rule on a finding that already carries the tag adds no duplicate and writes no spurious audit entry. That makes bulk re-evaluation safe to run as a routine sweep after a policy refresh, without tags piling up or the audit log filling with no-ops. Rules can also carry an expiry, after which the evaluator skips them automatically while keeping the historical record intact.

author avatar
PMAP Security Team

Newsletter

Get the next writeup in your inbox

One short email when a new case writeup or detection deep dive ships. No marketing drip, no third-party tracking.