Spaces:
Running
Running
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>PROJECT: I.O.S. D.F.I.R. // ENHANCEMENT SKELETON</title> | |
| <!-- Import Fonts --> | |
| <link rel="preconnect" href="https://fonts.googleapis.com"> | |
| <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> | |
| <link | |
| href="https://fonts.googleapis.com/css2?family=Rajdhani:wght@400;500;600;700&family=Share+Tech+Mono&family=Space+Mono:ital,wght@0,400;0,700;1,400&display=swap" | |
| rel="stylesheet"> | |
| <style> | |
| /* --- CSS VARIABLES & RESET --- */ | |
| :root { | |
| --bg-dark: #050608; | |
| --bg-panel: #0e1016; | |
| --bg-card: #14161f; | |
| --border-subtle: #2a2f3d; | |
| --text-main: #e0e6ed; | |
| --text-muted: #94a3b8; | |
| --font-body: 'Rajdhani', sans-serif; | |
| --font-mono: 'Share Tech Mono', monospace; | |
| --font-meta: 'Space Mono', monospace; | |
| /* Semantic Colors */ | |
| --cyan: #00f0ff; | |
| --green: #00ff9d; | |
| --orange: #ff9d00; | |
| --gold: #ffd700; | |
| --purple: #bd00ff; | |
| --red: #ff0055; | |
| } | |
| * { | |
| box-sizing: border-box; | |
| margin: 0; | |
| padding: 0; | |
| } | |
| body { | |
| background-color: var(--bg-dark); | |
| color: var(--text-main); | |
| font-family: var(--font-body); | |
| line-height: 1.6; | |
| overflow-x: hidden; | |
| font-size: 16px; | |
| } | |
| /* --- BACKGROUND GRID --- */ | |
| .bg-grid { | |
| position: fixed; | |
| top: 0; | |
| left: 0; | |
| width: 100%; | |
| height: 100vh; | |
| z-index: -1; | |
| background-image: | |
| linear-gradient(rgba(0, 240, 255, 0.03) 1px, transparent 1px), | |
| linear-gradient(90deg, rgba(0, 240, 255, 0.03) 1px, transparent 1px); | |
| background-size: 40px 40px; | |
| pointer-events: none; | |
| } | |
| /* --- TYPOGRAPHY UTILS --- */ | |
| h1, | |
| h2, | |
| h3, | |
| h4 { | |
| text-transform: uppercase; | |
| letter-spacing: 1px; | |
| } | |
| .mono { | |
| font-family: var(--font-mono); | |
| } | |
| .meta { | |
| font-family: var(--font-meta); | |
| font-size: 0.85rem; | |
| } | |
| .text-cyan { | |
| color: var(--cyan); | |
| } | |
| .text-green { | |
| color: var(--green); | |
| } | |
| .text-orange { | |
| color: var(--orange); | |
| } | |
| .text-gold { | |
| color: var(--gold); | |
| } | |
| .text-purple { | |
| color: var(--purple); | |
| } | |
| .text-red { | |
| color: var(--red); | |
| } | |
| /* --- HEADER --- */ | |
| .header { | |
| padding: 3rem 2rem 2rem; | |
| border-bottom: 1px solid var(--border-subtle); | |
| background: radial-gradient(circle at top, rgba(0, 240, 255, 0.05), transparent 70%); | |
| } | |
| .header-top { | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| margin-bottom: 1rem; | |
| font-family: var(--font-meta); | |
| font-size: 0.75rem; | |
| color: var(--text-muted); | |
| } | |
| .header-main { | |
| display: flex; | |
| flex-direction: column; | |
| gap: 0.5rem; | |
| } | |
| .header-title { | |
| font-size: 3rem; | |
| font-weight: 700; | |
| line-height: 1; | |
| } | |
| .header-title span { | |
| color: var(--cyan); | |
| text-shadow: 0 0 10px rgba(0, 240, 255, 0.4); | |
| } | |
| .header-subtitle { | |
| font-size: 1.2rem; | |
| color: var(--text-muted); | |
| max-width: 800px; | |
| } | |
| .badges { | |
| display: flex; | |
| gap: 0.5rem; | |
| margin-top: 1rem; | |
| flex-wrap: wrap; | |
| } | |
| .badge { | |
| font-family: var(--font-mono); | |
| font-size: 0.7rem; | |
| padding: 2px 8px; | |
| border: 1px solid var(--border-subtle); | |
| border-radius: 2px; | |
| text-transform: uppercase; | |
| } | |
| .badge.cyan { | |
| border-color: var(--cyan); | |
| color: var(--cyan); | |
| } | |
| .badge.gold { | |
| border-color: var(--gold); | |
| color: var(--gold); | |
| } | |
| .badge.green { | |
| border-color: var(--green); | |
| color: var(--green); | |
| } | |
| /* --- STICKY NAV --- */ | |
| .nav-container { | |
| position: sticky; | |
| top: 0; | |
| z-index: 100; | |
| background: rgba(5, 6, 8, 0.85); | |
| backdrop-filter: blur(10px); | |
| border-bottom: 1px solid var(--border-subtle); | |
| padding: 0.5rem 2rem; | |
| overflow-x: auto; | |
| white-space: nowrap; | |
| -ms-overflow-style: none; | |
| scrollbar-width: none; | |
| } | |
| .nav-container::-webkit-scrollbar { | |
| display: none; | |
| } | |
| .nav-btn { | |
| background: transparent; | |
| border: none; | |
| color: var(--text-muted); | |
| font-family: var(--font-mono); | |
| font-size: 0.9rem; | |
| padding: 0.8rem 1.2rem; | |
| cursor: pointer; | |
| text-transform: uppercase; | |
| transition: all 0.2s ease; | |
| border-bottom: 2px solid transparent; | |
| } | |
| .nav-btn:hover { | |
| color: var(--text-main); | |
| background: rgba(255, 255, 255, 0.03); | |
| } | |
| .nav-btn.active { | |
| color: var(--cyan); | |
| border-bottom-color: var(--cyan); | |
| text-shadow: 0 0 8px rgba(0, 240, 255, 0.3); | |
| } | |
| /* --- CONTENT LAYOUT --- */ | |
| .container { | |
| max-width: 1200px; | |
| margin: 0 auto; | |
| padding: 2rem; | |
| } | |
| .section { | |
| display: none; | |
| animation: fadeIn 0.4s ease-out; | |
| } | |
| .section.active { | |
| display: block; | |
| } | |
| @keyframes fadeIn { | |
| from { | |
| opacity: 0; | |
| transform: translateY(10px); | |
| } | |
| to { | |
| opacity: 1; | |
| transform: translateY(0); | |
| } | |
| } | |
| .section-header { | |
| margin-bottom: 2rem; | |
| border-left: 4px solid var(--cyan); | |
| padding-left: 1rem; | |
| } | |
| .section-title { | |
| font-size: 2rem; | |
| color: var(--text-main); | |
| } | |
| .section-desc { | |
| font-family: var(--font-meta); | |
| color: var(--text-muted); | |
| font-size: 0.9rem; | |
| } | |
| /* --- CARDS --- */ | |
| .card { | |
| background: var(--bg-card); | |
| border: 1px solid var(--border-subtle); | |
| padding: 1.5rem; | |
| position: relative; | |
| transition: transform 0.2s; | |
| } | |
| .card:hover { | |
| border-color: rgba(255, 255, 255, 0.1); | |
| } | |
| .card::before { | |
| content: ''; | |
| position: absolute; | |
| left: 0; | |
| top: 0; | |
| bottom: 0; | |
| width: 3px; | |
| } | |
| .card.cyan::before { | |
| background: var(--cyan); | |
| } | |
| .card.green::before { | |
| background: var(--green); | |
| } | |
| .card.orange::before { | |
| background: var(--orange); | |
| } | |
| .card.gold::before { | |
| background: var(--gold); | |
| } | |
| .card.purple::before { | |
| background: var(--purple); | |
| } | |
| .card.red::before { | |
| background: var(--red); | |
| } | |
| .card-title { | |
| font-family: var(--font-mono); | |
| font-size: 0.85rem; | |
| text-transform: uppercase; | |
| margin-bottom: 1rem; | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| } | |
| /* --- GRIDS --- */ | |
| .grid-2 { | |
| display: grid; | |
| grid-template-columns: 1fr 1fr; | |
| gap: 1.5rem; | |
| } | |
| .grid-3 { | |
| display: grid; | |
| grid-template-columns: repeat(3, 1fr); | |
| gap: 1.5rem; | |
| } | |
| .grid-4 { | |
| display: grid; | |
| grid-template-columns: repeat(4, 1fr); | |
| gap: 1.5rem; | |
| } | |
| @media (max-width: 900px) { | |
| .grid-2, | |
| .grid-3, | |
| .grid-4 { | |
| grid-template-columns: 1fr; | |
| } | |
| .header-title { | |
| font-size: 2.2rem; | |
| } | |
| } | |
| /* --- ROLE CARDS --- */ | |
| .role-card { | |
| overflow: hidden; | |
| position: relative; | |
| } | |
| .role-watermark { | |
| position: absolute; | |
| bottom: -10px; | |
| right: -10px; | |
| font-size: 5rem; | |
| font-weight: 700; | |
| opacity: 0.05; | |
| font-family: var(--font-mono); | |
| line-height: 1; | |
| pointer-events: none; | |
| } | |
| .role-tag { | |
| font-family: var(--font-meta); | |
| font-size: 0.7rem; | |
| color: var(--cyan); | |
| margin-bottom: 0.5rem; | |
| display: block; | |
| } | |
| .role-name { | |
| font-size: 1.4rem; | |
| font-weight: 600; | |
| margin-bottom: 0.2rem; | |
| } | |
| .role-handle { | |
| font-family: var(--font-mono); | |
| color: var(--text-muted); | |
| font-size: 0.8rem; | |
| margin-bottom: 1rem; | |
| } | |
| .role-list { | |
| list-style: none; | |
| margin-top: 1rem; | |
| } | |
| .role-list li { | |
| font-size: 0.9rem; | |
| margin-bottom: 0.4rem; | |
| padding-left: 1rem; | |
| position: relative; | |
| } | |
| .role-list li::before { | |
| content: '>'; | |
| position: absolute; | |
| left: 0; | |
| color: var(--text-muted); | |
| } | |
| /* --- SIGNALS & TAGS --- */ | |
| .signal { | |
| display: inline-block; | |
| font-family: var(--font-mono); | |
| font-size: 0.65rem; | |
| padding: 1px 4px; | |
| margin-right: 4px; | |
| border-radius: 2px; | |
| vertical-align: middle; | |
| } | |
| .sig-ref { | |
| color: var(--cyan); | |
| border: 1px solid var(--cyan); | |
| } | |
| .sig-block { | |
| color: var(--red); | |
| border: 1px solid var(--red); | |
| } | |
| .sig-done { | |
| color: var(--green); | |
| border: 1px solid var(--green); | |
| } | |
| .sig-gate { | |
| color: var(--gold); | |
| border: 1px solid var(--gold); | |
| } | |
| /* --- CODE BLOCKS --- */ | |
| .code-block { | |
| background: #000; | |
| border: 1px solid var(--border-subtle); | |
| padding: 1rem; | |
| font-family: 'Space Mono', monospace; | |
| font-size: 0.85rem; | |
| overflow-x: auto; | |
| color: #d4d4d4; | |
| max-height: 600px; | |
| overflow-y: auto; | |
| white-space: pre; | |
| } | |
| /* Python Syntax Highlighting */ | |
| .py-kw { color: #569cd6; } | |
| .py-fn { color: #dcdcaa; } | |
| .py-cmt { color: #6a9955; } | |
| .py-type { color: #4ec9b0; } | |
| .py-str { color: #ce9178; } | |
| .py-dec { color: #c586c0; } | |
| /* --- DIAGRAMS (CSS ONLY) --- */ | |
| .diagram-box { | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| gap: 1rem; | |
| padding: 2rem; | |
| background: rgba(0, 0, 0, 0.2); | |
| border: 1px dashed var(--border-subtle); | |
| } | |
| .diag-node { | |
| border: 1px solid var(--cyan); | |
| background: rgba(0, 240, 255, 0.1); | |
| padding: 0.5rem 1rem; | |
| font-family: var(--font-mono); | |
| font-size: 0.8rem; | |
| } | |
| .diag-arrow { | |
| color: var(--cyan); | |
| font-size: 1.2rem; | |
| } | |
| /* --- ANYCODER LINK --- */ | |
| .anycoder-link { | |
| font-family: var(--font-mono); | |
| font-size: 0.7rem; | |
| color: var(--text-muted); | |
| text-decoration: none; | |
| border: 1px solid var(--border-subtle); | |
| padding: 2px 6px; | |
| border-radius: 4px; | |
| transition: color 0.2s; | |
| } | |
| .anycoder-link:hover { | |
| color: var(--cyan); | |
| border-color: var(--cyan); | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <!-- Background Grid --> | |
| <div class="bg-grid"></div> | |
| <!-- Header --> | |
| <header class="header"> | |
| <div class="header-top"> | |
| <span>SYS_REF: DFIR_IOS_V1.0</span> | |
| <span> | |
| <a href="https://huggingface.co/spaces/akhaliq/anycoder" target="_blank" class="anycoder-link">Built with anycoder</a> | |
| </span> | |
| </div> | |
| <div class="header-main"> | |
| <h1 class="header-title">Project <span>DFIR</span> // iOS ENHANCEMENT</h1> | |
| <p class="header-subtitle">Structured SQLite-backed iOS backup extractor pipeline. Artifact hashing, logging, and extensible parser modules.</p> | |
| <div class="badges"> | |
| <span class="badge cyan">STATUS: SKELETON</span> | |
| <span class="badge gold">LANG: PYTHON3</span> | |
| <span class="badge green">LOGGING: ENABLED</span> | |
| <span class="badge">HASHING: SHA256</span> | |
| </div> | |
| </div> | |
| </header> | |
| <!-- Navigation --> | |
| <nav class="nav-container"> | |
| <button class="nav-btn active" onclick="showSection('overview')">Manifesto</button> | |
| <button class="nav-btn" onclick="showSection('modules')">Modules</button> | |
| <button class="nav-btn" onclick="showSection('pipeline')">Pipeline</button> | |
| <button class="nav-btn" onclick="showSection('schema')">Data Structs</button> | |
| <button class="nav-btn" onclick="showSection('source')">Source Code</button> | |
| </nav> | |
| <!-- Main Content --> | |
| <div class="container"> | |
| <!-- SECTION: OVERVIEW --> | |
| <div id="overview" class="section active"> | |
| <div class="section-header"> | |
| <h2 class="section-title">System Manifesto</h2> | |
| <span class="section-desc">:: EXEC_SUMMARY :: Purpose and operational goals of the DFIR skeleton.</span> | |
| </div> | |
| <div class="grid-2"> | |
| <div class="card cyan"> | |
| <div class="card-title"> | |
| <span>Primary Objective</span> | |
| <span class="signal sig-gate">CORE</span> | |
| </div> | |
| <p>Upgrade simple SQLite-backed iOS backup extractors into a structured DFIR pipeline. Preserve analyst visibility, logging, and artifact hashing.</p> | |
| </div> | |
| <div class="card gold"> | |
| <div class="card-title"> | |
| <span>Current Status</span> | |
| <span class="signal sig-ref">DEV</span> | |
| </div> | |
| <p>This file is intentionally verbose. Several modules are scaffolded as placeholders (Stubs) for future parser implementation.</p> | |
| </div> | |
| </div> | |
| <div style="margin-top: 2rem;"> | |
| <div class="card purple"> | |
| <div class="card-title"> | |
| <span>Operational Flow</span> | |
| <span class="signal sig-ref">REF</span> | |
| </div> | |
| <div class="diagram-box"> | |
| <div class="diag-node">CLI_ARGS</div> | |
| <div class="diag-arrow">→</div> | |
| <div class="diag-node" style="border-color: var(--purple);">MANIFEST_DB</div> | |
| <div class="diag-arrow">→</div> | |
| <div class="diag-node" style="border-color: var(--green);">PARSER_MODULES</div> | |
| <div class="diag-arrow">→</div> | |
| <div class="diag-node">TIMELINE_CSV</div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- SECTION: MODULES --> | |
| <div id="modules" class="section"> | |
| <div class="section-header"> | |
| <h2 class="section-title">Parser Modules</h2> | |
| <span class="section-desc">:: AGENTS :: Specialized extraction subroutines.</span> | |
| </div> | |
| <div class="grid-3"> | |
| <!-- Module 1 --> | |
| <div class="card cyan role-card"> | |
| <div class="role-watermark">CORE</div> | |
| <span class="role-tag">CONTACTS</span> | |
| <h3 class="role-name">AddressBook Parser</h3> | |
| <div class="role-handle">export_contacts()</div> | |
| <ul class="role-list"> | |
| <li>Modern & Legacy Schema</li> | |
| <li>Phone Normalization</li> | |
| <li>CSV/JSON Export</li> | |
| </ul> | |
| </div> | |
| <!-- Module 2 --> | |
| <div class="card green role-card"> | |
| <div class="role-watermark">CORE</div> | |
| <span class="role-tag">MESSAGING</span> | |
| <h3 class="role-name">SMS / iMessage</h3> | |
| <div class="role-handle">extract_sms()</div> | |
| <ul class="role-list"> | |
| <li>Handle Resolution</li> | |
| <li>AttributedBody Stub</li> | |
| <li>Timeline Event Gen</li> | |
| </ul> | |
| </div> | |
| <!-- Module 3 --> | |
| <div class="card orange role-card"> | |
| <div class="role-watermark">GEO</div> | |
| <span class="role-tag">LOCATION</span> | |
| <h3 class="role-name">LocationD / Cache</h3> | |
| <div class="role-handle">extract_locations()</div> | |
| <ul class="role-list"> | |
| <li>RTCL Location MO</li> | |
| <li>Visit MO Parsing</li> | |
| <li>Cell Tower Logs</li> | |
| </ul> | |
| </div> | |
| <!-- Module 4 --> | |
| <div class="card red role-card"> | |
| <div class="role-watermark">STUB</div> | |
| <span class="role-tag">FUTURE</span> | |
| <h3 class="role-name">Safari / Notes</h3> | |
| <div class="role-handle">extract_safari_stub()</div> | |
| <ul class="role-list"> | |
| <li>History / Downloads</li> | |
| <li>Rich Notes</li> | |
| <li>App Containers</li> | |
| </ul> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- SECTION: PIPELINE --> | |
| <div id="pipeline" class="section"> | |
| <div class="section-header"> | |
| <h2 class="section-title">Execution Pipeline</h2> | |
| <span class="section-desc">:: LOGIC :: The `run()` orchestration sequence.</span> | |
| </div> | |
| <div class="timeline"> | |
| <div class="timeline-item done"> | |
| <div class="timeline-marker"></div> | |
| <div class="timeline-date">STEP 1</div> | |
| <div class="timeline-title">Initialization</div> | |
| <div class="card" style="margin-top: 0.5rem; border: none; background: rgba(255,255,255,0.02); padding: 1rem;"> | |
| <span class="signal sig-done">DONE</span> Setup Logging. Validate Backup Root. Parse CLI Arguments. | |
| </div> | |
| </div> | |
| <div class="timeline-item active"> | |
| <div class="timeline-marker"></div> | |
| <div class="timeline-date">STEP 2</div> | |
| <div class="timeline-title">Manifest Indexing</div> | |
| <div class="card" style="margin-top: 0.5rem; border: none; background: rgba(255,255,255,0.02); padding: 1rem;"> | |
| <span class="signal sig-gate">ACTIVE</span> Load `Manifest.db`. Search for `sms.db`, `AddressBook`, and Location artifacts. | |
| </div> | |
| </div> | |
| <div class="timeline-item next"> | |
| <div class="timeline-marker"></div> | |
| <div class="timeline-date">STEP 3</div> | |
| <div class="timeline-title">Extraction & Export</div> | |
| <div class="card" style="margin-top: 0.5rem; border: none; background: rgba(255,255,255,0.02); padding: 1rem;"> | |
| <span class="signal sig-ref">NEXT</span> Run Parser Modules. Generate CSV/JSON. Build Unified Timeline. | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- SECTION: SCHEMA --> | |
| <div id="schema" class="section"> | |
| <div class="section-header"> | |
| <h2 class="section-title">Data Structures</h2> | |
| <span class="section-desc">:: STRUCT :: Python Dataclasses used for typing.</span> | |
| </div> | |
| <div class="card cyan"> | |
| <div class="card-title">Config & Metadata</div> | |
| <div class="code-block" style="max-height: 400px;"> | |
| <span class="py-dec">@dataclass</span> | |
| <span class="py-kw">class</span> <span class="py-type">CaseMetadata</span>: | |
| case_name: <span class="py-type">str</span> = <span class="py-str">"UNSPECIFIED_CASE"</span> | |
| examiner: <span class="py-type">str</span> = <span class="py-str">"UNSPECIFIED_EXAMINER"</span> | |
| evidence_id: <span class="py-type">str</span> = <span class="py-str">"UNSPECIFIED_EVIDENCE"</span> | |
| <span class="py-dec">@dataclass</span> | |
| <span class="py-kw">class</span> <span class="py-type">AppConfig</span>: | |
| backup_root: <span class="py-type">Path</span> | |
| manifest_db: <span class="py-type">Path</span> | |
| output_root: <span class="py-type">Path</span> | |
| query_terms: <span class="py-type">List</span>[<span class="py-type">str</span>] = field(default_factory=<span class="py-type">list</span>) | |
| verbose: <span class="py-type">bool</span> = <span class="py-kw">False</span> | |
| hash_exports: <span class="py-type">bool</span> = <span class="py-kw">True</span> | |
| copy_raw_files: <span class="py-type">bool</span> = <span class="py-kw">True</span> | |
| export_csv: <span class="py-type">bool</span> = <span class="py-kw">True</span> | |
| export_jsonl: <span class="py-type">bool</span> = <span class="py-kw">True</span> | |
| </div> | |
| </div> | |
| <div class="card purple" style="margin-top: 1.5rem;"> | |
| <div class="card-title">Timeline Event Model</div> | |
| <div class="code-block" style="max-height: 300px;"> | |
| <span class="py-dec">@dataclass</span> | |
| <span class="py-kw">class</span> <span class="py-type">TimelineEvent</span>: | |
| timestamp: <span class="py-type">Optional</span>[<span class="py-type">str</span>] | |
| artifact_type: <span class="py-type">str</span> | |
| source_file: <span class="py-type">str</span> | |
| summary: <span class="py-type">str</span> | |
| attributes: <span class="py-type">Dict</span>[<span class="py-type">str</span>, <span class="py-type">Any</span>] = field(default_factory=<span class="py-type">dict</span>) | |
| </div> | |
| </div> | |
| </div> | |
| <!-- SECTION: SOURCE --> | |
| <div id="source" class="section"> | |
| <div class="section-header"> | |
| <h2 class="section-title">Source Implementation</h2> | |
| <span class="section-desc">:: CODE :: The complete Python script logic.</span> | |
| </div> | |
| <div class="card gold"> | |
| <div class="card-title"> | |
| <span>dfir_ios_enhancement.py</span> | |
| <span class="signal sig-done">READY</span> | |
| </div> | |
| <div class="code-block" style="font-size: 0.75rem; line-height: 1.4;"> | |
| <span class="py-cmt">#!/usr/bin/env python3</span> | |
| <span class="py-cmt">""" DFIR iOS Backup Enhancement Skeleton """</span> | |
| <span class="py-kw">from</span> __future__ <span class="py-kw">import</span> annotations | |
| <span class="py-kw">import</span> argparse, csv, dataclasses, hashlib, json, logging, os, shutil, sqlite3, sys, traceback | |
| <span class="py-kw">from</span> dataclasses <span class="py-kw">import</span> dataclass, field | |
| <span class="py-kw">from</span> datetime <span class="py-kw">import</span> datetime, timedelta, timezone | |
| <span class="py-kw">from</span> pathlib <span class="py-kw">import</span> Path | |
| <span class="py-kw">from</span> typing <span class="py-kw">import</span> Any, Dict, Iterable, List, Optional, Tuple | |
| <span class="py-type">APPLE_EPOCH</span> = datetime(<span class="py-str">2001</span>, <span class="py-str">1</span>, <span class="py-str">1</span>, tzinfo=timezone.utc) | |
| <span class="py-cmt"># ============================================================</span> | |
| <span class="py-cmt"># CONFIG / DATA MODELS</span> | |
| <span class="py-cmt"># ============================================================</span> | |
| <span class="py-dec">@dataclass</span> | |
| <span class="py-kw">class</span> <span class="py-type">CaseMetadata</span>: | |
| case_name: <span class="py-type">str</span> = <span class="py-str">"UNSPECIFIED_CASE"</span> | |
| examiner: <span class="py-type">str</span> = <span class="py-str">"UNSPECIFIED_EXAMINER"</span> | |
| evidence_id: <span class="py-type">str</span> = <span class="py-str">"UNSPECIFIED_EVIDENCE"</span> | |
| notes: <span class="py-type">str</span> = <span class="py-str">""</span> | |
| <span class="py-dec">@dataclass</span> | |
| <span class="py-kw">class</span> <span class="py-type">AppConfig</span>: | |
| backup_root: <span class="py-type">Path</span> | |
| manifest_db: <span class="py-type">Path</span> | |
| output_root: <span class="py-type">Path</span> | |
| query_terms: <span class="py-type">List</span>[<span class="py-type">str</span>] = field(default_factory=<span class="py-type">list</span>) | |
| verbose: <span class="py-type">bool</span> = <span class="py-kw">False</span> | |
| hash_exports: <span class="py-type">bool</span> = <span class="py-kw">True</span> | |
| copy_raw_files: <span class="py-type">bool</span> = <span class="py-kw">True</span> | |
| export_csv: <span class="py-type">bool</span> = <span class="py-kw">True</span> | |
| export_jsonl: <span class="py-type">bool</span> = <span class="py-kw">True</span> | |
| export_kml: <span class="py-type">bool</span> = <span class="py-kw">False</span> | |
| export_geojson: <span class="py-type">bool</span> = <span class="py-kw">False</span> | |
| case: <span class="py-type">CaseMetadata</span> = field(default_factory=<span class="py-type">CaseMetadata</span>) | |
| <span class="py-dec">@dataclass</span> | |
| <span class="py-kw">class</span> <span class="py-type">LocatedFile</span>: | |
| file_id: <span class="py-type">str</span> | |
| relative_path: <span class="py-type">str</span> | |
| domain: <span class="py-type">Optional</span>[<span class="py-type">str</span>] | |
| source_path: <span class="py-type">Path</span> | |
| <span class="py-dec">@dataclass</span> | |
| <span class="py-kw">class</span> <span class="py-type">TimelineEvent</span>: | |
| timestamp: <span class="py-type">Optional</span>[<span class="py-type">str</span>] | |
| artifact_type: <span class="py-type">str</span> | |
| source_file: <span class="py-type">str</span> | |
| summary: <span class="py-type">str</span> | |
| attributes: <span class="py-type">Dict</span>[<span class="py-type">str</span>, <span class="py-type">Any</span>] = field(default_factory=<span class="py-type">dict</span>) | |
| <span class="py-cmt"># ============================================================</span> | |
| <span class="py-cmt"># LOGGING</span> | |
| <span class="py-cmt"># ============================================================</span> | |
| <span class="py-kw">def</span> <span class="py-fn">setup_logging</span>(output_root: <span class="py-type">Path</span>, verbose: <span class="py-type">bool</span> = <span class="py-kw">False</span>) -> logging.<span class="py-type">Logger</span>: | |
| output_root.mkdir(parents=<span class="py-kw">True</span>, exist_ok=<span class="py-kw">True</span>) | |
| log_path = output_root / <span class="py-str">"dfir_run.log"</span> | |
| logger = logging.getLogger(<span class="py-str">"dfir_ios"</span>) | |
| logger.setLevel(logging.DEBUG <span class="py-kw">if</span> verbose <span class="py-kw">else</span> logging.INFO) | |
| logger.handlers.clear() | |
| fmt = logging.Formatter(<span class="py-str">"%(asctime)s | %(levelname)s | %(message)s"</span>) | |
| fh = logging.FileHandler(log_path, encoding=<span class="py-str">"utf-8"</span>) | |
| fh.setLevel(logging.DEBUG) | |
| fh.setFormatter(fmt) | |
| logger.addHandler(fh) | |
| sh = logging.StreamHandler(sys.stdout) | |
| sh.setLevel(logging.DEBUG <span class="py-kw">if</span> verbose <span class="py-kw">else</span> logging.INFO) | |
| sh.setFormatter(fmt) | |
| logger.addHandler(sh) | |
| logger.debug(<span class="py-str">"Logging initialized"</span>) | |
| <span class="py-kw">return</span> logger | |
| <span class="py-cmt"># ============================================================</span> | |
| <span class="py-cmt"># UTILS</span> | |
| <span class="py-cmt"># ============================================================</span> | |
| <span class="py-kw">def</span> <span class="py-fn">apple_time_to_datetime</span>(ts: <span class="py-type">Any</span>) -> <span class="py-type">Optional</span>[datetime]: | |
| <span class="py-kw">if</span> ts <span class="py-kw">is</span> <span class="py-kw">None</span>: <span class="py-kw">return</span> <span class="py-kw">None</span> | |
| <span class="py-kw">try</span>: | |
| ts = <span class="py-type">float</span>(ts) | |
| <span class="py-kw">if</span> ts > <span class="py-str">1e12</span>: ts = ts / <span class="py-str">1e9</span> | |
| <span class="py-kw">return</span> <span class="py-type">APPLE_EPOCH</span> + timedelta(seconds=ts) | |
| <span class="py-kw">except</span> <span class="py-type">Exception</span>: <span class="py-kw">return</span> <span class="py-kw">None</span> | |
| <span class="py-kw">def</span> <span class="py-fn">normalize_phone</span>(phone: <span class="py-type">Optional</span>[<span class="py-type">str</span>]) -> <span class="py-type">Optional</span>[<span class="py-type">str</span>]: | |
| <span class="py-kw">if</span> <span class="py-kw">not</span> phone: <span class="py-kw">return</span> phone | |
| value = <span class="py-type">str</span>(phone).replace(<span class="py-str">"+1"</span>, <span class="py-str">""</span>).replace(<span class="py-str">" "</span>, <span class="py-str">""</span>).replace(<span class="py-str">"-"</span>, <span class="py-str">""</span>).replace(<span class="py-str">"("</span>, <span class="py-str">""</span>).replace(<span class="py-str">")"</span>, <span class="py-str">""</span>) | |
| <span class="py-kw">return</span> value.strip() <span class="py-kw">or</span> <span class="py-kw">None</span> | |
| <span class="py-kw">def</span> <span class="py-fn">sha256_file</span>(path: <span class="py-type">Path</span>, chunk_size: <span class="py-type">int</span> = <span class="py-str">1024</span> * <span class="py-str">1024</span>) -> <span class="py-type">str</span>: | |
| h = hashlib.sha256() | |
| <span class="py-kw">with</span> path.open(<span class="py-str">"rb"</span>) <span class="py-kw">as</span> f: | |
| <span class="py-kw">while</span> <span class="py-kw">True</span>: | |
| chunk = f.read(chunk_size) | |
| <span class="py-kw">if</span> <span class="py-kw">not</span> chunk: <span class="py-kw">break</span> | |
| h.update(chunk) | |
| <span class="py-kw">return</span> h.hexdigest() | |
| <span class="py-cmt"># ... [Manifest Access Layer and Extraction Logic Truncated for Brevity in View] ...</span> | |
| <span class="py-cmt"># ============================================================</span> | |
| <span class="py-cmt"># MAIN ORCHESTRATION</span> | |
| <span class="py-cmt"># ============================================================</span> | |
| <span class="py-kw">def</span> <span class="py-fn">run</span>(cfg: <span class="py-type">AppConfig</span>) -> <span class="py-type">int</span>: | |
| logger = setup_logging(cfg.output_root, cfg.verbose) | |
| logger.info(<span class="py-str">"Starting DFIR iOS backup extraction"</span>) |