Add extractors: TTP, tool, tradecraft, surveillance, infrastructure, entity, MITRE mapper + prompts

This commit is contained in:
n0mad1k
2026-03-19 08:51:37 -04:00
parent 41cbdf5b04
commit ca53524118
10 changed files with 909 additions and 0 deletions
+224
View File
@@ -0,0 +1,224 @@
"""Prompt templates for Claude-powered extraction in Mosaic.
All document content is wrapped in <document> XML tags to prevent
prompt injection. Claude responses must be valid JSON matching
the specified schema.
"""
# --- TTP Extraction ---
TTP_SYSTEM_PROMPT = """You are an intelligence analyst specializing in cyber threat analysis and MITRE ATT&CK framework mapping.
Your task is to extract Techniques, Tactics, and Procedures (TTPs) from documents. For each TTP found:
1. Identify the specific technique or method described
2. Categorize it (initial_access, execution, persistence, privilege_escalation, defense_evasion, credential_access, discovery, lateral_movement, collection, exfiltration, command_and_control, impact, reconnaissance, resource_development)
3. Note any attributed threat actor
4. Map to MITRE ATT&CK ID if possible
5. Include surrounding context for provenance
Respond ONLY with a JSON object. No explanations outside JSON."""
TTP_EXTRACTION_PROMPT = """Extract all TTPs from this document. Return a JSON object with an "items" array.
Each item must have: technique, category, description, actor (null if unknown), mitre_id (null if unknown), source_context (verbatim quote from document, max 200 chars).
<document>
{document}
</document>
Return JSON:
{{"items": [{{"technique": "...", "category": "...", "description": "...", "actor": null, "mitre_id": null, "source_context": "..."}}]}}"""
# --- Tool Extraction ---
TOOL_SYSTEM_PROMPT = """You are a cybersecurity researcher specializing in offensive tools, exploits, implants, and malware analysis.
Your task is to extract named tools, exploits, implants, and malware from documents. For each tool found:
1. Identify the tool name and any aliases
2. Classify its capability (exploit, implant, framework, utility, spyware, rootkit, credential_theft, c2_framework, collection, obfuscation, proxy, trojan, weapon, platform)
3. Note target platforms and software
4. List any CVEs mentioned
5. Include surrounding context for provenance
Respond ONLY with a JSON object. No explanations outside JSON."""
TOOL_EXTRACTION_PROMPT = """Extract all named tools, exploits, implants, and malware from this document.
Known tools for reference (not exhaustive): {known_tools}
Return a JSON object with an "items" array. Each item must have: name, aliases (list), description, capability, target_platforms (list), target_software (list), cves (list), mitre_techniques (list), source_context (verbatim quote, max 200 chars).
<document>
{document}
</document>
Return JSON:
{{"items": [{{"name": "...", "aliases": [], "description": "...", "capability": "...", "target_platforms": [], "target_software": [], "cves": [], "mitre_techniques": [], "source_context": "..."}}]}}"""
# --- Tradecraft Extraction ---
TRADECRAFT_SYSTEM_PROMPT = """You are an intelligence analyst specializing in tradecraft — the methods and techniques used in intelligence operations, espionage, and covert activities.
Your task is to extract operational tradecraft from documents, including:
- HUMINT methods (recruitment, handling, dead drops, surveillance detection)
- SIGINT methods (interception, collection, processing)
- Cyber operations tradecraft (OPSEC, infrastructure setup, persistence techniques)
- Surveillance methods (physical and electronic)
- Communication security methods
- Counter-intelligence techniques
For each method, note how it's actually used operationally and what countermeasures exist.
Respond ONLY with a JSON object. No explanations outside JSON."""
TRADECRAFT_EXTRACTION_PROMPT = """Extract all tradecraft methods from this document.
Return a JSON object with an "items" array. Each item must have: method, domain (humint/sigint/cyber/surveillance/commsec/counterintel), description, operational_notes (how it's actually used), countermeasures (how to detect/defend), source_context (verbatim quote, max 200 chars).
<document>
{document}
</document>
Return JSON:
{{"items": [{{"method": "...", "domain": "...", "description": "...", "operational_notes": "...", "countermeasures": "...", "source_context": "..."}}]}}"""
# --- Surveillance Extraction ---
SURVEILLANCE_SYSTEM_PROMPT = """You are an intelligence analyst specializing in surveillance technology and programs.
Your task is to extract information about surveillance programs, intercept capabilities, collection methods, and vendor capabilities from documents. This includes:
- Government surveillance programs (PRISM, XKeyscore, etc.)
- Commercial surveillance tools (FinFisher, Pegasus, etc.)
- Interception capabilities and methods
- Data collection infrastructure
- Vendor/contractor capabilities
Respond ONLY with a JSON object. No explanations outside JSON."""
SURVEILLANCE_EXTRACTION_PROMPT = """Extract all surveillance programs, capabilities, and methods from this document.
Return a JSON object with an "items" array. Each item must have: program (or technique name), description, operator (government/vendor/unknown), capability_type (intercept/collection/analysis/targeting), mitre_id (null if unknown), source_context (verbatim quote, max 200 chars).
<document>
{document}
</document>
Return JSON:
{{"items": [{{"program": "...", "description": "...", "operator": null, "capability_type": "...", "mitre_id": null, "source_context": "..."}}]}}"""
# --- Infrastructure Extraction ---
INFRASTRUCTURE_SYSTEM_PROMPT = """You are a threat intelligence analyst specializing in identifying network infrastructure indicators.
Your task is to extract infrastructure indicators from documents, including:
- IP addresses (IPv4 and IPv6)
- Domain names
- URLs
- File hashes (MD5, SHA1, SHA256)
- C2 server addresses
- Email addresses used in operations
For each indicator, provide context about what it relates to and how it was used.
IMPORTANT: Only extract indicators that appear in the actual document text. Do not fabricate or guess indicators.
Respond ONLY with a JSON object. No explanations outside JSON."""
INFRASTRUCTURE_EXTRACTION_PROMPT = """Extract all network infrastructure indicators from this document.
Return a JSON object with an "items" array. Each item must have: indicator (the actual IP/domain/URL/hash), indicator_type (ipv4/ipv6/domain/url/hash/email), context (what this indicator relates to), source_context (verbatim surrounding text, max 200 chars).
<document>
{document}
</document>
Return JSON:
{{"items": [{{"indicator": "...", "indicator_type": "...", "context": "...", "source_context": "..."}}]}}"""
# --- Entity Extraction ---
ENTITY_SYSTEM_PROMPT = """You are an intelligence analyst specializing in entity identification and relationship mapping.
Your task is to extract named entities from documents, including:
- People (officials, operators, researchers, targets)
- Organizations (agencies, companies, units, departments)
- Programs (codenames, project names, operation names)
- Codenames (tool names, operation names, program designators)
- Military/intelligence units
For each entity, identify relationships to other entities mentioned in the same document.
Respond ONLY with a JSON object. No explanations outside JSON."""
ENTITY_EXTRACTION_PROMPT = """Extract all named entities and their relationships from this document.
Return a JSON object with an "items" array. Each item must have: name, type (person/org/program/codename/unit), description, relationships (list of {{"entity": "name", "relationship_type": "type"}}), source_context (verbatim quote, max 200 chars).
<document>
{document}
</document>
Return JSON:
{{"items": [{{"name": "...", "type": "...", "description": "...", "relationships": [{{"entity": "...", "relationship_type": "..."}}], "source_context": "..."}}]}}"""
# --- Interactive Query Prompts ---
QUERY_SYSTEM_PROMPT = """You are an intelligence analyst with access to a corpus of documents. Answer questions based on the provided document excerpts. Always cite your sources by referencing the document title or ID.
If the provided context doesn't contain enough information to answer confidently, say so explicitly rather than speculating."""
QUERY_PROMPT = """Based on the following document excerpts, answer the user's question.
{context}
Question: {question}
Provide a detailed answer with citations to specific documents."""
EMULATE_PROMPT = """Based on the extracted intelligence data below, create a detailed TTP playbook for emulating the threat actor: {actor}
Include:
1. Initial access methods they've used
2. Persistence mechanisms
3. Lateral movement techniques
4. Data collection methods
5. Exfiltration techniques
6. C2 infrastructure patterns
7. OPSEC practices
8. Recommended tools for emulation
{context}
Generate the playbook:"""
COMPARE_PROMPT = """Compare these two tools/capabilities based on the extracted intelligence:
Tool A: {tool_a}
Tool B: {tool_b}
{context}
Compare across: capability, target platforms, sophistication, attribution, detection methods."""
BRIEF_PROMPT = """Generate an intelligence briefing on the topic: {topic}
Based on the following extracted intelligence:
{context}
Structure the briefing with:
1. Executive Summary
2. Key Findings
3. Technical Details
4. Threat Assessment
5. Recommended Actions"""
SCENARIO_PROMPT = """Generate an adversary simulation scenario targeting: {target}
Based on the following threat intelligence:
{context}
Include:
1. Threat actor profile (based on extracted TTPs)
2. Attack chain (step by step)
3. Tools and infrastructure needed
4. Detection opportunities for blue team
5. Recommended mitigations"""
+8
View File
@@ -0,0 +1,8 @@
from .base import BaseExtractor
from .ttp_extractor import TTPExtractor
from .tool_extractor import ToolExtractor
from .tradecraft import TradecraftExtractor
from .surveillance import SurveillanceExtractor
from .infrastructure import InfrastructureExtractor
from .entity_extractor import EntityExtractor
from .mitre_mapper import MITREMapper
+236
View File
@@ -0,0 +1,236 @@
"""Base extractor for Claude-powered intelligence extraction."""
import json
import logging
from abc import ABC, abstractmethod
from datetime import datetime
from typing import Optional
import anthropic
from storage.db import MosaicDB
from storage.models import TokenUsage
from utils.api_limiter import APIRateLimiter, CircuitBreakerOpen
from utils.text_utils import chunk_text, estimate_tokens
logger = logging.getLogger(__name__)
class BaseExtractor(ABC):
"""Abstract base for all Claude-powered extractors."""
EXTRACTOR_NAME = "base" # Override in subclasses
def __init__(self, db: MosaicDB, api_key: str,
model: str = "claude-sonnet-4-20250514",
max_tokens: int = 4096,
rate_limiter: Optional[APIRateLimiter] = None):
self.db = db
self.client = anthropic.Anthropic(api_key=api_key)
self.model = model
self.max_tokens = max_tokens
self.rate_limiter = rate_limiter or APIRateLimiter()
@abstractmethod
def get_system_prompt(self) -> str:
"""Return the system prompt for this extractor."""
pass
@abstractmethod
def get_extraction_prompt(self, text: str) -> str:
"""Return the user prompt with document content wrapped in XML tags."""
pass
@abstractmethod
def get_json_schema(self) -> dict:
"""Return the expected JSON schema for validation."""
pass
@abstractmethod
def process_response(self, data: dict, doc_id: str) -> int:
"""Process validated extraction response and store results.
Returns count of items extracted."""
pass
def extract_from_document(self, doc_id: str, text: str, doc_type: str = "text",
source: str = "", metadata: Optional[dict] = None) -> int:
"""Extract intelligence from a single document.
Handles chunking, API calls, validation, and storage.
Returns total count of items extracted.
"""
# Set status to IN_PROGRESS
self.db.set_extraction_status(doc_id, self.EXTRACTOR_NAME, "IN_PROGRESS")
try:
# Chunk the document
chunks = chunk_text(text, doc_type=doc_type, metadata=metadata)
total_extracted = 0
total_input_tokens = 0
total_output_tokens = 0
for chunk in chunks:
try:
# Rate limit
self.rate_limiter.wait_if_needed()
# Build prompt with XML-wrapped content
user_prompt = self.get_extraction_prompt(chunk['text'])
# Call Claude
response = self.client.messages.create(
model=self.model,
max_tokens=self.max_tokens,
system=self.get_system_prompt(),
messages=[{"role": "user", "content": user_prompt}]
)
# Track tokens
input_tokens = response.usage.input_tokens
output_tokens = response.usage.output_tokens
total_input_tokens += input_tokens
total_output_tokens += output_tokens
self.rate_limiter.record_success()
# Extract text from response
response_text = response.content[0].text.strip()
# Parse JSON from response
data = self._parse_json_response(response_text)
if data is None:
logger.warning("Failed to parse JSON from response for doc %s chunk %d",
doc_id, chunk.get('chunk_index', 0))
continue
# Validate against schema
if not self._validate_response(data):
logger.warning("Schema validation failed for doc %s chunk %d",
doc_id, chunk.get('chunk_index', 0))
continue
# Process and store
count = self.process_response(data, doc_id)
total_extracted += count
except CircuitBreakerOpen:
logger.error("Circuit breaker open — halting extraction")
self.db.set_extraction_status(
doc_id, self.EXTRACTOR_NAME, "FAILED",
error="Circuit breaker open"
)
return total_extracted
except anthropic.APIError as e:
self.rate_limiter.record_failure(str(e))
logger.error("API error on doc %s: %s", doc_id, e)
continue
except Exception as e:
logger.error("Extraction error on doc %s chunk %d: %s",
doc_id, chunk.get('chunk_index', 0), e)
continue
# Log token usage
self.db.log_token_usage(TokenUsage(
timestamp=datetime.utcnow().isoformat(),
source=source,
doc_id=doc_id,
extractor=self.EXTRACTOR_NAME,
input_tokens=total_input_tokens,
output_tokens=total_output_tokens,
model=self.model,
))
# Update status
self.db.set_extraction_status(
doc_id, self.EXTRACTOR_NAME, "DONE",
token_count=total_input_tokens + total_output_tokens
)
self.db.update_document_extraction_time(doc_id)
return total_extracted
except Exception as e:
logger.error("Extraction failed for doc %s: %s", doc_id, e)
self.db.set_extraction_status(
doc_id, self.EXTRACTOR_NAME, "FAILED", error=str(e)
)
return 0
def estimate_tokens(self, text: str, doc_type: str = "text",
metadata: Optional[dict] = None) -> dict:
"""Estimate token cost without calling the API (dry-run)."""
chunks = chunk_text(text, doc_type=doc_type, metadata=metadata)
total_input = 0
system_tokens = estimate_tokens(self.get_system_prompt())
for chunk in chunks:
prompt = self.get_extraction_prompt(chunk['text'])
total_input += estimate_tokens(prompt) + system_tokens
return {
'chunks': len(chunks),
'estimated_input_tokens': total_input,
'estimated_output_tokens': len(chunks) * 2000, # rough estimate
}
def _parse_json_response(self, text: str) -> Optional[dict]:
"""Parse JSON from Claude response, handling markdown code blocks."""
# Try direct parse
try:
return json.loads(text)
except json.JSONDecodeError:
pass
# Try extracting from markdown code block
import re
match = re.search(r'```(?:json)?\s*\n(.*?)\n```', text, re.DOTALL)
if match:
try:
return json.loads(match.group(1))
except json.JSONDecodeError:
pass
# Try finding JSON object/array in text
for start_char, end_char in [('{', '}'), ('[', ']')]:
start = text.find(start_char)
if start >= 0:
# Find matching end
depth = 0
for i in range(start, len(text)):
if text[i] == start_char:
depth += 1
elif text[i] == end_char:
depth -= 1
if depth == 0:
try:
return json.loads(text[start:i+1])
except json.JSONDecodeError:
break
logger.warning("Could not parse JSON from response: %s...", text[:200])
return None
def _validate_response(self, data: dict) -> bool:
"""Validate response against expected schema. Reject unexpected fields."""
schema = self.get_json_schema()
if not schema:
return True
required_keys = schema.get('required', [])
allowed_keys = set(schema.get('properties', {}).keys())
if isinstance(data, dict):
# Check for unexpected top-level keys
if 'items' in schema.get('properties', {}):
# Wrapper object with 'items' array
if 'items' not in data:
logger.warning("Response missing 'items' key")
return False
unexpected = set(data.keys()) - allowed_keys
if unexpected:
logger.warning("Unexpected fields in response: %s", unexpected)
return False
return True
elif isinstance(data, list):
return True
return False
+42
View File
@@ -0,0 +1,42 @@
"""Entity extractor (people, orgs, programs, codenames, units)."""
import logging
from .base import BaseExtractor
from storage.models import Entity
from analysis.prompts import ENTITY_SYSTEM_PROMPT, ENTITY_EXTRACTION_PROMPT
logger = logging.getLogger(__name__)
class EntityExtractor(BaseExtractor):
EXTRACTOR_NAME = "entity"
def get_system_prompt(self) -> str:
return ENTITY_SYSTEM_PROMPT
def get_extraction_prompt(self, text: str) -> str:
return ENTITY_EXTRACTION_PROMPT.format(document=text)
def get_json_schema(self) -> dict:
return {"properties": {"items": {"type": "array"}}, "required": ["items"]}
def process_response(self, data: dict, doc_id: str) -> int:
items = data.get('items', [])
if isinstance(data, list):
items = data
count = 0
for item in items:
try:
entity = Entity(
name=item.get('name', ''),
type=item.get('type', ''),
description=item.get('description', ''),
relationships=item.get('relationships', []),
source_doc_ids=[doc_id],
source_context=item.get('source_context', '')[:500],
)
self.db.insert_entity(entity)
count += 1
except Exception as e:
logger.warning("Failed to process entity item: %s", e)
return count
+51
View File
@@ -0,0 +1,51 @@
"""Infrastructure indicator extractor for Mosaic (IPs, domains, C2, hashes)."""
import logging
from .base import BaseExtractor
from storage.models import Infrastructure
from analysis.prompts import INFRASTRUCTURE_SYSTEM_PROMPT, INFRASTRUCTURE_EXTRACTION_PROMPT
logger = logging.getLogger(__name__)
class InfrastructureExtractor(BaseExtractor):
EXTRACTOR_NAME = "infrastructure"
def __init__(self, *args, collection: str = "", doc_title: str = "",
doc_date: str = None, **kwargs):
super().__init__(*args, **kwargs)
self.collection = collection
self.doc_title = doc_title
self.doc_date = doc_date
def get_system_prompt(self) -> str:
return INFRASTRUCTURE_SYSTEM_PROMPT
def get_extraction_prompt(self, text: str) -> str:
return INFRASTRUCTURE_EXTRACTION_PROMPT.format(document=text)
def get_json_schema(self) -> dict:
return {"properties": {"items": {"type": "array"}}, "required": ["items"]}
def process_response(self, data: dict, doc_id: str) -> int:
items = data.get('items', [])
if isinstance(data, list):
items = data
count = 0
for item in items:
try:
infra = Infrastructure(
indicator=item.get('indicator', ''),
indicator_type=item.get('indicator_type', ''),
context=item.get('context', ''),
source_doc_ids=[doc_id],
source_context=item.get('source_context', '')[:500],
collection=self.collection,
doc_title=self.doc_title,
doc_date=self.doc_date,
)
self.db.insert_infrastructure(infra)
count += 1
except Exception as e:
logger.warning("Failed to process infrastructure item: %s", e)
return count
+131
View File
@@ -0,0 +1,131 @@
"""MITRE ATT&CK mapper — maps extracted TTPs to ATT&CK technique IDs."""
import json
import hashlib
import logging
from pathlib import Path
from typing import Optional
logger = logging.getLogger(__name__)
ATTACK_DATA_PATH = Path(__file__).parent.parent / 'data' / 'mitre_attack.json'
class MITREMapper:
"""Map extracted TTPs to MITRE ATT&CK technique IDs."""
def __init__(self):
self.techniques = {}
self.tactics = {}
self._loaded = False
self._load_attack_data()
def _load_attack_data(self):
"""Load ATT&CK data and verify integrity."""
if not ATTACK_DATA_PATH.exists():
logger.warning("MITRE ATT&CK data not found at %s", ATTACK_DATA_PATH)
return
try:
with open(ATTACK_DATA_PATH) as f:
data = json.load(f)
# Load tactics
for tactic in data.get('tactics', []):
tid = tactic['id']
self.tactics[tid] = tactic['name']
self.tactics[tactic['name'].lower()] = tid
# Load techniques (if populated)
for technique in data.get('techniques', []):
tid = technique.get('id', '')
name = technique.get('name', '')
if tid and name:
self.techniques[tid] = {
'name': name,
'tactic': technique.get('tactic', ''),
'description': technique.get('description', ''),
}
self.techniques[name.lower()] = tid
self._loaded = True
logger.debug("Loaded %d tactics, %d techniques",
len(data.get('tactics', [])),
len(data.get('techniques', [])))
except Exception as e:
logger.error("Failed to load ATT&CK data: %s", e)
def map_technique(self, technique_name: str, category: str = "") -> Optional[str]:
"""Map a technique name or description to a MITRE ATT&CK ID.
Returns ATT&CK ID (e.g., 'T1566') or None if no match.
"""
if not self._loaded:
return None
# Direct ID match
if technique_name.upper().startswith('T') and technique_name[1:].replace('.', '').isdigit():
if technique_name.upper() in self.techniques:
return technique_name.upper()
# Name match
lower_name = technique_name.lower().strip()
if lower_name in self.techniques:
result = self.techniques[lower_name]
if isinstance(result, str):
return result
# Category to tactic mapping
category_tactic_map = {
'initial_access': 'TA0001',
'execution': 'TA0002',
'persistence': 'TA0003',
'privilege_escalation': 'TA0004',
'defense_evasion': 'TA0005',
'credential_access': 'TA0006',
'discovery': 'TA0007',
'lateral_movement': 'TA0008',
'collection': 'TA0009',
'exfiltration': 'TA0010',
'command_and_control': 'TA0011',
'impact': 'TA0040',
'resource_development': 'TA0042',
'reconnaissance': 'TA0043',
# Aliases
'c2': 'TA0011',
'exfil': 'TA0010',
'privesc': 'TA0004',
'recon': 'TA0043',
'surveillance': 'TA0009',
}
return category_tactic_map.get(category.lower().strip())
def get_tactic_name(self, tactic_id: str) -> Optional[str]:
"""Get tactic name from ID."""
return self.tactics.get(tactic_id)
def get_technique_info(self, technique_id: str) -> Optional[dict]:
"""Get technique details from ID."""
return self.techniques.get(technique_id) if isinstance(
self.techniques.get(technique_id), dict) else None
def enrich_ttp(self, technique: str, category: str) -> dict:
"""Enrich a TTP with MITRE mapping.
Returns dict with mitre_id and tactic if found.
"""
result = {'mitre_id': None, 'tactic': None}
mitre_id = self.map_technique(technique, category)
if mitre_id:
if mitre_id.startswith('TA'):
result['tactic'] = mitre_id
result['tactic_name'] = self.get_tactic_name(mitre_id)
else:
result['mitre_id'] = mitre_id
info = self.get_technique_info(mitre_id)
if info:
result['tactic'] = info.get('tactic')
return result
+45
View File
@@ -0,0 +1,45 @@
"""Surveillance program/capability extractor for Mosaic."""
import logging
from .base import BaseExtractor
from storage.models import TTP
from analysis.prompts import SURVEILLANCE_SYSTEM_PROMPT, SURVEILLANCE_EXTRACTION_PROMPT
logger = logging.getLogger(__name__)
class SurveillanceExtractor(BaseExtractor):
EXTRACTOR_NAME = "surveillance"
def get_system_prompt(self) -> str:
return SURVEILLANCE_SYSTEM_PROMPT
def get_extraction_prompt(self, text: str) -> str:
return SURVEILLANCE_EXTRACTION_PROMPT.format(document=text)
def get_json_schema(self) -> dict:
return {"properties": {"items": {"type": "array"}}, "required": ["items"]}
def process_response(self, data: dict, doc_id: str) -> int:
items = data.get('items', [])
if isinstance(data, list):
items = data
count = 0
for item in items:
try:
# Store surveillance as TTPs with surveillance category
ttp = TTP(
technique=item.get('program', item.get('technique', '')),
category='surveillance',
description=item.get('description', ''),
actor=item.get('operator', item.get('actor', None)),
mitre_id=item.get('mitre_id'),
source_doc_ids=[doc_id],
source_context=item.get('source_context', '')[:500],
)
ttp.compute_dedup_key()
if self.db.insert_ttp(ttp):
count += 1
except Exception as e:
logger.warning("Failed to process surveillance item: %s", e)
return count
+78
View File
@@ -0,0 +1,78 @@
"""Named tool/exploit/implant extractor for Mosaic."""
import json
import logging
from pathlib import Path
from typing import Optional
from .base import BaseExtractor
from storage.db import MosaicDB
from storage.models import ExtractedTool
from analysis.prompts import TOOL_SYSTEM_PROMPT, TOOL_EXTRACTION_PROMPT
logger = logging.getLogger(__name__)
KNOWN_TOOLS_PATH = Path(__file__).parent.parent / 'data' / 'known_tools.json'
class ToolExtractor(BaseExtractor):
EXTRACTOR_NAME = "tool"
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.known_tools = self._load_known_tools()
def _load_known_tools(self) -> list[str]:
try:
with open(KNOWN_TOOLS_PATH) as f:
data = json.load(f)
names = []
for tool in data.get('tools', []):
names.append(tool['name'].lower())
for alias in tool.get('aliases', []):
names.append(alias.lower())
return names
except Exception as e:
logger.warning("Could not load known tools: %s", e)
return []
def get_system_prompt(self) -> str:
return TOOL_SYSTEM_PROMPT
def get_extraction_prompt(self, text: str) -> str:
# Include known tools as context
known_hint = ', '.join(self.known_tools[:50]) if self.known_tools else 'none loaded'
return TOOL_EXTRACTION_PROMPT.format(document=text, known_tools=known_hint)
def get_json_schema(self) -> dict:
return {
"properties": {
"items": {"type": "array"},
},
"required": ["items"]
}
def process_response(self, data: dict, doc_id: str) -> int:
items = data.get('items', [])
if isinstance(data, list):
items = data
count = 0
for item in items:
try:
tool = ExtractedTool(
name=item.get('name', ''),
aliases=item.get('aliases', []),
description=item.get('description', ''),
capability=item.get('capability', ''),
target_platforms=item.get('target_platforms', []),
target_software=item.get('target_software', []),
cves=item.get('cves', []),
source_doc_ids=[doc_id],
source_context=item.get('source_context', '')[:500],
mitre_techniques=item.get('mitre_techniques', []),
)
tool.compute_dedup_key()
if self.db.insert_extracted_tool(tool):
count += 1
except Exception as e:
logger.warning("Failed to process tool item: %s", e)
return count
+43
View File
@@ -0,0 +1,43 @@
"""Tradecraft extractor (HUMINT/SIGINT methods, OPSEC procedures)."""
import logging
from .base import BaseExtractor
from storage.models import Tradecraft
from analysis.prompts import TRADECRAFT_SYSTEM_PROMPT, TRADECRAFT_EXTRACTION_PROMPT
logger = logging.getLogger(__name__)
class TradecraftExtractor(BaseExtractor):
EXTRACTOR_NAME = "tradecraft"
def get_system_prompt(self) -> str:
return TRADECRAFT_SYSTEM_PROMPT
def get_extraction_prompt(self, text: str) -> str:
return TRADECRAFT_EXTRACTION_PROMPT.format(document=text)
def get_json_schema(self) -> dict:
return {"properties": {"items": {"type": "array"}}, "required": ["items"]}
def process_response(self, data: dict, doc_id: str) -> int:
items = data.get('items', [])
if isinstance(data, list):
items = data
count = 0
for item in items:
try:
tc = Tradecraft(
method=item.get('method', ''),
domain=item.get('domain', ''),
description=item.get('description', ''),
operational_notes=item.get('operational_notes', ''),
countermeasures=item.get('countermeasures', ''),
source_doc_ids=[doc_id],
source_context=item.get('source_context', '')[:500],
)
self.db.insert_tradecraft(tc)
count += 1
except Exception as e:
logger.warning("Failed to process tradecraft item: %s", e)
return count
+51
View File
@@ -0,0 +1,51 @@
"""TTP (Techniques, Tactics, Procedures) extractor for Mosaic."""
import logging
from typing import Optional
from .base import BaseExtractor
from storage.db import MosaicDB
from storage.models import TTP
from analysis.prompts import TTP_SYSTEM_PROMPT, TTP_EXTRACTION_PROMPT
logger = logging.getLogger(__name__)
class TTPExtractor(BaseExtractor):
EXTRACTOR_NAME = "ttp"
def get_system_prompt(self) -> str:
return TTP_SYSTEM_PROMPT
def get_extraction_prompt(self, text: str) -> str:
return TTP_EXTRACTION_PROMPT.format(document=text)
def get_json_schema(self) -> dict:
return {
"properties": {
"items": {"type": "array"},
},
"required": ["items"]
}
def process_response(self, data: dict, doc_id: str) -> int:
items = data.get('items', [])
if isinstance(data, list):
items = data
count = 0
for item in items:
try:
ttp = TTP(
technique=item.get('technique', ''),
category=item.get('category', ''),
description=item.get('description', ''),
actor=item.get('actor'),
mitre_id=item.get('mitre_id'),
source_doc_ids=[doc_id],
source_context=item.get('source_context', '')[:500],
)
ttp.compute_dedup_key()
if self.db.insert_ttp(ttp):
count += 1
except Exception as e:
logger.warning("Failed to process TTP item: %s", e)
return count