Skip to content

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:

f_peer = clip( (user_recency - peer_mean) / peer_stddev, 0, 1 )

Edge Cases

  • Fewer than 3 peers: f_peer falls 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