S-Dreamer commited on
Commit
24a214d
·
verified ·
1 Parent(s): ded0382

Create tests/test_drift.py

Browse files
Files changed (1) hide show
  1. tests/test_drift.py +500 -0
tests/test_drift.py ADDED
@@ -0,0 +1,500 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ tests/test_drift.py
3
+ ===================
4
+
5
+ Contract tests for osint_core.drift.
6
+
7
+ These tests define the expected behavior of the drift layer before implementation.
8
+
9
+ Core invariants:
10
+ - Drift is represented as a vector, not a scalar.
11
+ - Drift detection is pure: it does not mutate baseline, manifest, telemetry, or policy input.
12
+ - Policy drift outranks all other drift.
13
+ - Structural and behavioral drift are revert-class.
14
+ - Adversarial drift constrains before the system adapts.
15
+ - Statistical drift may adapt only when higher-priority drift classes are absent.
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ import copy
21
+ from dataclasses import asdict
22
+
23
+ import pytest
24
+
25
+ from osint_core.drift import (
26
+ DriftAssessment,
27
+ DriftSignal,
28
+ DriftType,
29
+ DriftVector,
30
+ TelemetrySnapshot,
31
+ aggregate_signals,
32
+ assess_drift,
33
+ choose_dominant_drift_type,
34
+ estimate_confidence,
35
+ recommend_correction,
36
+ )
37
+
38
+
39
+ def make_telemetry(**overrides):
40
+ data = {
41
+ "run_id": "run_test_001",
42
+ "manifest_hash": "manifest_good",
43
+ "dependency_hash": "deps_good",
44
+ "runtime_python_version": "3.13.0",
45
+ "indicator_hash": "hmac_abc123",
46
+ "indicator_type": "domain",
47
+ "input_rejected": False,
48
+ "rejection_reason": "",
49
+ "sanitized_input_trace": "",
50
+ "modules_requested": ["resource_links"],
51
+ "modules_executed": ["resource_links"],
52
+ "modules_blocked": [],
53
+ "authorized_target": False,
54
+ "duration_ms": 100,
55
+ "error_count": 0,
56
+ "timeout_count": 0,
57
+ "output_hash": "output_good",
58
+ "output_schema_valid": True,
59
+ }
60
+ data.update(overrides)
61
+ return TelemetrySnapshot(**data)
62
+
63
+
64
+ def make_baseline(**overrides):
65
+ data = {
66
+ "runtime_p95_ms": 500,
67
+ "error_rate_threshold": 2,
68
+ "timeout_threshold": 1,
69
+ "expected_manifest_hash": "manifest_good",
70
+ "expected_dependency_hash": "deps_good",
71
+ "expected_runtime_python_version": "3.13.0",
72
+ "known_output_hashes": {
73
+ "hmac_abc123": "output_good",
74
+ },
75
+ "input_type_distribution": {
76
+ "domain": 0.8,
77
+ "username": 0.2,
78
+ },
79
+ "module_usage_distribution": {
80
+ "resource_links": 1.0,
81
+ },
82
+ "input_entropy_avg": 3.2,
83
+ }
84
+ data.update(overrides)
85
+ return data
86
+
87
+
88
+ def make_policy_result(**overrides):
89
+ data = {
90
+ "decision": "allow",
91
+ "allowed_modules": ["resource_links"],
92
+ "blocked_modules": [],
93
+ "violations": [],
94
+ }
95
+ data.update(overrides)
96
+ return data
97
+
98
+
99
+ def test_drift_vector_defaults_to_zero():
100
+ vector = DriftVector()
101
+
102
+ assert vector.statistical == 0.0
103
+ assert vector.behavioral == 0.0
104
+ assert vector.structural == 0.0
105
+ assert vector.adversarial == 0.0
106
+ assert vector.operational == 0.0
107
+ assert vector.policy == 0.0
108
+
109
+
110
+ def test_aggregate_signals_uses_max_score_per_type():
111
+ signals = [
112
+ DriftSignal(
113
+ name="weak_adversarial_signal",
114
+ drift_type=DriftType.ADVERSARIAL,
115
+ score=0.2,
116
+ reason="weak suspicious pattern",
117
+ tier="T2",
118
+ evidence={"pattern": ";"},
119
+ ),
120
+ DriftSignal(
121
+ name="strong_adversarial_signal",
122
+ drift_type=DriftType.ADVERSARIAL,
123
+ score=0.7,
124
+ reason="strong suspicious pattern",
125
+ tier="T2",
126
+ evidence={"pattern": "169.254.169.254"},
127
+ ),
128
+ DriftSignal(
129
+ name="operational_signal",
130
+ drift_type=DriftType.OPERATIONAL,
131
+ score=0.4,
132
+ reason="runtime elevated",
133
+ tier="T3",
134
+ evidence={"duration_ms": 1500},
135
+ ),
136
+ ]
137
+
138
+ vector = aggregate_signals(signals)
139
+
140
+ assert vector.adversarial == 0.7
141
+ assert vector.operational == 0.4
142
+ assert vector.policy == 0.0
143
+
144
+
145
+ def test_dominant_type_respects_priority_not_raw_score():
146
+ vector = DriftVector(
147
+ statistical=0.9,
148
+ adversarial=0.4,
149
+ policy=0.0,
150
+ )
151
+
152
+ assert choose_dominant_drift_type(vector) == DriftType.ADVERSARIAL
153
+
154
+ vector = DriftVector(
155
+ statistical=0.9,
156
+ adversarial=0.4,
157
+ policy=0.6,
158
+ )
159
+
160
+ assert choose_dominant_drift_type(vector) == DriftType.POLICY
161
+
162
+
163
+ def test_recommend_correction_policy_drift_reverts():
164
+ vector = DriftVector(policy=0.6, statistical=1.0, adversarial=0.2)
165
+
166
+ assert recommend_correction(vector) == "REVERT"
167
+
168
+
169
+ def test_recommend_correction_structural_drift_reverts():
170
+ vector = DriftVector(structural=0.5)
171
+
172
+ assert recommend_correction(vector) == "REVERT"
173
+
174
+
175
+ def test_recommend_correction_behavioral_drift_reverts():
176
+ vector = DriftVector(behavioral=0.7)
177
+
178
+ assert recommend_correction(vector) == "REVERT"
179
+
180
+
181
+ def test_recommend_correction_adversarial_drift_constrains():
182
+ vector = DriftVector(adversarial=0.3, statistical=0.9)
183
+
184
+ assert recommend_correction(vector) == "CONSTRAIN"
185
+
186
+
187
+ def test_recommend_correction_statistical_drift_adapts_only_when_clean():
188
+ vector = DriftVector(statistical=0.5)
189
+
190
+ assert recommend_correction(vector) == "ADAPT"
191
+
192
+
193
+ def test_recommend_correction_defaults_to_observe():
194
+ vector = DriftVector(statistical=0.1, operational=0.1)
195
+
196
+ assert recommend_correction(vector) == "OBSERVE"
197
+
198
+
199
+ def test_policy_violation_creates_policy_signal_and_revert_recommendation():
200
+ telemetry = make_telemetry()
201
+ baseline = make_baseline()
202
+ policy_result = make_policy_result(
203
+ decision="constrain",
204
+ blocked_modules=["port_scan"],
205
+ violations=[
206
+ {
207
+ "code": "forbidden_module",
208
+ "message": "Forbidden module blocked: Port Scan",
209
+ "module": "port_scan",
210
+ }
211
+ ],
212
+ )
213
+
214
+ assessment = assess_drift(
215
+ telemetry=telemetry,
216
+ baseline=baseline,
217
+ policy_result=policy_result,
218
+ )
219
+
220
+ assert isinstance(assessment, DriftAssessment)
221
+ assert assessment.drift_vector.policy == 1.0
222
+ assert assessment.dominant_type == DriftType.POLICY
223
+ assert assessment.recommended_correction == "REVERT"
224
+ assert any(signal.drift_type == DriftType.POLICY for signal in assessment.signals)
225
+
226
+
227
+ def test_authorization_gate_trigger_creates_policy_signal():
228
+ telemetry = make_telemetry(
229
+ modules_requested=["http_headers"],
230
+ modules_blocked=["http_headers"],
231
+ authorized_target=False,
232
+ )
233
+ baseline = make_baseline()
234
+ policy_result = make_policy_result(
235
+ decision="constrain",
236
+ blocked_modules=["http_headers"],
237
+ violations=[
238
+ {
239
+ "code": "authorization_required",
240
+ "message": "Authorization required for module: HTTP Headers",
241
+ "module": "http_headers",
242
+ }
243
+ ],
244
+ )
245
+
246
+ assessment = assess_drift(
247
+ telemetry=telemetry,
248
+ baseline=baseline,
249
+ policy_result=policy_result,
250
+ )
251
+
252
+ assert assessment.drift_vector.policy >= 0.6
253
+ assert assessment.recommended_correction == "REVERT"
254
+
255
+
256
+ def test_adversarial_patterns_create_constrain_recommendation():
257
+ telemetry = make_telemetry(
258
+ input_rejected=True,
259
+ rejection_reason="Input contains a blocked pattern.",
260
+ sanitized_input_trace="https://example.com/?next=http://169.254.169.254/latest",
261
+ )
262
+ baseline = make_baseline()
263
+ policy_result = make_policy_result()
264
+
265
+ assessment = assess_drift(
266
+ telemetry=telemetry,
267
+ baseline=baseline,
268
+ policy_result=policy_result,
269
+ )
270
+
271
+ assert assessment.drift_vector.adversarial >= 0.7
272
+ assert assessment.dominant_type == DriftType.ADVERSARIAL
273
+ assert assessment.recommended_correction == "CONSTRAIN"
274
+
275
+
276
+ def test_operational_runtime_drift_detected():
277
+ telemetry = make_telemetry(duration_ms=1200)
278
+ baseline = make_baseline(runtime_p95_ms=500)
279
+ policy_result = make_policy_result()
280
+
281
+ assessment = assess_drift(
282
+ telemetry=telemetry,
283
+ baseline=baseline,
284
+ policy_result=policy_result,
285
+ )
286
+
287
+ assert assessment.drift_vector.operational >= 0.5
288
+ assert any(signal.name == "runtime_boundary_exceeded" for signal in assessment.signals)
289
+
290
+
291
+ def test_operational_error_drift_detected():
292
+ telemetry = make_telemetry(error_count=3)
293
+ baseline = make_baseline(error_rate_threshold=2)
294
+ policy_result = make_policy_result()
295
+
296
+ assessment = assess_drift(
297
+ telemetry=telemetry,
298
+ baseline=baseline,
299
+ policy_result=policy_result,
300
+ )
301
+
302
+ assert assessment.drift_vector.operational >= 0.6
303
+ assert any(signal.name == "error_threshold_exceeded" for signal in assessment.signals)
304
+
305
+
306
+ def test_structural_manifest_mismatch_reverts():
307
+ telemetry = make_telemetry(manifest_hash="manifest_changed")
308
+ baseline = make_baseline(expected_manifest_hash="manifest_good")
309
+ policy_result = make_policy_result()
310
+
311
+ assessment = assess_drift(
312
+ telemetry=telemetry,
313
+ baseline=baseline,
314
+ policy_result=policy_result,
315
+ )
316
+
317
+ assert assessment.drift_vector.structural == 1.0
318
+ assert assessment.dominant_type == DriftType.STRUCTURAL
319
+ assert assessment.recommended_correction == "REVERT"
320
+
321
+
322
+ def test_structural_dependency_mismatch_reverts():
323
+ telemetry = make_telemetry(dependency_hash="deps_changed")
324
+ baseline = make_baseline(expected_dependency_hash="deps_good")
325
+ policy_result = make_policy_result()
326
+
327
+ assessment = assess_drift(
328
+ telemetry=telemetry,
329
+ baseline=baseline,
330
+ policy_result=policy_result,
331
+ )
332
+
333
+ assert assessment.drift_vector.structural >= 0.9
334
+ assert assessment.recommended_correction == "REVERT"
335
+
336
+
337
+ def test_behavioral_same_input_different_output_reverts():
338
+ telemetry = make_telemetry(
339
+ indicator_hash="hmac_abc123",
340
+ output_hash="output_changed",
341
+ )
342
+ baseline = make_baseline(
343
+ known_output_hashes={"hmac_abc123": "output_good"},
344
+ )
345
+ policy_result = make_policy_result()
346
+
347
+ assessment = assess_drift(
348
+ telemetry=telemetry,
349
+ baseline=baseline,
350
+ policy_result=policy_result,
351
+ )
352
+
353
+ assert assessment.drift_vector.behavioral >= 0.9
354
+ assert assessment.dominant_type == DriftType.BEHAVIORAL
355
+ assert assessment.recommended_correction == "REVERT"
356
+
357
+
358
+ def test_behavioral_invalid_schema_reverts():
359
+ telemetry = make_telemetry(output_schema_valid=False)
360
+ baseline = make_baseline()
361
+ policy_result = make_policy_result()
362
+
363
+ assessment = assess_drift(
364
+ telemetry=telemetry,
365
+ baseline=baseline,
366
+ policy_result=policy_result,
367
+ )
368
+
369
+ assert assessment.drift_vector.behavioral >= 0.8
370
+ assert assessment.recommended_correction == "REVERT"
371
+
372
+
373
+ def test_statistical_shift_can_adapt_when_no_higher_priority_signal():
374
+ telemetry = make_telemetry(indicator_type="ip")
375
+ baseline = make_baseline(
376
+ input_type_distribution={"domain": 0.9, "username": 0.1},
377
+ )
378
+ policy_result = make_policy_result()
379
+
380
+ assessment = assess_drift(
381
+ telemetry=telemetry,
382
+ baseline=baseline,
383
+ policy_result=policy_result,
384
+ )
385
+
386
+ assert assessment.drift_vector.statistical >= 0.5
387
+ assert assessment.dominant_type == DriftType.STATISTICAL
388
+ assert assessment.recommended_correction == "ADAPT"
389
+
390
+
391
+ def test_policy_drift_overrides_statistical_adaptation():
392
+ telemetry = make_telemetry(indicator_type="ip")
393
+ baseline = make_baseline(
394
+ input_type_distribution={"domain": 0.9, "username": 0.1},
395
+ )
396
+ policy_result = make_policy_result(
397
+ decision="constrain",
398
+ blocked_modules=["port_scan"],
399
+ violations=[
400
+ {
401
+ "code": "forbidden_module",
402
+ "message": "Forbidden module blocked",
403
+ "module": "port_scan",
404
+ }
405
+ ],
406
+ )
407
+
408
+ assessment = assess_drift(
409
+ telemetry=telemetry,
410
+ baseline=baseline,
411
+ policy_result=policy_result,
412
+ )
413
+
414
+ assert assessment.drift_vector.statistical >= 0.5
415
+ assert assessment.drift_vector.policy == 1.0
416
+ assert assessment.dominant_type == DriftType.POLICY
417
+ assert assessment.recommended_correction == "REVERT"
418
+
419
+
420
+ def test_adversarial_drift_overrides_statistical_adaptation():
421
+ telemetry = make_telemetry(
422
+ indicator_type="ip",
423
+ sanitized_input_trace="http://169.254.169.254/latest",
424
+ )
425
+ baseline = make_baseline(
426
+ input_type_distribution={"domain": 0.9, "username": 0.1},
427
+ )
428
+ policy_result = make_policy_result()
429
+
430
+ assessment = assess_drift(
431
+ telemetry=telemetry,
432
+ baseline=baseline,
433
+ policy_result=policy_result,
434
+ )
435
+
436
+ assert assessment.drift_vector.statistical >= 0.5
437
+ assert assessment.drift_vector.adversarial >= 0.7
438
+ assert assessment.dominant_type == DriftType.ADVERSARIAL
439
+ assert assessment.recommended_correction == "CONSTRAIN"
440
+
441
+
442
+ def test_estimate_confidence_increases_with_signal_count_and_tier():
443
+ low_signal = DriftSignal(
444
+ name="weak",
445
+ drift_type=DriftType.STATISTICAL,
446
+ score=0.3,
447
+ reason="weak distribution shift",
448
+ tier="T4",
449
+ evidence={},
450
+ )
451
+ high_signal = DriftSignal(
452
+ name="policy",
453
+ drift_type=DriftType.POLICY,
454
+ score=1.0,
455
+ reason="forbidden module",
456
+ tier="T1",
457
+ evidence={},
458
+ )
459
+
460
+ assert estimate_confidence([]) == 0.0
461
+ assert estimate_confidence([high_signal]) > estimate_confidence([low_signal])
462
+ assert estimate_confidence([low_signal, high_signal]) >= estimate_confidence([high_signal])
463
+
464
+
465
+ def test_assess_drift_is_pure_and_does_not_mutate_inputs():
466
+ telemetry = make_telemetry()
467
+ baseline = make_baseline()
468
+ policy_result = make_policy_result()
469
+
470
+ telemetry_before = copy.deepcopy(asdict(telemetry))
471
+ baseline_before = copy.deepcopy(baseline)
472
+ policy_before = copy.deepcopy(policy_result)
473
+
474
+ assess_drift(
475
+ telemetry=telemetry,
476
+ baseline=baseline,
477
+ policy_result=policy_result,
478
+ )
479
+
480
+ assert asdict(telemetry) == telemetry_before
481
+ assert baseline == baseline_before
482
+ assert policy_result == policy_before
483
+
484
+
485
+ def test_clean_execution_observes_without_significant_drift():
486
+ telemetry = make_telemetry()
487
+ baseline = make_baseline()
488
+ policy_result = make_policy_result()
489
+
490
+ assessment = assess_drift(
491
+ telemetry=telemetry,
492
+ baseline=baseline,
493
+ policy_result=policy_result,
494
+ )
495
+
496
+ assert assessment.drift_vector == DriftVector()
497
+ assert assessment.signals == []
498
+ assert assessment.dominant_type is None
499
+ assert assessment.recommended_correction == "OBSERVE"
500
+ assert assessment.confidence == 0.0