Files
mosaic/tests/test_mosaic.py
T

501 lines
18 KiB
Python

"""Core tests for Mosaic intelligence extraction platform."""
import os
import sys
import json
import tempfile
import sqlite3
import pytest
from pathlib import Path
# Add project root to path
sys.path.insert(0, str(Path(__file__).parent.parent))
class TestModels:
"""Test data model serialization and deserialization."""
def test_document_to_dict(self):
from storage.models import Document
doc = Document(
id="abc123",
source="vault7",
source_url="https://example.com/doc",
doc_type="html",
title="Test Doc",
metadata={"key": "value"},
content_hash="abc123",
status="CACHED",
)
d = doc.to_dict()
assert d['id'] == 'abc123'
assert d['source'] == 'vault7'
assert d['metadata'] == '{"key": "value"}' # JSON serialized
def test_document_from_row(self):
from storage.models import Document
row = {
'id': 'abc123',
'source': 'vault7',
'source_url': 'https://example.com',
'doc_type': 'html',
'title': 'Test',
'date': None,
'classification': None,
'raw_path': '/tmp/test',
'text': 'hello',
'metadata': '{"key": "value"}',
'content_hash': 'abc123',
'fetch_date': '2024-01-01',
'etag': None,
'last_modified': None,
'char_count': 5,
'status': 'CACHED',
'parse_status': 'PENDING',
'last_extracted_at': None,
}
doc = Document.from_row(row)
assert doc.id == 'abc123'
assert doc.metadata == {'key': 'value'}
def test_extracted_tool_dedup_key(self):
from storage.models import ExtractedTool
tool = ExtractedTool(name="HIVE", capability="implant")
key = tool.compute_dedup_key()
assert len(key) == 16
# Same name+capability should produce same key
tool2 = ExtractedTool(name="hive", capability="IMPLANT")
assert tool2.compute_dedup_key() == key
def test_ttp_dedup_key(self):
from storage.models import TTP
ttp = TTP(technique="Spearphishing", category="initial_access")
key = ttp.compute_dedup_key()
assert len(key) == 16
def test_extracted_tool_json_fields(self):
from storage.models import ExtractedTool
tool = ExtractedTool(
name="Test",
aliases=["t1", "t2"],
target_platforms=["windows"],
cves=["CVE-2024-001"],
source_doc_ids=["doc1", "doc2"],
)
d = tool.to_dict()
assert json.loads(d['aliases']) == ["t1", "t2"]
assert json.loads(d['source_doc_ids']) == ["doc1", "doc2"]
# Round-trip
restored = ExtractedTool.from_row(d)
assert restored.aliases == ["t1", "t2"]
assert restored.source_doc_ids == ["doc1", "doc2"]
class TestDatabase:
"""Test SQLite database operations."""
def setup_method(self):
self.tmp = tempfile.mktemp(suffix='.db')
from storage.db import MosaicDB
self.db = MosaicDB(self.tmp)
def teardown_method(self):
self.db.close()
for ext in ('', '-wal', '-shm'):
path = self.tmp + ext
if os.path.exists(path):
os.unlink(path)
def test_wal_mode(self):
cursor = self.db.conn.execute("PRAGMA journal_mode")
assert cursor.fetchone()[0] == 'wal'
def test_schema_version(self):
cursor = self.db.conn.execute("SELECT MAX(version) FROM schema_version")
version = cursor.fetchone()[0]
assert version >= 1
def test_insert_and_get_document(self):
from storage.models import Document
doc = Document(
id="test_hash_123",
source="vault7",
source_url="https://example.com/test",
doc_type="html",
title="Test Document",
text="This is test content",
content_hash="test_hash_123",
status="CACHED",
)
is_new = self.db.insert_document(doc)
assert is_new is True
retrieved = self.db.get_document("test_hash_123")
assert retrieved is not None
assert retrieved.title == "Test Document"
assert retrieved.source == "vault7"
def test_document_update(self):
from storage.models import Document
doc = Document(id="up_123", source="test", source_url="http://x",
doc_type="html", title="Original", content_hash="up_123",
status="PENDING")
self.db.insert_document(doc)
doc.title = "Updated"
doc.status = "CACHED"
is_new = self.db.insert_document(doc)
assert is_new is False
retrieved = self.db.get_document("up_123")
assert retrieved.title == "Updated"
assert retrieved.status == "CACHED"
def test_fts_search(self):
from storage.models import Document
doc = Document(id="fts_123", source="vault7", source_url="http://x",
doc_type="html", title="CIA Hacking Tools Overview",
text="This document describes various CIA hacking tools including HIVE and Grasshopper",
content_hash="fts_123", status="CACHED")
self.db.insert_document(doc)
results = self.db.search_documents("CIA hacking tools")
assert len(results) > 0
assert results[0].id == "fts_123"
def test_fts_sanitization(self):
# Should not crash on special characters
results = self.db.search_documents('test "injection" (attack)')
assert isinstance(results, list)
def test_tool_dedup(self):
from storage.models import ExtractedTool
tool1 = ExtractedTool(name="HIVE", capability="implant",
description="First", source_doc_ids=["doc1"])
tool1.compute_dedup_key()
assert self.db.insert_extracted_tool(tool1) is True
tool2 = ExtractedTool(name="HIVE", capability="implant",
description="Second", source_doc_ids=["doc2"])
tool2.compute_dedup_key()
assert self.db.insert_extracted_tool(tool2) is False # Dedup
# Verify source_doc_ids merged
tools = self.db.get_all_tools()
assert len(tools) == 1
assert "doc1" in tools[0].source_doc_ids
assert "doc2" in tools[0].source_doc_ids
def test_extraction_status(self):
self.db.set_extraction_status("doc1", "ttp", "IN_PROGRESS")
status = self.db.get_extraction_status("doc1", "ttp")
assert status.status == "IN_PROGRESS"
self.db.set_extraction_status("doc1", "ttp", "DONE", token_count=1500)
status = self.db.get_extraction_status("doc1", "ttp")
assert status.status == "DONE"
assert status.token_count == 1500
def test_crash_recovery(self):
"""IN_PROGRESS states should reset to PENDING on DB init."""
from storage.models import Document
doc = Document(id="crash_test", source="test", source_url="http://x",
doc_type="html", title="Crash", content_hash="crash_test",
status="DOWNLOADING", parse_status="IN_PROGRESS")
self.db.insert_document(doc)
self.db.set_extraction_status("crash_test", "ttp", "IN_PROGRESS")
# Re-open database (simulates restart)
self.db.close()
from storage.db import MosaicDB
self.db = MosaicDB(self.tmp)
doc = self.db.get_document("crash_test")
assert doc.status == "PENDING"
assert doc.parse_status == "PENDING"
status = self.db.get_extraction_status("crash_test", "ttp")
assert status.status == "PENDING"
def test_status_counts(self):
from storage.models import Document
for i, status in enumerate(['CACHED', 'CACHED', 'DOWNLOAD_FAILED']):
doc = Document(id=f"sc_{i}", source="test", source_url=f"http://x/{i}",
doc_type="html", title=f"Doc {i}", content_hash=f"sc_{i}",
status=status)
self.db.insert_document(doc)
counts = self.db.get_status_counts("test")
assert counts['total'] == 3
assert counts['collection']['CACHED'] == 2
assert counts['collection']['DOWNLOAD_FAILED'] == 1
def test_token_usage(self):
from storage.models import TokenUsage
usage = TokenUsage(
timestamp="2024-01-01T00:00:00",
source="vault7",
doc_id="doc1",
extractor="ttp",
input_tokens=1000,
output_tokens=500,
model="claude-sonnet-4-20250514",
)
self.db.log_token_usage(usage)
totals = self.db.get_total_tokens("vault7")
assert totals['input_tokens'] == 1000
assert totals['output_tokens'] == 500
assert totals['total_tokens'] == 1500
class TestSanitize:
"""Test input sanitization utilities."""
def test_sanitize_filename_basic(self):
from utils.sanitize import sanitize_filename
assert sanitize_filename("hello.txt") == "hello.txt"
assert sanitize_filename("my file (1).pdf") == "my_file_1_.pdf"
def test_sanitize_filename_traversal(self):
from utils.sanitize import sanitize_filename
assert sanitize_filename("../../etc/passwd") == "passwd"
assert sanitize_filename("/tmp/evil.sh") == "evil.sh"
def test_sanitize_filename_empty(self):
from utils.sanitize import sanitize_filename
assert sanitize_filename("") == "unnamed"
assert sanitize_filename("...") == "..."
def test_sanitize_path_safe(self):
from utils.sanitize import sanitize_path
result = sanitize_path("subdir/file.txt", "/tmp/base")
assert result is not None
assert result.startswith("/tmp/base")
def test_sanitize_path_traversal(self):
from utils.sanitize import sanitize_path
result = sanitize_path("../../etc/passwd", "/tmp/base")
assert result is None
def test_strip_ansi(self):
from utils.sanitize import strip_ansi
assert strip_ansi("\x1b[31mred\x1b[0m") == "red"
assert strip_ansi("no ansi") == "no ansi"
def test_content_type_fallback(self):
"""Test magic bytes detection fallback."""
from utils.sanitize import _detect_by_magic_bytes
# Create a temp PDF-like file
with tempfile.NamedTemporaryFile(suffix='.pdf', delete=False) as f:
f.write(b'%PDF-1.4 test')
f.flush()
detected = _detect_by_magic_bytes(f.name)
assert detected == 'application/pdf'
os.unlink(f.name)
class TestTextUtils:
"""Test text chunking and cleaning."""
def test_estimate_tokens(self):
from utils.text_utils import estimate_tokens
assert estimate_tokens("hello world") > 0
assert estimate_tokens("") == 0
def test_clean_text(self):
from utils.text_utils import clean_text
assert clean_text("hello\x00world") == "helloworld" # null bytes stripped
assert clean_text("line1\r\nline2") == "line1\nline2"
assert clean_text("a\n\n\n\nb") == "a\n\nb"
def test_chunk_default_small(self):
from utils.text_utils import chunk_text
chunks = chunk_text("short text", doc_type="text")
assert len(chunks) == 1
assert chunks[0]['text'] == "short text"
assert chunks[0]['chunk_index'] == 0
assert chunks[0]['total_chunks'] == 1
def test_chunk_default_large(self):
from utils.text_utils import chunk_text
# Create text that's definitely larger than default chunk size
big_text = "This is a paragraph.\n\n" * 5000
chunks = chunk_text(big_text, doc_type="text", max_tokens=1000)
assert len(chunks) > 1
for i, chunk in enumerate(chunks):
assert chunk['chunk_index'] == i
assert chunk['total_chunks'] == len(chunks)
def test_chunk_cable(self):
from utils.text_utils import chunk_text
text = "SUBJECT: Test Cable\n\nBody paragraph about intelligence."
metadata = {'SUBJECT': 'Test Cable', 'CLASSIFICATION': 'CONFIDENTIAL'}
chunks = chunk_text(text, doc_type="cable", metadata=metadata)
assert len(chunks) >= 1
assert 'SUBJECT: Test Cable' in chunks[0]['text']
def test_dedup_text(self):
from utils.text_utils import dedup_text
texts = ["hello", "world", "hello", "hello"]
unique = dedup_text(texts)
assert len(unique) == 2
def test_chunk_empty(self):
from utils.text_utils import chunk_text
assert chunk_text("", doc_type="text") == []
assert chunk_text("", doc_type="pdf") == []
class TestCache:
"""Test download cache operations."""
def setup_method(self):
self.tmpdir = tempfile.mkdtemp()
from utils.cache import DownloadCache
self.cache = DownloadCache(self.tmpdir)
def test_hash_content(self):
h = self.cache.hash_content(b"hello world")
assert len(h) == 64 # SHA256 hex
def test_shard_path(self):
path = self.cache.get_shard_path("abcdef1234567890", "vault7", "html")
assert "vault7" in str(path)
assert "/ab/cd/" in str(path)
assert path.name == "abcdef1234567890.html"
def test_store_and_verify(self):
content = b"test document content"
content_hash = self.cache.hash_content(content)
# Write temp file
tmp_path = os.path.join(self.tmpdir, "test.tmp")
with open(tmp_path, 'wb') as f:
f.write(content)
# Store atomically
final = self.cache.store_atomic(tmp_path, content_hash, "test_collection", "txt")
assert os.path.exists(final)
assert not os.path.exists(tmp_path) # Temp file moved
# Verify
assert self.cache.is_cached(content_hash, "test_collection", "txt")
assert self.cache.verify_file(final, content_hash)
def test_store_hash_mismatch(self):
content = b"test content"
tmp_path = os.path.join(self.tmpdir, "bad.tmp")
with open(tmp_path, 'wb') as f:
f.write(content)
with pytest.raises(ValueError, match="Hash mismatch"):
self.cache.store_atomic(tmp_path, "wrong_hash", "test", "txt")
def test_disk_space_check(self):
assert self.cache.check_disk_space(required_mb=1) is True
class TestRateLimiters:
"""Test rate limiting utilities."""
def test_http_limiter_basic(self):
from utils.http_limiter import HTTPRateLimiter
limiter = HTTPRateLimiter(rate=100, jitter=0) # Fast for testing
limiter.wait() # Should not raise
def test_api_limiter_success(self):
from utils.api_limiter import APIRateLimiter
limiter = APIRateLimiter()
limiter.record_success()
assert not limiter.is_circuit_open
def test_api_limiter_circuit_breaker(self):
from utils.api_limiter import APIRateLimiter, CircuitBreakerOpen
limiter = APIRateLimiter(circuit_threshold=3)
limiter.record_failure("err1")
limiter.record_failure("err2")
limiter.record_failure("err3")
assert limiter.is_circuit_open
with pytest.raises(CircuitBreakerOpen):
limiter.wait_if_needed()
def test_api_limiter_reset(self):
from utils.api_limiter import APIRateLimiter
limiter = APIRateLimiter(circuit_threshold=2)
limiter.record_failure("err")
limiter.record_failure("err")
assert limiter.is_circuit_open
limiter.reset()
assert not limiter.is_circuit_open
class TestProfileLoader:
"""Test YAML profile loading."""
def test_load_vault7(self):
from collectors.profile_loader import load_profile
profile = load_profile('vault7')
assert profile['name'] == 'vault7'
assert 'wikileaks.org' in profile['domain_allowlist']
assert profile['crawl_rules']['max_depth'] > 0
def test_load_nonexistent(self):
from collectors.profile_loader import load_profile
with pytest.raises(FileNotFoundError):
load_profile('nonexistent_profile_xyz')
def test_list_profiles(self):
from collectors.profile_loader import list_profiles
profiles = list_profiles()
assert len(profiles) >= 5 # vault7, vault8, cablegate, spyfiles, emails
names = [p['name'] for p in profiles]
assert 'vault7' in names
def test_create_adhoc(self):
from collectors.profile_loader import create_adhoc_profile
profile = create_adhoc_profile("https://example.com/docs/", "test_source")
assert profile['name'] == 'test_source'
assert 'example.com' in profile['domain_allowlist']
class TestJSONLStore:
"""Test JSONL export/import."""
def setup_method(self):
self.tmp_db = tempfile.mktemp(suffix='.db')
self.tmp_dir = tempfile.mkdtemp()
from storage.db import MosaicDB
from storage.jsonl_store import JSONLStore
self.db = MosaicDB(self.tmp_db)
self.store = JSONLStore(self.db, self.tmp_dir)
def teardown_method(self):
self.db.close()
for ext in ('', '-wal', '-shm'):
path = self.tmp_db + ext
if os.path.exists(path):
os.unlink(path)
def test_export_empty(self):
files = self.store.export(category='tools')
assert files == []
def test_export_and_checksum(self):
from storage.models import ExtractedTool
tool = ExtractedTool(name="TestTool", capability="exploit",
description="A test", source_doc_ids=["doc1"])
tool.compute_dedup_key()
self.db.insert_extracted_tool(tool)
files = self.store.export(category='tools')
assert len(files) == 1
assert files[0].endswith('.jsonl')
# Verify checksum
assert self.store.verify_checksum(files[0])
if __name__ == '__main__':
pytest.main([__file__, '-v'])