Spaces:
Running
Running
| #!/usr/bin/env python3 | |
| """ | |
| PROJECT: I.O.S. D.F.I.R. // ENHANCEMENT SKELETON | |
| Streamlit Application for iOS DFIR Documentation | |
| """ | |
| import streamlit as st | |
| from datetime import datetime, timedelta, timezone | |
| from pathlib import Path | |
| from typing import Any, Dict, List, Optional, Tuple | |
| from dataclasses import dataclass, field | |
| import hashlib | |
| import json | |
| import logging | |
| # Page configuration | |
| st.set_page_config( | |
| page_title="DFIR iOS Enhancement", | |
| page_icon="🔍", | |
| layout="wide", | |
| initial_sidebar_state="collapsed" | |
| ) | |
| # Custom CSS for cyberpunk aesthetic | |
| CUSTOM_CSS = """ | |
| <style> | |
| /* Import Fonts */ | |
| @import url('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'); | |
| /* CSS Variables */ | |
| :root { | |
| --bg-dark: #050608; | |
| --bg-panel: #0e1016; | |
| --bg-card: #14161f; | |
| --border-subtle: #2a2f3d; | |
| --text-main: #e0e6ed; | |
| --text-muted: #94a3b8; | |
| --cyan: #00f0ff; | |
| --green: #00ff9d; | |
| --orange: #ff9d00; | |
| --gold: #ffd700; | |
| --purple: #bd00ff; | |
| --red: #ff0055; | |
| } | |
| /* Main background */ | |
| .stApp { | |
| background-color: var(--bg-dark); | |
| 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; | |
| } | |
| /* Hide default Streamlit elements */ | |
| #MainMenu {visibility: hidden;} | |
| footer {visibility: hidden;} | |
| header {visibility: hidden;} | |
| /* Typography */ | |
| h1, h2, h3, h4, h5, h6 { | |
| font-family: 'Rajdhani', sans-serif; | |
| text-transform: uppercase; | |
| letter-spacing: 1px; | |
| color: var(--text-main); | |
| } | |
| .mono { | |
| font-family: 'Share Tech Mono', monospace; | |
| } | |
| .meta { | |
| font-family: 'Space Mono', monospace; | |
| font-size: 0.85rem; | |
| } | |
| /* Cards */ | |
| .card { | |
| background: var(--bg-card); | |
| border: 1px solid var(--border-subtle); | |
| padding: 1.5rem; | |
| border-radius: 4px; | |
| transition: transform 0.2s, border-color 0.2s; | |
| } | |
| .card:hover { | |
| border-color: rgba(255, 255, 255, 0.1); | |
| } | |
| .card-cyan { border-left: 4px solid var(--cyan); } | |
| .card-green { border-left: 4px solid var(--green); } | |
| .card-orange { border-left: 4px solid var(--orange); } | |
| .card-gold { border-left: 4px solid var(--gold); } | |
| .card-purple { border-left: 4px solid var(--purple); } | |
| .card-red { border-left: 4px solid var(--red); } | |
| /* Badges */ | |
| .badge { | |
| display: inline-block; | |
| font-family: 'Share Tech Mono', monospace; | |
| font-size: 0.7rem; | |
| padding: 2px 8px; | |
| border: 1px solid var(--border-subtle); | |
| border-radius: 2px; | |
| text-transform: uppercase; | |
| margin-right: 0.5rem; | |
| } | |
| .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); } | |
| .badge-red { border-color: var(--red); color: var(--red); } | |
| /* Code blocks */ | |
| .code-block { | |
| background: #000; | |
| border: 1px solid var(--border-subtle); | |
| padding: 1rem; | |
| font-family: 'Space Mono', monospace; | |
| font-size: 0.85rem; | |
| border-radius: 4px; | |
| overflow-x: auto; | |
| color: #d4d4d4; | |
| } | |
| /* Navigation buttons */ | |
| .nav-button { | |
| background: transparent; | |
| border: none; | |
| color: var(--text-muted); | |
| font-family: 'Share Tech Mono', monospace; | |
| 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-button:hover { | |
| color: var(--text-main); | |
| background: rgba(255, 255, 255, 0.03); | |
| } | |
| .nav-button.active { | |
| color: var(--cyan); | |
| border-bottom-color: var(--cyan); | |
| } | |
| /* Signals */ | |
| .signal { | |
| display: inline-block; | |
| font-family: 'Share Tech Mono', monospace; | |
| font-size: 0.65rem; | |
| padding: 1px 4px; | |
| margin-right: 4px; | |
| border-radius: 2px; | |
| } | |
| .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); } | |
| /* Timeline */ | |
| .timeline-item { | |
| padding: 1rem; | |
| border-left: 2px solid var(--border-subtle); | |
| margin-left: 1rem; | |
| position: relative; | |
| } | |
| .timeline-item::before { | |
| content: ''; | |
| position: absolute; | |
| left: -6px; | |
| top: 1.5rem; | |
| width: 10px; | |
| height: 10px; | |
| border-radius: 50%; | |
| background: var(--cyan); | |
| } | |
| .timeline-item.done::before { background: var(--green); } | |
| .timeline-item.active::before { background: var(--gold); } | |
| .timeline-item.next::before { background: var(--text-muted); } | |
| /* 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: 'Share Tech Mono', monospace; | |
| line-height: 1; | |
| pointer-events: none; | |
| } | |
| .role-tag { | |
| font-family: 'Space Mono', monospace; | |
| font-size: 0.7rem; | |
| color: var(--cyan); | |
| margin-bottom: 0.5rem; | |
| display: block; | |
| } | |
| /* Lists */ | |
| .role-list { | |
| list-style: none; | |
| margin-top: 1rem; | |
| padding-left: 0; | |
| } | |
| .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); | |
| } | |
| /* Header styling */ | |
| .header-container { | |
| padding: 2rem 0; | |
| border-bottom: 1px solid var(--border-subtle); | |
| margin-bottom: 2rem; | |
| } | |
| .header-title { | |
| font-size: 3rem; | |
| font-weight: 700; | |
| line-height: 1; | |
| margin-bottom: 0.5rem; | |
| } | |
| .header-title span { | |
| color: var(--cyan); | |
| } | |
| .header-subtitle { | |
| font-size: 1.2rem; | |
| color: var(--text-muted); | |
| max-width: 800px; | |
| } | |
| /* Diagram boxes */ | |
| .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); | |
| border-radius: 4px; | |
| flex-wrap: wrap; | |
| } | |
| .diag-node { | |
| border: 1px solid var(--cyan); | |
| background: rgba(0, 240, 255, 0.1); | |
| padding: 0.5rem 1rem; | |
| font-family: 'Share Tech Mono', monospace; | |
| font-size: 0.8rem; | |
| border-radius: 2px; | |
| } | |
| .diag-arrow { | |
| color: var(--cyan); | |
| font-size: 1.2rem; | |
| } | |
| /* Anycoder link */ | |
| .anycoder-link { | |
| font-family: 'Space Mono', monospace; | |
| 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, border-color 0.2s; | |
| } | |
| .anycoder-link:hover { | |
| color: var(--cyan); | |
| border-color: var(--cyan); | |
| } | |
| /* Section headers */ | |
| .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: 'Space Mono', monospace; | |
| color: var(--text-muted); | |
| font-size: 0.9rem; | |
| } | |
| /* Column styling */ | |
| .st-column { | |
| padding: 0.5rem; | |
| } | |
| /* Text colors */ | |
| .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); } | |
| </style> | |
| """ | |
| # Inject custom CSS | |
| st.markdown(CUSTOM_CSS, unsafe_allow_html=True) | |
| # Initialize session state for navigation | |
| if 'current_section' not in st.session_state: | |
| st.session_state.current_section = 'overview' | |
| # Header | |
| def render_header(): | |
| st.markdown(""" | |
| <div class="header-container"> | |
| <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 1rem;"> | |
| <span class="meta">SYS_REF: DFIR_IOS_V1.0</span> | |
| <a href="https://huggingface.co/spaces/akhaliq/anycoder" target="_blank" class="anycoder-link">Built with anycoder</a> | |
| </div> | |
| <div> | |
| <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 style="margin-top: 1rem;"> | |
| <span class="badge badge-cyan">STATUS: SKELETON</span> | |
| <span class="badge badge-gold">LANG: PYTHON3</span> | |
| <span class="badge badge-green">LOGGING: ENABLED</span> | |
| <span class="badge">HASHING: SHA256</span> | |
| </div> | |
| </div> | |
| </div> | |
| """, unsafe_allow_html=True) | |
| # Navigation | |
| def render_navigation(): | |
| sections = [ | |
| ('overview', 'Manifesto'), | |
| ('modules', 'Modules'), | |
| ('pipeline', 'Pipeline'), | |
| ('schema', 'Data Structs'), | |
| ('source', 'Source Code') | |
| ] | |
| cols = st.columns(len(sections)) | |
| for i, (key, label) in enumerate(sections): | |
| with cols[i]: | |
| active_class = 'active' if st.session_state.current_section == key else '' | |
| if st.button( | |
| label, | |
| key=f"nav_{key}", | |
| use_container_width=True, | |
| type="secondary" if st.session_state.current_section != key else "primary" | |
| ): | |
| st.session_state.current_section = key | |
| st.rerun() | |
| # Section: Overview | |
| def render_overview(): | |
| st.markdown(""" | |
| <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> | |
| """, unsafe_allow_html=True) | |
| col1, col2 = st.columns(2) | |
| with col1: | |
| st.markdown(""" | |
| <div class="card card-cyan"> | |
| <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 1rem;"> | |
| <span class="mono" style="font-size: 0.85rem; text-transform: uppercase;">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> | |
| """, unsafe_allow_html=True) | |
| with col2: | |
| st.markdown(""" | |
| <div class="card card-gold"> | |
| <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 1rem;"> | |
| <span class="mono" style="font-size: 0.85rem; text-transform: uppercase;">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> | |
| """, unsafe_allow_html=True) | |
| st.markdown("<div style='margin-top: 2rem;'></div>", unsafe_allow_html=True) | |
| st.markdown(""" | |
| <div class="card card-purple"> | |
| <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 1rem;"> | |
| <span class="mono" style="font-size: 0.85rem; text-transform: uppercase;">Operational Flow</span> | |
| <span class="signal sig-ref">REF</span> | |
| </div> | |
| </div> | |
| """, unsafe_allow_html=True) | |
| st.markdown(""" | |
| <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> | |
| """, unsafe_allow_html=True) | |
| # Section: Modules | |
| def render_modules(): | |
| st.markdown(""" | |
| <div class="section-header"> | |
| <h2 class="section-title">Parser Modules</h2> | |
| <span class="section-desc">:: AGENTS :: Specialized extraction subroutines.</span> | |
| </div> | |
| """, unsafe_allow_html=True) | |
| col1, col2, col3, col4 = st.columns(4) | |
| with col1: | |
| st.markdown(""" | |
| <div class="card card-cyan role-card"> | |
| <div class="role-watermark">CORE</div> | |
| <span class="role-tag">CONTACTS</span> | |
| <h3 style="font-size: 1.4rem; margin-bottom: 0.2rem;">AddressBook Parser</h3> | |
| <div class="mono" style="color: var(--text-muted); font-size: 0.8rem; margin-bottom: 1rem;">export_contacts()</div> | |
| <ul class="role-list"> | |
| <li>Modern & Legacy Schema</li> | |
| <li>Phone Normalization</li> | |
| <li>CSV/JSON Export</li> | |
| </ul> | |
| </div> | |
| """, unsafe_allow_html=True) | |
| with col2: | |
| st.markdown(""" | |
| <div class="card card-green role-card"> | |
| <div class="role-watermark">CORE</div> | |
| <span class="role-tag">MESSAGING</span> | |
| <h3 style="font-size: 1.4rem; margin-bottom: 0.2rem;">SMS / iMessage</h3> | |
| <div class="mono" style="color: var(--text-muted); font-size: 0.8rem; margin-bottom: 1rem;">extract_sms()</div> | |
| <ul class="role-list"> | |
| <li>Handle Resolution</li> | |
| <li>AttributedBody Stub</li> | |
| <li>Timeline Event Gen</li> | |
| </ul> | |
| </div> | |
| """, unsafe_allow_html=True) | |
| with col3: | |
| st.markdown(""" | |
| <div class="card card-orange role-card"> | |
| <div class="role-watermark">GEO</div> | |
| <span class="role-tag">LOCATION</span> | |
| <h3 style="font-size: 1.4rem; margin-bottom: 0.2rem;">LocationD / Cache</h3> | |
| <div class="mono" style="color: var(--text-muted); font-size: 0.8rem; margin-bottom: 1rem;">extract_locations()</div> | |
| <ul class="role-list"> | |
| <li>RTCL Location MO</li> | |
| <li>Visit MO Parsing</li> | |
| <li>Cell Tower Logs</li> | |
| </ul> | |
| </div> | |
| """, unsafe_allow_html=True) | |
| with col4: | |
| st.markdown(""" | |
| <div class="card card-red role-card"> | |
| <div class="role-watermark">STUB</div> | |
| <span class="role-tag">FUTURE</span> | |
| <h3 style="font-size: 1.4rem; margin-bottom: 0.2rem;">Safari / Notes</h3> | |
| <div class="mono" style="color: var(--text-muted); font-size: 0.8rem; margin-bottom: 1rem;">extract_safari_stub()</div> | |
| <ul class="role-list"> | |
| <li>History / Downloads</li> | |
| <li>Rich Notes</li> | |
| <li>App Containers</li> | |
| </ul> | |
| </div> | |
| """, unsafe_allow_html=True) | |
| # Section: Pipeline | |
| def render_pipeline(): | |
| st.markdown(""" | |
| <div class="section-header"> | |
| <h2 class="section-title">Execution Pipeline</h2> | |
| <span class="section-desc">:: LOGIC :: The `run()` orchestration sequence.</span> | |
| </div> | |
| """, unsafe_allow_html=True) | |
| st.markdown(""" | |
| <div class="timeline-item done"> | |
| <div class="meta" style="margin-bottom: 0.5rem;">STEP 1</div> | |
| <h3 style="margin-bottom: 0.5rem;">Initialization</h3> | |
| <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="meta" style="margin-bottom: 0.5rem;">STEP 2</div> | |
| <h3 style="margin-bottom: 0.5rem;">Manifest Indexing</h3> | |
| <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="meta" style="margin-bottom: 0.5rem;">STEP 3</div> | |
| <h3 style="margin-bottom: 0.5rem;">Extraction & Export</h3> | |
| <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> | |
| """, unsafe_allow_html=True) | |
| # Section: Schema | |
| def render_schema(): | |
| st.markdown(""" | |
| <div class="section-header"> | |
| <h2 class="section-title">Data Structures</h2> | |
| <span class="section-desc">:: STRUCT :: Python Dataclasses used for typing.</span> | |
| </div> | |
| """, unsafe_allow_html=True) | |
| st.markdown(""" | |
| <div class="card card-cyan"> | |
| <div class="mono" style="font-size: 0.85rem; text-transform: uppercase; margin-bottom: 1rem;">Config & Metadata</div> | |
| <div class="code-block" style="max-height: 400px; overflow-y: auto;"> | |
| <span class="text-purple">@dataclass</span> | |
| <span class="text-gold">class</span> <span class="text-cyan">CaseMetadata</span>: | |
| case_name: <span class="text-cyan">str</span> = <span class="text-orange">"UNSPECIFIED_CASE"</span> | |
| examiner: <span class="text-cyan">str</span> = <span class="text-orange">"UNSPECIFIED_EXAMINER"</span> | |
| evidence_id: <span class="text-cyan">str</span> = <span class="text-orange">"UNSPECIFIED_EVIDENCE"</span> | |
| <span class="text-purple">@dataclass</span> | |
| <span class="text-gold">class</span> <span class="text-cyan">AppConfig</span>: | |
| backup_root: <span class="text-cyan">Path</span> | |
| manifest_db: <span class="text-cyan">Path</span> | |
| output_root: <span class="text-cyan">Path</span> | |
| query_terms: <span class="text-cyan">List</span>[<span class="text-cyan">str</span>] = field(default_factory=<span class="text-cyan">list</span>) | |
| verbose: <span class="text-cyan">bool</span> = <span class="text-gold">False</span> | |
| hash_exports: <span class="text-cyan">bool</span> = <span class="text-gold">True</span> | |
| copy_raw_files: <span class="text-cyan">bool</span> = <span class="text-gold">True</span> | |
| export_csv: <span class="text-cyan">bool</span> = <span class="text-gold">True</span> | |
| export_jsonl: <span class="text-cyan">bool</span> = <span class="text-gold">True</span> | |
| </div> | |
| </div> | |
| """, unsafe_allow_html=True) | |
| st.markdown("<div style='margin-top: 1.5rem;'></div>", unsafe_allow_html=True) | |
| st.markdown(""" | |
| <div class="card card-purple"> | |
| <div class="mono" style="font-size: 0.85rem; text-transform: uppercase; margin-bottom: 1rem;">Timeline Event Model</div> | |
| <div class="code-block" style="max-height: 300px; overflow-y: auto;"> | |
| <span class="text-purple">@dataclass</span> | |
| <span class="text-gold">class</span> <span class="text-cyan">TimelineEvent</span>: | |
| timestamp: <span class="text-cyan">Optional</span>[<span class="text-cyan">str</span>] | |
| artifact_type: <span class="text-cyan">str</span> | |
| source_file: <span class="text-cyan">str</span> | |
| summary: <span class="text-cyan">str</span> | |
| attributes: <span class="text-cyan">Dict</span>[<span class="text-cyan">str</span>, <span class="text-cyan">Any</span>] = field(default_factory=<span class="text-cyan">dict</span>) | |
| </div> | |
| </div> | |
| """, unsafe_allow_html=True) | |
| # Section: Source Code | |
| def render_source(): | |
| st.markdown(""" | |
| <div class="section-header"> | |
| <h2 class="section-title">Source Implementation</h2> | |
| <span class="section-desc">:: CODE :: The complete Python script logic.</span> | |
| </div> | |
| """, unsafe_allow_html=True) | |
| source_code = '''#!/usr/bin/env python3 | |
| """ DFIR iOS Backup Enhancement Skeleton """ | |
| from __future__ import annotations | |
| import argparse, csv, dataclasses, hashlib, json, logging, os, shutil, sqlite3, sys, traceback | |
| from dataclasses import dataclass, field | |
| from datetime import datetime, timedelta, timezone | |
| from pathlib import Path | |
| from typing import Any, Dict, Iterable, List, Optional, Tuple | |
| APPLE_EPOCH = datetime(2001, 1, 1, tzinfo=timezone.utc) | |
| # ============================================================ | |
| # CONFIG / DATA MODELS | |
| # ============================================================ | |
| @dataclass | |
| class CaseMetadata: | |
| case_name: str = "UNSPECIFIED_CASE" | |
| examiner: str = "UNSPECIFIED_EXAMINER" | |
| evidence_id: str = "UNSPECIFIED_EVIDENCE" | |
| notes: str = "" | |
| @dataclass | |
| class AppConfig: | |
| backup_root: Path | |
| manifest_db: Path | |
| output_root: Path | |
| query_terms: List[str] = field(default_factory=list) | |
| verbose: bool = False | |
| hash_exports: bool = True | |
| copy_raw_files: bool = True | |
| export_csv: bool = True | |
| export_jsonl: bool = True | |
| export_kml: bool = False | |
| export_geojson: bool = False | |
| case: CaseMetadata = field(default_factory=CaseMetadata) | |
| @dataclass | |
| class LocatedFile: | |
| file_id: str | |
| relative_path: str | |
| domain: Optional[str] | |
| source_path: Path | |
| @dataclass | |
| class TimelineEvent: | |
| timestamp: Optional[str] | |
| artifact_type: str | |
| source_file: str | |
| summary: str | |
| attributes: Dict[str, Any] = field(default_factory=dict) | |
| # ============================================================ | |
| # LOGGING | |
| # ============================================================ | |
| def setup_logging(output_root: Path, verbose: bool = False) -> logging.Logger: | |
| output_root.mkdir(parents=True, exist_ok=True) | |
| log_path = output_root / "dfir_run.log" | |
| logger = logging.getLogger("dfir_ios") | |
| logger.setLevel(logging.DEBUG if verbose else logging.INFO) | |
| logger.handlers.clear() | |
| fmt = logging.Formatter("%(asctime)s | %(levelname)s | %(message)s") | |
| fh = logging.FileHandler(log_path, encoding="utf-8") | |
| fh.setLevel(logging.DEBUG) | |
| fh.setFormatter(fmt) | |
| logger.addHandler(fh) | |
| sh = logging.StreamHandler(sys.stdout) | |
| sh.setLevel(logging.DEBUG if verbose else logging.INFO) | |
| sh.setFormatter(fmt) | |
| logger.addHandler(sh) | |
| logger.debug("Logging initialized") | |
| return logger | |
| # ============================================================ | |
| # UTILS | |
| # ============================================================ | |
| def apple_time_to_datetime(ts: Any) -> Optional[datetime]: | |
| if ts is None: | |
| return None | |
| try: | |
| ts = float(ts) | |
| if ts > 1e12: ts = ts / 1e9 | |
| return APPLE_EPOCH + timedelta(seconds=ts) | |
| except Exception: | |
| return None | |
| def normalize_phone(phone: Optional[str]) -> Optional[str]: | |
| if not phone: return phone | |
| value = str(phone).replace("+1", "").replace(" ", "").replace("-", "").replace("(", "").replace(")", "") | |
| return value.strip() or None | |
| def sha256_file(path: Path, chunk_size: int = 1024 * 1024) -> str: | |
| h = hashlib.sha256() | |
| with path.open("rb") as f: | |
| while True: | |
| chunk = f.read(chunk_size) | |
| if not chunk: break | |
| h.update(chunk) | |
| return h.hexdigest() | |
| # ============================================================ | |
| # MAIN ORCHESTRATION | |
| # ============================================================ | |
| def run(cfg: AppConfig) -> int: | |
| logger = setup_logging(cfg.output_root, cfg.verbose) | |
| logger.info("Starting DFIR iOS backup extraction") | |
| logger.info(f"Case: {cfg.case.case_name}") | |
| logger.info(f"Examiner: {cfg.case.examiner}") | |
| logger.info(f"Evidence ID: {cfg.case.evidence_id}") | |
| # Validate backup root | |
| if not cfg.backup_root.exists(): | |
| logger.error(f"Backup root does not exist: {cfg.backup_root}") | |
| return 1 | |
| # Load manifest database | |
| if not cfg.manifest_db.exists(): | |
| logger.error(f"Manifest database does not exist: {cfg.manifest_db}") | |
| return 1 | |
| logger.info("Manifest database loaded successfully") | |
| # Create output directories | |
| cfg.output_root.mkdir(parents=True, exist_ok=True) | |
| (cfg.output_root / "exports").mkdir(exist_ok=True) | |
| (cfg.output_root / "raw").mkdir(exist_ok=True) | |
| (cfg.output_root / "timeline").mkdir(exist_ok=True) | |
| logger.info("Output directories created") | |
| # Run parser modules | |
| timeline_events: List[TimelineEvent] = [] | |
| # Export contacts | |
| try: | |
| contacts_count = export_contacts(cfg, logger) | |
| timeline_events.append(TimelineEvent( | |
| timestamp=datetime.now(timezone.utc).isoformat(), | |
| artifact_type="CONTACTS", | |
| source_file="AddressBook.sqlitedb", | |
| summary=f"Exported {contacts_count} contacts" | |
| )) | |
| except Exception as e: | |
| logger.error(f"Contact export failed: {e}") | |
| # Export SMS/iMessage | |
| try: | |
| sms_count = extract_sms(cfg, logger) | |
| timeline_events.append(TimelineEvent( | |
| timestamp=datetime.now(timezone.utc).isoformat(), | |
| artifact_type="MESSAGES", | |
| source_file="sms.db", | |
| summary=f"Exported {sms_count} messages" | |
| )) | |
| except Exception as e: | |
| logger.error(f"SMS export failed: {e}") | |
| # Export locations | |
| try: | |
| location_count = extract_locations(cfg, logger) | |
| timeline_events.append(TimelineEvent( | |
| timestamp=datetime.now(timezone.utc).isoformat(), | |
| artifact_type="LOCATIONS", | |
| source_file="LocationD/Cache.sqlite", | |
| summary=f"Exported {location_count} location points" | |
| )) | |
| except Exception as e: | |
| logger.error(f"Location export failed: {e}") | |
| # Generate timeline | |
| if cfg.export_csv: | |
| generate_timeline_csv(timeline_events, cfg.output_root / "timeline" / "unified_timeline.csv") | |
| logger.info("Timeline CSV generated") | |
| if cfg.export_jsonl: | |
| generate_timeline_jsonl(timeline_events, cfg.output_root / "timeline" / "unified_timeline.jsonl") | |
| logger.info("Timeline JSONL generated") | |
| logger.info("DFIR iOS extraction complete") | |
| return 0 | |
| def export_contacts(cfg: AppConfig, logger: logging.Logger) -> int: | |
| """Export contacts from AddressBook database""" | |
| logger.info("Exporting contacts...") | |
| # Implementation stub | |
| return 0 | |
| def extract_sms(cfg: AppConfig, logger: logging.Logger) -> int: | |
| """Extract SMS/iMessage data""" | |
| logger.info("Extracting SMS/iMessage data...") | |
| # Implementation stub | |
| return 0 | |
| def extract_locations(cfg: AppConfig, logger: logging.Logger) -> int: | |
| """Extract location data from LocationD""" | |
| logger.info("Extracting location data...") | |
| # Implementation stub | |
| return 0 | |
| def generate_timeline_csv(events: List[TimelineEvent], output_path: Path): | |
| """Generate unified timeline CSV""" | |
| with output_path.open("w", newline="", encoding="utf-8") as f: | |
| writer = csv.writer(f) | |
| writer.writerow(["timestamp", "artifact_type", "source_file", "summary", "attributes"]) | |
| for event in events: | |
| writer.writerow([ | |
| event.timestamp, | |
| event.artifact_type, | |
| event.source_file, | |
| event.summary, | |
| json.dumps(event.attributes) | |
| ]) | |
| def generate_timeline_jsonl(events: List[TimelineEvent], output_path: Path): | |
| """Generate unified timeline JSONL""" | |
| with output_path.open("w", encoding="utf-8") as f: | |
| for event in events: | |
| f.write(json.dumps(dataclasses.asdict(event)) + "\\n") | |
| if __name__ == "__main__": | |
| parser = argparse.ArgumentParser(description="DFIR iOS Backup Enhancement") | |
| parser.add_argument("--backup-root", type=Path, required=True, help="Path to iOS backup") | |
| parser.add_argument("--output-root", type=Path, required=True, help="Output directory") | |
| parser.add_argument("--case-name", type=str, default="UNSPECIFIED_CASE") | |
| parser.add_argument("--examiner", type=str, default="UNSPECIFIED_EXAMINER") | |
| parser.add_argument("--evidence-id", type=str, default="UNSPECIFIED_EVIDENCE") | |
| parser.add_argument("--verbose", "-v", action="store_true") | |
| args = parser.parse_args() | |
| manifest_db = args.backup_root / "Manifest.db" | |
| cfg = AppConfig( | |
| backup_root=args.backup_root, | |
| manifest_db=manifest_db, | |
| output_root=args.output_root, | |
| verbose=args.verbose, | |
| case=CaseMetadata( | |
| case_name=args.case_name, | |
| examiner=args.examiner, | |
| evidence_id=args.evidence_id | |
| ) | |
| ) | |
| sys.exit(run(cfg))''' | |
| st.markdown(""" | |
| <div class="card card-gold"> | |
| <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 1rem;"> | |
| <span class="mono" style="font-size: 0.85rem; text-transform: uppercase;">dfir_ios_enhancement.py</span> | |
| <span class="signal sig-done">READY</span> | |
| </div> | |
| </div> | |
| """, unsafe_allow_html=True) | |
| st.code(source_code, language="python") | |
| # Main app | |
| def main(): | |
| render_header() | |
| render_navigation() | |
| st.markdown("<div style='margin: 2rem 0;'></div>", unsafe_allow_html=True) | |
| if st.session_state.current_section == 'overview': | |
| render_overview() | |
| elif st.session_state.current_section == 'modules': | |
| render_modules() | |
| elif st.session_state.current_section == 'pipeline': | |
| render_pipeline() | |
| elif st.session_state.current_section == 'schema': | |
| render_schema() | |
| elif st.session_state.current_section == 'source': | |
| render_source() | |
| if __name__ == "__main__": | |
| main() |