feat(ttp): implement evidence-shape validation and confidence range constraint

- TolerantTagger.tag validates evidence keys against EVIDENCE_SCHEMA TypedDicts;
  TypeError (programmer error) propagates instead of being swallowed
- IntelEvidence and EmailEvidence expanded from stubs to full per-provider
  key sets (total=False); IntelEvidence old stub fields replaced wholesale
- EVIDENCE_SCHEMA map added to models/ttp.py and imported by base.py
- TTPTag __table_args__ gains confidence [0,1] CheckConstraint (DB-enforced)
- xfail removed from test_confidence_outside_range_rejected_at_insert and
  test_evidence_shape_violation_propagates_as_typeerror — both now pass
- TypeError removed from _SWALLOWED_EXCS fuzz list; test_intel_evidence_keys
  updated to assert the real provider key set
This commit is contained in:
2026-05-10 07:56:52 -04:00
parent a8f6a28f3a
commit 39518e33b4
5 changed files with 92 additions and 16 deletions

View File

@@ -19,7 +19,7 @@ import logging
from abc import ABC, abstractmethod
from typing import Any, Final, NamedTuple, Protocol, runtime_checkable
from decnet.web.db.models.ttp import TTPTag
from decnet.web.db.models.ttp import EVIDENCE_SCHEMA, TTPTag
_log = logging.getLogger(__name__)
@@ -109,7 +109,32 @@ class TolerantTagger(Tagger):
async def tag(self, event: TaggerEvent) -> list[TTPTag]:
try:
return await self._tag_impl(event)
results = await self._tag_impl(event)
# Validate evidence shape: unknown keys are a programmer error,
# not a runtime absence. Raise TypeError so the caller sees the
# bug rather than silently dropping the tag.
td = EVIDENCE_SCHEMA.get(event.source_kind)
if td is not None:
declared = (
getattr(td, "__required_keys__", frozenset())
| getattr(td, "__optional_keys__", frozenset())
)
for tag in results:
ev = getattr(tag, "evidence", None)
if ev is None:
continue
unknown = set(ev) - declared
if unknown:
raise TypeError(
f"lifter {self.name!r} emitted evidence keys "
f"{unknown!r} not declared in "
f"{td.__name__} for source_kind={event.source_kind!r}"
)
return results
except TypeError:
# Programmer error — bad evidence shape or type mismatch.
# Propagate; do NOT swallow.
raise
except Exception:
# ``Exception`` deliberately, not ``BaseException``:
# ``KeyboardInterrupt`` / ``SystemExit`` /