EvidenceOps - Forensic Media Triage
What It Does
EvidenceOps provides forensic-grade handling of media files with complete chain of custody:
- Media Intake - Accept images, videos, audio, PDFs, and documents from any channel
- Immutable Storage - Store originals in append-only vault with cryptographic hashes
- Metadata Extraction - Extract EXIF, file properties, and media information without altering originals
- Derivative Generation - Create thumbnails, transcripts, previews in separate folders
- Chain of Custody - Maintain tamper-evident audit trail with hash chain
- Integrity Verification - Verify evidence hasn't been modified post-ingest
- Audit Trail - Complete JSONL audit log for compliance
What It NEVER Does
- NEVER modifies original evidence files after ingest
- NEVER stores secrets, API keys, or credentials in manifests or logs
- NEVER accepts unsanitized paths from user input
- NEVER executes untrusted code or downloads remote scripts
- NEVER exfiltrates data to external services without explicit configuration
- NEVER bypasses channel allowlists or pairing requirements
- NEVER stores real personal data in example files
Prerequisites
Before using this skill, ensure:
- Plugin
@openclaw/evidence-vaultis installed and initialized - Vault storage directory is configured with appropriate permissions
- Channel allowlist is configured for trusted sources only
- Retention policies comply with your legal requirements
Workflow
Step 1: Receive Media
When media is received via any channel:
User sends: [image/video/document]
Required Information:
- File content (from attachment)
- Original filename
- Source channel (whatsapp, telegram, email, etc.)
- Sender identifier
- Message ID (if available)
Step 2: Create or Select Case
IF user specifies existing caseId:
USE that caseId
ELSE IF user requests new case:
CREATE case with format: case-{YYYY}-{NNN}
EXAMPLE: case-2026-001
ELSE:
ASK user: "Should I create a new case or add to existing case [case-2026-XXX]?"
Case ID Format: case-{year}-{sequence}
- Must match pattern:
^case-[a-zA-Z0-9_-]+$ - Examples:
case-2026-001,case-incident-alpha,case-legal-2026-q1
Step 3: Stage Original (Read-Only)
Before ingest:
- Save received file to temporary staging area
- Calculate SHA-256 hash immediately
- Record file size and MIME type
- DO NOT modify the file
# Staging directory structure
/tmp/evidence-staging/
├── {caseId}/
│ └── {timestamp}-{filename}
Step 4: Extract Metadata
Extract metadata WITHOUT modifying original:
For Images:
- EXIF data (camera, GPS, timestamps)
- Dimensions
- Color profile
For Videos:
- Duration
- Codec information
- Resolution
For Audio:
- Duration
- Sample rate
- Codec
For PDFs:
- Page count
- Author (if embedded)
- Creation date
# Use Read tool or appropriate extraction commands
# NEVER write back to original file
Step 5: Generate Derivatives (Optional)
Create derivative artifacts in SEPARATE folder:
derivatives/
├── thumbnails/
│ └── {evidenceId}-thumb.jpg
├── transcripts/
│ └── {evidenceId}-transcript.txt
└── previews/
└── {evidenceId}-preview.pdf
Derivative Types:
thumbnail- Reduced resolution image/video previewtranscript- Speech-to-text for audio/videopreview- PDF or text representationocr- Extracted text from images
Step 6: Ingest to Vault
Call the evidence.ingest tool:
{
"filePath": "/path/to/staged/file",
"filename": "original-filename.jpg",
"caseId": "case-2026-001",
"channel": "whatsapp",
"sender": "user@example.com",
"messageId": "msg-abc123",
"retentionDays": 2555,
"metadata": {
"exif": { ... },
"extracted": { ... }
}
}
Response:
{
"success": true,
"evidenceId": "ev-abc123...",
"sha256": "a1b2c3...",
"vaultUrl": "file:///vault/cases/case-2026-001/originals/ev-abc123.jpg",
"timestamp": "2026-02-17T10:30:00.000Z"
}
Step 7: Update Manifest
The manifest is automatically updated by the ingest operation.
Manifest Location: {vault}/cases/{caseId}/manifest.json
Manifest Contents:
- Case metadata
- Evidence items with hashes
- Derivatives
- Chain of custody entries
- Retention policy
Step 8: Return Receipt
Provide user with evidence receipt:
📋 EVIDENCE RECEIPT
Case ID: case-2026-001
Evidence ID: ev-abc123...
File: original-filename.jpg
SHA-256: a1b2c3d4e5f6...
Size: 1.2 MB
Received: 2026-02-17 10:30:00 UTC
Vault: file:///vault/cases/case-2026-001/originals/ev-abc123.jpg
✅ Chain of custody established
✅ Original preserved immutably
✅ Audit trail active
Tool Reference
evidence.ingest
Ingest a file into the evidence vault.
Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
| filePath | string | Yes | Path to the staged file |
| filename | string | Yes | Original filename |
| caseId | string | Yes | Case identifier |
| channel | string | No | Source channel |
| sender | string | No | Sender identifier |
| messageId | string | No | Message ID from source |
| retentionDays | number | No | Retention period |
| metadata | object | No | Additional metadata |
Returns: { evidenceId, sha256, vaultUrl, timestamp }
evidence.verify
Verify evidence integrity.
Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
| evidenceId | string | Yes | Evidence to verify |
| caseId | string | No | Limit search to case |
Returns: { verified, details: { originalIntact, hashMatch, lastVerifiedAt } }
evidence.manifest
Get case manifest.
Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
| caseId | string | Yes | Case identifier |
Returns: Complete case manifest with all items
evidence.export
Export case as archive.
Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
| caseId | string | Yes | Case identifier |
| format | string | No | 'zip' or 'tar' (default: zip) |
Returns: { exportPath, sha256, size, itemCount }
evidence.access_log
Get audit trail.
Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
| caseId | string | Yes | Case identifier |
| limit | number | No | Max events (default: 100) |
Returns: { events: [ ... ], count }
SECURITY POSTURE
Channel Allowlist Configuration
Configure which channels can submit evidence:
# ~/.openclaw/openclaw.json
{
"plugins": {
"evidence-vault": {
"channelAllowlist": ["whatsapp", "telegram", "email"],
"channelDenylist": ["public-discord"],
"requirePairing": true
}
}
}
Pairing Requirements
For Direct Messages (DMs):
- REQUIRE pairing before accepting evidence
- BLOCK unpaired DMs by default
- LOG rejected ingestion attempts
For Group Channels:
- VERIFY channel is in allowlist
- REJECT evidence from untrusted channels
- NEVER auto-ingest from public channels
Path Sanitization Rules
ALL paths are validated:
- No path traversal -
../sequences rejected - No null bytes -
\0rejected - No home directory -
~/rejected - Canonicalization - Paths resolved before validation
- Base path check - Must be within vault directory
Violations result in: E_PATH_TRAVERSAL error + audit log entry
Destructive Action Confirmation
Before any potentially destructive operation:
- EXPORT - Confirm export destination
- RETENTION DELETE - Require explicit confirmation (if enabled)
- LEGAL HOLD RELEASE - Require explicit confirmation
Confirmation format:
⚠️ DESTRUCTIVE ACTION
You are about to: [describe action]
Case: [caseId]
Items affected: [count]
Type "CONFIRM" to proceed:
Secrets Handling
NEVER include in logs or manifests:
- API keys
- Passwords
- Tokens
- Credit card numbers
- Email addresses (redacted)
- Phone numbers (redacted)
- SSN (redacted)
Redaction is automatic via regex patterns.
Troubleshooting
Error: E_PATH_TRAVERSAL
Cause: Filename or path contains disallowed characters
Solution:
- Filenames are automatically sanitized
- If error persists, check for unusual characters
- Original filename is preserved in manifest metadata
Error: E_INVALID_MIME
Cause: File type not in allowed list
Solution:
- Check
allowedMimeTypesconfiguration - Add MIME type to allowlist if appropriate
- Verify file is not corrupted
Error: E_SIZE_LIMIT
Cause: File exceeds maximum size
Solution:
- Check
maxFileSizeBytesconfiguration - Split large files if appropriate
- Compress before ingest (note in metadata)
Error: E_CHANNEL_BLOCKED
Cause: Evidence from blocked or non-allowlisted channel
Solution:
- Verify channel in allowlist
- Check if pairing is required
- Review security configuration
Error: E_HASH_MISMATCH
Cause: Evidence file modified after ingest
Solution:
- This indicates potential tampering
- Review access logs for case
- Escalate per incident response procedures
Verification Fails
Symptoms: verified: false in verify response
Diagnostic steps:
- Check if original file exists
- Compare current hash with manifest hash
- Review audit log for modifications
- Check filesystem permissions
Configuration Example
# openclaw.yaml
plugins:
evidence-vault:
driver: filesystem # or s3
basePath: /var/evidence-vault
maxFileSizeBytes: 524288000 # 500MB
defaultRetentionDays: 2555 # 7 years
allowedMimeTypes:
- image/jpeg
- image/png
- video/mp4
- audio/mpeg
- application/pdf
channelAllowlist:
- whatsapp
- telegram
- slack
requirePairing: true
# S3 driver options (if driver: s3)
s3:
endpoint: https://s3.example.com
bucket: evidence-vault
region: us-east-1
objectLock: true
Legal and Compliance Notes
LGPD/GDPR Considerations
- Tag sensitive data with
sensitivityTag - Configure appropriate retention periods
- Never store real personal data in examples
- Implement data subject access request procedures
Retention Policies
| Sensitivity | Default Retention |
|---|---|
| public | 365 days |
| internal | 1825 days (5 years) |
| confidential | 2555 days (7 years) |
| restricted | Legal hold |
Audit Requirements
All operations logged to JSONL audit file:
- Case creation/deletion
- Evidence ingest
- Verification attempts
- Export operations
- Access requests
Audit log retention: Same as case retention