File size: 4,458 Bytes
d543fc1
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
"""
core/confidence.py — Confidence Tier Tracker
=============================================
Defines three evidence tiers for injection/exploit findings and enforces
automatic severity/CVSS capping so that weak-signal signals never appear as
Critical or High.

Tiers
-----
CONFIRMED   Direct proof: reflected unique token, OOB callback received,
            timing delta reproduced ≥3×, or actual data exfiltrated.
LIKELY      Two or more independent weak signals agree (e.g. error pattern
            PLUS response-time anomaly), but no direct proof.
UNCONFIRMED Single weak signal only (e.g. one error substring, one generic
            500 response). Auto-capped to severity=Low, CVSS≤3.9.
PENDING     OOB probe sent; awaiting external callback. Resolved to CONFIRMED
            or EXPIRED at end-of-scan.

Usage
-----
    from scanners.core.confidence import ConfidenceTracker as CT

    sev  = CT.cap_severity(severity, confidence)
    cvss = CT.cap_cvss(cvss_score, confidence)

    self.add_vuln(..., severity=sev, cvss_score=cvss, confidence=confidence)
"""

from __future__ import annotations


class ConfidenceTracker:
    # Canonical tier strings (use these constants everywhere)
    CONFIRMED   = "Confirmed"
    LIKELY      = "Likely"
    UNCONFIRMED = "Unconfirmed"
    PENDING     = "Pending"

    VALID_TIERS = {CONFIRMED, LIKELY, UNCONFIRMED, PENDING}

    # Severity order (ascending)
    _SEV_ORDER = ["Info", "Low", "Medium", "High", "Critical"]

    # Maximum allowed severity per tier
    _MAX_SEV: dict[str, str] = {
        CONFIRMED:   "Critical",   # no cap
        LIKELY:      "High",       # confirmed-tier cap not needed; High is reasonable
        UNCONFIRMED: "Low",        # single weak signal — never escalate
        PENDING:     "Medium",     # OOB probe sent but no callback yet
    }

    # Maximum allowed CVSS per tier
    _MAX_CVSS: dict[str, float] = {
        CONFIRMED:   10.0,
        LIKELY:       6.9,
        UNCONFIRMED:  3.9,
        PENDING:      5.9,
    }

    # ------------------------------------------------------------------
    @classmethod
    def cap_severity(cls, severity: str, confidence: str) -> str:
        """
        Return severity capped to the tier maximum.

        Example:
            cap_severity("Critical", "Unconfirmed") → "Low"
            cap_severity("Critical", "Confirmed")   → "Critical"
        """
        confidence = cls.normalize(confidence)
        max_sev = cls._MAX_SEV.get(confidence, "Low")

        try:
            sev_idx = cls._SEV_ORDER.index(severity)
        except ValueError:
            sev_idx = 2  # default to Medium index

        try:
            max_idx = cls._SEV_ORDER.index(max_sev)
        except ValueError:
            max_idx = 2

        return cls._SEV_ORDER[min(sev_idx, max_idx)]

    # ------------------------------------------------------------------
    @classmethod
    def cap_cvss(cls, cvss: float, confidence: str) -> float:
        """
        Return CVSS score capped to the tier maximum.

        Example:
            cap_cvss(9.8, "Unconfirmed") → 3.9
            cap_cvss(9.8, "Likely")      → 6.9
            cap_cvss(9.8, "Confirmed")   → 9.8
        """
        confidence = cls.normalize(confidence)
        max_cvss = cls._MAX_CVSS.get(confidence, 3.9)
        return min(float(cvss), max_cvss)

    # ------------------------------------------------------------------
    @classmethod
    def normalize(cls, confidence: str) -> str:
        """Return a valid tier string, defaulting to UNCONFIRMED for unknown values."""
        if confidence in cls.VALID_TIERS:
            return confidence
        # Case-insensitive lookup
        for tier in cls.VALID_TIERS:
            if confidence.lower() == tier.lower():
                return tier
        return cls.UNCONFIRMED

    # ------------------------------------------------------------------
    @classmethod
    def apply(
        cls,
        severity: str,
        cvss: float,
        confidence: str,
    ) -> tuple[str, float, str]:
        """
        Convenience: return (capped_severity, capped_cvss, normalized_confidence).

        Usage:
            sev, cvss, conf = ConfidenceTracker.apply(sev, cvss, confidence)
            self.add_vuln(..., severity=sev, cvss_score=cvss, confidence=conf)
        """
        conf = cls.normalize(confidence)
        return cls.cap_severity(severity, conf), cls.cap_cvss(cvss, conf), conf