Skip to main content

Chain of Custody

Maintaining forensic integrity through comprehensive audit trails.

Overview

Chain of custody is a critical component of forensic data collection. Every action in Traces is logged to create an immutable audit trail that can be used in legal proceedings.

Why Chain of Custody Matters

In legal and compliance contexts, data must be:

  1. Authentic - Proven to be what it claims to be
  2. Reliable - Collected through consistent, documented processes
  3. Complete - No gaps in the handling record
  4. Tamper-evident - Any modifications are detectable

Event Model

Every significant action creates a ChainOfCustodyEvent (table chain_of_custody_events, defined in packages/database/prisma/schema.prisma):

interface ChainOfCustodyEvent {
id: string;
collection_id: string;
event_type: ChainOfCustodyEventType; // see enum below
actor_id?: string; // FK -> users.id (nullable for system events)
actor_type: 'system' | 'analyst' | 'customer' | 'automated';
actor_ip?: string; // INET
on_behalf_of?: string; // FK -> customers.id (analyst acting for a customer)
event_data?: object; // JSONB
previous_state?: string; // collection status before the transition
new_state?: string; // collection status after the transition
timestamp: string; // ISO 8601
created_at: string;
}
Append-only, not hash-chained

This table is append-only — events are never updated or deleted. Tamper-evidence comes from this immutability plus the per-response forensic hashes recorded in api_logs and the per-file SHA-256 hashes in collected_files. Individual chain-of-custody events are not linked by a per-event hash / previous_hash blockchain; ordering is established by timestamp (and api_logs.chain_of_custody_sequence for API calls). State changes are captured via the previous_state / new_state columns rather than a cryptographic event chain.

Event Types

The ChainOfCustodyEventType enum (defined in schema.prisma) is the complete, authoritative set of event types:

Collection Lifecycle

EventTriggered When
collection_createdCustomer or analyst creates a collection
credentials_validatedCredentials validated before collection
collection_startedCollection processing begins (also used for queued / in_progress)
collection_pausedCollection temporarily paused
collection_resumedPaused collection resumed
collection_completedCollection finished (used for both completed and partial)
collection_cancelledCollection cancelled by customer or analyst
collection_archivedCollection archived
error_occurredCollection entered the error state

Data Collection

EventTriggered When
data_collectedData collected from an endpoint
endpoint_skippedAn endpoint was skipped (e.g. unmet path-parameter dependency)

Analyst Actions

EventTriggered When
analyst_assignedAnalyst assigned to collection
analyst_reviewedAnalyst reviewed collection data (also used for the reviewing transition)
analyst_signed_offAnalyst signed off on collection

Credential Events

EventTriggered When
credentials_validatedCredentials tested against the platform API
credentials_revokedCredentials revoked

Delivery Events

EventTriggered When
zip_createdZIP archive created
sas_url_generatedSigned download URL generated
customer_notifiedCustomer notified
collection_deliveredCollection delivered to customer
note

There is no collection_failed, analyst_rejected, endpoint_started/completed/failed, file_collected/hash_verified, credentials_created/accessed, or delivery_* event type. Use the enum values above. A rejection during review is recorded as the reviewing → error transition (error_occurred).

Tamper-Evidence

The integrity guarantees come from three layers, not from a per-event hash chain:

  1. Append-only eventschain_of_custody_events rows are never updated or deleted.
  2. Per-response forensic hashing — every API call is logged in api_logs with response_body_hash (and request_body_hash), an ordering chain_of_custody_sequence, and a dual-library validation flag (response_body_hash_validated).
  3. Per-file hashing — every collected file in collected_files carries a sha256_hash and sha256_hash_validated flag.

This ensures:

  • Events cannot be silently modified or deleted
  • Each collected artifact's integrity is independently verifiable via its SHA-256 hash
  • The sequence of API calls is preserved via chain_of_custody_sequence

Event Data Examples

Collection Created

{
"event_type": "collection_created",
"actor_type": "customer",
"event_data": {
"connector_id": "conn_slack001",
"connector_version": "1.2.0",
"scope_config": {
"date_range": {
"from": "2024-01-01T00:00:00Z",
"to": "2024-01-31T23:59:59Z"
}
},
"priority": "high",
"business_context": "Internal investigation"
}
}

Data Collected

{
"event_type": "data_collected",
"actor_type": "system",
"event_data": {
"filename": "channel_C0123ABC_messages.json",
"file_size_bytes": 24576,
"sha256_hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
"source_endpoint": "/api/conversations.history",
"source_params": {
"channel": "C0123ABC"
}
}
}

Analyst Sign-Off

{
"event_type": "analyst_signed_off",
"actor_type": "analyst",
"event_data": {
"signature_data": "data:image/png;base64,iVBORw0KGgo...",
"certification": "I certify that this collection is complete and accurate",
"files_reviewed": 1247,
"total_bytes_reviewed": 524288000,
"notes": "All data verified. No anomalies detected."
}
}

Querying the Chain

Get Collection History

const history = await prisma.chainOfCustodyEvent.findMany({
where: { collection_id: 'col_abc123' },
orderBy: { timestamp: 'asc' }
});

Verify Artifact Integrity

Integrity verification operates on the hashed artifacts (api_logs responses and collected_files), not on a per-event hash chain. Re-hash each stored artifact and compare against the recorded SHA-256:

async function verifyAllHashes(collectionId: string): Promise<boolean> {
const files = await prisma.collectedFile.findMany({
where: { collectionId, deletedAt: null }
});

for (const file of files) {
if (!file.sha256HashValidated) return false;
// (re-hash the stored bytes and compare against file.sha256Hash for a deep check)
}
return true;
}

Reporting

Chain of Custody Report

Generate a formal report for legal proceedings:

async function generateCustodyReport(collectionId: string) {
const collection = await getCollection(collectionId);
const events = await getChainOfCustodyEvents(collectionId);

return {
collection_id: collection.id,
customer: collection.customer.company_name,
platform: collection.connector.platform_name,
date_range: collection.scope_config.date_range,

timeline: events.map(e => ({
timestamp: e.timestamp,
action: e.event_type,
actor: e.actor_type,
details: summarizeEvent(e)
})),

integrity: {
total_events: events.length,
first_event: events[0].timestamp,
last_event: events[events.length - 1].timestamp
},

files: {
total_count: collection.totalFilesCollected,
total_size: collection.totalBytesCollected,
all_hashes_verified: await verifyAllHashes(collectionId)
},

sign_off: {
analyst: collection.assigned_analyst.full_name,
timestamp: collection.signed_off_at,
signature: getSignatureData(collectionId)
}
};
}

Best Practices

1. Never Modify Events

Events are immutable. If corrections are needed, create a new event referencing the original:

{
"event_type": "correction",
"event_data": {
"corrects_event_id": "evt_abc123",
"reason": "Typo in business context",
"correction": {
"field": "business_context",
"old_value": "Intenral investigation",
"new_value": "Internal investigation"
}
}
}

2. Include Context

Always capture relevant context:

await createEvent({
collection_id: collectionId,
event_type: 'analyst_reviewed',
actor_id: analystId,
actor_type: 'analyst',
event_data: {
review_duration_minutes: 45,
files_examined: 234,
notes: 'Spot-checked 10% of files for completeness'
},
ip_address: request.ip,
user_agent: request.headers['user-agent']
});

3. Verify on Access

Verify artifact-hash integrity before generating reports:

async function getCustodyReport(collectionId: string) {
const isValid = await verifyAllHashes(collectionId);

if (!isValid) {
throw new Error('Chain of custody integrity compromised');
}

return generateReport(collectionId);
}
Implementation

A chain-of-custody PDF is generated as part of delivery by the worker — see apps/worker/src/jobs/delivery/chain-of-custody-pdf.ts. The customer- and analyst-facing chain-of-custody views are at app/(protected)/analyst/collections/[id]/chain-of-custody/page.tsx and the api/collections/[id]/chain-of-custody/route.ts handler.

The chain of custody system is designed to meet requirements for:

  • Federal Rules of Evidence (FRE 901, 902)
  • eDiscovery requirements
  • GDPR audit trail requirements
  • SOC 2 compliance
  • ISO 27001 information security standards

Consult with legal counsel when using Traces data in legal proceedings.