-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathcontracts.py
More file actions
74 lines (58 loc) · 2.81 KB
/
Copy pathcontracts.py
File metadata and controls
74 lines (58 loc) · 2.81 KB
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
"""Producer/consumer event-type contract for the detection pipeline.
The pipeline is producers (parsers) -> typed events -> consumers (detectors).
Each parser declares the ``event_type`` values it can emit; each event-type-gated
detector declares the types it requires. ``check_event_contract`` asserts that
every required type has at least one producer.
This exists because of a real silent failure: ``detect_404_flood`` consumes
``http_404`` events, but for a while no parser emitted them, so the detector ran
every analysis and produced nothing — no error, no warning, just an empty result.
This turns that class of bug ("a consumer with no producer") into a loud failure
at startup/CI instead of a silent no-op in production.
NOTE: ``detect_port_scan`` is intentionally absent — it is field-gated (it needs a
``port`` field), not ``event_type``-gated, so it is not part of the type contract.
"""
# event_type values each parser can emit
PARSER_EMITS = {
"parse_ssh_log": {"failed_login", "successful_login"},
"parse_windows_csv": {"failed_login", "successful_login"},
"parse_web_log": {"http_404", "http_request"},
}
# event_type values each event-type-gated detector requires
DETECTOR_REQUIRES = {
"detect_brute_force": {"failed_login"},
"detect_404_flood": {"http_404"},
}
__all__ = [
"ContractError", "produced_event_types",
"check_event_contract", "assert_event_contract",
]
class ContractError(RuntimeError):
"""Raised when a detector requires an event type no parser produces."""
def produced_event_types(parser_emits=None):
"""Return the union of every event_type any parser can emit. Defaults to
PARSER_EMITS; pass parser_emits to check a different producer mapping."""
out = set()
for types in (parser_emits or PARSER_EMITS).values():
out |= types
return out
def check_event_contract(parser_emits=None, detector_requires=None):
"""Return a list of ``(detector, [missing_types])`` violations.
An empty list means every detector's required event types are produced by
at least one parser (a healthy contract).
"""
emits = produced_event_types(parser_emits)
violations = []
for detector, required in (detector_requires or DETECTOR_REQUIRES).items():
missing = required - emits
if missing:
violations.append((detector, sorted(missing)))
return violations
def assert_event_contract(parser_emits=None, detector_requires=None):
"""Raise ContractError (fail loud) if any detector is orphaned."""
violations = check_event_contract(parser_emits, detector_requires)
if violations:
detail = "; ".join(
f"{d} requires {m} but no parser emits {'it' if len(m) == 1 else 'them'}"
for d, m in violations
)
raise ContractError(f"Event-type contract violated: {detail}")