Encode a triage decision once as governed policy, then apply it automatically, reversibly, and under four-eye approval to every finding the platform sees.
A finding rule is a named, prioritized policy that matches active findings against a declarative criteria tree and applies an action whenever the criteria is satisfied. Instead of an analyst hand-triaging every import, an operator encodes the decision once, and the engine applies it automatically when a finding is created or imported.
PMAP answers with three disciplines built into the engine. Criteria are validated and fail closed, so a tree that would match everything is rejected by construction. High-impact rules pass through a four-eye approval gate before they can fire. Every application is audit-logged, so it can be walked backward and undone, safely or by force.

The hard problem is not writing one rule. It is letting an organization run hundreds without losing control, because a rule that matches too broadly can rewrite a whole estate in one pass.
At a glance
- Backend domain: internal/rule (Go modular monolith, base path /api/v1/rules)
- Criteria model: Nested AND/OR CriteriaGroup tree, unlimited depth, fails closed on empty
- Matchable surface: 25+ fields and 16 operators, including severity-ordinal and array set-ops
- Actions: Eight action types: severity, risk, assignment, tags, remediation, vuln type, title
- Governance: Four-eye approval (draft to pending to approved); no self-approval
- Reversibility: Audit log per application; safe and force revoke; async for large blast radius
- Evaluation: Inline on every create and import; priority ASC; atomic apply and audit
How it works
Encode the triage decision once as a governed, reversible policy. The engine applies it automatically to every finding, a second reviewer approves the high-impact ones, and the audit log can undo any of it.
A rule matching logic is a recursive CriteriaGroup tree, not a flat list. Each group carries an AND or OR logic and members that are either leaf conditions (a field, an operator, a value) or nested groups. Because sub-groups can mix both logics, a rule can express precise policy, and the critical safety property is that an empty tree evaluates to false, not true.
Evaluation is driven by finding lifecycle, not by a schedule. The finding service calls the engine after every create or import, and the engine loads all active, non-expired rules for the company in ascending priority order and walks them in sequence. Each application commits atomically, and every change is previewable, governable, and reversible.
Key capabilities
- Four-eye approval. A rule that opts in with requires_approval is skipped by the evaluator until its state reaches approved, so it cannot mutate a finding while in draft or pending. The approver must not be the creator: self-approval is blocked at the API with ErrSelfApproval, and rejection requires a non-empty reason.
- Preview before you commit. The preview endpoint dry-runs a rule against up to five hundred active findings and returns the before and after severity and status per finding, with no mutation and no audit entry. The affected endpoint live-evaluates against up to five thousand recent findings and returns a paginated page with a change summary.
- In-context test. The test endpoint evaluates a rule against a caller-supplied FindingContext and returns whether it matched plus the resolved mutation. Because the context comes from the caller, an operator can probe how a rule behaves against a hand-crafted edge case, with no write occurring.
- Audit and revoke. Every successful application writes an audit-log row with the before and after summary. Revoke walks that log to restore each finding. Safe mode restores only findings whose state still matches the snapshot, protecting later manual edits, and force mode restores unconditionally.
Use cases
- Encode a prioritization policy once. A CISO wants scanner-reported critical findings with no known exploit to arrive as high. A program manager authors a severity-override rule with that two-condition tree, and from then on every matching finding is downgraded automatically at import, with an override badge recording the change.
- Route exposure to the right team. A SOC lead needs every internet-exposed finding to land with the AppSec team without manual hand-off. They build an assign rule keyed on internet_exposed, the unified action writes the team to the assignees on arrival, and the same-company invariant keeps routing inside the tenant boundary.
- Quantify blast radius before approving. A vulnerability manager is asked to approve a broad new rule. Before signing off they open the preview, which dry-runs it against up to five hundred live findings and shows the before and after severity per finding, plus the affected view for the total count.
Triage encoded once as policy, approved by two, applied to all, reversible by design.


