Policy
Policy is what turns a risk score into a decision your application can act on. It's kept as a separate step from scoring deliberately: the signals and weights that produce a score are Sentriq's domain expertise, but what you want to happen at a given risk level is your own business decision — and it can change at any time without touching how risk is computed.
Two-step decision
Every environment has exactly one active policy, evaluated in two steps for each event:
- The score is mapped through a threshold map —
allow_max,monitor_max,challenge_max— to a baseline decision. Anything abovechallenge_maxisblock. Every environment has a threshold map, with sensible defaults, even with zero custom rules configured. - Your rules for that event type are then checked, highest priority first. The first rule whose conditions match overrides the baseline decision. If none match, the baseline decision from step 1 stands.
Example rule
A rule is one group of conditions — all (every condition must match) or any (at least one must match) — that produces a decision when it matches:
{
"name": "Challenge risky login",
"event_type": "login",
"conditions": { "all": [
{ "field": "risk.score", "operator": "gte", "value": 60 },
{ "field": "device.trusted", "operator": "eq", "value": false }
]},
"action": { "decision": "challenge" },
"priority": 100
}event_type can be left unset so a rule applies to every event type — useful for something like "never automatically block login events, challenge instead." Rules are one level deep by design: no nested groups, no arbitrary expression language, no code execution. Every condition is a plain field/operator/value triple checked against a fixed whitelist.
Condition fields
| Field | Meaning |
|---|---|
risk.score | The event's numeric risk score (0–100). |
risk.level | The banded risk level. |
event.type | The event type (e.g. login). |
signal.present / signal.absent | Whether a specific signal code fired on this assessment. |
automation.score | The event's automation confidence score. |
device.new | Whether this is a new device. |
device.trusted | Whether this device has been explicitly trusted. |
Condition operators
| Operator | Meaning |
|---|---|
eq | Equals. |
neq | Not equal to. |
gt | Greater than. |
gte | Greater than or equal to. |
lt | Less than. |
lte | Less than or equal to. |
contains | Value contains the given substring/item. |
in | Value is one of a given set. |
A rule with a malformed or unrecognized field, operator, or value never throws or blocks scoring — the condition simply never matches. Rules are validated strictly when you create or edit them, so this is a safety net, not something you should rely on in practice.
Explainability
Every risk assessment records exactly which policy version decided it, and which rule (if any) matched:
Risk score: 72 Default decision: challenge
Applied policy: v4 Matched rule: "Challenge risky login"
Final decision: challengeA null matched rule is a real, valid outcome — it means the plain threshold map decided the event, not that something went wrong.
Versioning
Your policy's version increments on every threshold change or rule create/update/delete. Each risk assessment permanently snapshots the exact policy version that decided it, so a past decision never appears to have been made under today's rules — even after you've since changed your thresholds or rules. Recomputing a past event against your current policy only ever happens explicitly, through the simulator below, never automatically.
Simulator
Before changing thresholds or adding a rule, you can preview what decision a hypothetical event would produce, without creating any real security event, risk assessment, or outcome:
POST /v1/environments/{env}/policy/simulate
Authorization: Bearer <dashboard session>
Content-Type: application/json
{ "event_type": "login", "risk_score": 85, "signals": ["NEW_DEVICE"] }
# => { "data": { "decision": "challenge", "matched_rule": { "id": "polr_...", "name": "..." }, "policy_version": 4 } }What this is not
- Signal weights are not customer-tunable — only the decision-mapping layer (thresholds and rules) is.
- Rule conditions are a fixed field/operator whitelist, not an arbitrary query language — this is deliberate, to avoid arbitrary code execution.
- There's no rule-vs-rule conflict warning beyond unique priority enforcement — two overlapping rules simply have the higher-priority one win, silently.