The same detection on a test VM and on a domain controller are not the same incident, yet a vendor severity tag treats them identically. This guide implements the asset-tier multiplier that injects business context into the score, as one precise technique within dynamic severity scoring and the broader alert correlation rule engines pipeline.

Root-Cause Context

Detection content assigns severity based on the signature, before it knows anything about the asset. A signature-based “medium” for suspicious PowerShell is a medium whether it fires on an ephemeral CI runner or on the tier-0 identity infrastructure that would let an attacker forge tickets for the whole domain. The business impact — the thing triage actually cares about — lives in an asset inventory that the detection engine never consulted.

The failure this produces is subtle but corrosive: analysts learn that “high” does not reliably mean “important,” because a flood of highs on disposable infrastructure trains them to distrust the label. Injecting asset criticality as a multiplier restores the correlation between score and consequence. The multiplier must come from an authoritative, version-controlled tier mapping — not a hard-coded list — so that reclassifying an asset (a test box promoted to production) immediately changes how its alerts are scored, with a full audit trail.

Asset criticality tiers mapping to severity multipliers Four asset tiers — sandbox, standard, sensitive, and crown-jewel — map to increasing severity multipliers from 0.5 to 2.0. The same base detection severity is multiplied by the tier factor so identical signatures diverge in triage priority according to the business criticality of the affected asset. score = base severity × tier multiplier Sandbox / ephemeral CI runners · scratch VMs ×0.5 Standard workload general endpoints ×1.0 Sensitive databases · app servers ×1.5 Crown jewel domain controllers · PKI ×2.0

Prerequisites

The implementation targets Python 3.11+ and uses only the standard library plus pydantic. It assumes an authoritative asset inventory keyed by the ECS host.id, normalized upstream by JSON event normalization, and feeds its weighted score to the parent dynamic severity scoring engine.

pip install "pydantic>=2.6,<3.0"

Production-Ready Implementation

The multiplier is looked up from a versioned tier map; an unknown asset defaults to a conservative standard tier and is flagged for inventory reconciliation rather than silently under- or over-scored.

from __future__ import annotations

import logging
from dataclasses import dataclass

from pydantic import BaseModel, Field

logger = logging.getLogger("soc.scoring.asset")

# Versioned tier -> multiplier map (loaded from config in production).
TIER_MULTIPLIERS: dict[str, float] = {
    "sandbox": 0.5,
    "standard": 1.0,
    "sensitive": 1.5,
    "crown_jewel": 2.0,
}
DEFAULT_TIER = "standard"


class ScoredAlert(BaseModel):
    alert_id: str = Field(min_length=1)
    host_id: str = Field(min_length=1)
    base_severity: float = Field(ge=0, le=100)


@dataclass(frozen=True)
class AssetRecord:
    host_id: str
    tier: str


class AssetWeighter:
    def __init__(self, inventory: dict[str, AssetRecord], ceiling: float = 100.0) -> None:
        self._inv = inventory
        self._ceiling = ceiling

    def multiplier_for(self, host_id: str) -> tuple[float, str]:
        record = self._inv.get(host_id)
        if record is None:
            logger.warning("ERR_SCORE_051 unknown asset host=%s -> default", host_id)
            return TIER_MULTIPLIERS[DEFAULT_TIER], DEFAULT_TIER
        mult = TIER_MULTIPLIERS.get(record.tier)
        if mult is None:
            logger.warning("ERR_SCORE_052 unknown tier=%s host=%s", record.tier, host_id)
            return TIER_MULTIPLIERS[DEFAULT_TIER], DEFAULT_TIER
        return mult, record.tier

    def weight(self, alert: ScoredAlert) -> dict:
        mult, tier = self.multiplier_for(alert.host_id)
        weighted = min(self._ceiling, alert.base_severity * mult)
        return {
            "alert_id": alert.alert_id,
            "host_id": alert.host_id,
            "base_severity": alert.base_severity,
            "asset_tier": tier,
            "multiplier": mult,
            "weighted_severity": round(weighted, 2),
        }


if __name__ == "__main__":
    inv = {
        "DC-1": AssetRecord("DC-1", "crown_jewel"),
        "CI-7": AssetRecord("CI-7", "sandbox"),
    }
    w = AssetWeighter(inv)
    dc = w.weight(ScoredAlert(alert_id="a1", host_id="DC-1", base_severity=50))
    ci = w.weight(ScoredAlert(alert_id="a2", host_id="CI-7", base_severity=50))
    assert dc["weighted_severity"] == 100.0   # 50 * 2.0, clamped at ceiling
    assert ci["weighted_severity"] == 25.0    # 50 * 0.5

The clamp at the ceiling keeps a crown-jewel multiplier from producing an out-of-range score, and the default-tier fallback ensures an asset missing from inventory is scored conservatively (never silently at zero) while its absence is surfaced as ERR_SCORE_051 for reconciliation.

Error-Code Reference

Code Meaning Action
ERR_SCORE_051 Asset not found in inventory Apply default tier; emit metric; reconcile the CMDB
ERR_SCORE_052 Asset has an unknown tier value Apply default tier; fix the tier taxonomy
ERR_SCORE_053 Tier multiplier config failed to load Fall back to all-1.0 (identity) weighting; alert loudly
ERR_SCORE_054 Weighted score exceeds ceiling before clamp Clamp to ceiling; expected for high base × high tier

Operational Notes

  • Tiers are config, not code. Store the tier map and the inventory in version control so reclassifying an asset is a reviewed, audited change that immediately affects scoring.
  • Default conservatively. An unknown asset should score at the standard tier, never zero — under-scoring an unclassified crown jewel is the dangerous direction.
  • Compose, don’t collide. The asset multiplier is one factor; combine it multiplicatively with the ATT&CK and convergence multipliers in the parent engine, then clamp once, so factors reinforce rather than one dominating.
  • Reconcile drift. Track the ERR_SCORE_051 rate; a rising unknown-asset rate means inventory coverage is decaying and scores are increasingly defaulted.

Verification Checklist

FAQ

Should asset criticality add to or multiply the score?

Multiply. A multiplier makes asset context compound with the other risk factors the way real risk does — a dangerous technique on a critical asset reinforces — whereas adding a fixed asset bonus lets that term dominate low-severity signals and flattens high-severity ones. Multiplicative weighting, followed by a single clamp to the ceiling, keeps the factors composable and bounded.

What multiplier should a crown-jewel asset get?

Enough to reliably float a medium-severity detection on a tier-0 asset above a high-severity detection on a disposable one, but not so much that every crown-jewel alert pins at the ceiling and loses resolution. A ×2.0 top tier against a ×0.5 bottom tier gives a 4× spread, which is usually sufficient; validate by replaying real alerts and confirming the resulting queue order matches analyst intuition.