feat(stix_export): wire fingerprint bounties through all endpoints + tests

Remaining files from the fingerprint-bounties + characterizes-SRO commit:
misp_export, repository, bounties mixin, all 4 router endpoints, and test suite
updates. Prerequisite: previous commit added _extract_fingerprint_bounty_data
and the stix_export changes.
This commit is contained in:
2026-05-09 09:14:48 -04:00
parent 51d0fc7b6c
commit 4c6b12dcf8
13 changed files with 181 additions and 4 deletions

View File

@@ -394,6 +394,27 @@ class BaseRepository(ABC):
"""
raise NotImplementedError
async def get_fingerprint_bounties_by_ip(
self, ip: str
) -> list[dict[str, Any]]:
"""Return all fingerprint bounties for *ip* with parsed payload dicts.
Filters to ``bounty_type='fingerprint'``. Each returned dict has the
standard Bounty columns plus a decoded ``payload`` dict.
"""
raise NotImplementedError
async def get_all_fingerprint_bounties_for_export(
self,
) -> dict[str, list[dict[str, Any]]]:
"""Return ``{attacker_ip: [bounty_dict, ...]}`` for all fingerprint
bounties in the database.
Used by fleet exports to attach per-attacker fingerprint bounties
(JARM hashes, HTTP quirks) without N+1 queries.
"""
raise NotImplementedError
async def upsert_observed_attachment(
self,
*,

View File

@@ -141,6 +141,40 @@ class BountiesMixin(_MixinBase):
grouped[item.attacker_ip].append(d)
return dict(grouped)
async def get_fingerprint_bounties_by_ip(self, ip: str) -> List[dict[str, Any]]:
async with self._session() as session:
result = await session.execute(
select(Bounty)
.where(Bounty.attacker_ip == ip, Bounty.bounty_type == "fingerprint")
.order_by(asc(Bounty.timestamp))
)
out: List[dict[str, Any]] = []
for item in result.scalars().all():
d = item.model_dump(mode="json")
try:
d["payload"] = json.loads(d["payload"])
except (json.JSONDecodeError, TypeError):
pass
out.append(d)
return out
async def get_all_fingerprint_bounties_for_export(self) -> dict[str, List[dict[str, Any]]]:
async with self._session() as session:
result = await session.execute(
select(Bounty)
.where(Bounty.bounty_type == "fingerprint")
.order_by(asc(Bounty.timestamp))
)
grouped: dict[str, List[dict[str, Any]]] = defaultdict(list)
for item in result.scalars().all():
d = item.model_dump(mode="json")
try:
d["payload"] = json.loads(d["payload"])
except (json.JSONDecodeError, TypeError):
pass
grouped[item.attacker_ip].append(d)
return dict(grouped)
async def count_probe_relays(self, attacker_ip: str, decky: str) -> int:
"""Return how many probe_relay bounties exist for this (attacker_ip, decky) pair."""
async with self._session() as session: