Reference

Output format

Every collection produces a deterministic ZIP — the evidence container — with a fixed top-level layout. Designed to be re-hashable, tamper-evident, and easy to ingest.

Container layout

evidence-container.zip
├── manifest.json              # Collection metadata (source, host, plugins run)
├── chain-of-custody.json      # Append-only access audit log
├── hashes.json                # SHA-256 for every collected file
├── artifacts/                 # Original files, copied byte-for-byte
│   └── {category}/{plugin_id}/files/{user}/{filename}
├── records/                   # Parsed structured data, one JSONL per plugin
│   └── {plugin_id}/parsed.jsonl
└── logs/                      # Per-plugin run log
    └── {plugin_id}/collection.log

manifest.json

The single source of truth for what was collected, when, and from where. The structured source object captures the forensic context (OS, hardware, security state, temporal anchors) you need on day one of analysis:

{
  "macfor_version": "1.4.0",
  "collection_id": "01HZK5...",
  "started_at": "2026-05-03T11:30:42Z",
  "completed_at": "2026-05-03T11:34:18Z",
  "source": {
    "type": "live",
    "os":        { "version": "14.4.1", "codename": "Sonoma", "build": "23E224" },
    "hardware":  { "model": "Mac14,2", "model_name": "MacBook Air (M2, 2022)", "architecture": "arm64" },
    "security":  { "sip_enabled": true, "filevault_enabled": true, "gatekeeper_enabled": true },
    "network":   { "hostname": "host.local", "computer_name": "Analyst Mac" },
    "temporal":  { "timezone": "Europe/Stockholm", "boot_time": "2026-05-01T07:12:08Z" }
  },
  "plugins": [
    { "id": "shell.history", "version": "1.0.0", "artifacts": 4, "errors": 0 },
    { "id": "browser.safari", "version": "1.0.0", "artifacts": 5, "errors": 0 }
  ]
}

hashes.json & integrity

Every file under artifacts/is hashed with SHA-256 at the moment it's read from the source. The hash is recorded into hashes.json and re-verified when the container is sealed. To re-verify after the fact:

unzip -p evidence.zip hashes.json | jq -r '.files[] | "\(.sha256)  \(.path)"' \
  > /tmp/hashes.txt

# Extract & verify
unzip evidence.zip 'artifacts/*' -d /tmp/check/
(cd /tmp/check && shasum -a 256 -c /tmp/hashes.txt)

records/ — parsed JSONL

Plugins that parse a structured source emit one record per line into records/<plugin_id>/parsed.jsonl. Records are stable enough to feed straight into Splunk, OpenSearch, DuckDB, or macfor-analyze:

# All Safari history rows
unzip -p evidence.zip records/browser.safari/parsed.jsonl | \
  jq 'select(.artifact_id == "history") | {visited: .visit_time, url: .url}'

# Count Signal messages per conversation
unzip -p evidence.zip records/messaging.signal/parsed.jsonl | \
  jq -r 'select(.artifact_id == "signal_messages") | .conversation_id' | \
  sort | uniq -c | sort -rn

chain-of-custody.json

Every action that touched the container — create, write, seal, re-open — is logged with timestamp, user, host, and a SHA-256 of the in-progress manifest. The log is append-only; a tampered entry is detectable by re-hashing the chain.

logs/

Per-plugin log files capture every error, skipped path, and permission denial. If a path was unreadable, you'll find it here rather than in your terminal output.

Future: Parquet

A Parquet output mode is in development — see the roadmap. The JSONL format will continue to ship as the canonical record form.