mirror of
https://github.com/mukul975/Anthropic-Cybersecurity-Skills.git
synced 2026-06-10 21:24:56 +03:00
Add folder anatomy (scripts/agent.py + references/api-reference.md) for 648 cybersecurity skills
Complete skill folder anatomy across all cybersecurity skills: - scripts/agent.py: 80-150 line Python agents using real libraries (impacket, boto3, azure-mgmt-*, kubernetes, pefile, yara, scapy, shodan, stix2, etc.) - references/api-reference.md: real API documentation with method signatures - LICENSE: MIT license for all skill folders
This commit is contained in:
@@ -0,0 +1,199 @@
|
||||
#!/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
|
||||
from datetime import datetime, timedelta
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
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:
|
||||
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()
|
||||
Reference in New Issue
Block a user