Claude 24f816b6a3
Consolidate 22 sibling repos into layered organism structure
Place useful parts of the surrounding repos into sica-fondt by layer, per the
body model (Ada = membrane; brain/endocrine/capabilities/knowledge non-Ada):

- brain/        LLM reasoning + providers (dapr, hermes, MoMoA)
- capabilities/ REPRAG sidecars: hermes tools/skills, dapr tools, parallel
                dispatch, A51 channels, and the OSINT cluster
- knowledge/    LORAG corpus: 754 cyber-skills, agency personas, secure-coding,
                MITRE ATT&CK data
- reference/    defensive threat-reference (C3, shhbruh doc) + AdaYaml parser

License handling: AGPL sources (worldosint, advanced_evolution, mercury,
Reticulum) and GPL DeTTECT are SPEC-only clean-room/port descriptions — no
copyleft code copied. MIT/Apache/data parts copied as working trees.

Safety: shhbruh escape/persistence material and C3 covert-C2 kept as reference
only, not wired into the running organism. See CONSOLIDATION.md.

https://claude.ai/code/session_01UehUqEXXJJCsHoA4voCU5c
2026-06-10 06:53:01 +00:00

205 lines
7.7 KiB
Python

#!/usr/bin/env python3
"""Agent for SQLite database forensics.
Parses SQLite file headers, analyzes freelist pages for deleted records,
examines WAL files, decodes browser/app timestamps, and extracts
evidence from common forensic databases.
"""
import struct
import sqlite3
import json
import sys
import os
import re
from datetime import datetime, timedelta
from pathlib import Path
_SAFE_TABLE_RE = re.compile(r'^[a-zA-Z_][a-zA-Z0-9_]*$')
class SQLiteForensicsAgent:
"""Performs forensic analysis on SQLite database files."""
def __init__(self, db_path, output_dir="./sqlite_forensics"):
self.db_path = db_path
self.output_dir = Path(output_dir)
self.output_dir.mkdir(parents=True, exist_ok=True)
self.findings = []
def parse_header(self):
"""Parse the 100-byte SQLite database header."""
with open(self.db_path, "rb") as f:
header = f.read(100)
magic = header[0:16]
if magic != b"SQLite format 3\x00":
return {"error": "Not a valid SQLite database"}
page_size = struct.unpack(">H", header[16:18])[0]
if page_size == 1:
page_size = 65536
return {
"magic": magic[:15].decode("ascii"),
"page_size": page_size,
"write_format": header[18],
"read_format": header[19],
"change_counter": struct.unpack(">I", header[24:28])[0],
"db_size_pages": struct.unpack(">I", header[28:32])[0],
"first_freelist_page": struct.unpack(">I", header[32:36])[0],
"total_freelist_pages": struct.unpack(">I", header[36:40])[0],
"schema_cookie": struct.unpack(">I", header[40:44])[0],
"text_encoding": {1: "UTF-8", 2: "UTF-16le", 3: "UTF-16be"}.get(
struct.unpack(">I", header[52:56])[0], "unknown"),
"db_size_bytes": os.path.getsize(self.db_path),
}
def analyze_freelist(self):
"""Walk freelist trunk chain to identify pages with deleted data."""
with open(self.db_path, "rb") as f:
header = f.read(100)
page_size = struct.unpack(">H", header[16:18])[0]
if page_size == 1:
page_size = 65536
first_trunk = struct.unpack(">I", header[32:36])[0]
total_free = struct.unpack(">I", header[36:40])[0]
if first_trunk == 0:
return {"freelist_pages": 0, "trunk_pages": [], "leaf_pages": []}
trunk_pages, leaf_pages = [], []
trunk = first_trunk
while trunk != 0:
offset = (trunk - 1) * page_size
f.seek(offset)
page_data = f.read(page_size)
next_trunk = struct.unpack(">I", page_data[0:4])[0]
leaf_count = struct.unpack(">I", page_data[4:8])[0]
leaves = []
for i in range(leaf_count):
lp = struct.unpack(">I", page_data[8 + i * 4:12 + i * 4])[0]
leaves.append(lp)
trunk_pages.append({"page": trunk, "leaf_count": leaf_count})
leaf_pages.extend(leaves)
trunk = next_trunk
if leaf_pages:
self.findings.append({"type": "freelist_data",
"pages": len(leaf_pages),
"note": "Deleted records may be recoverable"})
return {"freelist_pages": total_free,
"trunk_pages": trunk_pages, "leaf_pages": leaf_pages}
def extract_freelist_pages(self):
"""Dump raw freelist leaf pages for hex analysis."""
info = self.analyze_freelist()
with open(self.db_path, "rb") as f:
hdr = f.read(100)
page_size = struct.unpack(">H", hdr[16:18])[0]
if page_size == 1:
page_size = 65536
out_dir = self.output_dir / "freelist_pages"
out_dir.mkdir(exist_ok=True)
for pn in info["leaf_pages"]:
f.seek((pn - 1) * page_size)
data = f.read(page_size)
(out_dir / f"page_{pn}.bin").write_bytes(data)
return len(info["leaf_pages"])
def parse_wal(self):
"""Parse WAL file frames for transaction history."""
wal_path = self.db_path + "-wal"
if not os.path.exists(wal_path):
return {"wal_exists": False}
with open(wal_path, "rb") as f:
header = f.read(32)
magic = struct.unpack(">I", header[0:4])[0]
page_size = struct.unpack(">I", header[8:12])[0]
checkpoint_seq = struct.unpack(">I", header[12:16])[0]
file_size = os.path.getsize(wal_path)
frames = []
offset = 32
frame_num = 0
while offset + 24 + page_size <= file_size:
f.seek(offset)
fh = f.read(24)
page_number = struct.unpack(">I", fh[0:4])[0]
frames.append({"frame": frame_num, "page": page_number,
"offset": offset})
offset += 24 + page_size
frame_num += 1
return {"wal_exists": True, "magic": hex(magic),
"page_size": page_size, "checkpoint_seq": checkpoint_seq,
"total_frames": len(frames), "frames": frames[:50]}
def query_tables(self):
"""List all tables and row counts in the database."""
conn = sqlite3.connect(f"file:{self.db_path}?mode=ro", uri=True)
cursor = conn.cursor()
cursor.execute("SELECT name FROM sqlite_master WHERE type='table'")
tables = []
for (name,) in cursor.fetchall():
try:
if not _SAFE_TABLE_RE.match(name):
continue
cursor.execute(f"SELECT COUNT(*) FROM [{name}]")
count = cursor.fetchone()[0]
except sqlite3.OperationalError:
count = -1
tables.append({"table": name, "row_count": count})
conn.close()
return tables
@staticmethod
def decode_timestamp(value, fmt="unix"):
"""Decode timestamps from common database formats."""
try:
if fmt == "unix":
return datetime.utcfromtimestamp(value).isoformat()
elif fmt == "chrome":
epoch_delta = 11644473600
return datetime.utcfromtimestamp(
(value / 1_000_000) - epoch_delta).isoformat()
elif fmt == "mac_absolute":
mac_epoch = datetime(2001, 1, 1)
return (mac_epoch + timedelta(seconds=value)).isoformat()
elif fmt == "mozilla":
return datetime.utcfromtimestamp(value / 1_000_000).isoformat()
except (OSError, ValueError, OverflowError):
return None
def generate_report(self):
"""Generate comprehensive forensic analysis report."""
report = {
"database": self.db_path,
"analysis_date": datetime.utcnow().isoformat(),
"header": self.parse_header(),
"tables": self.query_tables(),
"freelist": self.analyze_freelist(),
"wal": self.parse_wal(),
"findings": self.findings,
}
report_path = self.output_dir / "sqlite_forensics_report.json"
with open(report_path, "w") as f:
json.dump(report, f, indent=2, default=str)
print(json.dumps(report, indent=2, default=str))
return report
def main():
if len(sys.argv) < 2:
print("Usage: agent.py <database.db> [output_dir]")
sys.exit(1)
db_path = sys.argv[1]
output_dir = sys.argv[2] if len(sys.argv) > 2 else "./sqlite_forensics"
agent = SQLiteForensicsAgent(db_path, output_dir)
agent.generate_report()
if __name__ == "__main__":
main()