Skip to content

DetectionResult

DetectionResult is the dataclass returned by detect_penetration_attempt() and detect_penetration_patterns(). Replaces the legacy tuple[bool, str] return so callers can read per-category metadata alongside the boolean verdict.

from dataclasses import dataclass, field


@dataclass
class DetectionResult:
    is_threat: bool
    trigger_info: str
    threat_categories: list[str] = field(default_factory=list)
    threat_scores: dict[str, float] = field(default_factory=dict)

Fields

Field Type Description
is_threat bool True when at least one regex or semantic pattern matched.
trigger_info str Human-readable description of the first hit (e.g. "Query param 'q': Value matched pattern '...'"). Empty when is_threat=False.
threat_categories list[str] Ordered list of categories that contributed at least one match ("sqli", "xss", "custom", ...). Deduplicated; preserves first-seen order.
threat_scores dict[str, float] Maximum score per category. Regex matches contribute 1.0; semantic matches contribute their probability or threat score.

When to inspect each field

is_threat is the gate. Most callers short-circuit on it and return early.

threat_categories is what you read when the next decision depends on which kind of attack you saw. Per-category ban policy uses it (SecurityConfig.threat_ban_config); dashboards use it to render the right glyph. The list is empty when only legacy semantic hits occurred without category labels.

threat_scores is what you read when you want a numeric handle for ranking, alerting, or dashboard severity. The values are not normalized across regex and semantic — regex always reports 1.0, semantic reports its model probability.


Example

from guard_core.utils import detect_penetration_attempt


async def handle_submission(request, logger):
    result = await detect_penetration_attempt(request)
    if not result.is_threat:
        return {"ok": True}

    logger.warning(f"Detection: {result.trigger_info}")
    for category in result.threat_categories:
        score = result.threat_scores.get(category, 0.0)
        logger.warning(f"  category={category} score={score:.2f}")
    return {"error": "Suspicious activity detected"}

Migration from tuple[bool, str]

detected, trigger = await detect_penetration_attempt(request)

result = await detect_penetration_attempt(request)
detected, trigger = result.is_threat, result.trigger_info

The legacy 2-tuple is no longer returned. Callers that destructured the old tuple must migrate. See Migration guide for the full breaking-change list.