Scoring Model¶
Overview¶
Every access grant in Verity carries a composite decay score — a single number from 0 (fresh, actively used) to 100 (fully decayed, dangerous).
The score answers one question:
"How likely is it that this access is no longer needed?"
Scores are not computed on a schedule. They update continuously as new signals arrive — a login event, a role change, a peer's activity shift. This makes Verity's scoring model fundamentally different from periodic reviews, which produce a binary keep/revoke decision once every 90 or 365 days.
Score Semantics
- 0 = Access was just used; fully justified.
- 50 = Significant evidence of decay; review recommended.
- 100 = Overwhelming evidence that access is no longer needed.
Scores are intentionally not probabilities. They are ordinal risk indicators designed for ranking, thresholding, and SLA assignment.
The Six Scoring Factors¶
The composite score is a weighted sum of six normalised factors, each producing a value between 0 and 1:
$$ S = \sum_{i=1}^{6} w_i \cdot f_i \times 100 $$
graph LR
subgraph Factors
F1["f_recency<br/>Weight: 0.30"]
F2["f_trend<br/>Weight: 0.15"]
F3["f_org<br/>Weight: 0.15"]
F4["f_peer<br/>Weight: 0.15"]
F5["f_review<br/>Weight: 0.10"]
F6["sensitivity_mult<br/>Weight: 0.15"]
end
F1 & F2 & F3 & F4 & F5 & F6 --> SUM["Σ Weighted Sum"]
SUM --> SCORE["Decay Score<br/>0 – 100"]
style F1 fill:#7c4dff,color:#fff,stroke:none
style F2 fill:#651fff,color:#fff,stroke:none
style F3 fill:#536dfe,color:#fff,stroke:none
style F4 fill:#448aff,color:#fff,stroke:none
style F5 fill:#00b0ff,color:#000,stroke:none
style F6 fill:#40c4ff,color:#000,stroke:none
style SUM fill:#263238,color:#fff,stroke:none
style SCORE fill:#f44336,color:#fff,stroke:none
f_recency — Time Since Last Meaningful Access¶
Weight: 0.30 (the single strongest signal)
Measures elapsed time since the user last meaningfully accessed the resource. "Meaningful" access excludes passive events such as metadata queries or automated health checks.
| Days Since Last Access | f_recency Value |
|---|---|
| 0 – 7 | 0.00 – 0.05 |
| 8 – 30 | 0.05 – 0.25 |
| 31 – 60 | 0.25 – 0.50 |
| 61 – 90 | 0.50 – 0.75 |
| 91 – 180 | 0.75 – 0.90 |
| > 180 | 0.90 – 1.00 |
Logarithmic Curve
The mapping uses a logarithmic curve, not a linear scale. The first 30 days of inactivity contribute less than the next 30 because early inactivity is often normal (vacations, sprint cycles). Prolonged inactivity is weighted more heavily.
f_trend — Score Trajectory¶
Weight: 0.15
Computes the slope of the decay score over a rolling 30-day window using linear regression. A steadily climbing score is a stronger decay indicator than a score that has been flat for months.
| Trend | f_trend Value |
Interpretation |
|---|---|---|
| Rapidly decreasing | 0.00 | User resumed active use |
| Stable low | 0.10 | Consistent healthy access |
| Flat medium | 0.40 | No change — uncertain |
| Steadily increasing | 0.70 | Decay in progress |
| Rapidly increasing | 1.00 | Strong decay acceleration |
f_org — Organisational Context¶
Weight: 0.15
Incorporates HR and directory signals that indicate a change in the user's role, team, or employment relationship:
| Signal | Score Contribution |
|---|---|
| Department transfer | +0.35 |
| Manager change | +0.25 |
| Role title change | +0.15 to +0.35 (based on similarity) |
| Cost-centre change | +0.25 |
| Employment type change (FTE → contractor) | +0.50 |
| No organisational change | 0.00 |
Values are additive but capped at 1.0. For example, a department transfer
and a manager change produce min(0.35 + 0.25, 1.0) = 0.60.
f_peer — Peer Comparison¶
Weight: 0.15
Compares the user's access pattern against peers in the same role and team. If peers actively use the resource but this user does not, the access is likely decayed.
The peer factor uses a z-score normalised against the peer group's recency distribution:
Edge Cases
- Fewer than 3 peers:
f_peerfalls back to 0.5 (neutral) to avoid noisy comparisons. - All peers are decayed: The entire peer group may receive elevated scores, surfacing systemic over-provisioning.
- New team member: First 30 days are excluded from peer comparison to allow ramp-up time.
f_review — Previous Review Outcomes¶
Weight: 0.10
Encodes the outcome of previous reviews for this access grant:
| Outcome | f_review Value |
Duration |
|---|---|---|
| Explicitly approved in last 90 days | 0.00 | Decays linearly back to 0.5 over 180 days |
| Approved with "revisit later" flag | 0.20 | Decays back to 0.5 over 90 days |
| No prior review | 0.50 | Default neutral |
| Previously flagged by reviewer | 0.70 | Persists until next review |
| Previously revoked, then re-granted | 0.80 | Persists for 365 days |
sensitivity_mult — Asset Sensitivity Multiplier¶
Weight: 0.15
Adjusts the score based on the classification of the target resource. Access decay on a restricted production database is more urgent than on a public wiki.
| Classification | Multiplier | Effective Weight Range |
|---|---|---|
| Public | × 0.5 | 0.00 – 0.075 |
| Internal | × 1.0 | 0.00 – 0.15 |
| Confidential | × 1.5 | 0.00 – 0.225 |
| Restricted | × 2.0 | 0.00 – 0.30 |
Score Can Exceed Factor Weight
Because the sensitivity multiplier can exceed 1.0, the composite score for highly sensitive resources can climb faster than the raw factor weights suggest. This is intentional: a decayed credential on a restricted system should trigger faster than one on a public system.
Risk Levels¶
Composite scores are mapped to four risk levels that drive review SLAs and dashboard visualisation:
| Risk Level | Score Range | Colour | Meaning |
|---|---|---|---|
| LOW | 0 – 24 | Green | Access is healthy. No action required. |
| MEDIUM | 25 – 49 | Amber | Access shows early decay signals. Monitor. |
| HIGH | 50 – 74 | Red | Significant decay. Review recommended. |
| CRITICAL | 75 – 100 | Dark Red | Severe decay. Immediate review and likely remediation. |
SLA Matrix¶
Each risk level carries a Service-Level Agreement that defines maximum response time for review completion. SLAs are enforced by Temporal workflows.
| Risk Level | Max Review Time | Reminder After | Escalation After |
|---|---|---|---|
| CRITICAL | 48 hours | 12 hours | 24 hours |
| HIGH | 168 hours (7 days) | 48 hours | 96 hours |
| MEDIUM | 720 hours (30 days) | 168 hours | 360 hours |
| LOW | 2 160 hours (90 days) | 720 hours | 1 440 hours |
SLAs Are Configurable
These are default values. Organisations can override SLA timings per risk level, per asset classification, or per connector in the platform configuration.
Score Components Example¶
Every score persisted to TimescaleDB includes a full components breakdown for auditability and debugging:
{
"grant_id": "g_01HZ3V9K7QWXR5YJNBM2C8F6D",
"identity_id": "id_01HZ3V9K7QWXR5YJNBM2C8F6E",
"resource_id": "res_01HZ3V9K7QWXR5YJNBM2C8F6F",
"score": 67,
"risk_level": "HIGH",
"computed_at": "2025-01-15T14:32:00Z",
"components": {
"f_recency": {
"weight": 0.30,
"raw_value": 0.72,
"weighted_value": 0.216,
"last_access": "2024-10-18T09:15:00Z",
"days_since_access": 89
},
"f_trend": {
"weight": 0.15,
"raw_value": 0.65,
"weighted_value": 0.0975,
"slope_30d": 0.82
},
"f_org": {
"weight": 0.15,
"raw_value": 0.35,
"weighted_value": 0.0525,
"signals": ["department_transfer"]
},
"f_peer": {
"weight": 0.15,
"raw_value": 0.58,
"weighted_value": 0.087,
"peer_group_size": 12,
"peer_mean_recency": 14.3,
"user_recency": 89
},
"f_review": {
"weight": 0.10,
"raw_value": 0.50,
"weighted_value": 0.05,
"last_review": null
},
"sensitivity_mult": {
"weight": 0.15,
"raw_value": 1.0,
"weighted_value": 0.15,
"classification": "confidential",
"multiplier": 1.5
}
}
}
Scoring Pipeline¶
The scoring pipeline runs as a Kafka consumer within the Decay Engine service:
graph TD
NE["Normalised Events<br/>(Kafka topic: normalised.events)"]
FE["Factor Extractors<br/>(parallel computation)"]
subgraph Extractors["Factor Extraction"]
direction LR
E1["Recency<br/>Extractor"]
E2["Trend<br/>Extractor"]
E3["Org<br/>Extractor"]
E4["Peer<br/>Extractor"]
E5["Review<br/>Extractor"]
E6["Sensitivity<br/>Extractor"]
end
AGG["Weighted Aggregator"]
RL["Risk Level Classifier"]
TS["TimescaleDB<br/>(access_scores hypertable)"]
KF["Kafka<br/>(score.events topic)"]
TH["Threshold Evaluator"]
RG["Review Generator<br/>(if threshold crossed)"]
NE --> FE --> Extractors --> AGG --> RL
RL --> TS
RL --> KF --> TH --> RG
style NE fill:#7c4dff,color:#fff,stroke:none
style AGG fill:#536dfe,color:#fff,stroke:none
style RL fill:#448aff,color:#fff,stroke:none
style TS fill:#263238,color:#fff,stroke:none
style KF fill:#263238,color:#fff,stroke:none
style TH fill:#ff9800,color:#000,stroke:none
style RG fill:#f44336,color:#fff,stroke:none
Processing Guarantees¶
| Property | Guarantee |
|---|---|
| Ordering | Per-grant ordering via Kafka partition key (grant_id) |
| Delivery | At-least-once; idempotent score writes via (grant_id, computed_at) unique constraint |
| Latency | p50 < 200 ms, p99 < 2 s from event ingestion to score persistence |
| Throughput | ~10 000 scores/second per Decay Engine replica |
Storage: TimescaleDB Hypertable¶
Scores are stored in a TimescaleDB hypertable partitioned by computed_at,
enabling efficient time-range queries and automatic data retention.
CREATE TABLE access_scores (
grant_id TEXT NOT NULL,
identity_id TEXT NOT NULL,
resource_id TEXT NOT NULL,
score SMALLINT NOT NULL CHECK (score BETWEEN 0 AND 100),
risk_level TEXT NOT NULL,
components JSONB NOT NULL,
computed_at TIMESTAMPTZ NOT NULL,
PRIMARY KEY (grant_id, computed_at)
);
-- Convert to hypertable (7-day chunks)
SELECT create_hypertable('access_scores', 'computed_at',
chunk_time_interval => INTERVAL '7 days'
);
-- Continuous aggregate for dashboard queries
CREATE MATERIALIZED VIEW access_scores_daily
WITH (timescaledb.continuous) AS
SELECT
time_bucket('1 day', computed_at) AS day,
resource_id,
avg(score)::SMALLINT AS avg_score,
max(score) AS max_score,
count(*) AS grant_count
FROM access_scores
GROUP BY day, resource_id;
Retention Policy¶
| Granularity | Retention | Purpose |
|---|---|---|
| Raw scores | 90 days | Detailed debugging and trend analysis |
| Daily aggregates | 2 years | Dashboard and reporting |
| Monthly aggregates | 7 years | Compliance and audit |
Next Steps¶
-
Review Lifecycle
See what happens when a score crosses the review threshold.
-
Remediation Pipeline
Learn how review decisions are executed safely.