Assessor_2.0/src/compliance/regulatory-query.ts

77 lines
3.9 KiB
TypeScript

import type { RegulatoryFindingQuery } from './regulatory-rag';
import { REGULATORY_FINDING_QUERY_SCHEMA_VERSION } from './regulatory-rag';
import type { UnifiedResultProjection } from '../domain/unified-result';
import { privacySafeFinding } from '../domain/privacy-safe-finding';
import { discloseResultDbProjection } from '../domain/result-db-disclosure';
import catalog from '../profiles/public-search/catalog.json';
import { publicSecurityTopics } from '../profiles/public-search/public-security-topics';
const MAXIMUM_TITLE_CHARACTERS = 500;
const MAXIMUM_DETAIL_CHARACTERS = 1_200;
const MAXIMUM_RECOMMENDATION_CHARACTERS = 600;
const publicTools: Record<string, string> = {
'cis-cat': 'CIS-CAT / CIS Benchmarks',
gpos: 'Microsoft Group Policy',
maester: 'Maester',
pingcastle: 'PingCastle',
'purple-knight': 'Semperis Purple Knight'
};
const publicRuleIds: Record<string, readonly string[]> = {
maester: catalog.maesterIds,
pingcastle: catalog.pingcastleIds
};
/**
* Builds an embedding query from the finding's rule-level check text — its title,
* description and recommendation projected through the shared privacy boundary
* ({@link privacySafeFinding}). That text describes the check, not the tenant: the
* result-table / occurrence data (affectedAsset, per-object values) is never read,
* and email addresses are redacted. Every scanner finding or GPO baseline comparison
* with a check title maps; the configured GPO customer value is never part of that text;
* catalog rule IDs and fixed public topics only sharpen the query when present.
*/
export function buildRegulatoryFindingQuery(finding: UnifiedResultProjection): RegulatoryFindingQuery | undefined {
if (!isRegulatoryMappableResult(finding)) return undefined;
// A GPO comparison crosses only through the Result DB disclosure policy, which withholds the customer value.
const safe = privacySafeFinding(finding.sourceKind === 'policy-evaluation' ? discloseResultDbProjection(finding) : finding);
const title = safe.title.trim().slice(0, MAXIMUM_TITLE_CHARACTERS);
if (!title) return undefined;
const toolId = finding.sourceTool?.trim().toLowerCase() ?? '';
const publicTool = publicTools[toolId];
const publicRuleId = recognizedRuleId(toolId, finding.sourceRuleId);
const detail = safe.description?.slice(0, MAXIMUM_DETAIL_CHARACTERS);
const recommendation = safe.recommendation?.slice(0, MAXIMUM_RECOMMENDATION_CHARACTERS);
const topics = publicSecurityTopics([title, detail, recommendation].filter(Boolean).join('\n'));
const query = [
'Retrieve regulatory controls whose normative requirements directly address this security finding.',
publicTool && `Public assessment tool: ${publicTool}.`,
publicRuleId && `Public rule ID: ${publicRuleId}.`,
`Security check: ${title}.`,
detail && `Check detail: ${detail}.`,
recommendation && `Recommended action: ${recommendation}.`,
topics.length && `Public technical topics: ${topics.join('; ')}.`,
'Prefer controls with direct requirement text over merely related guidance. Do not infer customer scope, assets, configuration values, or compliance status.'
].filter(Boolean).join(' ');
return {
schemaVersion: REGULATORY_FINDING_QUERY_SCHEMA_VERSION,
query,
specificity: publicRuleId ? 'rule' : 'topic',
...(publicTool ? { publicTool } : {}),
...(publicRuleId ? { publicRuleId } : {}),
publicTopics: topics
};
}
function recognizedRuleId(toolId: string, value: string | undefined): string | undefined {
const candidate = value?.trim();
if (!candidate) return undefined;
return publicRuleIds[toolId]?.find(id => id.toLowerCase() === candidate.toLowerCase());
}
/** Rule-level results only: raw GPO observations and environment facts describe context, not a check. */
export function isRegulatoryMappableResult(finding: UnifiedResultProjection): boolean {
return finding.sourceKind === 'scanner-finding' || finding.sourceKind === 'policy-evaluation';
}