68 lines
3.2 KiB
TypeScript
68 lines
3.2 KiB
TypeScript
import type { RegulatoryFindingQuery } from '../../domain/regulatory-rag';
|
|
import { REGULATORY_FINDING_QUERY_SCHEMA_VERSION } from '../../domain/regulatory-rag';
|
|
import type { UnifiedResultProjection } from '../../domain/unified-result';
|
|
import { privacySafeFinding } from '../../domain/privacy-safe-finding';
|
|
import catalog from './catalog.json';
|
|
import { publicSecurityTopics } from './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',
|
|
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 with a check title maps;
|
|
* catalog rule IDs and fixed public topics only sharpen the query when present.
|
|
*/
|
|
export function buildRegulatoryFindingQuery(finding: UnifiedResultProjection): RegulatoryFindingQuery | undefined {
|
|
if (finding.sourceKind !== 'scanner-finding') return undefined;
|
|
const safe = privacySafeFinding(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());
|
|
}
|