167 lines
7.5 KiB
TypeScript
167 lines
7.5 KiB
TypeScript
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<string, ComplianceAiExport['isoMappingQueries'][number]>();
|
|
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<string, unknown> } {
|
|
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<string, {
|
|
title: string;
|
|
property: string;
|
|
value: unknown;
|
|
state: string;
|
|
tool: string;
|
|
count: number;
|
|
}>();
|
|
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<string, number> {
|
|
const counts: Record<string, number> = {};
|
|
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;
|
|
}
|