50 lines
2.0 KiB
Python
50 lines
2.0 KiB
Python
"""Severity scoring (Chapter 10): a simple, transparent rank-escalation
|
|
model — no ML, easy to explain to a non-expert user (Ch10's own requirement).
|
|
|
|
Chapter 07 assigns a PROVISIONAL severity per rule type at parse time.
|
|
This module computes an EFFECTIVE severity by escalating that base rank
|
|
for repeated/rapid hits from the same IP — the exact rule Ch10 names.
|
|
|
|
Escalation is rank-based and strictly non-decreasing: it starts at the
|
|
base severity's rank and only ever moves up (repeat-count / hit-rate
|
|
bonuses), clamped at "high". An earlier weighted-score-vs-fixed-threshold
|
|
version could silently *downgrade* an isolated high-severity event (e.g.
|
|
a single sqlmap hit) to "medium" purely because the thresholds weren't
|
|
calibrated to the base weights — caught by running the pipeline against
|
|
real sample data. A single dangerous event must never end up rated below
|
|
its own base severity; only repetition/rate should push it higher.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
from dataclasses import dataclass
|
|
|
|
_SEVERITY_RANKS = ["low", "medium", "high"]
|
|
_RANK_BY_SEVERITY = {name: rank for rank, name in enumerate(_SEVERITY_RANKS)}
|
|
|
|
_REPEAT_COUNT_HIGH = 20
|
|
_REPEAT_COUNT_MEDIUM = 5
|
|
_RAPID_AVG_INTERVAL_SECONDS = 60 # >1 event/minute from one IP suggests automation
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class SeverityInputs:
|
|
base_severity: str
|
|
ip_event_count: int
|
|
avg_interval_seconds: float | None # None if this IP has < 2 events in the window
|
|
|
|
|
|
def compute_effective_severity(inputs: SeverityInputs) -> str:
|
|
"""Two named, inspectable escalation rules: repeat-count and hit-rate."""
|
|
rank = _RANK_BY_SEVERITY.get(inputs.base_severity, 0)
|
|
|
|
if inputs.ip_event_count >= _REPEAT_COUNT_HIGH:
|
|
rank += 2
|
|
elif inputs.ip_event_count >= _REPEAT_COUNT_MEDIUM:
|
|
rank += 1
|
|
|
|
if inputs.avg_interval_seconds is not None and inputs.avg_interval_seconds < _RAPID_AVG_INTERVAL_SECONDS:
|
|
rank += 1
|
|
|
|
rank = min(rank, len(_SEVERITY_RANKS) - 1)
|
|
return _SEVERITY_RANKS[rank]
|