Add extractors: TTP, tool, tradecraft, surveillance, infrastructure, entity, MITRE mapper + prompts
This commit is contained in:
@@ -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"""
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
Reference in New Issue
Block a user