import { InvalidLocalSecurityAuditRetentionValueError, LocalSecurityAuditCompactionMutationConflictError, LocalSecurityAuditRetentionAuthorizationFenceConflictError, LocalSecurityAuditRetentionUnavailableError, MAX_LOCAL_SECURITY_AUDIT_RETENTION_MS, MIN_LOCAL_SECURITY_AUDIT_RETENTION_MS, localSecurityAuditCompactionPayload, type CompactAuthorizedLocalSecurityAuditCommand, type CompactAuthorizedLocalSecurityAuditResult, type LocalSecurityAuditCompactionRecord, type LocalSecurityAuditRetentionRepository, } from '@qinglong/runtime-core/local-security-audit-retention'; import { InvalidProjectPolicyValueError } from '@qinglong/runtime-core/project-policy'; import { SecurityAuditUnavailableError, normalizeSecurityAuditRecord, type SecurityAuditRecord, } from '@qinglong/runtime-core/security-audit'; import { LocalSqliteOperationAuthority } from '../authority/operationAuthority'; import { assertLocalSecurityAuditInstanceOwnerInTransaction, normalizeLocalSecurityAuditInstanceAuthorization, } from './securityAuditAuthority'; import { insertLocalSecurityAudit, LOCAL_SECURITY_AUDIT_JOIN_SELECT, LOCAL_SECURITY_AUDIT_SELECT, localSecurityAuditFromRow, sameSecurityAuditSemantic, } from './securityPersistence'; import { LocalSqliteSecurityAuthorityStore } from './securityAuthorityStore'; type Row = Record; const UUID_V4_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/; const REQUEST_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/; const COMPACTION_SELECT = ` compaction."mutation_id" AS "compactionMutationId", compaction."request_id" AS "compactionRequestId", compaction."authority_project_id" AS "compactionAuthorityProjectId", compaction."retention_ms" AS "compactionRetentionMs", compaction."eligible_before_ms" AS "compactionEligibleBeforeMs", compaction."batch_limit" AS "compactionBatchLimit", compaction."deleted_count" AS "compactionDeletedCount", compaction."deleted_payload_bytes" AS "compactionDeletedPayloadBytes", compaction."first_occurred_at_ms" AS "compactionFirstOccurredAtMs", compaction."first_event_id" AS "compactionFirstEventId", compaction."last_occurred_at_ms" AS "compactionLastOccurredAtMs", compaction."last_event_id" AS "compactionLastEventId", compaction."records_digest" AS "compactionRecordsDigest", compaction."created_at_ms" AS "compactionCreatedAtMs" `; function integer(row: Row, key: string): number { const value = row[key]; if (!Number.isSafeInteger(value) || (value as number) < 0) { throw new LocalSecurityAuditRetentionUnavailableError(); } return value as number; } function text(row: Row, key: string): string { const value = row[key]; if (typeof value !== 'string') { throw new LocalSecurityAuditRetentionUnavailableError(); } return value; } function optionalInteger(row: Row, key: string): number | null { const value = row[key]; if (value === null) return null; return integer(row, key); } function optionalText(row: Row, key: string): string | null { const value = row[key]; if (value === null) return null; return text(row, key); } function compactionFromRow( row: Row, ): Readonly { const deletedCount = integer(row, 'compactionDeletedCount'); const firstOccurredAtMs = optionalInteger(row, 'compactionFirstOccurredAtMs'); const firstEventId = optionalText(row, 'compactionFirstEventId'); const lastOccurredAtMs = optionalInteger(row, 'compactionLastOccurredAtMs'); const lastEventId = optionalText(row, 'compactionLastEventId'); if ( (deletedCount === 0 && (firstOccurredAtMs !== null || firstEventId !== null || lastOccurredAtMs !== null || lastEventId !== null)) || (deletedCount > 0 && (firstOccurredAtMs === null || firstEventId === null || lastOccurredAtMs === null || lastEventId === null)) ) { throw new LocalSecurityAuditRetentionUnavailableError(); } return Object.freeze({ mutationId: text(row, 'compactionMutationId'), requestId: text(row, 'compactionRequestId'), authorityProjectId: text(row, 'compactionAuthorityProjectId'), retentionMs: integer(row, 'compactionRetentionMs'), eligibleBeforeMs: integer(row, 'compactionEligibleBeforeMs'), batchLimit: integer(row, 'compactionBatchLimit'), deletedCount, deletedPayloadBytes: integer(row, 'compactionDeletedPayloadBytes'), first: firstOccurredAtMs === null ? null : Object.freeze({ occurredAtMs: firstOccurredAtMs, eventId: firstEventId!, }), last: lastOccurredAtMs === null ? null : Object.freeze({ occurredAtMs: lastOccurredAtMs, eventId: lastEventId!, }), recordsDigest: text(row, 'compactionRecordsDigest'), createdAtMs: integer(row, 'compactionCreatedAtMs'), }); } function command( input: CompactAuthorizedLocalSecurityAuditCommand, maxBatchSize: number, ): Readonly { if ( !input || typeof input !== 'object' || Array.isArray(input) || Object.keys(input).sort().join(',') !== [ 'audit', 'authorization', 'eligibleBeforeMs', 'limit', 'mutationId', 'requestId', 'retentionMs', ] .sort() .join(',') || !UUID_V4_PATTERN.test(input.mutationId) || !REQUEST_ID_PATTERN.test(input.requestId) || !Number.isSafeInteger(input.retentionMs) || input.retentionMs < MIN_LOCAL_SECURITY_AUDIT_RETENTION_MS || input.retentionMs > MAX_LOCAL_SECURITY_AUDIT_RETENTION_MS || !Number.isSafeInteger(input.eligibleBeforeMs) || input.eligibleBeforeMs < 0 || !Number.isSafeInteger(input.limit) || input.limit < 1 || input.limit > maxBatchSize ) { throw new InvalidLocalSecurityAuditRetentionValueError( 'compaction command shape is invalid', ); } const authorization = normalizeLocalSecurityAuditInstanceAuthorization( input.authorization, ); const audit = normalizeSecurityAuditRecord(input.audit); if ( audit.eventId !== input.mutationId || audit.requestId !== input.requestId || audit.operationId !== 'security.audit.compact' || audit.projectId !== authorization.authorityProjectId || audit.subject?.type !== authorization.actor.type || audit.subject.id !== authorization.actor.id || audit.outcome !== 'allowed' || audit.reasons.length !== 1 || audit.reasons[0] !== 'instance_authority_security_audit_compaction' || audit.fence?.projectVersion !== authorization.fence.projectVersion || audit.fence.bindingVersion !== authorization.fence.bindingVersion || input.eligibleBeforeMs + input.retentionMs > audit.occurredAtMs ) { throw new InvalidLocalSecurityAuditRetentionValueError( 'compaction audit or retention fence is invalid', ); } return Object.freeze({ ...input, authorization, audit, }); } function sameCommand( record: Readonly, audit: Readonly, input: Readonly, ): boolean { return ( record.mutationId === input.mutationId && record.requestId === input.requestId && record.authorityProjectId === input.authorization.authorityProjectId && record.retentionMs === input.retentionMs && record.eligibleBeforeMs === input.eligibleBeforeMs && record.batchLimit === input.limit && sameSecurityAuditSemantic(audit, input.audit) ); } export class LocalSqliteSecurityAuditRetentionRepository implements LocalSecurityAuditRetentionRepository { constructor( private readonly authority: LocalSqliteOperationAuthority, private readonly beforeCompaction: () => void, private readonly maxBatchSize: number, ) { if ( !(authority instanceof LocalSqliteOperationAuthority) || typeof beforeCompaction !== 'function' || !Number.isSafeInteger(maxBatchSize) || maxBatchSize < 1 || maxBatchSize > 512 ) { throw new TypeError( 'Local SQLite security audit retention dependencies are invalid', ); } } record(value: SecurityAuditRecord): Promise { return new LocalSqliteSecurityAuthorityStore(this.authority).record(value); } resolveCompaction( mutationId: string, ): Promise | null> { if (!UUID_V4_PATTERN.test(mutationId)) { throw new InvalidLocalSecurityAuditRetentionValueError( 'mutation identity is invalid', ); } return this.authority.enqueue( async () => { try { const row = this.authority.client .prepare( `SELECT ${COMPACTION_SELECT} FROM "QingLong3SecurityAuditCompactions" AS compaction WHERE compaction."mutation_id" = ?`, ) .get(mutationId) as Row | undefined; return row ? compactionFromRow(row) : null; } catch (error) { if (error instanceof InvalidLocalSecurityAuditRetentionValueError) { throw error; } throw new LocalSecurityAuditRetentionUnavailableError(); } }, () => new LocalSecurityAuditRetentionUnavailableError(), ); } compactAuthorized( input: CompactAuthorizedLocalSecurityAuditCommand, ): Promise { const value = command(input, this.maxBatchSize); return this.authority.enqueue( async () => { const client = this.authority.client; client.exec('BEGIN IMMEDIATE'); try { assertLocalSecurityAuditInstanceOwnerInTransaction( this.authority, value.authorization, this.beforeCompaction, () => new LocalSecurityAuditRetentionAuthorizationFenceConflictError(), ); const replay = client .prepare( `SELECT ${COMPACTION_SELECT}, ${LOCAL_SECURITY_AUDIT_JOIN_SELECT} FROM "QingLong3SecurityAuditCompactions" AS compaction JOIN "QingLong3SecurityAuditEvents" AS audit ON audit."event_id" = compaction."audit_event_id" WHERE compaction."mutation_id" = ?`, ) .get(value.mutationId) as Row | undefined; if (replay) { const record = compactionFromRow(replay); const audit = localSecurityAuditFromRow(replay); if (!sameCommand(record, audit, value)) { throw new LocalSecurityAuditCompactionMutationConflictError(); } client.exec('COMMIT'); return Object.freeze({ status: 'existing' as const, record, audit, }); } const rows = client .prepare( `SELECT ${LOCAL_SECURITY_AUDIT_SELECT} FROM "QingLong3SecurityAuditEvents" AS candidate WHERE candidate."occurred_at_ms" < ? AND ( candidate."outcome" <> 'allowed' OR candidate."operation_id" IN ( 'identity.inspect', 'credential.inspect', 'policy.project.inspect', 'policy.project.list', 'policy.role_binding.inspect', 'policy.role_binding.list', 'security.audit.list' ) ) AND NOT EXISTS ( SELECT 1 FROM "QingLong3ApiCredentialAdministrationMutations" AS ref WHERE ref."audit_event_id" = candidate."event_id" ) AND NOT EXISTS ( SELECT 1 FROM "QingLong3ApiCredentialDeliveryAcknowledgements" AS ref WHERE ref."audit_event_id" = candidate."event_id" ) AND NOT EXISTS ( SELECT 1 FROM "QingLong3IdentityAdministrationMutations" AS ref WHERE ref."audit_event_id" = candidate."event_id" ) AND NOT EXISTS ( SELECT 1 FROM "QingLong3LegacyAdoptions" AS ref WHERE ref."audit_event_id" = candidate."event_id" ) AND NOT EXISTS ( SELECT 1 FROM "QingLong3LocalIdentityProvisionings" AS ref WHERE ref."audit_event_id" = candidate."event_id" ) AND NOT EXISTS ( SELECT 1 FROM "QingLong3LocalOwnerBootstrapChallenges" AS ref WHERE ref."issue_audit_event_id" = candidate."event_id" OR ref."claim_audit_event_id" = candidate."event_id" ) AND NOT EXISTS ( SELECT 1 FROM "QingLong3LocalOwnerCredentialRecoveries" AS ref WHERE ref."issue_audit_event_id" = candidate."event_id" OR ref."complete_audit_event_id" = candidate."event_id" ) AND NOT EXISTS ( SELECT 1 FROM "QingLong3LocalOwnerDeliveryAcknowledgementGc" AS ref WHERE ref."audit_event_id" = candidate."event_id" ) AND NOT EXISTS ( SELECT 1 FROM "QingLong3LocalOwnerPepperMaterialGc" AS ref WHERE ref."prepare_audit_event_id" = candidate."event_id" OR ref."complete_audit_event_id" = candidate."event_id" ) AND NOT EXISTS ( SELECT 1 FROM "QingLong3PluginPackageAdmissionReceipts" AS ref WHERE ref."audit_event_id" = candidate."event_id" ) AND NOT EXISTS ( SELECT 1 FROM "QingLong3ProjectAdministrationMutations" AS ref WHERE ref."audit_event_id" = candidate."event_id" ) AND NOT EXISTS ( SELECT 1 FROM "ToolExecutionAuditReceipts" AS ref WHERE ref."event_id" = candidate."event_id" ) AND NOT EXISTS ( SELECT 1 FROM "ToolExecutionStartBarriers" AS ref WHERE ref."audit_event_id" = candidate."event_id" ) AND NOT EXISTS ( SELECT 1 FROM "QingLong3SecurityAuditCompactions" AS ref WHERE ref."audit_event_id" = candidate."event_id" ) ORDER BY candidate."occurred_at_ms" ASC, candidate."event_id" ASC LIMIT ?`, ) .all(value.eligibleBeforeMs, value.limit) as Row[]; const records = Object.freeze( rows.map((row) => localSecurityAuditFromRow(row)), ); const payload = localSecurityAuditCompactionPayload(records); const firstRecord = records.at(0); const lastRecord = records.at(-1); const record: Readonly = Object.freeze({ mutationId: value.mutationId, requestId: value.requestId, authorityProjectId: value.authorization.authorityProjectId, retentionMs: value.retentionMs, eligibleBeforeMs: value.eligibleBeforeMs, batchLimit: value.limit, deletedCount: records.length, deletedPayloadBytes: payload.payloadBytes, first: firstRecord ? Object.freeze({ occurredAtMs: firstRecord.occurredAtMs, eventId: firstRecord.eventId, }) : null, last: lastRecord ? Object.freeze({ occurredAtMs: lastRecord.occurredAtMs, eventId: lastRecord.eventId, }) : null, recordsDigest: payload.recordsDigest, createdAtMs: value.audit.occurredAtMs, }); insertLocalSecurityAudit(client, value.audit); client .prepare( `INSERT INTO "QingLong3SecurityAuditCompactions" ( "mutation_id", "request_id", "authority_project_id", "retention_ms", "eligible_before_ms", "batch_limit", "deleted_count", "deleted_payload_bytes", "first_occurred_at_ms", "first_event_id", "last_occurred_at_ms", "last_event_id", "records_digest", "audit_event_id", "created_at_ms" ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, ) .run( record.mutationId, record.requestId, record.authorityProjectId, record.retentionMs, record.eligibleBeforeMs, record.batchLimit, record.deletedCount, record.deletedPayloadBytes, record.first?.occurredAtMs ?? null, record.first?.eventId ?? null, record.last?.occurredAtMs ?? null, record.last?.eventId ?? null, record.recordsDigest, value.audit.eventId, record.createdAtMs, ); if (records.length > 0) { const placeholders = records.map(() => '?').join(','); const deleted = client .prepare( `DELETE FROM "QingLong3SecurityAuditEvents" WHERE "event_id" IN (${placeholders})`, ) .run(...records.map((candidate) => candidate.eventId)); if (deleted.changes !== records.length) { throw new LocalSecurityAuditRetentionUnavailableError(); } } client.exec('COMMIT'); return Object.freeze({ status: 'inserted' as const, record, audit: value.audit, }); } catch (error) { if (client.isTransaction) client.exec('ROLLBACK'); if ( error instanceof InvalidLocalSecurityAuditRetentionValueError || error instanceof LocalSecurityAuditRetentionAuthorizationFenceConflictError || error instanceof LocalSecurityAuditCompactionMutationConflictError || error instanceof SecurityAuditUnavailableError ) { throw error; } throw new LocalSecurityAuditRetentionUnavailableError(); } }, () => new LocalSecurityAuditRetentionUnavailableError(), ); } }