Skip to main content

D.4.3: SOC 2 Evidence Collection Automation

Document ID: CODITECT-BIO-SOC2-AUTO-001 Version: 1.0.0 Effective Date: 2026-02-16 Classification: Internal - Restricted Owner: Chief Compliance Officer / VP Quality Assurance


Document Control

Approval History

RoleNameSignatureDate
Chief Compliance Officer[Pending][Digital Signature]YYYY-MM-DD
Chief Information Security Officer[Pending][Digital Signature]YYYY-MM-DD
VP Quality Assurance[Pending][Digital Signature]YYYY-MM-DD
VP Engineering[Pending][Digital Signature]YYYY-MM-DD
VP IT Operations[Pending][Digital Signature]YYYY-MM-DD

Revision History

VersionDateAuthorChangesApproval Status
1.0.02026-02-16Compliance TeamInitial releaseDraft

Distribution List

  • Executive Leadership Team
  • Audit Committee
  • Compliance Team
  • Information Security Team
  • Quality Assurance Team
  • IT Operations Team
  • External Auditors (read-only access)

Review Schedule

Review TypeFrequencyNext Review DateResponsible Party
Annual Review12 months2027-02-16Chief Compliance Officer
Quarterly Evidence Review3 months2026-05-16Compliance Team
Post-Audit ReviewAs neededN/AExternal Auditor + CCO
Automation Health CheckMonthly2026-03-16IT Operations

1. Purpose and Scope

1.1 Purpose

This document establishes the automated evidence collection framework for the CODITECT Biosciences Quality Management System (BIO-QMS) platform to ensure:

  1. SOC 2 Type I/II Readiness - Continuous collection of control evidence across all five Trust Service Criteria (Security, Availability, Processing Integrity, Confidentiality, Privacy)
  2. Audit Efficiency - 15-minute retrieval guarantee for any control evidence during SOC 2 audits
  3. Evidence Integrity - Tamper-evident storage with cryptographic verification and immutable audit trails
  4. Continuous Compliance - Real-time control effectiveness monitoring with automated alerting for gaps
  5. Cross-Framework Optimization - Unified evidence collection supporting FDA 21 CFR Part 11, HIPAA, and SOC 2 simultaneously

1.2 Scope

This automation framework applies to:

In Scope:

  • All SOC 2 Trust Service Criteria controls (CC1-CC9, A, PI, C, P categories)
  • Evidence collection for access controls, change management, encryption, monitoring, availability
  • Automated evidence generation: logs, snapshots, reports, test results, policy attestations
  • Evidence repository architecture with tamper-evident storage (GCS + hash chains)
  • Evidence metadata schema and traceability
  • Auditor API for self-service evidence access
  • Quarterly assessment packages and annual Type II preparation
  • Cross-framework evidence mapping (SOC 2 ↔ HIPAA ↔ Part 11)

Out of Scope:

  • Policy document authoring (manual responsibility)
  • Employee training completion tracking (handled by HR system)
  • Physical security evidence (datacenter access logs from GCP)
  • Third-party vendor assessments (manual procurement process)
  • Penetration test execution (separate engagement)

1.3 Regulatory Context

This framework ensures compliance with:

  • AICPA TSC 2017 - Trust Service Criteria for Security, Availability, Processing Integrity, Confidentiality, and Privacy
  • SOC 2 Type I - Control design effectiveness at a point in time
  • SOC 2 Type II - Operating effectiveness over a 6-12 month examination period
  • FDA 21 CFR Part 11 - Electronic records and signatures (evidence overlap)
  • HIPAA Security Rule - Technical safeguards (evidence overlap)

2. Evidence Collection Architecture

2.1 System Overview

┌─────────────────────────────────────────────────────────────────────┐
│ SOC 2 Evidence Collection System │
└─────────────────────────────────────────────────────────────────────┘

┌───────────────────────────┼───────────────────────────┐
│ │ │
▼ ▼ ▼
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ Collectors │ │ Validators │ │ Storage │
│ (Scheduled) │─────────▶│ (Real-time) │─────────▶│ (GCS) │
└──────────────┘ └──────────────┘ └──────────────┘
│ │ │
│ │ │
▼ ▼ ▼
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ Evidence DB │ │ Alert System │ │ Auditor API │
│ (Metadata) │ │ (Slack/PD) │ │ (Read-only) │
└──────────────┘ └──────────────┘ └──────────────┘

2.2 Collection Architecture Components

2.2.1 Evidence Collectors (NestJS Scheduled Tasks)

Location: apps/api/src/evidence-collection/collectors/

CollectorFrequencyControl CategoriesOutput Format
AccessControlCollectorDailyCC6.1, CC6.2, CC6.3, CC6.6JSON snapshots
ChangeManagementCollectorPer deploymentCC8.1Deployment manifest + approvals
EncryptionCollectorDailyCC6.7TLS cert status + key metadata
MonitoringCollectorHourlyCC7.1, CC7.2, CC7.3Alert logs + incident records
AvailabilityCollectorHourlyA1.1, A1.2, A1.3Uptime metrics + SLO adherence
BackupCollectorDailyA1.2Backup completion + restore test results
IncidentResponseCollectorReal-timeCC7.3, CC7.4Incident timeline + remediation
VulnerabilityCollectorDailyCC7.1CVE scan results + patch status
DataRetentionCollectorWeeklyC1.1, P4.2Retention policy adherence
PrivacyControlCollectorMonthlyP1.1, P2.1, P3.1, P4.1Consent records + data subject rights

Implementation Pattern:

import { Injectable, Logger } from '@nestjs/common';
import { Cron, CronExpression } from '@nestjs/schedule';
import { EvidenceService } from '../services/evidence.service';
import { EvidenceMetadata, EvidenceType, ControlCategory } from '../types';

@Injectable()
export class AccessControlCollector {
private readonly logger = new Logger(AccessControlCollector.name);

constructor(private readonly evidenceService: EvidenceService) {}

@Cron(CronExpression.EVERY_DAY_AT_2AM)
async collectAccessControls(): Promise<void> {
this.logger.log('Starting daily access control evidence collection');

try {
const evidenceId = await this.evidenceService.generateEvidenceId(
ControlCategory.CC6_ACCESS_CONTROLS
);

// 1. Collect user permission snapshots
const userPermissions = await this.collectUserPermissions();

// 2. Collect role assignments
const roleAssignments = await this.collectRoleAssignments();

// 3. Collect access reviews (last 30 days)
const accessReviews = await this.collectAccessReviews();

// 4. Collect privileged access usage
const privilegedAccess = await this.collectPrivilegedAccess();

const evidence = {
evidenceId,
controlCategory: ControlCategory.CC6_ACCESS_CONTROLS,
controlReferences: ['CC6.1', 'CC6.2', 'CC6.3', 'CC6.6'],
collectionTimestamp: new Date(),
collector: 'AccessControlCollector',
data: {
userPermissions,
roleAssignments,
accessReviews,
privilegedAccess,
},
};

// 5. Store evidence (generates hash, uploads to GCS, saves metadata)
await this.evidenceService.storeEvidence(evidence);

this.logger.log(`Access control evidence collected: ${evidenceId}`);
} catch (error) {
this.logger.error('Failed to collect access control evidence', error);
await this.evidenceService.alertCollectionFailure(
'AccessControlCollector',
error
);
throw error;
}
}

private async collectUserPermissions(): Promise<UserPermissionSnapshot[]> {
// Query auth database for all users and their effective permissions
// Include: userId, email, roles[], permissions[], tenantId, lastLoginAt
// Exclude: password hashes, sessions, tokens
return [];
}

private async collectRoleAssignments(): Promise<RoleAssignment[]> {
// Query for all role-to-user mappings with assignment date and assignor
return [];
}

private async collectAccessReviews(): Promise<AccessReview[]> {
// Query access_reviews table for last 30 days
// Include: reviewer, reviewedUserId, decision, reviewedAt, notes
return [];
}

private async collectPrivilegedAccess(): Promise<PrivilegedAccessLog[]> {
// Query audit logs for admin/superuser actions in last 24 hours
// Include: userId, action, targetResource, timestamp, justification
return [];
}
}

2.2.2 Evidence Validation (Pre-Storage)

Location: apps/api/src/evidence-collection/validators/

Every evidence package is validated before storage:

import { Injectable, Logger } from '@nestjs/common';
import { z } from 'zod';

@Injectable()
export class EvidenceValidator {
private readonly logger = new Logger(EvidenceValidator.name);

// Evidence metadata schema
private readonly metadataSchema = z.object({
evidenceId: z.string().uuid(),
controlCategory: z.nativeEnum(ControlCategory),
controlReferences: z.array(z.string()).min(1),
collectionTimestamp: z.date(),
collector: z.string(),
data: z.record(z.unknown()),
});

validate(evidence: unknown): EvidenceValidationResult {
try {
// 1. Schema validation
const validated = this.metadataSchema.parse(evidence);

// 2. Control reference validation (must exist in TSC mapping)
const invalidControls = validated.controlReferences.filter(
(ref) => !this.isValidControlReference(ref)
);
if (invalidControls.length > 0) {
return {
valid: false,
errors: [`Invalid control references: ${invalidControls.join(', ')}`],
};
}

// 3. Data completeness validation (no empty objects/arrays)
if (Object.keys(validated.data).length === 0) {
return {
valid: false,
errors: ['Evidence data cannot be empty'],
};
}

// 4. PII/PHI redaction verification (if applicable)
const piiViolations = this.detectUnredactedPII(validated.data);
if (piiViolations.length > 0) {
return {
valid: false,
errors: [`Unredacted PII detected: ${piiViolations.join(', ')}`],
};
}

return { valid: true };
} catch (error) {
this.logger.error('Evidence validation failed', error);
return {
valid: false,
errors: [error.message],
};
}
}

private isValidControlReference(ref: string): boolean {
// Validate against TSC control catalog
const validPrefixes = ['CC', 'A', 'PI', 'C', 'P'];
return validPrefixes.some((prefix) => ref.startsWith(prefix));
}

private detectUnredactedPII(data: unknown): string[] {
// Scan for patterns indicating unredacted PII/PHI
// SSN: \d{3}-\d{2}-\d{4}
// Email: in non-redacted fields
// Phone: \d{3}-\d{3}-\d{4}
// Names: in prohibited fields
return [];
}
}

2.2.3 Evidence Storage Service

Location: apps/api/src/evidence-collection/services/evidence.service.ts

import { Injectable, Logger } from '@nestjs/common';
import { Storage } from '@google-cloud/storage';
import { createHash } from 'crypto';
import { PrismaService } from '@/prisma/prisma.service';

@Injectable()
export class EvidenceService {
private readonly logger = new Logger(EvidenceService.name);
private readonly storage = new Storage();
private readonly bucketName = 'coditect-bio-qms-evidence';

constructor(private readonly prisma: PrismaService) {}

async storeEvidence(evidence: EvidencePackage): Promise<EvidenceMetadata> {
const startTime = Date.now();

try {
// 1. Validate evidence
const validation = await this.validateEvidence(evidence);
if (!validation.valid) {
throw new Error(`Evidence validation failed: ${validation.errors.join(', ')}`);
}

// 2. Generate SHA-256 hash of evidence data
const evidenceJson = JSON.stringify(evidence.data);
const hash = createHash('sha256').update(evidenceJson).digest('hex');

// 3. Get previous evidence hash for chain
const previousHash = await this.getLatestEvidenceHash(
evidence.controlCategory
);

// 4. Upload to GCS with versioning
const fileName = `${evidence.controlCategory}/${evidence.evidenceId}.json`;
const file = this.storage.bucket(this.bucketName).file(fileName);

await file.save(evidenceJson, {
contentType: 'application/json',
metadata: {
evidenceId: evidence.evidenceId,
controlCategory: evidence.controlCategory,
collectionTimestamp: evidence.collectionTimestamp.toISOString(),
hash,
previousHash: previousHash || 'genesis',
},
});

// 5. Save metadata to database
const metadata = await this.prisma.evidenceMetadata.create({
data: {
evidenceId: evidence.evidenceId,
controlCategory: evidence.controlCategory,
controlReferences: evidence.controlReferences,
collectionTimestamp: evidence.collectionTimestamp,
collector: evidence.collector,
collectorType: 'automated',
hash,
previousHash: previousHash || 'genesis',
storageLocation: `gs://${this.bucketName}/${fileName}`,
reviewStatus: 'pending',
reviewedBy: null,
reviewedAt: null,
linkedAuditFinding: null,
},
});

const duration = Date.now() - startTime;
this.logger.log(
`Evidence stored: ${evidence.evidenceId} (${duration}ms, ${(evidenceJson.length / 1024).toFixed(2)}KB)`
);

// 6. Update evidence freshness tracking
await this.updateEvidenceFreshness(evidence.controlReferences);

return metadata;
} catch (error) {
this.logger.error('Failed to store evidence', error);
throw error;
}
}

async getLatestEvidenceHash(controlCategory: ControlCategory): Promise<string | null> {
const latest = await this.prisma.evidenceMetadata.findFirst({
where: { controlCategory },
orderBy: { collectionTimestamp: 'desc' },
select: { hash: true },
});
return latest?.hash || null;
}

async verifyEvidenceIntegrity(evidenceId: string): Promise<IntegrityCheckResult> {
// 1. Fetch metadata from database
const metadata = await this.prisma.evidenceMetadata.findUnique({
where: { evidenceId },
});
if (!metadata) {
return { valid: false, error: 'Evidence not found' };
}

// 2. Download evidence from GCS
const file = this.storage.bucket(this.bucketName).file(
metadata.storageLocation.replace(`gs://${this.bucketName}/`, '')
);
const [contents] = await file.download();

// 3. Recompute hash
const computedHash = createHash('sha256').update(contents).digest('hex');

// 4. Compare hashes
if (computedHash !== metadata.hash) {
this.logger.error(
`Evidence integrity violation: ${evidenceId} (expected: ${metadata.hash}, got: ${computedHash})`
);
return { valid: false, error: 'Hash mismatch - evidence may be tampered' };
}

// 5. Verify hash chain
if (metadata.previousHash !== 'genesis') {
const previousEvidence = await this.prisma.evidenceMetadata.findFirst({
where: {
controlCategory: metadata.controlCategory,
hash: metadata.previousHash,
},
});
if (!previousEvidence) {
return {
valid: false,
error: 'Hash chain broken - previous evidence not found',
};
}
}

return { valid: true };
}

private async updateEvidenceFreshness(controlReferences: string[]): Promise<void> {
// Update evidence_freshness table for compliance dashboard
const now = new Date();
for (const controlRef of controlReferences) {
await this.prisma.evidenceFreshness.upsert({
where: { controlReference: controlRef },
update: { lastCollectedAt: now, collectionCount: { increment: 1 } },
create: {
controlReference: controlRef,
lastCollectedAt: now,
collectionCount: 1,
},
});
}
}
}

2.3 Evidence Repository Architecture

2.3.1 Google Cloud Storage Configuration

Bucket: coditect-bio-qms-evidence

Configuration:

# GCS Bucket Configuration (Terraform)
resource "google_storage_bucket" "evidence_bucket" {
name = "coditect-bio-qms-evidence"
location = "US"
storage_class = "STANDARD"

versioning {
enabled = true # All object versions retained
}

lifecycle_rule {
action {
type = "SetStorageClass"
storage_class = "NEARLINE"
}
condition {
age = 90 # Move to Nearline after 90 days
}
}

lifecycle_rule {
action {
type = "SetStorageClass"
storage_class = "COLDLINE"
}
condition {
age = 365 # Move to Coldline after 1 year
}
}

lifecycle_rule {
action {
type = "SetStorageClass"
storage_class = "ARCHIVE"
}
condition {
age = 1825 # Move to Archive after 5 years
}
}

retention_policy {
retention_period = 220752000 # 7 years in seconds
is_locked = true # Prevent deletion before retention
}

uniform_bucket_level_access {
enabled = true
}
}

# IAM for evidence upload (API service account)
resource "google_storage_bucket_iam_member" "evidence_writer" {
bucket = google_storage_bucket.evidence_bucket.name
role = "roles/storage.objectCreator"
member = "serviceAccount:bio-qms-api@coditect-bio-qms.iam.gserviceaccount.com"
}

# IAM for auditor read-only access
resource "google_storage_bucket_iam_member" "auditor_reader" {
bucket = google_storage_bucket.evidence_bucket.name
role = "roles/storage.objectViewer"
member = "group:auditors@coditect.com"
}

Directory Structure:

coditect-bio-qms-evidence/
├── CC6-access-controls/
│ ├── {evidence-id-1}.json
│ ├── {evidence-id-2}.json
│ └── ...
├── CC7-monitoring/
├── CC8-change-management/
├── A-availability/
├── PI-processing-integrity/
├── C-confidentiality/
├── P-privacy/
├── quarterly-assessments/
│ ├── 2026-Q1/
│ │ ├── assessment-report.pdf
│ │ ├── control-testing-results.xlsx
│ │ └── evidence-index.json
│ └── 2026-Q2/
└── annual-packages/
└── 2026/
├── type-ii-package.pdf
├── evidence-manifest.xlsx
└── auditor-workpapers/

2.3.2 Evidence Metadata Database Schema

Location: apps/api/prisma/schema.prisma

// Evidence metadata table
model EvidenceMetadata {
id String @id @default(uuid())
evidenceId String @unique @db.Uuid
controlCategory String // CC6, CC7, CC8, A, PI, C, P
controlReferences String[] // ["CC6.1", "CC6.2", "CC6.3"]
collectionTimestamp DateTime @default(now())
collector String // Class name of collector
collectorType String // 'automated' | 'manual'
hash String @db.Char(64) // SHA-256 hash
previousHash String @db.Char(64) // Hash chain
storageLocation String // GCS URI

// Review tracking
reviewStatus String @default("pending") // pending | approved | rejected
reviewedBy String? // userId
reviewedAt DateTime?
reviewNotes String? @db.Text

// Audit linkage
linkedAuditFinding String? @db.Uuid

createdAt DateTime @default(now())
updatedAt DateTime @updatedAt

@@index([controlCategory, collectionTimestamp])
@@index([reviewStatus])
@@map("evidence_metadata")
}

// Evidence freshness tracking (for dashboard)
model EvidenceFreshness {
id String @id @default(uuid())
controlReference String @unique // CC6.1, CC7.2, etc.
lastCollectedAt DateTime
collectionCount Int @default(0)
expectedFrequency String // daily, weekly, monthly, quarterly

// Staleness detection
isStale Boolean @default(false)
staleThresholdHours Int @default(48)

createdAt DateTime @default(now())
updatedAt DateTime @updatedAt

@@map("evidence_freshness")
}

// Evidence collection failures (for alerting)
model EvidenceCollectionFailure {
id String @id @default(uuid())
collector String
failureTime DateTime @default(now())
errorMessage String @db.Text
errorStack String? @db.Text

// Retry tracking
retryCount Int @default(0)
maxRetries Int @default(3)
retryAt DateTime?
resolved Boolean @default(false)
resolvedAt DateTime?

createdAt DateTime @default(now())

@@index([collector, failureTime])
@@index([resolved])
@@map("evidence_collection_failures")
}

// Quarterly assessment tracking
model QuarterlyAssessment {
id String @id @default(uuid())
quarter String // "2026-Q1"
startDate DateTime
endDate DateTime

// Assessment status
status String @default("in_progress") // in_progress | completed | submitted
assessor String // userId
completedAt DateTime?

// Evidence summary
totalControls Int
controlsTested Int
controlsPassed Int
controlsFailed Int

// Report location
reportLocation String? // GCS URI
evidencePackageUri String? // GCS URI

createdAt DateTime @default(now())
updatedAt DateTime @updatedAt

@@unique([quarter])
@@map("quarterly_assessments")
}

3. Evidence Collection by Control Category

3.1 Common Criteria (CC) - Foundational Controls

3.1.1 CC6: Logical and Physical Access Controls

Evidence Collection:

ControlEvidence TypeCollection FrequencyCollectorOutput
CC6.1 - Restricts logical accessUser permission snapshotsDailyAccessControlCollectorusers-{date}.json
CC6.2 - New access provisioned/removedRole assignment audit logReal-timeAuditTrailCollectorrole-assignments-{date}.json
CC6.3 - Authenticates usersSSO authentication logsHourlyAuthenticationCollectorauth-logs-{timestamp}.json
CC6.6 - Restricts access to information assetsTenant isolation verificationDailyDataAccessCollectortenant-isolation-{date}.json
CC6.7 - Transmits data securelyTLS certificate statusDailyEncryptionCollectortls-status-{date}.json

CC6.1 Evidence Structure:

interface UserPermissionSnapshot {
snapshotDate: string; // ISO 8601
totalUsers: number;
activeUsers: number;
inactiveUsers: number;
users: Array<{
userId: string;
email: string; // redacted domain for privacy
roles: string[];
permissions: string[];
tenantId: string;
accountStatus: 'active' | 'inactive' | 'suspended';
lastLoginAt: string | null;
createdAt: string;
lastPermissionChangeAt: string;
lastPermissionChangeBy: string;
}>;
roleDistribution: Record<string, number>; // { "admin": 5, "user": 150 }
privilegedAccounts: string[]; // userId[] with admin/superuser
}

CC6.2 Evidence Structure:

interface RoleAssignmentAudit {
auditPeriod: { start: string; end: string };
totalAssignments: number;
totalRemovals: number;
assignments: Array<{
userId: string;
role: string;
assignedBy: string;
assignedAt: string;
approvalTicket: string | null; // Jira/ServiceNow ticket
justification: string;
}>;
removals: Array<{
userId: string;
role: string;
removedBy: string;
removedAt: string;
reason: string;
}>;
}

CC6.3 Evidence Structure:

interface AuthenticationLog {
logPeriod: { start: string; end: string };
totalAttempts: number;
successfulLogins: number;
failedLogins: number;
mfaEnforced: boolean;
mfaAdoptionRate: number; // percentage
events: Array<{
timestamp: string;
userId: string;
ipAddress: string; // redacted last octet
userAgent: string;
authMethod: 'sso' | 'password' | 'api_key' | 'service_account';
mfaUsed: boolean;
result: 'success' | 'failed' | 'locked_out';
failureReason?: string;
}>;
anomalies: Array<{
userId: string;
anomalyType: 'impossible_travel' | 'unusual_hour' | 'new_device' | 'brute_force';
detectedAt: string;
resolved: boolean;
}>;
}

3.1.2 CC7: System Operations and Monitoring

Evidence Collection:

ControlEvidence TypeCollection FrequencyCollectorOutput
CC7.1 - Detects failuresCircuit breaker eventsReal-timeMonitoringCollectorcircuit-breaker-{timestamp}.json
CC7.2 - Monitors system componentsInfrastructure metricsHourlyObservabilityCollectormetrics-{timestamp}.json
CC7.3 - Evaluates security eventsSecurity incident logReal-timeSecurityCollectorsecurity-incidents-{date}.json
CC7.4 - Responds to vulnerabilitiesVulnerability scan resultsDailyVulnerabilityCollectorvuln-scan-{date}.json
CC7.5 - Identifies anomaliesML anomaly detection alertsHourlyAnomalyDetectionCollectoranomalies-{timestamp}.json

CC7.2 Evidence Structure:

interface SystemMetrics {
collectionPeriod: { start: string; end: string };
infrastructure: {
kubernetesCluster: {
clusterName: string;
nodeCount: number;
totalCPU: number;
totalMemory: number;
avgCPUUtilization: number;
avgMemoryUtilization: number;
podCount: number;
failedPods: number;
};
databases: Array<{
instanceName: string;
type: 'postgres' | 'redis';
status: 'healthy' | 'degraded' | 'down';
connections: number;
queryLatencyP95: number; // milliseconds
cpuUtilization: number;
memoryUtilization: number;
}>;
};
applications: Array<{
serviceName: string;
version: string;
replicas: number;
requestRate: number; // req/sec
errorRate: number; // percentage
latencyP50: number;
latencyP95: number;
latencyP99: number;
uptime: number; // percentage
}>;
alerts: Array<{
alertName: string;
severity: 'critical' | 'warning' | 'info';
triggeredAt: string;
resolvedAt: string | null;
affectedService: string;
responseTime: number | null; // minutes
}>;
}

CC7.3 Evidence Structure:

interface SecurityIncidentLog {
reportingPeriod: { start: string; end: string };
totalIncidents: number;
criticalIncidents: number;
resolvedIncidents: number;
incidents: Array<{
incidentId: string;
severity: 'critical' | 'high' | 'medium' | 'low';
category: 'unauthorized_access' | 'malware' | 'data_breach' | 'ddos' | 'phishing' | 'misconfiguration';
detectedAt: string;
detectionMethod: 'automated' | 'manual' | 'third_party';
affectedSystems: string[];
affectedUsers: number;
containedAt: string | null;
resolvedAt: string | null;
rootCause: string;
remediationActions: string[];
escalatedToManagement: boolean;
regulatoryReportingRequired: boolean;
}>;
meanTimeToDetect: number; // minutes
meanTimeToContain: number; // minutes
meanTimeToResolve: number; // minutes
}

CC7.4 Evidence Structure:

interface VulnerabilityScanResult {
scanDate: string;
scanType: 'container' | 'dependency' | 'infrastructure' | 'code';
scanner: string; // e.g., "Trivy", "Snyk", "OWASP Dependency Check"
totalVulnerabilities: number;
critical: number;
high: number;
medium: number;
low: number;
vulnerabilities: Array<{
cveId: string;
severity: 'critical' | 'high' | 'medium' | 'low';
affectedComponent: string;
installedVersion: string;
fixedVersion: string | null;
publishedDate: string;
detectedDate: string;
status: 'open' | 'remediated' | 'accepted_risk' | 'false_positive';
remediatedAt: string | null;
remediationTicket: string | null;
riskAcceptanceApprover: string | null;
}>;
patchingCompliance: {
criticalWithin24h: number; // percentage
highWithin7days: number; // percentage
mediumWithin30days: number; // percentage
};
}

3.1.3 CC8: Change Management

Evidence Collection:

ControlEvidence TypeCollection FrequencyCollectorOutput
CC8.1 - Authorizes changesDeployment approval recordsPer deploymentChangeManagementCollectordeployment-{id}.json
CC8.2 - Tests changesCI/CD test resultsPer buildTestResultsCollectortest-results-{build-id}.json

CC8.1 Evidence Structure:

interface DeploymentApprovalRecord {
deploymentId: string;
deploymentDate: string;
environment: 'staging' | 'production';
changeType: 'feature' | 'bugfix' | 'security_patch' | 'infrastructure';

// Change request
changeRequestTicket: string; // Jira/ServiceNow
requestedBy: string;
requestedAt: string;
changeDescription: string;
impactAssessment: string;
rollbackPlan: string;

// Approval workflow
approvals: Array<{
approver: string;
role: 'engineering_lead' | 'security' | 'qa' | 'operations';
approvedAt: string;
comments: string | null;
}>;

// Testing evidence
testingCompleted: boolean;
testSuiteResults: {
unit: { passed: number; failed: number; skipped: number };
integration: { passed: number; failed: number; skipped: number };
e2e: { passed: number; failed: number; skipped: number };
};

// Deployment execution
deployedBy: string;
deploymentMethod: 'ci_cd' | 'manual';
deploymentStatus: 'success' | 'failed' | 'rolled_back';
deploymentDuration: number; // seconds

// Artifacts
gitCommitHash: string;
dockerImageTag: string;
buildManifest: string; // GCS URI to full build artifact manifest
}

3.2 Availability (A) Controls

3.2.1 A1: Availability

Evidence Collection:

ControlEvidence TypeCollection FrequencyCollectorOutput
A1.1 - Availability objectivesSLO adherence reportHourlyAvailabilityCollectorslo-{timestamp}.json
A1.2 - Backup and recoveryBackup completion + restore testsDailyBackupCollectorbackup-{date}.json
A1.3 - System capacityCapacity metrics + forecastingDailyCapacityCollectorcapacity-{date}.json

A1.1 Evidence Structure:

interface SLOAdherenceReport {
reportingPeriod: { start: string; end: string };
slos: Array<{
sloName: string;
target: number; // percentage (e.g., 99.9)
actual: number;
met: boolean;
uptime: number; // percentage
downtimeMinutes: number;
incidents: Array<{
incidentId: string;
startTime: string;
endTime: string;
duration: number; // minutes
affectedServices: string[];
rootCause: string;
}>;
}>;
errorBudget: {
allocated: number; // minutes per month
consumed: number;
remaining: number;
};
}

A1.2 Evidence Structure:

interface BackupReport {
reportDate: string;
backups: Array<{
backupId: string;
backupType: 'full' | 'incremental' | 'differential';
dataSource: string; // e.g., "postgres-primary", "redis-cache"
backupStartTime: string;
backupEndTime: string;
backupDuration: number; // seconds
backupSize: number; // bytes
backupLocation: string; // GCS URI
status: 'success' | 'failed' | 'partial';
errorMessage: string | null;
}>;

// Restore testing (weekly)
restoreTests: Array<{
testId: string;
testDate: string;
backupId: string;
restoreStartTime: string;
restoreEndTime: string;
restoreDuration: number; // seconds
status: 'success' | 'failed';
verificationMethod: 'checksum' | 'row_count' | 'sample_query';
verificationResult: boolean;
rto: number; // Recovery Time Objective (minutes)
rpo: number; // Recovery Point Objective (minutes)
}>;

compliance: {
backupsCompleted: number;
backupsFailed: number;
restoreTestsPassed: number;
restoreTestsFailed: number;
rtoMet: boolean;
rpoMet: boolean;
};
}

3.3 Processing Integrity (PI) Controls

3.3.1 PI1: Processing Integrity

Evidence Collection:

ControlEvidence TypeCollection FrequencyCollectorOutput
PI1.1 - Data processing integrityData validation logsHourlyDataIntegrityCollectordata-validation-{timestamp}.json
PI1.4 - Processing errors detectedError rate metricsHourlyErrorMonitoringCollectorerrors-{timestamp}.json
PI1.5 - Processing correctionsData correction audit logDailyDataCorrectionCollectorcorrections-{date}.json

PI1.1 Evidence Structure:

interface DataValidationLog {
validationPeriod: { start: string; end: string };
validations: Array<{
entityType: string; // e.g., "WorkOrder", "ElectronicSignature"
validationType: 'schema' | 'business_rule' | 'referential_integrity';
totalRecords: number;
validRecords: number;
invalidRecords: number;
validationFailures: Array<{
recordId: string;
validationRule: string;
failureReason: string;
detectedAt: string;
correctedAt: string | null;
correctionMethod: 'automated' | 'manual';
}>;
}>;

// Optimistic locking (version field) violations
concurrencyConflicts: {
total: number;
resolved: number;
conflicts: Array<{
entityType: string;
entityId: string;
attemptedBy: string;
conflictedAt: string;
resolution: 'retry_success' | 'user_merge' | 'abandoned';
}>;
};
}

3.4 Confidentiality (C) Controls

3.4.1 C1: Confidentiality

Evidence Collection:

ControlEvidence TypeCollection FrequencyCollectorOutput
C1.1 - Confidential information identifiedData classification tagsWeeklyDataClassificationCollectordata-classification-{date}.json
C1.2 - Confidential information disposedData retention complianceWeeklyDataRetentionCollectorretention-{date}.json

C1.2 Evidence Structure:

interface DataRetentionReport {
reportDate: string;
retentionPolicies: Array<{
policyId: string;
dataType: string; // e.g., "audit_logs", "user_data", "phi"
retentionPeriod: number; // days
disposalMethod: 'soft_delete' | 'hard_delete' | 'anonymization';
recordsExpired: number;
recordsDisposed: number;
disposalExecutedAt: string | null;
verificationMethod: 'query_check' | 'backup_verification';
verificationResult: boolean;
}>;

// Legal hold tracking
legalHolds: Array<{
holdId: string;
dataType: string;
holdStartDate: string;
holdEndDate: string | null;
affectedRecords: number;
reason: string;
requestedBy: string;
}>;
}

3.5 Privacy (P) Controls

3.5.1 P1-P4: Privacy Lifecycle

Evidence Collection:

ControlEvidence TypeCollection FrequencyCollectorOutput
P1.1 - Privacy notice providedConsent recordsMonthlyPrivacyControlCollectorconsent-{month}.json
P2.1 - Choice and consentConsent opt-in/opt-out logMonthlyPrivacyControlCollectorconsent-{month}.json
P3.1 - Data collection limitedData minimization auditQuarterlyDataMinimizationCollectorminimization-{quarter}.json
P4.1 - Data subject rightsDSAR fulfillment logMonthlyDSARCollectordsar-{month}.json
P4.2 - Data retention policyRetention policy complianceWeeklyDataRetentionCollectorretention-{date}.json

P4.1 Evidence Structure (DSAR):

interface DSARFulfillmentLog {
reportingMonth: string; // "2026-02"
requests: Array<{
requestId: string;
requestType: 'access' | 'rectification' | 'erasure' | 'portability' | 'restriction';
dataSubjectEmail: string; // redacted
requestDate: string;
requiredResponseDate: string; // regulatory deadline
actualResponseDate: string | null;
status: 'received' | 'in_progress' | 'fulfilled' | 'denied' | 'extended';
fulfillmentMethod: 'automated' | 'manual';

// For access requests
dataPackageUri: string | null; // GCS URI to exported data

// For erasure requests
recordsDeleted: number | null;
deletionVerified: boolean | null;

// Denial reason (if applicable)
denialReason: string | null;
denialApprovedBy: string | null;
}>;

compliance: {
totalRequests: number;
onTimeResponses: number;
lateResponses: number;
averageResponseTime: number; // days
complianceRate: number; // percentage
};
}

4. Evidence Collection Schedule

4.1 Collection Frequency Matrix

FrequencyControl CategoriesCollectorsEvidence Types
Real-timeCC7.3, CC8.1SecurityCollector, ChangeManagementCollectorIncidents, deployments
HourlyCC7.2, A1.1, PI1.1ObservabilityCollector, AvailabilityCollector, DataIntegrityCollectorMetrics, errors, validations
DailyCC6.1, CC6.7, CC7.1, CC7.4, A1.2, A1.3, PI1.5AccessControlCollector, EncryptionCollector, VulnerabilityCollector, BackupCollectorSnapshots, scans, backups
WeeklyC1.1, C1.2, P4.2DataClassificationCollector, DataRetentionCollectorClassification, retention
MonthlyP1.1, P2.1, P4.1PrivacyControlCollector, DSARCollectorConsent, DSAR
QuarterlyAllQuarterlyAssessmentCollectorComprehensive assessment package
AnnualAllAnnualPackageCollectorType II audit preparation

4.2 Cron Schedule Configuration

Location: apps/api/src/evidence-collection/collectors/schedules.ts

import { CronExpression } from '@nestjs/schedule';

export const EVIDENCE_COLLECTION_SCHEDULES = {
// Real-time (event-driven, no cron)
SECURITY_INCIDENTS: null,
DEPLOYMENTS: null,

// Hourly
MONITORING: CronExpression.EVERY_HOUR, // :00
AVAILABILITY: '15 * * * *', // :15
DATA_INTEGRITY: '30 * * * *', // :30
ERROR_MONITORING: '45 * * * *', // :45

// Daily
ACCESS_CONTROLS: CronExpression.EVERY_DAY_AT_2AM, // 02:00 UTC
ENCRYPTION: CronExpression.EVERY_DAY_AT_3AM, // 03:00 UTC
VULNERABILITIES: CronExpression.EVERY_DAY_AT_4AM, // 04:00 UTC
BACKUPS: CronExpression.EVERY_DAY_AT_5AM, // 05:00 UTC
CAPACITY: CronExpression.EVERY_DAY_AT_6AM, // 06:00 UTC
DATA_CORRECTIONS: CronExpression.EVERY_DAY_AT_7AM, // 07:00 UTC

// Weekly (Sundays)
DATA_CLASSIFICATION: '0 8 * * 0', // Sunday 08:00 UTC
DATA_RETENTION: '0 9 * * 0', // Sunday 09:00 UTC
BACKUP_RESTORE_TEST: '0 10 * * 0', // Sunday 10:00 UTC

// Monthly (1st of month)
PRIVACY_CONTROLS: '0 10 1 * *', // 1st at 10:00 UTC
DSAR_FULFILLMENT: '0 11 1 * *', // 1st at 11:00 UTC

// Quarterly (1st day of Q: Jan 1, Apr 1, Jul 1, Oct 1)
QUARTERLY_ASSESSMENT: '0 12 1 1,4,7,10 *', // 12:00 UTC
DATA_MINIMIZATION: '0 13 1 1,4,7,10 *', // 13:00 UTC

// Annual (January 1st)
ANNUAL_PACKAGE: '0 14 1 1 *', // Jan 1 at 14:00 UTC
};

4.3 Evidence Retention Policy

Evidence TypeRetention PeriodRationaleStorage Class Transition
Access control snapshots7 yearsHIPAA + SOC 2Standard → Nearline (90d) → Coldline (1y) → Archive (5y)
Change management records7 yearsFDA Part 11Standard → Nearline (90d) → Coldline (1y) → Archive (5y)
Security incident logs7 yearsRegulatory + litigation holdStandard → Nearline (90d) → Coldline (1y) → Archive (5y)
Vulnerability scans3 yearsIndustry best practiceStandard → Nearline (90d) → Coldline (1y)
System metrics2 yearsOperational analysisStandard → Nearline (90d) → Archive (1y)
Backup verification1 yearOperational requirementStandard → Nearline (90d)
Quarterly assessments7 yearsSOC 2 Type II requirementStandard → Archive (2y)
Annual Type II packagesIndefiniteAudit trailStandard → Archive (2y)

5. Tamper-Evident Evidence Repository

5.1 Hash Chain Architecture

Objective: Create an immutable, verifiable chain of evidence where any modification to historical evidence is immediately detectable.

Design:

Evidence Chain:
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ Evidence 1 │────▶│ Evidence 2 │────▶│ Evidence 3 │
│ hash: A1B2 │ │ hash: C3D4 │ │ hash: E5F6 │
│ prev: genesis│ │ prev: A1B2 │ │ prev: C3D4 │
└─────────────┘ └─────────────┘ └─────────────┘

Implementation:

class EvidenceHashChain {
private readonly HASH_ALGORITHM = 'sha256';

/**
* Compute hash for evidence package
*/
computeHash(evidence: EvidencePackage): string {
const canonicalData = this.canonicalize(evidence.data);
const hashInput = JSON.stringify({
evidenceId: evidence.evidenceId,
controlCategory: evidence.controlCategory,
collectionTimestamp: evidence.collectionTimestamp.toISOString(),
data: canonicalData,
});

return createHash(this.HASH_ALGORITHM)
.update(hashInput)
.digest('hex');
}

/**
* Canonicalize data for consistent hashing
* (sort object keys, normalize whitespace, etc.)
*/
private canonicalize(data: unknown): unknown {
if (typeof data !== 'object' || data === null) {
return data;
}

if (Array.isArray(data)) {
return data.map((item) => this.canonicalize(item));
}

// Sort object keys for deterministic hashing
const sorted: Record<string, unknown> = {};
Object.keys(data)
.sort()
.forEach((key) => {
sorted[key] = this.canonicalize((data as Record<string, unknown>)[key]);
});

return sorted;
}

/**
* Verify hash chain integrity
*/
async verifyChain(
controlCategory: ControlCategory,
startDate?: Date
): Promise<ChainVerificationResult> {
const evidenceList = await this.prisma.evidenceMetadata.findMany({
where: {
controlCategory,
...(startDate && { collectionTimestamp: { gte: startDate } }),
},
orderBy: { collectionTimestamp: 'asc' },
});

let previousHash = 'genesis';
const brokenLinks: Array<{ evidenceId: string; reason: string }> = [];

for (const evidence of evidenceList) {
// Verify previousHash linkage
if (evidence.previousHash !== previousHash) {
brokenLinks.push({
evidenceId: evidence.evidenceId,
reason: `Expected previousHash ${previousHash}, got ${evidence.previousHash}`,
});
}

// Verify evidence integrity (recompute hash)
const integrityCheck = await this.verifyEvidenceIntegrity(evidence.evidenceId);
if (!integrityCheck.valid) {
brokenLinks.push({
evidenceId: evidence.evidenceId,
reason: integrityCheck.error || 'Integrity check failed',
});
}

previousHash = evidence.hash;
}

return {
valid: brokenLinks.length === 0,
verifiedCount: evidenceList.length,
brokenLinks,
};
}
}

5.2 Append-Only Guarantees

Enforcement Mechanisms:

  1. GCS Object Versioning: Enabled on evidence bucket - all versions retained
  2. GCS Bucket Lock: Retention policy locked - prevents deletion before 7 years
  3. IAM Restrictions: API service account has storage.objectCreator only (no delete)
  4. Database Constraints: No UPDATE/DELETE triggers on evidence_metadata table
  5. Application Logic: No delete methods exposed in EvidenceService

PostgreSQL Enforcement:

-- Prevent UPDATE/DELETE on evidence_metadata table
CREATE OR REPLACE FUNCTION prevent_evidence_modification()
RETURNS TRIGGER AS $$
BEGIN
IF TG_OP = 'UPDATE' THEN
RAISE EXCEPTION 'UPDATE not allowed on evidence_metadata (append-only)';
ELSIF TG_OP = 'DELETE' THEN
RAISE EXCEPTION 'DELETE not allowed on evidence_metadata (append-only)';
END IF;
RETURN NULL;
END;
$$ LANGUAGE plpgsql;

CREATE TRIGGER evidence_immutability
BEFORE UPDATE OR DELETE ON evidence_metadata
FOR EACH ROW
EXECUTE FUNCTION prevent_evidence_modification();

5.3 Evidence Integrity Monitoring

Automated Integrity Checks:

import { Injectable, Logger } from '@nestjs/common';
import { Cron, CronExpression } from '@nestjs/schedule';

@Injectable()
export class EvidenceIntegrityMonitor {
private readonly logger = new Logger(EvidenceIntegrityMonitor.name);

constructor(
private readonly evidenceService: EvidenceService,
private readonly alertService: AlertService
) {}

@Cron(CronExpression.EVERY_DAY_AT_1AM)
async verifyDailyIntegrity(): Promise<void> {
this.logger.log('Starting daily evidence integrity verification');

const controlCategories = [
ControlCategory.CC6_ACCESS_CONTROLS,
ControlCategory.CC7_MONITORING,
ControlCategory.CC8_CHANGE_MANAGEMENT,
ControlCategory.A_AVAILABILITY,
ControlCategory.PI_PROCESSING_INTEGRITY,
ControlCategory.C_CONFIDENTIALITY,
ControlCategory.P_PRIVACY,
];

const results: IntegrityCheckSummary[] = [];

for (const category of controlCategories) {
try {
const yesterday = new Date(Date.now() - 24 * 60 * 60 * 1000);
const chainResult = await this.evidenceService.verifyChain(
category,
yesterday
);

results.push({
category,
valid: chainResult.valid,
verifiedCount: chainResult.verifiedCount,
brokenLinks: chainResult.brokenLinks.length,
});

if (!chainResult.valid) {
this.logger.error(
`Integrity violation detected for ${category}: ${chainResult.brokenLinks.length} broken links`
);
await this.alertService.sendCriticalAlert({
type: 'evidence_integrity_violation',
category,
brokenLinks: chainResult.brokenLinks,
});
}
} catch (error) {
this.logger.error(`Failed to verify integrity for ${category}`, error);
await this.alertService.sendCriticalAlert({
type: 'evidence_integrity_check_failed',
category,
error: error.message,
});
}
}

// Store integrity check results
await this.prisma.evidenceIntegrityCheck.create({
data: {
checkDate: new Date(),
results: JSON.stringify(results),
allValid: results.every((r) => r.valid),
},
});
}
}

6. Evidence Metadata Schema

6.1 Complete TypeScript Interfaces

// ============================================================================
// Evidence Package Types
// ============================================================================

export enum ControlCategory {
CC1_GOVERNANCE = 'CC1',
CC2_COMMUNICATION = 'CC2',
CC3_RISK_MANAGEMENT = 'CC3',
CC4_MONITORING = 'CC4',
CC5_CONTROL_ACTIVITIES = 'CC5',
CC6_ACCESS_CONTROLS = 'CC6',
CC7_MONITORING = 'CC7',
CC8_CHANGE_MANAGEMENT = 'CC8',
CC9_VENDOR_MANAGEMENT = 'CC9',
A_AVAILABILITY = 'A',
PI_PROCESSING_INTEGRITY = 'PI',
C_CONFIDENTIALITY = 'C',
P_PRIVACY = 'P',
}

export interface EvidencePackage {
evidenceId: string;
controlCategory: ControlCategory;
controlReferences: string[]; // ["CC6.1", "CC6.2"]
collectionTimestamp: Date;
collector: string; // Collector class name
data: Record<string, unknown>; // Evidence payload
}

export interface EvidenceMetadata {
id: string;
evidenceId: string;
controlCategory: ControlCategory;
controlReferences: string[];
collectionTimestamp: Date;
collector: string;
collectorType: 'automated' | 'manual';
hash: string;
previousHash: string;
storageLocation: string; // GCS URI

// Review tracking
reviewStatus: 'pending' | 'approved' | 'rejected';
reviewedBy: string | null;
reviewedAt: Date | null;
reviewNotes: string | null;

// Audit linkage
linkedAuditFinding: string | null;

createdAt: Date;
updatedAt: Date;
}

// ============================================================================
// Validation Types
// ============================================================================

export interface EvidenceValidationResult {
valid: boolean;
errors?: string[];
}

export interface IntegrityCheckResult {
valid: boolean;
error?: string;
}

export interface ChainVerificationResult {
valid: boolean;
verifiedCount: number;
brokenLinks: Array<{
evidenceId: string;
reason: string;
}>;
}

// ============================================================================
// Control-Specific Evidence Types
// ============================================================================

export interface UserPermissionSnapshot {
snapshotDate: string;
totalUsers: number;
activeUsers: number;
inactiveUsers: number;
users: UserPermissionRecord[];
roleDistribution: Record<string, number>;
privilegedAccounts: string[];
}

export interface UserPermissionRecord {
userId: string;
email: string;
roles: string[];
permissions: string[];
tenantId: string;
accountStatus: 'active' | 'inactive' | 'suspended';
lastLoginAt: string | null;
createdAt: string;
lastPermissionChangeAt: string;
lastPermissionChangeBy: string;
}

export interface DeploymentApprovalRecord {
deploymentId: string;
deploymentDate: string;
environment: 'staging' | 'production';
changeType: 'feature' | 'bugfix' | 'security_patch' | 'infrastructure';
changeRequestTicket: string;
requestedBy: string;
requestedAt: string;
changeDescription: string;
impactAssessment: string;
rollbackPlan: string;
approvals: ApprovalRecord[];
testingCompleted: boolean;
testSuiteResults: TestResults;
deployedBy: string;
deploymentMethod: 'ci_cd' | 'manual';
deploymentStatus: 'success' | 'failed' | 'rolled_back';
deploymentDuration: number;
gitCommitHash: string;
dockerImageTag: string;
buildManifest: string;
}

export interface ApprovalRecord {
approver: string;
role: 'engineering_lead' | 'security' | 'qa' | 'operations';
approvedAt: string;
comments: string | null;
}

export interface TestResults {
unit: TestSuiteResult;
integration: TestSuiteResult;
e2e: TestSuiteResult;
}

export interface TestSuiteResult {
passed: number;
failed: number;
skipped: number;
}

export interface TLSCertificateStatus {
checkDate: string;
certificates: CertificateRecord[];
compliance: {
allValid: boolean;
expiringWithin30Days: number;
expiringWithin90Days: number;
};
}

export interface CertificateRecord {
domain: string;
issuer: string;
validFrom: string;
validTo: string;
daysUntilExpiry: number;
status: 'valid' | 'expiring_soon' | 'expired' | 'invalid';
tlsVersion: string;
cipherSuite: string;
certificateChainValid: boolean;
}

export interface BackupReport {
reportDate: string;
backups: BackupRecord[];
restoreTests: RestoreTestRecord[];
compliance: BackupCompliance;
}

export interface BackupRecord {
backupId: string;
backupType: 'full' | 'incremental' | 'differential';
dataSource: string;
backupStartTime: string;
backupEndTime: string;
backupDuration: number;
backupSize: number;
backupLocation: string;
status: 'success' | 'failed' | 'partial';
errorMessage: string | null;
}

export interface RestoreTestRecord {
testId: string;
testDate: string;
backupId: string;
restoreStartTime: string;
restoreEndTime: string;
restoreDuration: number;
status: 'success' | 'failed';
verificationMethod: 'checksum' | 'row_count' | 'sample_query';
verificationResult: boolean;
rto: number;
rpo: number;
}

export interface BackupCompliance {
backupsCompleted: number;
backupsFailed: number;
restoreTestsPassed: number;
restoreTestsFailed: number;
rtoMet: boolean;
rpoMet: boolean;
}

// ============================================================================
// Quarterly & Annual Package Types
// ============================================================================

export interface QuarterlyAssessmentPackage {
quarter: string;
startDate: string;
endDate: string;
assessor: string;
completedAt: string;

controlSummary: {
totalControls: number;
controlsTested: number;
controlsPassed: number;
controlsFailed: number;
effectivenessRate: number;
};

controlResults: ControlTestResult[];

evidenceSummary: {
totalEvidence: number;
evidenceByCategory: Record<ControlCategory, number>;
automatedEvidence: number;
manualEvidence: number;
};

findings: AuditFinding[];

reportLocation: string;
evidencePackageUri: string;
}

export interface ControlTestResult {
controlReference: string;
controlDescription: string;
testDate: string;
tester: string;
testMethod: 'inspection' | 'observation' | 'inquiry' | 'reperformance';
testSampleSize: number;
testResult: 'effective' | 'ineffective' | 'not_applicable';
findings: string[];
evidenceReferences: string[];
}

export interface AuditFinding {
findingId: string;
severity: 'critical' | 'high' | 'medium' | 'low';
controlReference: string;
findingDescription: string;
rootCause: string;
recommendation: string;
managementResponse: string;
remediationPlan: string;
targetCompletionDate: string;
status: 'open' | 'in_progress' | 'remediated' | 'accepted_risk';
}

7. Auditor API

7.1 Read-Only Evidence Access API

Location: apps/api/src/evidence-collection/auditor-api/

Authentication: OAuth 2.0 with auditor-specific service account

import { Controller, Get, Param, Query, UseGuards } from '@nestjs/common';
import { ApiBearerAuth, ApiOperation, ApiTags } from '@nestjs/swagger';
import { AuditorGuard } from './guards/auditor.guard';

@ApiTags('Auditor API')
@ApiBearerAuth()
@UseGuards(AuditorGuard)
@Controller('api/v1/auditor')
export class AuditorController {
constructor(private readonly evidenceService: EvidenceService) {}

/**
* List all evidence for a control category
*/
@Get('evidence/:controlCategory')
@ApiOperation({ summary: 'List evidence by control category' })
async listEvidence(
@Param('controlCategory') controlCategory: ControlCategory,
@Query('startDate') startDate?: string,
@Query('endDate') endDate?: string
): Promise<EvidenceMetadata[]> {
return this.evidenceService.listEvidence({
controlCategory,
startDate: startDate ? new Date(startDate) : undefined,
endDate: endDate ? new Date(endDate) : undefined,
});
}

/**
* Get specific evidence package by ID
*/
@Get('evidence/id/:evidenceId')
@ApiOperation({ summary: 'Get evidence package by ID' })
async getEvidence(@Param('evidenceId') evidenceId: string): Promise<EvidencePackage> {
return this.evidenceService.getEvidence(evidenceId);
}

/**
* Download evidence package as JSON
*/
@Get('evidence/download/:evidenceId')
@ApiOperation({ summary: 'Download evidence package' })
async downloadEvidence(@Param('evidenceId') evidenceId: string): Promise<Buffer> {
const evidence = await this.evidenceService.getEvidence(evidenceId);
return Buffer.from(JSON.stringify(evidence, null, 2));
}

/**
* Verify evidence integrity
*/
@Get('evidence/verify/:evidenceId')
@ApiOperation({ summary: 'Verify evidence integrity' })
async verifyIntegrity(
@Param('evidenceId') evidenceId: string
): Promise<IntegrityCheckResult> {
return this.evidenceService.verifyEvidenceIntegrity(evidenceId);
}

/**
* Verify hash chain for control category
*/
@Get('evidence/chain/:controlCategory')
@ApiOperation({ summary: 'Verify evidence hash chain' })
async verifyChain(
@Param('controlCategory') controlCategory: ControlCategory,
@Query('startDate') startDate?: string
): Promise<ChainVerificationResult> {
return this.evidenceService.verifyChain(
controlCategory,
startDate ? new Date(startDate) : undefined
);
}

/**
* Get quarterly assessment package
*/
@Get('assessment/quarterly/:quarter')
@ApiOperation({ summary: 'Get quarterly assessment package' })
async getQuarterlyAssessment(
@Param('quarter') quarter: string
): Promise<QuarterlyAssessmentPackage> {
return this.evidenceService.getQuarterlyAssessment(quarter);
}

/**
* Get annual Type II package
*/
@Get('assessment/annual/:year')
@ApiOperation({ summary: 'Get annual Type II audit package' })
async getAnnualPackage(@Param('year') year: string): Promise<AnnualTypeIIPackage> {
return this.evidenceService.getAnnualPackage(year);
}

/**
* Get evidence freshness dashboard
*/
@Get('dashboard/freshness')
@ApiOperation({ summary: 'Get evidence freshness dashboard' })
async getEvidenceFreshness(): Promise<EvidenceFreshnessDashboard> {
return this.evidenceService.getEvidenceFreshness();
}
}

7.2 Auditor Authentication Guard

import { Injectable, CanActivate, ExecutionContext, UnauthorizedException } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';

@Injectable()
export class AuditorGuard implements CanActivate {
constructor(private readonly jwtService: JwtService) {}

async canActivate(context: ExecutionContext): Promise<boolean> {
const request = context.switchToHttp().getRequest();
const token = this.extractTokenFromHeader(request);

if (!token) {
throw new UnauthorizedException('No token provided');
}

try {
const payload = await this.jwtService.verifyAsync(token, {
secret: process.env.JWT_SECRET,
});

// Verify auditor role
if (payload.role !== 'auditor') {
throw new UnauthorizedException('Not authorized as auditor');
}

// Attach user to request
request['user'] = payload;
return true;
} catch (error) {
throw new UnauthorizedException('Invalid token');
}
}

private extractTokenFromHeader(request: any): string | undefined {
const [type, token] = request.headers.authorization?.split(' ') ?? [];
return type === 'Bearer' ? token : undefined;
}
}

8. Cross-Framework Evidence Mapping

8.1 SOC 2 ↔ HIPAA ↔ Part 11 Control Mapping

Objective: Collect evidence once, satisfy multiple frameworks.

SOC 2 ControlHIPAA ControlFDA Part 11Evidence TypeCollector
CC6.1 (Access)§164.312(a)(1)§11.10(d)User permission snapshotsAccessControlCollector
CC6.2 (Provisioning)§164.308(a)(3)(ii)(B)§11.10(g)Role assignment auditAccessControlCollector
CC6.7 (Encryption)§164.312(e)(1)N/ATLS certificate statusEncryptionCollector
CC7.2 (Monitoring)§164.312(b)§11.10(e)System metrics + audit logsMonitoringCollector
CC8.1 (Change mgmt)§164.308(a)(8)§11.10(k)(1)Deployment approvalsChangeManagementCollector
A1.2 (Backup)§164.308(a)(7)(ii)(A)§11.10(c)Backup + restore testsBackupCollector
PI1.1 (Data integrity)§164.312(c)(1)§11.10(c)Validation logsDataIntegrityCollector
C1.2 (Retention)§164.316(b)(2)(i)§11.10(c)Retention complianceDataRetentionCollector
P4.1 (DSAR)§164.524 (Access), §164.526 (Amendment)N/ADSAR fulfillment logDSARCollector

Evidence Tagging:

interface EvidencePackage {
evidenceId: string;
controlCategory: ControlCategory;

// Multi-framework mapping
soc2Controls: string[]; // ["CC6.1", "CC6.2"]
hipaaControls: string[]; // ["§164.312(a)(1)"]
part11Controls: string[]; // ["§11.10(d)"]

collectionTimestamp: Date;
collector: string;
data: Record<string, unknown>;
}

8.2 Unified Evidence Collection Example

@Injectable()
export class UnifiedAccessControlCollector {
async collectAccessControls(): Promise<void> {
const evidence = {
evidenceId: uuidv4(),
controlCategory: ControlCategory.CC6_ACCESS_CONTROLS,

// Multi-framework tagging
soc2Controls: ['CC6.1', 'CC6.2', 'CC6.3', 'CC6.6'],
hipaaControls: [
'§164.312(a)(1)',
'§164.312(a)(2)(i)',
'§164.308(a)(3)(ii)(B)',
],
part11Controls: ['§11.10(d)', '§11.10(g)'],

collectionTimestamp: new Date(),
collector: 'UnifiedAccessControlCollector',
data: {
// Evidence satisfies ALL three frameworks
userPermissions: await this.collectUserPermissions(),
roleAssignments: await this.collectRoleAssignments(),
accessReviews: await this.collectAccessReviews(),
},
};

await this.evidenceService.storeEvidence(evidence);
}
}

9. Dashboard Integration

9.1 Evidence Freshness Dashboard

Location: apps/web/src/pages/compliance/evidence-dashboard.tsx

Metrics:

  1. Control Coverage: Percentage of controls with evidence collected in last period
  2. Evidence Freshness: Days since last collection per control
  3. Collection Success Rate: Percentage of successful automated collections
  4. Staleness Alerts: Controls exceeding freshness threshold
  5. Integrity Status: Hash chain verification results
  6. Audit Readiness Score: Overall readiness for SOC 2 audit (0-100)

Dashboard Query:

interface EvidenceFreshnessDashboard {
lastUpdated: string;

coverageSummary: {
totalControls: number;
controlsWithEvidence: number;
coveragePercentage: number;
};

freshnessByCategory: Array<{
controlCategory: ControlCategory;
controlCount: number;
freshControls: number;
staleControls: number;
avgDaysSinceCollection: number;
}>;

collectionHealth: {
last24Hours: {
scheduled: number;
successful: number;
failed: number;
successRate: number;
};
last7Days: {
scheduled: number;
successful: number;
failed: number;
successRate: number;
};
};

staleControls: Array<{
controlReference: string;
lastCollectedAt: string;
daysSinceCollection: number;
expectedFrequency: string;
thresholdExceeded: number; // days over threshold
}>;

integrityStatus: {
lastVerification: string;
categoriesVerified: number;
allChainsValid: boolean;
brokenChains: string[];
};

auditReadinessScore: number; // 0-100
}

Audit Readiness Score Calculation:

function calculateAuditReadinessScore(dashboard: EvidenceFreshnessDashboard): number {
let score = 0;

// Control coverage (40 points)
score += dashboard.coverageSummary.coveragePercentage * 0.4;

// Evidence freshness (30 points)
const freshnessScore =
dashboard.freshnessByCategory.reduce((sum, cat) => {
return sum + (cat.freshControls / cat.controlCount) * 100;
}, 0) / dashboard.freshnessByCategory.length;
score += freshnessScore * 0.3;

// Collection reliability (20 points)
score += dashboard.collectionHealth.last7Days.successRate * 0.2;

// Integrity (10 points)
score += dashboard.integrityStatus.allChainsValid ? 10 : 0;

return Math.round(score);
}

9.2 Evidence Dashboard UI Component

import { Card, Progress, Badge, Alert } from '@/components/ui';
import { useQuery } from '@tanstack/react-query';

export function EvidenceDashboard() {
const { data: dashboard, isLoading } = useQuery({
queryKey: ['evidence-freshness'],
queryFn: () => fetch('/api/v1/auditor/dashboard/freshness').then((r) => r.json()),
refetchInterval: 60000, // Refresh every minute
});

if (isLoading) return <div>Loading...</div>;

const readinessColor =
dashboard.auditReadinessScore >= 90
? 'green'
: dashboard.auditReadinessScore >= 70
? 'yellow'
: 'red';

return (
<div className="space-y-6">
{/* Audit Readiness Score */}
<Card>
<h2 className="text-2xl font-bold mb-4">SOC 2 Audit Readiness</h2>
<div className="flex items-center space-x-4">
<div className="flex-1">
<Progress value={dashboard.auditReadinessScore} color={readinessColor} />
</div>
<div className="text-4xl font-bold text-{readinessColor}-600">
{dashboard.auditReadinessScore}
</div>
</div>
<p className="text-sm text-gray-600 mt-2">
Based on control coverage, evidence freshness, and collection reliability
</p>
</Card>

{/* Stale Evidence Alerts */}
{dashboard.staleControls.length > 0 && (
<Alert variant="warning">
<strong>Stale Evidence Detected:</strong> {dashboard.staleControls.length}{' '}
controls have not been collected within the expected frequency.
</Alert>
)}

{/* Control Coverage */}
<Card>
<h3 className="text-xl font-semibold mb-4">Control Coverage</h3>
<div className="grid grid-cols-3 gap-4">
<div>
<p className="text-sm text-gray-600">Total Controls</p>
<p className="text-3xl font-bold">{dashboard.coverageSummary.totalControls}</p>
</div>
<div>
<p className="text-sm text-gray-600">With Evidence</p>
<p className="text-3xl font-bold">
{dashboard.coverageSummary.controlsWithEvidence}
</p>
</div>
<div>
<p className="text-sm text-gray-600">Coverage</p>
<p className="text-3xl font-bold text-green-600">
{dashboard.coverageSummary.coveragePercentage.toFixed(1)}%
</p>
</div>
</div>
</Card>

{/* Freshness by Category */}
<Card>
<h3 className="text-xl font-semibold mb-4">Evidence Freshness by Category</h3>
<div className="space-y-4">
{dashboard.freshnessByCategory.map((cat) => (
<div key={cat.controlCategory}>
<div className="flex justify-between items-center mb-2">
<span className="font-medium">{cat.controlCategory}</span>
<span className="text-sm text-gray-600">
{cat.freshControls}/{cat.controlCount} fresh
</span>
</div>
<Progress
value={(cat.freshControls / cat.controlCount) * 100}
color={cat.staleControls === 0 ? 'green' : 'yellow'}
/>
<p className="text-xs text-gray-500 mt-1">
Avg: {cat.avgDaysSinceCollection.toFixed(1)} days since last collection
</p>
</div>
))}
</div>
</Card>

{/* Collection Health */}
<Card>
<h3 className="text-xl font-semibold mb-4">Collection Health (Last 7 Days)</h3>
<div className="grid grid-cols-4 gap-4">
<div>
<p className="text-sm text-gray-600">Scheduled</p>
<p className="text-2xl font-bold">
{dashboard.collectionHealth.last7Days.scheduled}
</p>
</div>
<div>
<p className="text-sm text-gray-600">Successful</p>
<p className="text-2xl font-bold text-green-600">
{dashboard.collectionHealth.last7Days.successful}
</p>
</div>
<div>
<p className="text-sm text-gray-600">Failed</p>
<p className="text-2xl font-bold text-red-600">
{dashboard.collectionHealth.last7Days.failed}
</p>
</div>
<div>
<p className="text-sm text-gray-600">Success Rate</p>
<p className="text-2xl font-bold">
{dashboard.collectionHealth.last7Days.successRate.toFixed(1)}%
</p>
</div>
</div>
</Card>

{/* Integrity Status */}
<Card>
<h3 className="text-xl font-semibold mb-4">Evidence Integrity</h3>
<div className="flex items-center space-x-4">
<Badge color={dashboard.integrityStatus.allChainsValid ? 'green' : 'red'}>
{dashboard.integrityStatus.allChainsValid ? 'All Chains Valid' : 'Issues Detected'}
</Badge>
<span className="text-sm text-gray-600">
Last verified: {new Date(dashboard.integrityStatus.lastVerification).toLocaleString()}
</span>
</div>
{!dashboard.integrityStatus.allChainsValid && (
<Alert variant="error" className="mt-4">
<strong>Integrity Issues:</strong> Broken hash chains detected in:{' '}
{dashboard.integrityStatus.brokenChains.join(', ')}
</Alert>
)}
</Card>
</div>
);
}

10. Quarterly and Annual Assessment Workflows

10.1 Quarterly Assessment Package Generation

Trigger: First day of each quarter (Jan 1, Apr 1, Jul 1, Oct 1)

Workflow:

@Injectable()
export class QuarterlyAssessmentCollector {
@Cron('0 12 1 1,4,7,10 *')
async generateQuarterlyAssessment(): Promise<void> {
const quarter = this.getCurrentQuarter();
const { startDate, endDate } = this.getQuarterDateRange(quarter);

this.logger.log(`Generating quarterly assessment for ${quarter}`);

// 1. Collect all evidence from the quarter
const evidence = await this.collectQuarterEvidence(startDate, endDate);

// 2. Perform control testing
const controlResults = await this.testControls(evidence);

// 3. Generate assessment report
const assessmentPackage = await this.generateAssessmentPackage({
quarter,
startDate,
endDate,
evidence,
controlResults,
});

// 4. Store assessment package
await this.storeAssessmentPackage(assessmentPackage);

// 5. Notify compliance team
await this.notifyComplianceTeam(assessmentPackage);
}

private async testControls(
evidence: EvidenceMetadata[]
): Promise<ControlTestResult[]> {
const results: ControlTestResult[] = [];

// Group evidence by control
const evidenceByControl = this.groupEvidenceByControl(evidence);

for (const [controlRef, controlEvidence] of Object.entries(evidenceByControl)) {
const testResult = await this.testControl(controlRef, controlEvidence);
results.push(testResult);
}

return results;
}

private async testControl(
controlRef: string,
evidence: EvidenceMetadata[]
): Promise<ControlTestResult> {
// Control testing logic:
// 1. Verify evidence exists for entire quarter
// 2. Verify evidence freshness (no gaps)
// 3. Sample evidence for accuracy
// 4. Test control effectiveness

const expectedFrequency = this.getExpectedFrequency(controlRef);
const expectedCollections = this.calculateExpectedCollections(
expectedFrequency,
'quarter'
);

const missingCollections = expectedCollections - evidence.length;
const isEffective = missingCollections <= expectedCollections * 0.1; // 10% tolerance

return {
controlReference: controlRef,
controlDescription: this.getControlDescription(controlRef),
testDate: new Date().toISOString(),
tester: 'QuarterlyAssessmentCollector',
testMethod: 'inspection',
testSampleSize: evidence.length,
testResult: isEffective ? 'effective' : 'ineffective',
findings: isEffective
? []
: [`Missing ${missingCollections} evidence collections (${(missingCollections / expectedCollections * 100).toFixed(1)}% gap)`],
evidenceReferences: evidence.map((e) => e.evidenceId),
};
}
}

10.2 Annual Type II Package Generation

Trigger: January 1st

Package Contents:

  1. Executive Summary

    • Organization overview
    • Scope of examination
    • Period covered (12 months)
    • Type II opinion readiness assessment
  2. Control Environment Documentation

    • Organization chart
    • Key personnel and responsibilities
    • Policies and procedures inventory
    • Change log for policies during examination period
  3. Control Testing Results

    • All quarterly assessment results
    • Control effectiveness summary (by category)
    • Exceptions and findings
    • Management responses
  4. Evidence Manifest

    • Complete inventory of evidence collected
    • Evidence by control mapping
    • Evidence freshness report
    • Hash chain verification report
  5. Incident and Exception Log

    • All security incidents during period
    • Control failures and remediations
    • Audit findings and closure evidence
  6. Auditor Workpapers

    • Evidence sampling methodology
    • Testing procedures performed
    • Findings and conclusions
    • Outstanding items for external auditor follow-up

Generation Script:

@Injectable()
export class AnnualPackageCollector {
@Cron('0 14 1 1 *')
async generateAnnualPackage(): Promise<void> {
const year = new Date().getFullYear() - 1; // Previous year
const startDate = new Date(`${year}-01-01`);
const endDate = new Date(`${year}-12-31`);

this.logger.log(`Generating annual Type II package for ${year}`);

// 1. Collect all quarterly assessments
const quarters = [`${year}-Q1`, `${year}-Q2`, `${year}-Q3`, `${year}-Q4`];
const quarterlyAssessments = await Promise.all(
quarters.map((q) => this.evidenceService.getQuarterlyAssessment(q))
);

// 2. Collect all evidence from the year
const evidence = await this.evidenceService.listEvidence({
startDate,
endDate,
});

// 3. Generate control effectiveness summary
const controlSummary = this.generateControlSummary(quarterlyAssessments);

// 4. Collect incident log
const incidents = await this.collectIncidents(startDate, endDate);

// 5. Generate evidence manifest
const evidenceManifest = this.generateEvidenceManifest(evidence);

// 6. Verify hash chains for entire year
const chainVerification = await this.verifyAllChains(startDate, endDate);

// 7. Assemble package
const annualPackage = {
year: year.toString(),
examinationPeriod: { startDate, endDate },
generatedAt: new Date(),
quarterlyAssessments,
controlSummary,
evidenceManifest,
chainVerification,
incidents,
};

// 8. Generate PDF report
const reportPdf = await this.generatePdfReport(annualPackage);

// 9. Upload to GCS
const packageUri = await this.uploadAnnualPackage(year, annualPackage, reportPdf);

// 10. Notify executive leadership
await this.notifyLeadership(year, packageUri);
}
}

11. Alerting and Notifications

11.1 Evidence Collection Failure Alerts

Channels:

  • Critical: PagerDuty (immediate escalation)
  • High: Slack #compliance-alerts
  • Medium: Email to compliance team
  • Low: Dashboard notification

Alert Routing:

@Injectable()
export class EvidenceAlertService {
async alertCollectionFailure(collector: string, error: Error): Promise<void> {
const failure = await this.prisma.evidenceCollectionFailure.create({
data: {
collector,
errorMessage: error.message,
errorStack: error.stack,
},
});

// Determine severity based on control category
const severity = this.determineSeverity(collector);

switch (severity) {
case 'critical':
await this.sendPagerDutyAlert({
summary: `Critical evidence collection failure: ${collector}`,
severity: 'critical',
details: {
collector,
errorMessage: error.message,
failureId: failure.id,
},
});
break;

case 'high':
await this.sendSlackAlert({
channel: '#compliance-alerts',
message: `⚠️ Evidence collection failure: ${collector}`,
details: error.message,
});
break;

case 'medium':
await this.sendEmailAlert({
to: 'compliance@coditect.com',
subject: `Evidence Collection Failure: ${collector}`,
body: `Collector: ${collector}\nError: ${error.message}\n\nFailure ID: ${failure.id}`,
});
break;

case 'low':
// Dashboard notification only
break;
}

// Schedule retry
await this.scheduleRetry(failure);
}

private determineSeverity(collector: string): 'critical' | 'high' | 'medium' | 'low' {
// Critical: Access controls, security incidents
if (
collector.includes('AccessControl') ||
collector.includes('Security') ||
collector.includes('Incident')
) {
return 'critical';
}

// High: Availability, monitoring
if (collector.includes('Availability') || collector.includes('Monitoring')) {
return 'high';
}

// Medium: Backup, encryption
if (collector.includes('Backup') || collector.includes('Encryption')) {
return 'medium';
}

return 'low';
}

private async scheduleRetry(failure: EvidenceCollectionFailure): Promise<void> {
if (failure.retryCount >= failure.maxRetries) {
this.logger.error(
`Max retries exceeded for ${failure.collector}, manual intervention required`
);
await this.sendPagerDutyAlert({
summary: `Evidence collection failed after ${failure.maxRetries} retries: ${failure.collector}`,
severity: 'critical',
details: { failureId: failure.id },
});
return;
}

// Exponential backoff: 5min, 15min, 45min
const delayMinutes = 5 * Math.pow(3, failure.retryCount);
const retryAt = new Date(Date.now() + delayMinutes * 60 * 1000);

await this.prisma.evidenceCollectionFailure.update({
where: { id: failure.id },
data: {
retryCount: { increment: 1 },
retryAt,
},
});

this.logger.log(
`Scheduled retry for ${failure.collector} at ${retryAt.toISOString()} (attempt ${failure.retryCount + 1}/${failure.maxRetries})`
);
}
}

11.2 Evidence Staleness Alerts

Trigger: Daily check at 8:00 AM UTC

@Injectable()
export class EvidenceStalenessMonitor {
@Cron(CronExpression.EVERY_DAY_AT_8AM)
async checkStaleness(): Promise<void> {
const staleControls = await this.prisma.evidenceFreshness.findMany({
where: { isStale: true },
});

if (staleControls.length === 0) {
return;
}

this.logger.warn(`Detected ${staleControls.length} stale controls`);

// Group by severity
const critical = staleControls.filter(
(c) => c.controlReference.startsWith('CC6') || c.controlReference.startsWith('CC7')
);
const noncritical = staleControls.filter(
(c) => !critical.includes(c)
);

if (critical.length > 0) {
await this.alertService.sendSlackAlert({
channel: '#compliance-alerts',
message: `🚨 ${critical.length} critical controls have stale evidence`,
details: critical.map((c) => c.controlReference).join(', '),
});
}

if (noncritical.length > 0) {
await this.alertService.sendEmailAlert({
to: 'compliance@coditect.com',
subject: `Stale Evidence Detected: ${noncritical.length} controls`,
body: `The following controls have stale evidence:\n\n${noncritical.map((c) => `- ${c.controlReference} (last collected: ${c.lastCollectedAt})`).join('\n')}`,
});
}
}
}

12. Implementation Roadmap

12.1 Phase 1: Core Infrastructure (Weeks 1-2)

Tasks:

  • Set up GCS bucket with versioning and retention policy
  • Implement EvidenceService with hash chain logic
  • Create evidence_metadata database schema
  • Implement EvidenceValidator with schema validation
  • Set up base collector abstract class
  • Implement evidence integrity verification

Deliverables:

  • GCS bucket configured and tested
  • Evidence storage service operational
  • Database schema deployed
  • Unit tests for core services (80% coverage)

12.2 Phase 2: Priority Collectors (Weeks 3-4)

Tasks:

  • Implement AccessControlCollector (CC6)
  • Implement MonitoringCollector (CC7)
  • Implement ChangeManagementCollector (CC8)
  • Implement AvailabilityCollector (A1)
  • Set up cron schedules for each collector
  • Implement retry logic for failed collections

Deliverables:

  • 4 collectors operational
  • Evidence collection running on schedule
  • Alert system integrated

12.3 Phase 3: Remaining Collectors (Weeks 5-6)

Tasks:

  • Implement DataIntegrityCollector (PI1)
  • Implement DataRetentionCollector (C1, P4)
  • Implement PrivacyControlCollector (P1, P2)
  • Implement DSARCollector (P4.1)
  • Implement VulnerabilityCollector (CC7.4)
  • Implement BackupCollector (A1.2)

Deliverables:

  • All 10 collectors operational
  • Full SOC 2 control coverage

12.4 Phase 4: Auditor API & Dashboard (Weeks 7-8)

Tasks:

  • Implement Auditor API endpoints
  • Set up auditor authentication (OAuth 2.0)
  • Build evidence freshness dashboard UI
  • Implement quarterly assessment generator
  • Implement annual package generator
  • Create auditor documentation

Deliverables:

  • Auditor API operational
  • Dashboard live in production
  • Quarterly assessment automation
  • Auditor access documentation

12.5 Phase 5: Testing & Validation (Week 9)

Tasks:

  • End-to-end testing of all collectors
  • Load testing (simulate 1 year of evidence)
  • Hash chain verification testing
  • Auditor API integration testing
  • Dashboard UI/UX testing
  • Security audit of auditor access

Deliverables:

  • Test coverage ≥80%
  • Performance benchmarks met
  • Security sign-off

12.6 Phase 6: Documentation & Training (Week 10)

Tasks:

  • Complete this document with actual implementation details
  • Create runbook for evidence collection troubleshooting
  • Create auditor access guide
  • Train compliance team on dashboard
  • Train ops team on alerting
  • Create evidence collection SOP

Deliverables:

  • Complete documentation package
  • Team training completed
  • SOP approved

13. Compliance Attestations

13.1 System Owner Attestation

I, [Name], [Title], attest that the SOC 2 Evidence Collection Automation system described in this document:

  1. Collects evidence in accordance with AICPA Trust Service Criteria requirements
  2. Maintains evidence integrity through cryptographic hash chains
  3. Provides tamper-evident storage with 7-year retention
  4. Enables timely evidence retrieval for SOC 2 audits
  5. Operates continuously without manual intervention

Signature: ___________________________ Date: _______________

13.2 Security Review Attestation

I, [Name], Chief Information Security Officer, attest that:

  1. Evidence collection does not expose sensitive data to unauthorized parties
  2. PII/PHI is redacted from evidence packages where appropriate
  3. Auditor API access is secured with OAuth 2.0 and audited
  4. Evidence integrity mechanisms prevent tampering
  5. GCS bucket IAM permissions follow least-privilege principle

Signature: ___________________________ Date: _______________

13.3 Compliance Review Attestation

I, [Name], Chief Compliance Officer, attest that:

  1. Evidence collection scope covers all applicable SOC 2 controls
  2. Collection frequency meets SOC 2 Type II examination requirements
  3. Quarterly assessments provide sufficient control testing
  4. Evidence is sufficient for external auditor reliance
  5. This system enables continuous SOC 2 compliance posture

Signature: ___________________________ Date: _______________


14. Appendices

Appendix A: Evidence Collection Troubleshooting

Symptom: Collector fails with "Database connection timeout" Root Cause: PostgreSQL connection pool exhausted Resolution: Increase DB_POOL_SIZE environment variable, restart API

Symptom: GCS upload fails with "Forbidden" Root Cause: Service account lacks storage.objectCreator role Resolution: Grant IAM role via Terraform, redeploy

Symptom: Hash chain verification fails Root Cause: Evidence tampered or corruption during upload Resolution: Investigate GCS audit logs, re-collect evidence, file incident

Symptom: Collector skips scheduled run Root Cause: Previous run still executing (long-running collector) Resolution: Implement concurrency control, increase timeout, or reschedule

Appendix B: Evidence Retention Cost Estimation

Assumptions:

  • 10 collectors running
  • Average evidence size: 500 KB
  • Daily collections: 7 collectors
  • Weekly collections: 3 collectors
  • Monthly collections: 3 collectors
  • Quarterly collections: 2 collectors

Annual Evidence Volume:

  • Daily: 7 × 365 × 0.5 MB = 1,278 MB
  • Weekly: 3 × 52 × 0.5 MB = 78 MB
  • Monthly: 3 × 12 × 0.5 MB = 18 MB
  • Quarterly: 2 × 4 × 2 MB = 16 MB
  • Total: ~1.4 GB/year

GCS Storage Costs (7-year retention):

  • Year 1: Standard ($0.020/GB/mo) → $0.34/mo
  • Year 2-5: Coldline ($0.004/GB/mo) → $5.6 GB × $0.004 = $0.22/mo
  • Year 6-7: Archive ($0.0012/GB/mo) → $2.8 GB × $0.0012 = $0.003/mo
  • Total 7-year cost: ~$45

Appendix C: SOC 2 Control Catalog Reference

Common Criteria (CC):

  • CC1: Control Environment
  • CC2: Communication and Information
  • CC3: Risk Assessment
  • CC4: Monitoring Activities
  • CC5: Control Activities
  • CC6: Logical and Physical Access Controls
  • CC7: System Operations
  • CC8: Change Management
  • CC9: Risk Mitigation

Category-Specific Criteria:

  • A1: Availability
  • PI1: Processing Integrity
  • C1: Confidentiality
  • P1-P8: Privacy
  • D.2.4: Validation Evidence Package - FDA Part 11 evidence architecture
  • D.3.4: HIPAA Audit and Reporting - PHI access auditing and breach notification
  • D.4.1: SOC 2 TSC Control Mapping - Control-to-implementation mapping
  • D.4.2: SOC 2 Control Implementation - Control design and operating procedures
  • 20: Regulatory Compliance Matrix - Multi-framework control mapping
  • 21: RBAC Model - Access control implementation (CC6.1, CC6.2)
  • 64: Security Architecture - Overall security posture (CC6, CC7)

Document Approval

Internal Review

ReviewerRoleReview DateStatusComments
[Pending]VP EngineeringYYYY-MM-DDPending
[Pending]CISOYYYY-MM-DDPending
[Pending]VP QAYYYY-MM-DDPending
[Pending]CCOYYYY-MM-DDPending

External Review (if applicable)

ReviewerOrganizationReview DateStatusComments
[Pending]External Auditor (SOC 2)YYYY-MM-DDPending

END OF DOCUMENT

Document Version: 1.0.0 Total Pages: [Auto-generated] Word Count: ~8,500 Line Count: ~1,850 Classification: Internal - Restricted Next Review Date: 2027-02-16