Risk Scoring

For: Platform operators tuning behavioral anomaly detection on Axemere Gateway.

Operations Overview | Approval Workflows | Quarantine | Risk Scoring | CP Connectivity | Telemetry

Axemere Gateway computes a composite risk score (0.0–1.0) for each request before policy evaluation. The score is built from four behavioral signals, each weighted independently. Policy rules read the score and signals via the context.risk.* DSL namespace and can trigger any decision: allow, deny, quarantine, or require_approval.

Table of Contents


How Risk Scoring Works

Incoming Request

Risk Scorer
(per-request)

rate_spike
weight: 0.40

cost_anomaly
weight: 0.30

target_diversity
weight: 0.15

time_of_day
weight: 0.15

composite score
0.0 - 1.0

EvaluationContext
context.risk.*

Policy Engine

Risk scoring runs on every request before policy evaluation. The score is computed in-process (no external call) and injected into the evaluation context. Policy rules read it from context.risk.score and context.risk.signals.

Risk scoring is per-org, per-workload: each workload maintains its own baseline counters. A spike on one workload does not affect the score for another.


Risk Signals

rate_spike

Weight: 0.40

Detects a sudden increase in request rate relative to the workload's recent baseline.

  • Compares current requests-per-minute against a sliding baseline window.
  • Fires when: current_rate > baseline_rate × MVGC_RISK_RATE_THRESHOLD
  • Default threshold: 3.0 (current rate 3× the baseline triggers full signal contribution)

Example: Baseline is 10 req/min. Current window hits 35 req/min → rate is 3.5× baseline → rate_spike fires.

cost_anomaly

Weight: 0.30

Detects abnormal cost accumulation rate relative to recent spend patterns.

  • Compares current estimated cost-per-minute against a sliding baseline window.
  • Fires when: current_cost_rate > baseline_cost_rate × MVGC_RISK_COST_THRESHOLD
  • Default threshold: 3.0

Example: Baseline spend is $0.10/min. Current window is $0.45/min → cost_anomaly fires.

target_diversity

Weight: 0.15

Detects a sudden increase in the number of distinct target hosts contacted.

  • Compares distinct target hosts in the recent window against the baseline window.
  • Fires when the diversity ratio exceeds MVGC_RISK_TARGET_DIVERSITY_THRESHOLD.
  • Default threshold: 2.0

Known design limitation: The 5-minute baseline window always includes the 1-minute recent window, making the diversity threshold difficult to exceed in practice. This signal is most useful for detecting sudden host fan-out (e.g. a compromised workload scanning many endpoints) rather than gradual drift.

time_of_day

Weight: 0.15

Detects requests outside configured business hours.

  • Fires when the request timestamp falls outside MVGC_RISK_BUSINESS_HOURS.
  • Default: 09:00-17:00 (24-hour UTC).
  • Timezone is always UTC; configure accordingly for your region.

Example: A request arrives at 02:30 UTC → time_of_day fires → contributes 0.15 to the composite score.


Composite Score Calculation

Each active signal contributes its full weight to the composite score:

composite_score = Σ (signal_weight × signal_active ? 1.0 : 0.0)
Signals activeScore
None0.00
time_of_day only0.15
target_diversity only0.15
time_of_day + target_diversity0.30
cost_anomaly only0.30
rate_spike only0.40
rate_spike + time_of_day0.55
rate_spike + cost_anomaly0.70
rate_spike + cost_anomaly + time_of_day0.85
All four signals1.00

Typical thresholds used in policy:

  • > 0.7require_approval or risk_alert SIEM event
  • > 0.8quarantine

Baseline Warm-Up

rate_spike and cost_anomaly require a minimum of 5 observations before they can fire. Until the baseline is warm:

  • The signal is treated as inactive (contributes 0.0 to the score).
  • time_of_day and target_diversity fire normally regardless of warm-up state.
  • This prevents false positives on fresh gateway restarts or brand-new workloads.

gateway start or new workload

first 4 requests received

5th observation recorded

gateway restart, counters reset

Cold

Warming

Warm

Baseline state is in-memory and resets on gateway restart. If restarts are frequent, consider increasing MVGC_RISK_BASELINE_WINDOW or accepting that rate_spike / cost_anomaly will be inactive for the first few requests after each restart.


DSL Fields

Once the risk scorer runs, these fields are available in all policy layers:

DSL fieldTypeDescription
context.risk.scoredecimal stringComposite risk score, "0.00" to "1.00"
context.risk.request_ratestringRequests per minute in the current window
context.risk.cost_ratestringEstimated cost per minute in the current window
context.risk.signalscomma-separated stringActive signal names, e.g. "rate_spike,time_of_day"

Caller-supplied context fields (also in the context namespace):

DSL fieldSourceDescription
context.purposeActionRequest.context.purposeCaller-declared intent string
context.labels.<key>ActionRequest.context.labelsCaller-supplied label (e.g. context.labels.env)

Policy Patterns

Quarantine on high composite score:

risk:
  - id: risk.quarantine.high_score
    priority: 100
    when:
      field: context.risk.score
      gt: "0.8"
    effect:
      decision: quarantine
      reason: "composite risk score exceeds threshold"

Require approval on moderate risk + rate spike:

risk:
  - id: risk.require_approval.moderate_with_spike
    priority: 90
    when:
      all:
        - field: context.risk.score
          gt: "0.6"
        - field: context.risk.signals
          operator: in
          value: "rate_spike"
    effect:
      decision: require_approval
      reason: "elevated risk with rate spike — manual review"

Deny outside business hours for sensitive action types:

risk:
  - id: risk.deny.outside_hours.sensitive
    priority: 80
    when:
      all:
        - field: context.risk.signals
          operator: in
          value: "time_of_day"
        - field: action.type
          operator: in
          value: "ai.infer"
    effect:
      decision: deny
      reason: "AI inference outside business hours not permitted"

Allow but require purpose declaration:

identity:
  - id: identity.deny.no_purpose
    priority: 100
    when:
      field: context.purpose
      operator: exists
      negate: true
    effect:
      decision: deny
      reason: "requests must declare a purpose"

Tuning Thresholds

Start permissive and tighten based on observed baseline data from mvgc_risk_score metrics:

  1. Run the gateway for 24–48 hours with no quarantine or deny rules on risk.
  2. Observe the mvgc_risk_score gauge and mvgc_risk_signals counter in Prometheus.
  3. Identify the normal score range for your workloads.
  4. Set your policy threshold above the 99th percentile of normal scores.
  5. Start with require_approval rather than quarantine to validate the threshold before auto-blocking.

Example tuning sequence:

Week 1: observe, no risk rules
Week 2: add require_approval at score > 0.7 — watch approval queue
Week 3: move to quarantine at score > 0.8, keep require_approval at 0.65–0.80

Configuration

Risk scoring has two independent configuration surfaces: detection thresholds (how sensitive each signal is) and policy score thresholds (what score level triggers a decision). These are configured differently depending on how your gateway is deployed.

Detection Thresholds

Detection thresholds control when each signal fires. Set them in mvgc.yaml under the risk: stanza, or via environment variable. Environment variables take precedence.

# mvgc.yaml
risk:
  rate_threshold: 3.0         # rate_spike fires when req/min > N × baseline (default 3.0)
  cost_threshold: 3.0         # cost_anomaly fires when cost/min > N × baseline (default 3.0)
  window_size: 5m             # sliding baseline window (default 5m)
  business_hours: "09:00-17:00"  # UTC; outside this window fires time_of_day

target_diversity_threshold is env-var only (not available in the YAML stanza):

VariableDefaultDescription
MVGC_RISK_RATE_THRESHOLD3.0Multiplier above baseline rate to fire rate_spike
MVGC_RISK_COST_THRESHOLD3.0Multiplier above baseline cost rate to fire cost_anomaly
MVGC_RISK_TARGET_DIVERSITY_THRESHOLD2.0Multiplier above baseline distinct hosts to fire target_diversity
MVGC_RISK_BUSINESS_HOURS09:00-17:00UTC time range considered normal; requests outside fire time_of_day
MVGC_RISK_BASELINE_WINDOW5mSliding window duration for baseline calculations

Policy Score Thresholds

Policy score thresholds determine what composite score triggers a quarantine, approval, or deny decision. These live in policy rules, not in gateway config.

CP-attached gateways: Adjust score thresholds in the policy rule via the console. Changes are pushed as a signed bundle and take effect on the next gateway check-in.

Free Gateway (no CP): Edit the risk rule directly in your local policy YAML file (default directory: configs/policies/):

risk:
  - id: risk.quarantine.high_score
    priority: 100
    when:
      field: context.risk.score
      gt: "0.8"    # raise this value to reduce quarantine frequency
    effect:
      decision: quarantine
      reason: "composite risk score exceeds threshold"

After editing a local policy file, restart the gateway or wait for the policy cache TTL to expire (default: 5 minutes, configurable via MVGC_POLICY_CACHE_TTL or policies.cache_ttl in mvgc.yaml).


Prometheus Metrics

MetricTypeLabelsDescription
mvgc_risk_scoreGaugeorg_idLast computed composite risk score per org
mvgc_risk_signalsCountersignal_type, org_idCount of each active signal type per org

Use mvgc_risk_score to build a dashboard panel showing risk score over time per org. Alert when the gauge stays above 0.5 for more than 5 minutes.


Troubleshooting

SymptomLikely causeFix
Risk score always 0.0Baseline not warm yet (< 5 observations) or all thresholds very highCheck observation count; lower thresholds to confirm scoring is working
rate_spike never firesThreshold too high for actual traffic patternsReduce MVGC_RISK_RATE_THRESHOLD; check mvgc_risk_signals for any activity
time_of_day firing during expected hoursMVGC_RISK_BUSINESS_HOURS not in UTCConvert your business hours to UTC when setting the env var
Workload quarantined on every restartBaseline resets at restart; first requests hit empty baselineIncrease threshold or add a warmup rule that bypasses risk for the first N minutes after restart
target_diversity never firesBaseline window subsumes recent window, by designUse this signal only for extreme fan-out (10+ new hosts); lower threshold to 1.5 for more sensitivity

See Also