Skip to content
crafted signal

Core Concepts

Risks

The Risk Ops Board turns each company attack path into a tracked risk with a state machine, priority score, and lifecycle audit trail. Hunt, accept residual, escalate, or schedule a re-hunt — the loop closes back into coverage.

Overview

A risk in CraftedSignal is a company attack path with state. The threat model declares what the attacker could do; the Risk Ops Board tracks what you are doing about each one.

Every accepted attack path becomes a risk. Each risk carries a priority score, a coverage percentage, a state, and a full audit trail of every transition. The Risks page is where analysts triage, hunt, and close out exposure, and where threat-feed briefs connect to the risks they relate to.

Risks live at /threats/risks. The drawer opens on row click.


The state machine

Every risk moves through a deterministic lifecycle. Each transition is recorded in the lifecycle timeline with the actor, timestamp, and reason.

StateMeaning
suggestedGenerated by the threat model, awaiting review
acceptedAn analyst has approved the risk for investigation
huntingA hunt anchored to this risk is running
evidence_gatheredHunt completed; an evidence summary is attached
mitigatedA detection rule was promoted from the hunt; the risk is covered
active_threatEvidence indicates ongoing exploitation — escalate to IR
residual_acceptedRisk acknowledged with no further action
rejectedTerminal — the risk does not apply
  • suggestedaccepted or rejected
  • acceptedhunting, residual_accepted, or rejected
  • huntingevidence_gathered (or back to accepted to pause)
  • evidence_gatheredmitigated, active_threat, or residual_accepted
  • mitigatedhunting (when a scheduled re-hunt fires)
  • active_threathunting or residual_accepted
  • residual_acceptedhunting (analyst reopens)
  • rejected is terminal

State pills in the drawer use a fixed color scheme: blue (hunting), amber (evidence_gathered), emerald (mitigated), red (active_threat), zinc (residual_accepted), slate (default).


Priority

Each risk carries a 0–100 priority score. The board sorts by priority descending so the most urgent risks surface first.

priority = severity × likelihood × (1 − coverage) × staleness × scale
  • Severity and likelihood are declared on the risk (low = 0.25, medium = 0.5, high = 0.75, critical = 1.0).
  • Coverage is the fraction of attack-path steps detected by at least one rule. A risk with no rules pinned to any step has coverage 0; a risk with full per-step coverage has coverage 1.
  • Staleness boosts risks that haven’t been hunted recently. Capped at 3× — a risk that has never been hunted always sits at the maximum boost.
  • The scale factor normalizes the worst case (critical × critical × fully uncovered × never hunted) to 100.

Priority recomputes:

  • On every load of the Risks page (rate-limited to once per minute per company).
  • Whenever a detection’s MITRE techniques change (auto-rebinds coverage edges).
  • Whenever a hunt is promoted to a rule (the new edge re-anchors the risk).

Coverage

Coverage links detection rules to risks via the detection_threats edge table. Every edge carries a source so you can tell automated coverage from analyst-driven coverage:

SourceWhen it’s written
promoted_from_huntA hunt query graduated into a detection. Confidence = 100.
auto_technique_matchA detection’s MITRE techniques overlap with steps on the risk. Confidence = matched-steps ÷ total-steps × 100.
manualReserved for analyst-driven linking.

Auto-matched edges refresh whenever a detection’s technique list changes. Hunt-promoted and manual edges are preserved across recomputes — analyst intent always wins over the heuristic.


Threat-feed relevance

The threat feed never mints a risk. An incoming brief relates to the risks you already have: the platform intersects the brief’s MITRE techniques with the techniques of your accepted attack paths, and every overlapping path is a related risk, shown on the brief detail page and surfaced in the exposure and TI workbench views.

Techniques in a brief that appear in no accepted attack path are surfaced as coverage gaps. From the brief you model those uncovered techniques into an attack path in one step (it posts to /threats/paths/quick, prefilled with the techniques). A risk exists only once you accept that modeled path through the normal flow; nothing is created automatically and there is no separate candidate queue.

Because the relationship is derived at read time from the current model, it stays consistent automatically: accept a new attack path and the briefs that share its techniques immediately show it as a related risk.


The drawer

Click any row on the Risks page to open the drawer. It shows:

  • State pill (color-coded as above) and priority badge (0–100).
  • Severity / likelihood and hunts run count.
  • Evidence summary — populated automatically when a hunt completes against this risk.
  • Lifecycle timeline — every state transition with who fired it and why.
  • Actions (only the ones legal from the current state are shown):
    • Hunt this risk — creates a new hunt anchored to the risk and transitions to hunting.
    • Accept residual — prompts for a note, then transitions to residual_accepted.
    • Mark as active threat — for risks in evidence_gathered, captures escalation context and transitions to active_threat.
    • Schedule re-hunt now — for risks in mitigated, immediately fires a fresh hunt.

Hunting from a risk

When you click Hunt this risk, the platform creates a new hunt with:

  • Title "Hunt: <risk name>" and a hypothesis pointing back to the risk.
  • CompanyAttackPathID set to the risk ID — the hunt is anchored.
  • All active SIEMs selected by default (override on the new-hunt form if needed).

The hunt detail page renders a Covers kill chain card showing every step on the risk, with crosshairs on the steps the hunt is investigating and shields on steps already covered by production rules.

The hunt proposer also sources from accepted risks. Risk-anchored hypotheses are ranked by the underlying risk’s priority, so the suggested hunts at the top of /hunts are the ones the model says you should run next.


Re-hunt

When a risk is promoted to mitigated (a hunt graduated into a rule), the platform schedules a re-hunt 90 days out. The clock is stored as cap_next_review_at on the risk; a Temporal cron transitions the risk back to hunting and creates a fresh hunt at the scheduled time.

You can also re-hunt manually from the drawer’s Schedule re-hunt now action — useful when threat intel surfaces a new variant of the same TTP.

Precision-based rehunt

After a detection rule is promoted from a hunt, the platform tracks alert verdicts (true positive / false positive) through the promotion_outcomes table. When the rule accumulates 10 or more verdicts and its rolling precision drops below 40%, the platform automatically schedules a re-hunt for the linked risk. This returns the risk to hunting state so the hunt team can investigate whether the query needs tightening or the underlying detection has become too broad.

The low-precision threshold (40%) and the minimum sample size (10 verdicts) are fixed constants. Promoted rules are the only rules this applies to — hand-authored rules are not subject to the automatic precision gate.

The dashboard Detection Quality card shows the top 5 promoted rules with rolling precision below 60% (with ≥10 verdicts), giving analysts early visibility before the re-hunt threshold is reached.


Stale CAP notifications

A weekly Temporal workflow (every Monday at 09:00 UTC) scans for company attack paths that have stalled and sends a notification to prompt action:

ConditionThreshold
accepted state, no activity> 14 days since accepted or created
hunting state, no progress> 30 days since last hunted or created

Staleness is measured against cap_reviewed_at (for accepted) or cap_last_hunted_at (for hunting), falling back to cap_created_at when those fields are unset. The notification links directly to the Risk Ops Board.

This is a notification only — the state machine does not advance automatically. An analyst still needs to act on the risk.


Auto-accept

Risks generated with severity critical — or severity high with likelihood high or critical — are auto-accepted on creation. They land in the accepted state with reviewed_by = "auto-accept:severity" so they flow straight into the hunt queue. Less severe risks stay in suggested for analyst review.

This is intentional and not configurable per-tenant: the model already weighs severity heavily, so paths the model considers extreme need to be visible to hunters immediately.


Routes

RoutePurpose
GET /threats/risksRisks list, filterable by state and coverage view
GET /threats/risks/{id}/drawerDrawer partial (HTMX)
POST /threats/risks/{id}/huntCreate hunt + transition to hunting
POST /threats/risks/{id}/accept-residualTransition to residual_accepted
POST /threats/risks/{id}/mark-active-threatTransition to active_threat
POST /threats/risks/{id}/schedule-rehuntRe-hunt a mitigated risk
POST /threats/paths/quickModel a brief’s uncovered techniques into an attack path

  • Threat Model — how attack paths are declared and weighted.
  • Hunts — the action arm for risks in accepted, hunting, or mitigated.
  • Threat Feed — where briefs are scored and related to risks.
  • Threat Actors — the catalog briefs are enriched against.