/** * System audit aggregator. * * Calls each drift-category check in parallel and assembles a * `SystemAuditReport`. Each category check is dependency-injected so * the aggregator can be tested in isolation and the CLI command can * wire the real DB / registry / health-runner / terraform binary. * * No mutations are performed by `runAudit`. The single source of * truth for what audit can detect is this file's `AuditDeps` shape. */ import { type AbandonedOperationsAuditDeps, auditAbandonedOperations, } from './abandoned-operations'; import { type BackupsAuditDeps, auditBackups } from './backups'; import { type BrowserPinAuditDeps, auditBrowserPin } from './browser-pin'; import { type CapabilityAbiAuditDeps, auditCapabilityAbi } from './capability-abi'; import { type CliVersionAuditDeps, auditCliVersion } from './cli-version'; import { type DetectWithoutConvergeAuditDeps, auditDetectWithoutConverge, } from './detect-without-converge'; import { type DiskSpaceAuditDeps, auditDiskSpace } from './disk-space'; import { type HealthAuditDeps, auditHealth } from './health'; import { type JailExemptionsAuditDeps, auditJailExemptions } from './jail-exemptions'; import { type MachinesReachableAuditDeps, auditMachinesReachable } from './machines-reachable'; import { type ModuleConfigsAuditDeps, auditModuleConfigs } from './module-configs'; import { type ModuleIntegrityAuditDeps, auditModuleIntegrity } from './module-integrity'; import { type ModuleVersionsAuditDeps, auditModuleVersions } from './module-versions'; import { type PublicDnsAuditDeps, auditPublicDns } from './public-dns'; import { type SchemaAuditDeps, auditSchema } from './schema'; import { type SecretsDecryptableAuditDeps, auditSecretsDecryptable } from './secrets-decryptable'; import { type ServicesCredentialsAuditDeps, auditServicesCredentials, } from './services-credentials'; import { type ServicesReachableAuditDeps, auditServicesReachable } from './services-reachable'; import { type TerraformPlanAuditDeps, auditTerraformPlan } from './terraform-plan'; import { type TransportReadsAuditDeps, auditTransportReads } from './transport-reads'; import { type TrustedSourcesAuditDeps, auditTrustedSources } from './trusted-sources'; import { type DriftCategory, type DriftFinding, type SystemAuditReport, computeVerdict, } from './types'; import { type UnconfiguredModulesAuditDeps, auditUnconfiguredModules, } from './unconfigured-modules'; import { type UndeployedModulesAuditDeps, auditUndeployedModules } from './undeployed-modules'; export interface AuditDeps { cliVersion: CliVersionAuditDeps; schema: SchemaAuditDeps; capabilityAbi: CapabilityAbiAuditDeps; browserPin: BrowserPinAuditDeps; terraformPlan: TerraformPlanAuditDeps; moduleVersions: ModuleVersionsAuditDeps; moduleConfigs: ModuleConfigsAuditDeps; moduleIntegrity: ModuleIntegrityAuditDeps; detectWithoutConverge: DetectWithoutConvergeAuditDeps; jailExemptions: JailExemptionsAuditDeps; health: HealthAuditDeps; backups: BackupsAuditDeps; abandonedOperations: AbandonedOperationsAuditDeps; undeployedModules: UndeployedModulesAuditDeps; unconfiguredModules: UnconfiguredModulesAuditDeps; servicesCredentials: ServicesCredentialsAuditDeps; secretsDecryptable: SecretsDecryptableAuditDeps; servicesReachable: ServicesReachableAuditDeps; machinesReachable: MachinesReachableAuditDeps; publicDns: PublicDnsAuditDeps; diskSpace: DiskSpaceAuditDeps; transportReads: TransportReadsAuditDeps; trustedSources: TrustedSourcesAuditDeps; /** Defaults to `Date.now()`-based ISO string. */ now?: () => Date; } /** * Per-category lifecycle event emitted by `runAudit`. Lets a UI * (the audit TUI) display per-category fuel-gauges instead of one * opaque spinner. `start` fires when the category's promise enters * the parallel pool; `end` fires when its promise resolves. */ export interface AuditCategoryEvent { category: DriftCategory; phase: 'start' | 'end'; /** Findings array — present on `phase: 'end'` only. */ findings?: DriftFinding[]; } export async function runAudit( deps: AuditDeps, onProgress?: (event: AuditCategoryEvent) => void, ): Promise { // Wrap each category's promise so the caller sees per-category // start/end events. `start` fires synchronously when this function // schedules the category; in practice all of them fire roughly at // once since Promise.all spawns them in parallel. const wrap = (category: DriftCategory, p: Promise): Promise => { onProgress?.({ category, phase: 'start' }); return p.then((findings) => { onProgress?.({ category, phase: 'end', findings }); return findings; }); }; // Run all categories in parallel; each returns its own array. const groups = await Promise.all([ wrap('cli_version', auditCliVersion(deps.cliVersion)), wrap('schema', auditSchema(deps.schema)), wrap('capability_abi', auditCapabilityAbi(deps.capabilityAbi)), wrap('browser_pin', auditBrowserPin(deps.browserPin)), wrap('terraform_plan', auditTerraformPlan(deps.terraformPlan)), wrap('module_versions', auditModuleVersions(deps.moduleVersions)), wrap('module_configs', auditModuleConfigs(deps.moduleConfigs)), wrap('module_integrity', Promise.resolve(auditModuleIntegrity(deps.moduleIntegrity))), wrap( 'detect_without_converge', Promise.resolve(auditDetectWithoutConverge(deps.detectWithoutConverge)), ), wrap('jail_exemptions', Promise.resolve(auditJailExemptions(deps.jailExemptions))), wrap('health', auditHealth(deps.health)), wrap('backups', auditBackups(deps.backups)), wrap( 'abandoned_operations', Promise.resolve(auditAbandonedOperations(deps.abandonedOperations)), ), wrap('undeployed_modules', auditUndeployedModules(deps.undeployedModules)), wrap('unconfigured_modules', auditUnconfiguredModules(deps.unconfiguredModules)), wrap('services_credentials', auditServicesCredentials(deps.servicesCredentials)), wrap('secrets_decryptable', auditSecretsDecryptable(deps.secretsDecryptable)), wrap('services_reachable', auditServicesReachable(deps.servicesReachable)), wrap('machines_reachable', auditMachinesReachable(deps.machinesReachable)), wrap('public_dns', auditPublicDns(deps.publicDns)), wrap('disk_space', Promise.resolve(auditDiskSpace(deps.diskSpace))), wrap('transport_reads', auditTransportReads(deps.transportReads)), wrap('trusted_sources', auditTrustedSources(deps.trustedSources)), ]); const findings: DriftFinding[] = groups.flat(); const now = (deps.now ?? (() => new Date()))(); return { version: 1, verdict: computeVerdict(findings), generatedAt: now.toISOString(), findings, }; } // Re-export types for callers. export type { SystemAuditReport, DriftFinding, AuditVerdict, DriftCategory } from './types';