import type { UnifiedResultProjection } from '../domain/unified-result'; import { POLICY_CLASSIFICATION_LABELS } from '../domain/policy-status-labels'; import { discloseResultDbProjection } from '../domain/result-db-disclosure'; import { privacySafeFinding, privacySafeText } from '../domain/privacy-safe-finding'; import { canonicalJson } from '../shared/canonical-json'; import type { RegulatoryFindingQuery } from './regulatory-rag'; import { buildRegulatoryFindingQuery } from './regulatory-query'; const SAFE_ENVIRONMENT_PROPERTIES = new Set([ 'domain.controller_count', 'domain.functional_level', 'domain.recycle_bin_enabled', 'forest.functional_level', 'logging.wef.configuration_count', 'operating_system.count', 'operating_system.name' ]); export function privacySafeResultJsonFileName(generatedAt: string): string { const timestamp = generatedAt.replace(/\.\d{3}Z$/, 'Z').replaceAll(':', '-'); return `assessor-result-db-privacy-safe-${timestamp}.json`; } function stableResultKey(item: UnifiedResultProjection): string { return [item.sourceKind, item.sourceTool ?? '', item.sourceRuleId ?? '', item.resultProjectionId].join('\u0000'); } /** The exact JSON the AI overview sends to the model, in its deterministic order. */ export function aiOverviewInputJson(items: UnifiedResultProjection[]): string { return resultsToPrivacySafeExportJson(items.slice().sort((left, right) => stableResultKey(left).localeCompare(stableResultKey(right)))); } export interface ComplianceAiExport { schemaVersion: 2; /** Exactly what the AI overview receives for these rows (it splits the same object into chunks when it is large). */ aiOverviewInput: unknown; /** Each distinct finding query the ISO mapping model receives, with the Result DB rows that produce it. */ isoMappingQueries: Array<{ query: RegulatoryFindingQuery; resultProjectionIds: string[] }>; } /** * The Result DB `Export JSON` of the Compliance Engine: a 1:1 copy of what its AI features receive, * built by the same functions they use. The ISO model additionally receives the retrieved candidate * controls, which depend on the index and are not part of this offline export. */ export function complianceAiExportJson(items: UnifiedResultProjection[]): string { const queries = new Map(); for (const item of items.slice().sort((left, right) => left.resultProjectionId.localeCompare(right.resultProjectionId))) { const query = buildRegulatoryFindingQuery(item); if (!query) continue; const key = canonicalJson(query); const entry = queries.get(key) ?? { query, resultProjectionIds: [] }; entry.resultProjectionIds.push(item.resultProjectionId); queries.set(key, entry); } const value: ComplianceAiExport = { schemaVersion: 2, aiOverviewInput: JSON.parse(aiOverviewInputJson(items)) as unknown, isoMappingQueries: [...queries.values()] }; return JSON.stringify(value, null, 2); } export function resultsToPrivacySafeExportJson(items: UnifiedResultProjection[]): string { // A raw GPO observation is context for its rolled-up comparison, not a finding of its own. const findings = items.filter(item => item.sourceKind !== 'environment-observation' && item.sourceKind !== 'policy-observation'); const policyObservations = items.filter(item => item.sourceKind === 'policy-observation'); const policyFindings = findings.filter(item => item.sourceKind === 'policy-evaluation'); const environment = items.filter(item => item.sourceKind === 'environment-observation'); const actionable = findings.filter(item => item.isDeviation || item.isIoe || ['fail', 'warning', 'execution_error'].includes(item.outcomeNormalized)); const notRun = findings.filter(item => item.outcomeNormalized === 'not_run'); return JSON.stringify({ schemaVersion: 1, summary: { totalResults: items.length, findingResults: findings.length, environmentResults: environment.length, policyObservationResults: policyObservations.length, actionableFindings: actionable.length, notRunChecks: notRun.length, findingOutcomes: countBy(findings, item => item.outcomeNormalized), findingSeverities: countBy(findings, item => item.severityNormalized), findingTools: countBy(findings, item => item.sourceTool ?? 'unknown'), policyStatuses: countBy(policyFindings, policyStatus), environmentStates: countBy(environment, item => item.environmentState ?? 'unknown') }, findings: actionable.map(item => ({ ...privacySafeFinding(item), ...privacySafePolicy(item) })), notRunChecks: notRun.map(item => ({ id: item.sourceRuleId ?? null, title: privacySafeText(item.title), severity: item.severityNormalized, resultType: item.resultType, tool: item.sourceTool ?? 'unknown' })), environmentFacts: groupedEnvironmentFacts(environment) }, null, 2); } function policyStatus(item: UnifiedResultProjection): string { return item.policyClassification ? POLICY_CLASSIFICATION_LABELS[item.policyClassification] : 'unknown'; } /** GPO comparison facts under the Result DB disclosure policy: the customer value stays withheld. */ function privacySafePolicy(item: UnifiedResultProjection): { policy?: Record } { if (item.sourceKind !== 'policy-evaluation') return {}; const disclosed = discloseResultDbProjection(item); const policy = { status: policyStatus(disclosed), baseline: disclosed.policyBaselineLabel, expectedValue: disclosed.policyBaselineValue, comparison: disclosed.policyComparisonRelation, customerState: disclosed.policyCustomerState, scope: disclosed.policyScope, delivery: disclosed.policyDeliveryMechanism, diagnostics: disclosed.policyDiagnosticCodes?.length ? disclosed.policyDiagnosticCodes : undefined }; return { policy: Object.fromEntries(Object.entries(policy).filter(([, value]) => value !== undefined)) }; } function groupedEnvironmentFacts(items: UnifiedResultProjection[]) { const grouped = new Map(); items.forEach(item => { const property = copiedEnvironmentProperty(item); if (!property) return; const fact = { title: privacySafeText(item.title), property, value: item.environmentNormalizedValue ?? null, state: item.environmentState ?? 'unknown', tool: item.sourceTool ?? 'unknown' }; const key = JSON.stringify(fact); const existing = grouped.get(key); if (existing) existing.count += 1; else grouped.set(key, { ...fact, count: 1 }); }); return [...grouped.values()]; } function countBy(items: UnifiedResultProjection[], valueOf: (item: UnifiedResultProjection) => string): Record { const counts: Record = {}; items.forEach(item => { const value = valueOf(item); counts[value] = (counts[value] ?? 0) + 1; }); return counts; } function copiedEnvironmentProperty(item: UnifiedResultProjection): string | undefined { if (item.sourceKind !== 'environment-observation' || !item.environmentProperty) return undefined; if (SAFE_ENVIRONMENT_PROPERTIES.has(item.environmentProperty) || item.environmentProperty.endsWith('.row_count')) { return item.environmentProperty; } const aggregateAccountValue = item.environmentValueType === 'number' && ['user-information.account-analysis', 'computer-information.account-analysis'].includes(item.environmentSubjectType ?? ''); return aggregateAccountValue ? item.environmentProperty : undefined; }