feat(ttp/stix): add deduped process SCOs for attacker commands

This commit is contained in:
2026-05-09 07:33:30 -04:00
parent 1ee7a4a481
commit f827197cc8
6 changed files with 68 additions and 2 deletions

View File

@@ -156,6 +156,7 @@ def build_attacker_bundle(
raw_tags: list[dict[str, Any]],
artifacts: list[dict[str, Any]],
smtp_targets: list[dict[str, Any]],
commands: list[str] | None = None,
) -> stix2.Bundle:
"""Assemble a STIX 2.1 Bundle for *attacker*.
@@ -275,6 +276,24 @@ def build_attacker_bundle(
)
)
# ── Shell commands (process SCOs + observed-data) ────────────────
seen_cmds: set[str] = set()
for cmd_line in commands or []:
if not cmd_line or cmd_line in seen_cmds:
continue
seen_cmds.add(cmd_line)
proc = stix2.Process(command_line=cmd_line, is_hidden=False)
objs.append(proc)
objs.append(
stix2.ObservedData(
first_observed=fs or now,
last_observed=ls or now,
number_observed=1,
object_refs=[proc.id],
created_by_ref=org.id,
)
)
# ── Intel note ───────────────────────────────────────────────────
if intel:
note = _intel_note(intel, ta.id, org.id)

View File

@@ -1492,6 +1492,10 @@ class BaseRepository(ABC):
"""Raw ``ttp_tag`` rows for one attacker (for STIX export + similar)."""
raise NotImplementedError
async def list_attacker_commands_deduped(self, uuid: str) -> list[str]:
"""Deduplicated ``command_text`` strings for one attacker, order-preserved."""
raise NotImplementedError
async def list_ttp_decky_phases(
self, identity_uuid: str,
) -> list[dict[str, Any]]:

View File

@@ -41,6 +41,24 @@ class AttackerActivityMixin(_MixinBase):
page = commands[offset: offset + limit]
return {"total": total, "data": page}
async def list_attacker_commands_deduped(self, uuid: str) -> list[str]:
async with self._session() as session:
result = await session.execute(
select(col(Attacker.commands)).where(Attacker.uuid == uuid)
)
raw = result.scalar_one_or_none()
if raw is None:
return []
commands: list = json.loads(raw) if isinstance(raw, str) else raw
seen: set[str] = set()
out: list[str] = []
for entry in commands:
text = str(entry.get("command_text", "")).strip()
if text and text not in seen:
seen.add(text)
out.append(text)
return out
async def get_attacker_service_activity(
self, attacker_uuid: str
) -> list[tuple[str, str]]:

View File

@@ -68,6 +68,7 @@ async def api_export_attacker_stix(
repo.list_ttp_tags_by_attacker(uuid),
repo.get_attacker_artifacts(uuid),
repo.list_smtp_targets(uuid),
repo.list_attacker_commands_deduped(uuid),
)
behavior = cast(dict[str, Any] | None, results[0])
identity = cast(dict[str, Any] | None, results[1])
@@ -76,6 +77,7 @@ async def api_export_attacker_stix(
raw_tags = cast(list[dict[str, Any]], results[4])
artifacts = cast(list[dict[str, Any]], results[5])
smtp_targets = cast(list[dict[str, Any]], results[6])
commands = cast(list[str], results[7])
bundle = build_attacker_bundle(
attacker=attacker,
@@ -89,6 +91,7 @@ async def api_export_attacker_stix(
raw_tags=raw_tags,
artifacts=artifacts,
smtp_targets=smtp_targets,
commands=commands,
)
return Response(
content=bundle.serialize(pretty=True, indent=2),