137 lines
8.7 KiB
TypeScript
137 lines
8.7 KiB
TypeScript
import { createElement } from 'react';
|
|
import { renderToStaticMarkup } from 'react-dom/server';
|
|
import { describe, expect, it } from 'vitest';
|
|
import type { FindingRecord } from '../../src/domain/finding';
|
|
import { findingWebSearch } from '../../src/profiles/public-search/search';
|
|
import catalog from '../../src/profiles/public-search/catalog.json';
|
|
import { FindingReader } from '../../src/features/finding-reader/FindingReader';
|
|
|
|
function record(title: string, values: Record<string, string> = {}): FindingRecord {
|
|
return { id: 'synthetic', title, index: 0, filled: [], meta: [], fields: Object.entries(values).map(([label, value], index) => ({ label, value, index })) };
|
|
}
|
|
|
|
describe('tool-independent public AI search', () => {
|
|
it('resolves native policy identity, location and bounded benchmark target', () => {
|
|
const result = findingWebSearch(record('system access · minimumpasswordlength', { 'Baseline value': '{"kind":"number","value":14}', Scope: 'computer', Baseline: 'Windows Server 2022 Security Baseline', Status: 'Fail' }), 'group-policy');
|
|
expect(result.specificity).toBe('setting');
|
|
expect(result.query).toContain('MinimumPasswordLength');
|
|
expect(result.query).toContain('Account Policies');
|
|
expect(result.query).toContain('Selected benchmark target: 14');
|
|
expect(result.query).toContain('Windows Server 2022');
|
|
});
|
|
|
|
it('resolves public rights without sending assigned identities', () => {
|
|
const result = findingWebSearch(record('privilege rights · sebackupprivilege', { 'Baseline value': '["S-1-5-21-123-456-789-1001"]', 'Configured Value': 'SYNTHETIC\\PrivateUser' }), 'group-policy');
|
|
expect(result.query).toContain('SeBackupPrivilege');
|
|
expect(result.query).toContain('User Rights Assignment');
|
|
expect(result.query).not.toContain('S-1-5-21');
|
|
expect(result.query).not.toContain('PrivateUser');
|
|
});
|
|
|
|
it('matches full public registry targets and refuses private extensions', () => {
|
|
const path = 'HKEY_LOCAL_MACHINE\\SOFTWARE\\Policies\\Microsoft\\Windows Defender';
|
|
const result = findingWebSearch(record('PUAProtection', { 'Registry path': path, 'Baseline value': '1' }), 'group-policy');
|
|
expect(result.specificity).toBe('setting');
|
|
expect(result.query).toContain('Public registry target: SOFTWARE\\Policies\\Microsoft\\Windows Defender\\PUAProtection');
|
|
expect(result.query).toContain('not the selected SCT baseline');
|
|
const privateResult = findingWebSearch(record('PUAProtection', { 'Registry path': path + '\\PrivateTenant' }), 'group-policy');
|
|
expect(privateResult.query).not.toContain('Public registry target:');
|
|
expect(privateResult.query).not.toContain('PrivateTenant');
|
|
});
|
|
|
|
it('resolves ASR GUIDs only within the supported registry family', () => {
|
|
const id = '5beb7efe-fd9a-4556-801d-275e5ffc04cc';
|
|
const result = findingWebSearch(record(id, { 'Registry path': 'SOFTWARE\\Policies\\Microsoft\\Windows Defender\\Windows Defender Exploit Guard\\ASR\\Rules' }), 'group-policy');
|
|
expect(result.query).toContain(id);
|
|
expect(result.query).toContain('Attack Surface Reduction');
|
|
expect(findingWebSearch(record(id), 'group-policy').query).not.toContain(id);
|
|
});
|
|
|
|
it.each(['maester', 'pingcastle'])('uses verified public IDs for %s without dynamic instance content', tool => {
|
|
const id = tool === 'maester' ? 'MT.1006' : 'A-AdminSDHolder';
|
|
const result = findingWebSearch(record(`${id}: private-user@synthetic.invalid`, { Description: 'PrivateTenant' }), tool);
|
|
expect(result.specificity).toBe('rule');
|
|
expect(result.query).toContain(`Public rule ID: ${id}`);
|
|
expect(result.query).not.toContain('private-user');
|
|
expect(result.query).not.toContain('PrivateTenant');
|
|
});
|
|
|
|
it('resolves the public CISA Exchange audit-retention rule without calling it Windows audit policy', () => {
|
|
const result = findingWebSearch(record(
|
|
'Audit logs SHALL be maintained for at least the minimum duration dictated by OMB M-21-31 (Appendix C).',
|
|
{ ID: 'CISA.MS.EXO.17.3', Status: 'Failed', Tag: '["MS.EXO","CISA.MS.EXO.17.3"]' }
|
|
), 'maester');
|
|
expect(result.specificity).toBe('rule');
|
|
expect(result.query).toContain('Public rule ID: CISA.MS.EXO.17.3');
|
|
expect(result.query).toContain('https://maester.dev/docs/tests/CISA.MS.EXO.17.3/');
|
|
expect(result.query).toContain('Microsoft Purview audit log retention');
|
|
expect(result.query).not.toContain('Windows audit policy');
|
|
expect(result.query).not.toContain('exact public rule is not resolved');
|
|
});
|
|
|
|
it('links a validated Entra recommendation to its Maester parent documentation', () => {
|
|
const id = 'MT.1024.applicationCredentialExpiry';
|
|
const result = findingWebSearch(record('Renew expiring application credentials.', { ID: id }), 'maester');
|
|
expect(result.specificity).toBe('rule');
|
|
expect(result.query).toContain(`Public rule ID: ${id}`);
|
|
expect(result.query).toContain('https://maester.dev/docs/tests/MT.1024/');
|
|
expect(result.query).not.toContain(`https://maester.dev/docs/tests/${id}/`);
|
|
});
|
|
|
|
it.each(['cis-cat', 'purple-knight', 'generic'])('adds useful fixed technical concepts for %s', tool => {
|
|
const result = findingWebSearch(record('SMB signing on private-host.synthetic.invalid', { Description: 'SMB signing is required for PrivateTenant', Benchmark: 'Windows Server 2022 PrivateTenant' }), tool);
|
|
expect(result.query).toContain('SMB signing requirements');
|
|
expect(result.query).toContain('Windows Server 2022');
|
|
expect(result.query).not.toContain('private-host');
|
|
expect(result.query).not.toContain('PrivateTenant');
|
|
});
|
|
|
|
it('supports a mixed-tool workspace and labels unresolved rules honestly', () => {
|
|
const mixed = record('MT.1006', { 'Source: Scan': 'Maester · Synthetic' });
|
|
expect(findingWebSearch(mixed, 'generic').query).toContain('Maester');
|
|
const result = findingWebSearch(record('Unknown private rule', { Description: 'person@example.invalid' }), 'cis-cat');
|
|
expect(result.specificity).toBe('topic');
|
|
expect(result.query).toContain('exact public rule is not resolved');
|
|
expect(result.query).not.toContain('person@');
|
|
});
|
|
|
|
it('never forwards free text, raw descriptions, source URLs, paths or customer values', () => {
|
|
const secret = 'Synthetic Person private@example.invalid 10.20.30.40 \\\\private\\share https://private.invalid';
|
|
const item = record(secret, Object.fromEntries(['Description', 'Remediation', 'Configured Value', 'Sources', 'Owner', 'Baseline value', 'Registry path', 'Rule ID', 'Search URL'].map(k => [k, secret])));
|
|
for (const tool of ['group-policy', 'cis-cat', 'maester', 'pingcastle', 'purple-knight', 'generic']) {
|
|
const result = findingWebSearch(item, tool);
|
|
expect(result.query).not.toMatch(/Synthetic Person|private@|10\.20|private\\|private\.invalid/);
|
|
expect(new URL(result.url).searchParams.get('q')).toBe(result.query);
|
|
expect(new URL(result.url).searchParams.get('udm')).toBe('50');
|
|
}
|
|
});
|
|
|
|
it('rejects malformed, string, unbounded or ambiguous benchmark values', () => {
|
|
for (const value of ['"private-person"', '99999999', '{"kind":"number","value":1,"secret":"private"}', '1 private']) {
|
|
const item = record('system access · MinimumPasswordLength', { 'Baseline value': value });
|
|
expect(findingWebSearch(item, 'group-policy').query).not.toContain('Selected benchmark target:');
|
|
}
|
|
const item = record('system access · MinimumPasswordLength', { 'Baseline value': '14' });
|
|
item.fields.push({ label: 'Baseline value', value: '12', index: 1 });
|
|
expect(findingWebSearch(item, 'group-policy').query).not.toContain('Selected benchmark target:');
|
|
});
|
|
|
|
it.each(['cis-cat', 'purple-knight', 'maester', 'pingcastle', 'group-policy', 'generic'])('renders an explicit protected search link for %s', toolId => {
|
|
const item = record('SMB signing');
|
|
const html = renderToStaticMarkup(createElement(FindingReader, { record: item, title: item.title, toolId, linkedSheets: [], resultReferences: [], search: '', position: 0, recordCount: 1, onPrevious() {}, onNext() {}, onReview() {} }));
|
|
expect(html).toContain('>AI search</a>');
|
|
expect(html).toContain('rel="noopener noreferrer"');
|
|
expect(html).toContain('referrerPolicy="no-referrer"');
|
|
expect(html).toContain('data-search-specificity=');
|
|
});
|
|
|
|
it('keeps a source-attributed public identifier catalog without report prose', () => {
|
|
expect(catalog.registryTargets.length).toBeGreaterThan(60);
|
|
expect(catalog.privilegeIds.length).toBe(45);
|
|
expect(catalog.maesterIds.length).toBeGreaterThan(150);
|
|
expect(catalog.pingcastleIds.length).toBeGreaterThan(40);
|
|
expect(catalog.asrIds).toContain('5beb7efe-fd9a-4556-801d-275e5ffc04cc');
|
|
for (const list of [catalog.registryTargets, catalog.privilegeIds, catalog.maesterIds, catalog.pingcastleIds, catalog.asrIds]) expect(new Set(list).size).toBe(list.length);
|
|
});
|
|
});
|