feat(phase-3): workstream 6 — SOC 2 Type II Preparation

Implements all 22 WS6 tasks completing Phase 3 Enterprise.

Column-level encryption (AES-256-CBC, Vault-backed key) via EncryptionService
applied to credentials.secret_hash, credentials.vault_path,
webhook_subscriptions.vault_secret_path, and agent_did_keys.vault_key_path.
Backward-compatible: isEncrypted() guard skips decryption for existing
plaintext rows until next read-write cycle.

Audit chain integrity (CC7.2): AuditRepository computes SHA-256 Merkle hash
on every INSERT (hash = SHA-256(eventId+timestamp+action+outcome+agentId+orgId+prevHash)).
AuditVerificationService walks the full chain verifying hash continuity.
AuditChainVerificationJob runs hourly; sets agentidp_audit_chain_integrity
Prometheus gauge to 1 (pass) or 0 (fail).

TLS enforcement (CC6.7): TLSEnforcementMiddleware registered as first
middleware in Express stack; 301 redirect on non-https X-Forwarded-Proto
in production.

SecretsRotationJob (CC9.2): hourly scan for credentials expiring within 7
days; increments agentidp_credentials_expiring_soon_total.

ComplianceController + routes: GET /audit/verify (auth+audit:read scope,
30/min rate-limit); GET /compliance/controls (public, Cache-Control 60s).
ComplianceStatusStore: module-level map updated by jobs, consumed by controller.

Prometheus: 2 new metrics (agentidp_credentials_expiring_soon_total,
agentidp_audit_chain_integrity); 6 alerting rules in alerts.yml.

Compliance docs: soc2-controls-matrix.md, encryption-runbook.md,
audit-log-runbook.md, incident-response.md, secrets-rotation.md.

Tests: 557 unit tests passing (35 suites); 26 new tests (EncryptionService,
AuditVerificationService); 19 compliance integration tests. TypeScript clean.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
SentryAgent.ai Developer
2026-03-31 00:41:53 +00:00
parent 272b69f18d
commit fd90b2acd1
35 changed files with 3715 additions and 26 deletions

View File

@@ -0,0 +1,130 @@
/**
* ComplianceController — SOC 2 Type II compliance endpoints.
*
* Handles two endpoints defined in docs/openapi/compliance.yaml:
* GET /api/v1/audit/verify — Audit chain integrity verification (auth required)
* GET /api/v1/compliance/controls — SOC 2 control status summary (public)
*/
import { Request, Response, NextFunction } from 'express';
import { AuditVerificationService } from '../services/AuditVerificationService.js';
import { getAllControlStatuses } from '../services/ComplianceStatusStore.js';
import { ValidationError } from '../utils/errors.js';
// ============================================================================
// Helpers
// ============================================================================
/**
* Returns `true` if the given string is a valid ISO 8601 date-time string.
* Uses `Date.parse` — valid ISO 8601 strings produce a finite number;
* invalid strings produce `NaN`.
*
* @param value - The string to validate.
* @returns `true` if valid ISO 8601 date-time; `false` otherwise.
*/
function isValidIsoDateTime(value: string): boolean {
const parsed = Date.parse(value);
return !isNaN(parsed);
}
// ============================================================================
// Controller
// ============================================================================
/**
* Controller for SOC 2 Type II compliance API endpoints.
* Exposes audit chain verification and live control status reporting.
*/
export class ComplianceController {
/**
* @param auditVerificationService - Service for cryptographic audit chain verification.
*/
constructor(
private readonly auditVerificationService: AuditVerificationService,
) {}
// ──────────────────────────────────────────────────────────────────────────
// Handlers
// ──────────────────────────────────────────────────────────────────────────
/**
* GET /api/v1/audit/verify
*
* Verifies the cryptographic integrity of the audit event hash chain.
* Accepts optional `fromDate` and `toDate` ISO 8601 query parameters to restrict
* the verification window. Returns 200 regardless of whether the chain is intact —
* check `verified` in the response body.
*
* Requires Bearer token with `audit:read` scope (enforced by route middleware).
*
* @param req - Express request; optional `fromDate` and `toDate` query params.
* @param res - Express response.
* @param next - Express next function.
*/
async verifyAuditChain(req: Request, res: Response, next: NextFunction): Promise<void> {
try {
const { fromDate, toDate } = req.query as Record<string, string | undefined>;
// Validate fromDate if provided
if (fromDate !== undefined && !isValidIsoDateTime(fromDate)) {
throw new ValidationError('Invalid query parameter value.', {
field: 'fromDate',
reason: 'Must be a valid ISO 8601 date-time string (e.g. 2026-03-01T00:00:00.000Z).',
});
}
// Validate toDate if provided
if (toDate !== undefined && !isValidIsoDateTime(toDate)) {
throw new ValidationError('Invalid query parameter value.', {
field: 'toDate',
reason: 'Must be a valid ISO 8601 date-time string (e.g. 2026-03-31T23:59:59.999Z).',
});
}
// Validate date range ordering
if (fromDate !== undefined && toDate !== undefined) {
if (new Date(fromDate) > new Date(toDate)) {
throw new ValidationError('Invalid date range.', {
reason: 'fromDate must be before or equal to toDate.',
});
}
}
const result = await this.auditVerificationService.verifyChain(fromDate, toDate);
res.status(200).json(result);
} catch (err) {
next(err);
}
}
/**
* GET /api/v1/compliance/controls
*
* Returns a live status snapshot for all five in-scope SOC 2 Trust Services
* Criteria controls. Status values are maintained by background jobs
* (SecretsRotationJob, AuditChainVerificationJob) via ComplianceStatusStore.
*
* No authentication required — this is a public health-style endpoint.
* Sets `Cache-Control: public, max-age=60` to permit 60-second downstream caching.
*
* @param _req - Express request (unused).
* @param res - Express response.
* @param next - Express next function.
*/
async getComplianceControls(
_req: Request,
res: Response,
next: NextFunction,
): Promise<void> {
try {
const controls = getAllControlStatuses();
res.setHeader('Cache-Control', 'public, max-age=60');
res.status(200).json({ controls });
} catch (err) {
next(err);
}
}
}