Skip to content

Commit a937f8d

Browse files
committed
introduce autodiscovery to rule chain
1 parent d0eb6af commit a937f8d

2 files changed

Lines changed: 73 additions & 20 deletions

File tree

src/mail_sovereignty/classifier.py

Lines changed: 21 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -84,30 +84,22 @@ def _rule_confidence(
8484
8585
Rule chain (ordered by specificity, first match wins):
8686
87-
```
88-
- MX only 80% (Most likely unfinished config)
89-
- MX + SPF 90% (Managed setup)
90-
- AD + SPF 90% (Security GW upfront)
91-
- MX + SPF + AD 95% (Cloud Setup)
92-
93-
AD Cases sind natürlich nur MS-relevant. Mit den restlichen Indikatoren könntest du bspw. dann die restlichen 5-10% auffüllen.  Die %-te kann man natürlich auch anders aufteilen.
94-
```
95-
96-
// messen wie viel Rule gebraucht wird, evtl R5-R8 nicht unbeding relevant
97-
98-
9987
==== ========================== ====
10088
Rule Condition Base
10189
==== ========================== ====
10290
R1 MX ∧ SPF ∧ TENANT 0.95
103-
R2 MX ∧ SPF 0.90
104-
R3 SPF ∧ TENANT ∧ GW 0.90
105-
R4 MX ∧ TENANT 0.85
106-
R5 SPF ∧ TENANT 0.80
107-
R6 SPF ∧ GW 0.70
108-
R7 MX 0.80
109-
R8 SPF 0.50
110-
R9 else 0.40
91+
R2 MX ∧ SPF ∧ AD 0.95
92+
R3 AD ∧ SPF ∧ TENANT 0.95
93+
R4 MX ∧ SPF 0.90
94+
R5 SPF ∧ TENANT ∧ GW 0.90
95+
R6 AD ∧ SPF 0.90
96+
R7 MX ∧ TENANT 0.85
97+
R8 MX ∧ AD 0.85
98+
R9 SPF ∧ TENANT 0.80
99+
R10 SPF ∧ GW 0.70
100+
R11 MX 0.80
101+
R12 SPF 0.50
102+
R13 else 0.40
111103
==== ========================== ====
112104
113105
After the base is selected, each signal in *signals* that was **not**
@@ -118,17 +110,26 @@ def _rule_confidence(
118110
has_mx = SignalKind.MX in signals
119111
has_spf = SignalKind.SPF in signals
120112
has_tenant = SignalKind.TENANT in signals and provider == Provider.MS365
113+
has_autodiscover = SignalKind.AUTODISCOVER in signals
121114
has_gateway = gateway is not None
122115

123116
# Base confidence from rules (ordered by specificity)
124117
if has_mx and has_spf and has_tenant:
125118
base, used = 0.95, {SignalKind.MX, SignalKind.SPF, SignalKind.TENANT}
119+
elif has_mx and has_spf and has_autodiscover:
120+
base, used = 0.95, {SignalKind.MX, SignalKind.SPF, SignalKind.AUTODISCOVER}
121+
elif has_autodiscover and has_spf and has_tenant:
122+
base, used = 0.95, {SignalKind.AUTODISCOVER, SignalKind.SPF, SignalKind.TENANT}
126123
elif has_mx and has_spf:
127124
base, used = 0.90, {SignalKind.MX, SignalKind.SPF}
128125
elif has_spf and has_tenant and has_gateway:
129126
base, used = 0.90, {SignalKind.SPF, SignalKind.TENANT}
127+
elif has_autodiscover and has_spf:
128+
base, used = 0.90, {SignalKind.AUTODISCOVER, SignalKind.SPF}
130129
elif has_mx and has_tenant:
131130
base, used = 0.85, {SignalKind.MX, SignalKind.TENANT}
131+
elif has_mx and has_autodiscover:
132+
base, used = 0.85, {SignalKind.MX, SignalKind.AUTODISCOVER}
132133
elif has_spf and has_tenant:
133134
base, used = 0.80, {SignalKind.SPF, SignalKind.TENANT}
134135
elif has_spf and has_gateway:

tests/test_classifier.py

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -385,6 +385,58 @@ def test_autodiscover_beats_asn(self):
385385
# Fallback rule (0.40) + AUTODISCOVER boost (0.02) = 0.42 (ASN is AWS's)
386386
assert result.confidence == pytest.approx(0.42)
387387

388+
def test_mx_spf_autodiscover(self):
389+
"""R2: MX + SPF + AUTODISCOVER → 0.95."""
390+
evidence = [
391+
_ev(SignalKind.MX, Provider.MS365),
392+
_ev(SignalKind.SPF, Provider.MS365),
393+
_ev(SignalKind.AUTODISCOVER, Provider.MS365),
394+
]
395+
result = _aggregate(evidence)
396+
assert result.provider == Provider.MS365
397+
assert result.confidence == pytest.approx(0.95)
398+
399+
def test_autodiscover_spf_tenant(self):
400+
"""R3: AD + SPF + TENANT → 0.95."""
401+
evidence = [
402+
_ev(SignalKind.AUTODISCOVER, Provider.MS365),
403+
_ev(SignalKind.SPF, Provider.MS365),
404+
_ev(SignalKind.TENANT, Provider.MS365),
405+
]
406+
result = _aggregate(evidence)
407+
assert result.provider == Provider.MS365
408+
assert result.confidence == pytest.approx(0.95)
409+
410+
def test_autodiscover_spf(self):
411+
"""R6: AD + SPF → 0.90 (gateway hides MX but SPF+AD confirm)."""
412+
evidence = [
413+
_ev(SignalKind.AUTODISCOVER, Provider.MS365),
414+
_ev(SignalKind.SPF, Provider.MS365),
415+
]
416+
result = _aggregate(evidence)
417+
assert result.provider == Provider.MS365
418+
assert result.confidence == pytest.approx(0.90)
419+
420+
def test_mx_autodiscover(self):
421+
"""R8: MX + AD → 0.85."""
422+
evidence = [
423+
_ev(SignalKind.MX, Provider.MS365),
424+
_ev(SignalKind.AUTODISCOVER, Provider.MS365),
425+
]
426+
result = _aggregate(evidence)
427+
assert result.provider == Provider.MS365
428+
assert result.confidence == pytest.approx(0.85)
429+
430+
def test_autodiscover_spf_gateway(self):
431+
"""AD + SPF + GW → 0.90 (matches R6, gateway doesn't boost)."""
432+
evidence = [
433+
_ev(SignalKind.AUTODISCOVER, Provider.MS365),
434+
_ev(SignalKind.SPF, Provider.MS365),
435+
]
436+
result = _aggregate(evidence, gateway="barracuda")
437+
assert result.provider == Provider.MS365
438+
assert result.confidence == pytest.approx(0.90)
439+
388440
def test_independent_with_mx_and_spf_full_confidence(self):
389441
"""Independent domain with MX + SPF → 90% base confidence."""
390442
result = _aggregate(

0 commit comments

Comments
 (0)