feat(ttp): enable 6 xfail tests — evidence shape + tracing spans

- test_evidence_shape.py: replace broken (command, BehavioralLifter)
  pairing with correct (http_fingerprint, HttpFingerprintLifter) case;
  expand _LIFTER_CASES to 5-tuples with per-lifter payloads and rule
  factories; wire StubRuleStore + _index.install() per lifter; remove
  xfail marker — all 4 parametrized cases now pass

- factory.py: add _span() helper gated on _telemetry._ENABLED; wrap
  each per-lifter dispatch in _tag_one() that opens a
  ttp.lifter.{name} child span per call

- http_fingerprint_lifter.py: add missing name = "http_fingerprint"

- test_tracing.py: replace pytest.fail() stubs in
  test_lifter_child_spans_emitted and test_no_pii_canary_in_span_attributes
  with real test bodies; remove xfail markers
This commit is contained in:
2026-05-10 08:51:07 -04:00
parent c39b63a431
commit de3634d739
4 changed files with 196 additions and 52 deletions

View File

@@ -21,10 +21,12 @@ from __future__ import annotations
import asyncio
import logging
import os
from typing import Final
from contextlib import contextmanager
from typing import Any, Final
from collections.abc import Iterator
from decnet import telemetry as _telemetry
from decnet.ttp.base import (
KNOWN_SOURCE_KINDS,
Tagger,
@@ -35,6 +37,22 @@ from decnet.web.db.models.ttp import TTPTag
_log = logging.getLogger(__name__)
@contextmanager
def _span(name: str, **attrs: Any) -> Iterator[Any]:
"""Tracing helper gated on ``DECNET_DEVELOPER_TRACING``."""
if not _telemetry._ENABLED:
yield None
return
tracer = _telemetry.get_tracer("ttp")
with tracer.start_as_current_span(name) as span:
for key, value in attrs.items():
try:
span.set_attribute(key, value)
except (TypeError, ValueError):
continue
yield span
_KNOWN: Final[tuple[str, ...]] = ("composite",)
_DEFAULT: Final[str] = "composite"
@@ -91,12 +109,16 @@ class CompositeTagger(Tagger):
if not lifters:
self._log_unhandled(event.source_kind)
return []
results = await asyncio.gather(*(t.tag(event) for t in lifters))
results = await asyncio.gather(*(self._tag_one(t, event) for t in lifters))
out: list[TTPTag] = []
for tags in results:
out.extend(tags)
return out
async def _tag_one(self, lifter: Tagger, event: TaggerEvent) -> list[TTPTag]:
with _span(f"ttp.lifter.{lifter.name}"):
return await lifter.tag(event)
def _log_unhandled(self, source_kind: str) -> None:
if source_kind in KNOWN_SOURCE_KINDS:
if source_kind not in self._warned_known:

View File

@@ -106,6 +106,7 @@ _PREDICATES: Final[dict[str, Predicate]] = {
class HttpFingerprintLifter(TolerantTagger):
"""Tags HTTP-layer fingerprint events with MITRE ATT&CK techniques."""
name = "http_fingerprint"
HANDLES: frozenset[str] = frozenset({"http_fingerprint"})
def __init__(self, store: RuleStore) -> None: