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
| Role | Name | Signature | Date |
|---|---|---|---|
| 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
| Version | Date | Author | Changes | Approval Status |
|---|---|---|---|---|
| 1.0.0 | 2026-02-16 | Compliance Team | Initial release | Draft |
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 Type | Frequency | Next Review Date | Responsible Party |
|---|---|---|---|
| Annual Review | 12 months | 2027-02-16 | Chief Compliance Officer |
| Quarterly Evidence Review | 3 months | 2026-05-16 | Compliance Team |
| Post-Audit Review | As needed | N/A | External Auditor + CCO |
| Automation Health Check | Monthly | 2026-03-16 | IT 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:
- SOC 2 Type I/II Readiness - Continuous collection of control evidence across all five Trust Service Criteria (Security, Availability, Processing Integrity, Confidentiality, Privacy)
- Audit Efficiency - 15-minute retrieval guarantee for any control evidence during SOC 2 audits
- Evidence Integrity - Tamper-evident storage with cryptographic verification and immutable audit trails
- Continuous Compliance - Real-time control effectiveness monitoring with automated alerting for gaps
- 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/
| Collector | Frequency | Control Categories | Output Format |
|---|---|---|---|
AccessControlCollector | Daily | CC6.1, CC6.2, CC6.3, CC6.6 | JSON snapshots |
ChangeManagementCollector | Per deployment | CC8.1 | Deployment manifest + approvals |
EncryptionCollector | Daily | CC6.7 | TLS cert status + key metadata |
MonitoringCollector | Hourly | CC7.1, CC7.2, CC7.3 | Alert logs + incident records |
AvailabilityCollector | Hourly | A1.1, A1.2, A1.3 | Uptime metrics + SLO adherence |
BackupCollector | Daily | A1.2 | Backup completion + restore test results |
IncidentResponseCollector | Real-time | CC7.3, CC7.4 | Incident timeline + remediation |
VulnerabilityCollector | Daily | CC7.1 | CVE scan results + patch status |
DataRetentionCollector | Weekly | C1.1, P4.2 | Retention policy adherence |
PrivacyControlCollector | Monthly | P1.1, P2.1, P3.1, P4.1 | Consent 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:
| Control | Evidence Type | Collection Frequency | Collector | Output |
|---|---|---|---|---|
| CC6.1 - Restricts logical access | User permission snapshots | Daily | AccessControlCollector | users-{date}.json |
| CC6.2 - New access provisioned/removed | Role assignment audit log | Real-time | AuditTrailCollector | role-assignments-{date}.json |
| CC6.3 - Authenticates users | SSO authentication logs | Hourly | AuthenticationCollector | auth-logs-{timestamp}.json |
| CC6.6 - Restricts access to information assets | Tenant isolation verification | Daily | DataAccessCollector | tenant-isolation-{date}.json |
| CC6.7 - Transmits data securely | TLS certificate status | Daily | EncryptionCollector | tls-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:
| Control | Evidence Type | Collection Frequency | Collector | Output |
|---|---|---|---|---|
| CC7.1 - Detects failures | Circuit breaker events | Real-time | MonitoringCollector | circuit-breaker-{timestamp}.json |
| CC7.2 - Monitors system components | Infrastructure metrics | Hourly | ObservabilityCollector | metrics-{timestamp}.json |
| CC7.3 - Evaluates security events | Security incident log | Real-time | SecurityCollector | security-incidents-{date}.json |
| CC7.4 - Responds to vulnerabilities | Vulnerability scan results | Daily | VulnerabilityCollector | vuln-scan-{date}.json |
| CC7.5 - Identifies anomalies | ML anomaly detection alerts | Hourly | AnomalyDetectionCollector | anomalies-{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:
| Control | Evidence Type | Collection Frequency | Collector | Output |
|---|---|---|---|---|
| CC8.1 - Authorizes changes | Deployment approval records | Per deployment | ChangeManagementCollector | deployment-{id}.json |
| CC8.2 - Tests changes | CI/CD test results | Per build | TestResultsCollector | test-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:
| Control | Evidence Type | Collection Frequency | Collector | Output |
|---|---|---|---|---|
| A1.1 - Availability objectives | SLO adherence report | Hourly | AvailabilityCollector | slo-{timestamp}.json |
| A1.2 - Backup and recovery | Backup completion + restore tests | Daily | BackupCollector | backup-{date}.json |
| A1.3 - System capacity | Capacity metrics + forecasting | Daily | CapacityCollector | capacity-{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:
| Control | Evidence Type | Collection Frequency | Collector | Output |
|---|---|---|---|---|
| PI1.1 - Data processing integrity | Data validation logs | Hourly | DataIntegrityCollector | data-validation-{timestamp}.json |
| PI1.4 - Processing errors detected | Error rate metrics | Hourly | ErrorMonitoringCollector | errors-{timestamp}.json |
| PI1.5 - Processing corrections | Data correction audit log | Daily | DataCorrectionCollector | corrections-{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:
| Control | Evidence Type | Collection Frequency | Collector | Output |
|---|---|---|---|---|
| C1.1 - Confidential information identified | Data classification tags | Weekly | DataClassificationCollector | data-classification-{date}.json |
| C1.2 - Confidential information disposed | Data retention compliance | Weekly | DataRetentionCollector | retention-{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:
| Control | Evidence Type | Collection Frequency | Collector | Output |
|---|---|---|---|---|
| P1.1 - Privacy notice provided | Consent records | Monthly | PrivacyControlCollector | consent-{month}.json |
| P2.1 - Choice and consent | Consent opt-in/opt-out log | Monthly | PrivacyControlCollector | consent-{month}.json |
| P3.1 - Data collection limited | Data minimization audit | Quarterly | DataMinimizationCollector | minimization-{quarter}.json |
| P4.1 - Data subject rights | DSAR fulfillment log | Monthly | DSARCollector | dsar-{month}.json |
| P4.2 - Data retention policy | Retention policy compliance | Weekly | DataRetentionCollector | retention-{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
| Frequency | Control Categories | Collectors | Evidence Types |
|---|---|---|---|
| Real-time | CC7.3, CC8.1 | SecurityCollector, ChangeManagementCollector | Incidents, deployments |
| Hourly | CC7.2, A1.1, PI1.1 | ObservabilityCollector, AvailabilityCollector, DataIntegrityCollector | Metrics, errors, validations |
| Daily | CC6.1, CC6.7, CC7.1, CC7.4, A1.2, A1.3, PI1.5 | AccessControlCollector, EncryptionCollector, VulnerabilityCollector, BackupCollector | Snapshots, scans, backups |
| Weekly | C1.1, C1.2, P4.2 | DataClassificationCollector, DataRetentionCollector | Classification, retention |
| Monthly | P1.1, P2.1, P4.1 | PrivacyControlCollector, DSARCollector | Consent, DSAR |
| Quarterly | All | QuarterlyAssessmentCollector | Comprehensive assessment package |
| Annual | All | AnnualPackageCollector | Type 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 Type | Retention Period | Rationale | Storage Class Transition |
|---|---|---|---|
| Access control snapshots | 7 years | HIPAA + SOC 2 | Standard → Nearline (90d) → Coldline (1y) → Archive (5y) |
| Change management records | 7 years | FDA Part 11 | Standard → Nearline (90d) → Coldline (1y) → Archive (5y) |
| Security incident logs | 7 years | Regulatory + litigation hold | Standard → Nearline (90d) → Coldline (1y) → Archive (5y) |
| Vulnerability scans | 3 years | Industry best practice | Standard → Nearline (90d) → Coldline (1y) |
| System metrics | 2 years | Operational analysis | Standard → Nearline (90d) → Archive (1y) |
| Backup verification | 1 year | Operational requirement | Standard → Nearline (90d) |
| Quarterly assessments | 7 years | SOC 2 Type II requirement | Standard → Archive (2y) |
| Annual Type II packages | Indefinite | Audit trail | Standard → 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:
- GCS Object Versioning: Enabled on evidence bucket - all versions retained
- GCS Bucket Lock: Retention policy locked - prevents deletion before 7 years
- IAM Restrictions: API service account has
storage.objectCreatoronly (no delete) - Database Constraints: No UPDATE/DELETE triggers on
evidence_metadatatable - 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 Control | HIPAA Control | FDA Part 11 | Evidence Type | Collector |
|---|---|---|---|---|
| CC6.1 (Access) | §164.312(a)(1) | §11.10(d) | User permission snapshots | AccessControlCollector |
| CC6.2 (Provisioning) | §164.308(a)(3)(ii)(B) | §11.10(g) | Role assignment audit | AccessControlCollector |
| CC6.7 (Encryption) | §164.312(e)(1) | N/A | TLS certificate status | EncryptionCollector |
| CC7.2 (Monitoring) | §164.312(b) | §11.10(e) | System metrics + audit logs | MonitoringCollector |
| CC8.1 (Change mgmt) | §164.308(a)(8) | §11.10(k)(1) | Deployment approvals | ChangeManagementCollector |
| A1.2 (Backup) | §164.308(a)(7)(ii)(A) | §11.10(c) | Backup + restore tests | BackupCollector |
| PI1.1 (Data integrity) | §164.312(c)(1) | §11.10(c) | Validation logs | DataIntegrityCollector |
| C1.2 (Retention) | §164.316(b)(2)(i) | §11.10(c) | Retention compliance | DataRetentionCollector |
| P4.1 (DSAR) | §164.524 (Access), §164.526 (Amendment) | N/A | DSAR fulfillment log | DSARCollector |
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:
- Control Coverage: Percentage of controls with evidence collected in last period
- Evidence Freshness: Days since last collection per control
- Collection Success Rate: Percentage of successful automated collections
- Staleness Alerts: Controls exceeding freshness threshold
- Integrity Status: Hash chain verification results
- 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:
-
Executive Summary
- Organization overview
- Scope of examination
- Period covered (12 months)
- Type II opinion readiness assessment
-
Control Environment Documentation
- Organization chart
- Key personnel and responsibilities
- Policies and procedures inventory
- Change log for policies during examination period
-
Control Testing Results
- All quarterly assessment results
- Control effectiveness summary (by category)
- Exceptions and findings
- Management responses
-
Evidence Manifest
- Complete inventory of evidence collected
- Evidence by control mapping
- Evidence freshness report
- Hash chain verification report
-
Incident and Exception Log
- All security incidents during period
- Control failures and remediations
- Audit findings and closure evidence
-
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
EvidenceServicewith hash chain logic - Create
evidence_metadatadatabase schema - Implement
EvidenceValidatorwith 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:
- Collects evidence in accordance with AICPA Trust Service Criteria requirements
- Maintains evidence integrity through cryptographic hash chains
- Provides tamper-evident storage with 7-year retention
- Enables timely evidence retrieval for SOC 2 audits
- Operates continuously without manual intervention
Signature: ___________________________ Date: _______________
13.2 Security Review Attestation
I, [Name], Chief Information Security Officer, attest that:
- Evidence collection does not expose sensitive data to unauthorized parties
- PII/PHI is redacted from evidence packages where appropriate
- Auditor API access is secured with OAuth 2.0 and audited
- Evidence integrity mechanisms prevent tampering
- GCS bucket IAM permissions follow least-privilege principle
Signature: ___________________________ Date: _______________
13.3 Compliance Review Attestation
I, [Name], Chief Compliance Officer, attest that:
- Evidence collection scope covers all applicable SOC 2 controls
- Collection frequency meets SOC 2 Type II examination requirements
- Quarterly assessments provide sufficient control testing
- Evidence is sufficient for external auditor reliance
- 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
Appendix D: Cross-Reference to Related Documents
- 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
| Reviewer | Role | Review Date | Status | Comments |
|---|---|---|---|---|
| [Pending] | VP Engineering | YYYY-MM-DD | Pending | |
| [Pending] | CISO | YYYY-MM-DD | Pending | |
| [Pending] | VP QA | YYYY-MM-DD | Pending | |
| [Pending] | CCO | YYYY-MM-DD | Pending |
External Review (if applicable)
| Reviewer | Organization | Review Date | Status | Comments |
|---|---|---|---|---|
| [Pending] | External Auditor (SOC 2) | YYYY-MM-DD | Pending |
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