568 lines
37 KiB
TypeScript
568 lines
37 KiB
TypeScript
import { describe, expect, it } from 'vitest';
|
||
import type { UnifiedResultProjection } from '../../src/domain/unified-result';
|
||
import {
|
||
discloseResultDbProjection, discloseResultDbProjectionWithTrace, removeResultDbScopeIdentifiers, RESULT_DB_DISCLOSURE_POLICY_VERSION
|
||
} from '../../src/domain/result-db-disclosure';
|
||
|
||
function unsafeScanner(): UnifiedResultProjection {
|
||
return {
|
||
resultProjectionId: 'scanner-finding:private-occurrence', sourceKind: 'scanner-finding',
|
||
sourceResultId: 'private-occurrence', projectId: 'private-company', assessmentId: 'private-assessment',
|
||
collectionRunId: 'private-run', scanRunId: 'private-run', sourceTool: 'maester', sourceRuleId: 'MT.1001',
|
||
sourceToolVersion: 'private-build', sourceProfileId: 'maester', canonicalDefinitionId: 'private-definition',
|
||
importArtifactId: 'private-artifact', sourceViewId: 'private-view', sourceLocator: { objectPath: 'private/path' },
|
||
title: 'Kerberos preauthentication is disabled for person@private-company.example',
|
||
description: 'Private Company GmbH tenant permits legacy authentication.',
|
||
recommendation: 'Disable legacy authentication and contact Jane Doe.', affectedAsset: 'private-company.example',
|
||
severityOriginal: 'Private severity', severityNormalized: 'high', outcomeOriginal: 'Private outcome',
|
||
outcomeNormalized: 'fail', technicalExecutionState: 'completed', resultType: 'compliance_check',
|
||
isIoe: false, isDeviation: true, lifecycleState: 'unknown', mappingConfidence: 1,
|
||
reviewStatus: 'open', reviewOwner: 'Jane Doe', reviewDueDate: '2026-10-01',
|
||
sourceObservedAt: '2026-09-22T08:00:00.000Z', importedAt: '2026-09-22T08:00:00.000Z',
|
||
createdAt: '2026-09-22T08:00:00.000Z',
|
||
frameworkReferences: [],
|
||
categories: [{ namespace: 'private-company', categoryCode: 'private', categoryLabel: 'Private Company',
|
||
categoryType: 'source', isPrimary: true }]
|
||
};
|
||
}
|
||
|
||
describe(`Result DB disclosure ${RESULT_DB_DISCLOSURE_POLICY_VERSION}`, () => {
|
||
it('keeps useful known-scanner guidance while redacting customer identifiers', () => {
|
||
const disclosed = discloseResultDbProjection(unsafeScanner());
|
||
expect(disclosed).toMatchObject({
|
||
title: 'Kerberos preauthentication is disabled for [email]', sourceTool: 'maester', sourceRuleId: 'MT.1001',
|
||
description: '[organization] tenant permits legacy authentication.',
|
||
recommendation: 'Disable legacy authentication and contact [redacted].',
|
||
severityOriginal: 'high', outcomeOriginal: 'fail', importedAt: '1970-01-01T00:00:00.000Z',
|
||
categories: [{ namespace: 'source-tool', categoryCode: 'maester', categoryLabel: 'Maester' }]
|
||
});
|
||
const serialized = JSON.stringify(disclosed);
|
||
for (const privateValue of ['person@private-company.example', 'Private Company GmbH', 'Jane Doe',
|
||
'private-company.example', 'private/path', 'private-build', 'private-definition', 'private-artifact']) {
|
||
expect(serialized).not.toContain(privateValue);
|
||
}
|
||
});
|
||
|
||
it('withholds review status, disposition and priority from the Result DB projection', () => {
|
||
const disclosed = discloseResultDbProjection({
|
||
...unsafeScanner(), reviewDisposition: 'accepted', reviewPriority: 'high'
|
||
});
|
||
|
||
for (const key of ['reviewStatus', 'reviewDisposition', 'reviewPriority', 'reviewOwner', 'reviewDueDate',
|
||
'sourceToolVersion', 'sourceObservedAt', 'affectedAsset', 'sourceLocator']) {
|
||
expect(Object.keys(disclosed)).not.toContain(key);
|
||
}
|
||
expect(discloseResultDbProjection(disclosed)).toEqual(disclosed);
|
||
});
|
||
|
||
it('replaces environment fingerprints in scanner prose but keeps public technical tokens', () => {
|
||
const disclosed = discloseResultDbProjection({
|
||
...unsafeScanner(), sourceTool: 'pingcastle', sourceRuleId: 'A-NotEnoughDC',
|
||
title: 'The number of DCs is too small to provide redundancy: 1 DC',
|
||
description: 'Last AD backup 55 days ago on 2026-08-04 at 13:37; built-in admin used 0 day(s) ago. '
|
||
+ '15 accounts and 1.015 objects, 3,250 computers, 42% of users, observed 28.09.2026.',
|
||
recommendation: 'Disable NTLMv1 and TLS 1.0 on Windows Server 2016 and Windows 10, see MT.1024, '
|
||
+ 'CVE-2021-34527, T1558.003, KB5005413, event ID 4769, port 445, 64-bit and S-1-5-32-544.'
|
||
});
|
||
|
||
expect(disclosed.title).toBe('The number of DCs is too small to provide redundancy: [count] DC');
|
||
expect(disclosed.description).toBe('Last AD backup [duration] ago on [date] at [time]; built-in admin used [duration] ago. '
|
||
+ '[count] accounts and [count] objects, [count] computers, [count]% of users, observed [date].');
|
||
expect(disclosed.recommendation).toBe('Disable NTLMv1 and TLS 1.0 on Windows Server 2016 and Windows 10, see MT.1024, '
|
||
+ 'CVE-2021-34527, T1558.003, KB5005413, event ID 4769, port 445, 64-bit and S-1-5-32-544.');
|
||
});
|
||
|
||
it('keeps numbered remediation steps intact', () => {
|
||
const disclosed = discloseResultDbProjection({
|
||
...unsafeScanner(), sourceTool: 'purple-knight', title: 'Synthetic',
|
||
recommendation: '1. From the impacted resources, select 3 accounts.\n2) Remove the delegation.\n 10. Verify.'
|
||
});
|
||
expect(disclosed.recommendation).toBe('1. From the impacted resources, select [count] accounts.\n2) Remove the delegation.\n10. Verify.');
|
||
});
|
||
|
||
it('keeps inline ordered lists but not a lone count followed by a sentence', () => {
|
||
const disclosed = discloseResultDbProjection({
|
||
...unsafeScanner(), sourceTool: 'maester', title: 'Synthetic',
|
||
description: 'Open the admin center. 1. Select Users. 2. Choose 3 accounts. 3. Fill the form. Found 1. Next steps follow.',
|
||
recommendation: 'Enable the rules. 1) Outbound rules. 2) Network Restrictions: apply them.'
|
||
});
|
||
expect(disclosed.description).toBe('Open the admin center. 1. Select Users. 2. Choose [count] accounts. 3. Fill the form. Found [count]. Next steps follow.');
|
||
expect(disclosed.recommendation).toBe('Enable the rules. 1) Outbound rules. 2) Network Restrictions: apply them.');
|
||
});
|
||
|
||
it('handles source quirks: repeated step numbers, markdown links showing their URL, code and sentence joins', () => {
|
||
const disclosed = discloseResultDbProjection({
|
||
...unsafeScanner(), sourceTool: 'maester', title: 'Navigate to the Microsoft 365 admin center',
|
||
description: '1. Navigate to Portal [https://portal.azure.com](https://portal.azure.com). 2. Search Entra. 3. Expand Manage. '
|
||
+ '3. On the Properties page continue. 4. Save. Open [blade](https://entra.microsoft.com/#view/Microsoft_AAD_IAM/'
|
||
+ 'RecommendationDetails.ReactView/id/x). Run (Get-Policy).keyRestrictions.isEnforced and System.DateTime '
|
||
+ 'or Windows.Storage with ASP.NET. It affects any endpoint.The indicator stays. See aka.ms/mfa and '
|
||
+ 'test.mysmartlogon.com; contoso.It remains.'
|
||
});
|
||
expect(disclosed.title).toBe('Navigate to the Microsoft 365 admin center');
|
||
expect(disclosed.description).toBe('1. Navigate to Portal [https://portal.azure.com](https://portal.azure.com). 2. Search Entra. 3. Expand Manage. '
|
||
+ '3. On the Properties page continue. 4. Save. Open [blade](https://entra.microsoft.com/#view/Microsoft_AAD_IAM/'
|
||
+ 'RecommendationDetails.ReactView/id/x). Run (Get-Policy).keyRestrictions.isEnforced and System.DateTime '
|
||
+ 'or Windows.Storage with ASP.NET. It affects any endpoint.The indicator stays. See aka.ms/mfa and '
|
||
+ '[domain]; [domain] remains.');
|
||
});
|
||
|
||
it('keeps skipped step numbers, attribute flags and CISA section numbers', () => {
|
||
const disclosed = discloseResultDbProjection({
|
||
...unsafeScanner(), sourceTool: 'purple-knight', title: 'Synthetic',
|
||
description: '1. Create a group. 2. Open Entra. 3. Select Security. 3. Select Manage. 5. Under Admin, save. '
|
||
+ 'Accounts with adminCount=1 or the adminCount attribute set to 1 are protected; 4 accounts were found. '
|
||
+ 'See [CISA 4. Centralized Log Collection - MS.AAD.4.1v1].'
|
||
});
|
||
expect(disclosed.description).toBe('1. Create a group. 2. Open Entra. 3. Select Security. 3. Select Manage. 5. Under Admin, save. '
|
||
+ 'Accounts with adminCount=1 or the adminCount attribute set to 1 are protected; [count] accounts were found. '
|
||
+ 'See [CISA 4. Centralized Log Collection - MS.AAD.4.1v1].');
|
||
});
|
||
|
||
it('keeps the space before a generic domain suffix such as .onmicrosoft.com', () => {
|
||
const disclosed = discloseResultDbProjection({
|
||
...unsafeScanner(), sourceTool: 'maester', sourceRuleId: 'MT.1074',
|
||
title: 'No more than 100 outbound mails per day are sent using the .onmicrosoft.com domain',
|
||
description: 'Spam from \'.onmicrosoft.com\' addresses , and contoso.onmicrosoft.com stays hidden .'
|
||
});
|
||
expect(disclosed.title).toBe('No more than [count] outbound mails per day are sent using the .onmicrosoft.com domain');
|
||
expect(disclosed.description).toBe('Spam from \'.onmicrosoft.com\' addresses, and [domain] stays hidden.');
|
||
});
|
||
|
||
it('keeps recommended target values in recommendations only', () => {
|
||
const disclosed = discloseResultDbProjection({
|
||
...unsafeScanner(), sourceTool: 'pingcastle', title: 'Synthetic',
|
||
description: 'The value is set to 3 on this system.',
|
||
recommendation: 'RequireIntegrity and RequireMutualAuthentication should be set to 1. LDAPOwnerModify configured to 0.'
|
||
});
|
||
expect(disclosed.description).toBe('The value is set to [count] on this system.');
|
||
expect(disclosed.recommendation).toBe('RequireIntegrity and RequireMutualAuthentication should be set to 1. LDAPOwnerModify configured to 0.');
|
||
});
|
||
|
||
it('keeps documented vendor values in recommendations only, never an observed count beside them', () => {
|
||
const guidance = 'Set the ms-DS-MachineAccountQuota attribute on the domain NC head to 0 to disable it. '
|
||
+ 'It is recommended to set it to 1. Set it to zero (0). The expected new value is 00000000010000000002000000011. '
|
||
+ 'Add the flag by adding the number 1048576. Assign 1 as the Value; other values include 2 for Audit mode and 6 for Warn mode. '
|
||
+ 'Use get-adobject -ldapfilter "(admincount=1)" and a 20+ length password. It can be done through 2 methods. '
|
||
+ 'Note: only 10 will be listed below.';
|
||
const disclosed = discloseResultDbProjection({
|
||
...unsafeScanner(), sourceTool: 'pingcastle', title: 'Synthetic',
|
||
description: guidance, recommendation: `${guidance} Currently 12 accounts are affected; set 4 of them to 0.`
|
||
});
|
||
expect(disclosed.recommendation).toBe(`${guidance} Currently [count] accounts are affected; set [count] of them to [count].`);
|
||
expect(disclosed.description).not.toMatch(/(?<![\w.])(?:0|1|2|6|10|20|1048576)(?![\w.])/);
|
||
});
|
||
|
||
it('keeps public documentation links, file names, policy IDs and Graph permissions but redacts tenant references', () => {
|
||
const disclosed = discloseResultDbProjection({
|
||
...unsafeScanner(), sourceTool: 'maester', title: 'Synthetic',
|
||
recommendation: 'See [Microsoft Learn](https://learn.microsoft.com/en-us/entra/identity/overview) and '
|
||
+ 'https://github.com/cisagov/ScubaGear/blob/main/baselines/aad.md for MS.AAD.2.3 in the Entra admin center '
|
||
+ 'https://docs.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_logging?view=powershell-7 '
|
||
+ 'https://www.whitehouse.gov/wp-content/uploads/2022/01/M-22-09.pdf '
|
||
+ '(entra.microsoft.com). Grant Directory.ReadWrite.All and Policy.ReadWrite.AuthenticationMethod. '
|
||
+ 'Check system32\\logfiles\\firewall\\pfirewall.log. '
|
||
+ 'Tenant links https://contoso.sharepoint.com/sites/it, https://login.microsoftonline.com/contoso.onmicrosoft.com/oauth2, '
|
||
+ 'https://portal.azure.com/#@contoso.onmicrosoft.com/resource, https://learn.microsoft.com/?tenant=contoso.de, '
|
||
+ 'https://portal.azure.com/#blade/subscriptions/0f1e2d3c-4b5a-6978-8a9b-0c1d2e3f4a5b/resourceGroups/rg-contoso-prod/overview, '
|
||
+ 'https://entra.microsoft.com/#view/Apps/objectId/0f1e2d3c-4b5a-6978-8a9b-0c1d2e3f4a5b/appId/x '
|
||
+ 'and domains contoso.onmicrosoft.com, mail.contoso.de stay hidden.'
|
||
});
|
||
expect(disclosed.recommendation).toBe('See [Microsoft Learn](https://learn.microsoft.com/en-us/entra/identity/overview) and '
|
||
+ 'https://github.com/cisagov/ScubaGear/blob/main/baselines/aad.md for MS.AAD.2.3 in the Entra admin center '
|
||
+ 'https://docs.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_logging?view=powershell-7 '
|
||
+ 'https://www.whitehouse.gov/wp-content/uploads/2022/01/M-22-09.pdf '
|
||
+ '(entra.microsoft.com). Grant Directory.ReadWrite.All and Policy.ReadWrite.AuthenticationMethod. '
|
||
+ 'Check system32\\logfiles\\firewall\\pfirewall.log. '
|
||
+ 'Tenant links [url], [url], [url], [url], [url], [url] '
|
||
+ 'and domains [domain], [domain] stay hidden.');
|
||
});
|
||
|
||
it('keeps vendor thresholds in recommendations but not in observed titles or descriptions', () => {
|
||
const disclosed = discloseResultDbProjection({
|
||
...unsafeScanner(), sourceTool: 'pingcastle', title: 'Password length is at least 7 characters for 15 accounts',
|
||
description: 'Observed: at least 7 characters on 15 accounts inactive for more than 55 days.',
|
||
recommendation: 'A strong password is at least 8 characters, rotated every 180 days, removed when older than 90 days, '
|
||
+ 'with 15 or more characters for service accounts and no more than 5 attempts; 3 accounts are affected.'
|
||
});
|
||
expect(disclosed.title).toBe('Password length is at least [count] characters for [count] accounts');
|
||
expect(disclosed.description).toBe('Observed: at least [count] characters on [count] accounts inactive for more than [duration].');
|
||
expect(disclosed.recommendation).toBe('A strong password is at least 8 characters, rotated every 180 days, removed when older than 90 days, '
|
||
+ 'with 15 or more characters for service accounts and no more than 5 attempts; [count] accounts are affected.');
|
||
});
|
||
|
||
it('keeps public policy and registry paths and documented authorities instead of turning them into accounts', () => {
|
||
const disclosed = discloseResultDbProjection({
|
||
...unsafeScanner(), sourceTool: 'cis-cat', sourceRuleId: '2.3.10.7',
|
||
recommendation: 'Set the following UI path to Enabled:\n'
|
||
+ 'Computer Configuration\\Policies\\Windows Settings\\Security Settings\\Local Policies\\Security Options\\Network access: Remotely accessible registry paths\n'
|
||
+ 'The value is stored in HKLM\\SYSTEM\\CurrentControlSet\\Control\\SecurePipeServers\\Winreg\\AllowedExactPaths:Machine and '
|
||
+ 'System\\CurrentControlSet\\Control\\ProductOptions.\n'
|
||
+ 'Grant BUILTIN\\Administrators, NT SERVICE\\WdiServiceHost and RESTRICTED SERVICES\\PrintSpoolerService.\n'
|
||
+ 'Do not grant CONTOSO\\jane or CONTOSO\\svc-backup.'
|
||
});
|
||
expect(disclosed.recommendation).toBe('Set the following UI path to Enabled:\n'
|
||
+ 'Computer Configuration\\Policies\\Windows Settings\\Security Settings\\Local Policies\\Security Options\\Network access: Remotely accessible registry paths\n'
|
||
+ 'The value is stored in HKLM\\SYSTEM\\CurrentControlSet\\Control\\SecurePipeServers\\Winreg\\AllowedExactPaths:Machine and '
|
||
+ 'System\\CurrentControlSet\\Control\\ProductOptions.\n'
|
||
+ 'Grant BUILTIN\\Administrators, NT SERVICE\\WdiServiceHost and RESTRICTED SERVICES\\PrintSpoolerService.\n'
|
||
+ 'Do not grant [account] or [account].');
|
||
});
|
||
|
||
it('keeps Group Policy node paths, hard-wrapped registry paths, rule references and documented address ranges', () => {
|
||
const disclosed = discloseResultDbProjection({
|
||
...unsafeScanner(), sourceTool: 'cis-cat', sourceRuleId: '2.3.10.9',
|
||
description: 'Use the built-in security template of Group Policy (e.g. Windows Settings\\Security Settings). '
|
||
+ 'It depends on Network security: LAN Manager Authentication Level (Rule 2.3.11.7).',
|
||
recommendation: 'System\\CurrentControlSet\\Control\\Terminal Server System\\CurrentControlSet\\Control\\Terminal\n'
|
||
+ 'Server\\UserConfig System\\CurrentControlSet\\Control\\ContentIndex.\nCONTOSO\\jane must not be listed.'
|
||
});
|
||
expect(disclosed.description).toBe('Use the built-in security template of Group Policy (e.g. Windows Settings\\Security Settings). '
|
||
+ 'It depends on Network security: LAN Manager Authentication Level (Rule 2.3.11.7).');
|
||
expect(disclosed.recommendation).toBe('System\\CurrentControlSet\\Control\\Terminal Server System\\CurrentControlSet\\Control\\Terminal\n'
|
||
+ 'Server\\UserConfig System\\CurrentControlSet\\Control\\ContentIndex.\n[account] must not be listed.');
|
||
|
||
const firewall = discloseResultDbProjection({
|
||
...unsafeScanner(), sourceTool: 'pingcastle', sourceRuleId: 'S-FirewallScript', title: 'Synthetic',
|
||
recommendation: 'Allow local network access: 10.0.0.0 to 10.255.255.255, 192.168.0.0 to 192.168.255.255; '
|
||
+ 'block 0.0.0.0 to 9.255.255.255 11.0.0.0 to 126.255.255.255 128.0.0.1 to 172.15.255.255 172.32.0.0 to 192.167.255.255 '
|
||
+ '192.169.0.0 to 255.255.255.255; host 10.20.30.40 and subnet 10.20.30.0 to 10.20.30.255 observed.'
|
||
});
|
||
expect(firewall.recommendation).toBe('Allow local network access: 10.0.0.0 to 10.255.255.255, 192.168.0.0 to 192.168.255.255; '
|
||
+ 'block 0.0.0.0 to 9.255.255.255 11.0.0.0 to 126.255.255.255 128.0.0.1 to 172.15.255.255 172.32.0.0 to 192.167.255.255 '
|
||
+ '192.169.0.0 to 255.255.255.255; '
|
||
+ 'host [ip] and subnet [ip] to [ip] observed.');
|
||
});
|
||
|
||
it('keeps multi-part CIS numbers in titles and redacts only standalone IPv4 addresses', () => {
|
||
const disclosed = discloseResultDbProjection({
|
||
...unsafeScanner(), sourceTool: 'cis-cat', sourceRuleId: '18.9.20.1.1',
|
||
title: "18.9.20.1.1 (L2) Ensure 'Turn off downloading of print drivers over HTTP' is set to 'Enabled'",
|
||
description: 'Rule 18.9.20.1 applies; the server 10.20.30.40 and 999.1.1.1 were observed.'
|
||
});
|
||
expect(disclosed.title).toBe("18.9.20.1.1 (L2) Ensure 'Turn off downloading of print drivers over HTTP' is set to 'Enabled'");
|
||
expect(disclosed.description).toBe('Rule 18.9.20.1 applies; the server [ip] and 999.1.1.1 were observed.');
|
||
});
|
||
|
||
it('replaces compact durations such as 24h or 30d', () => {
|
||
const disclosed = discloseResultDbProjection({
|
||
...unsafeScanner(), sourceTool: 'pingcastle', title: 'Synthetic', description: 'Rotated 30d ago, checked every 24h, 5min, 2yrs.'
|
||
});
|
||
expect(disclosed.description).toBe('Rotated [duration] ago, checked every [duration], [duration], [duration].');
|
||
});
|
||
|
||
it('keeps registry-path GPO titles readable instead of treating them as DOMAIN\\account pairs', () => {
|
||
const disclosed = discloseResultDbProjection({
|
||
...unsafeScanner(), sourceKind: 'policy-evaluation', sourceTool: 'GPOs',
|
||
title: 'Software\\Policies\\Microsoft\\Windows NT\\Printers · RestrictDriverInstallationToAdministrators'
|
||
});
|
||
expect(disclosed.title).toBe('Software\\Policies\\Microsoft\\Windows NT\\Printers · RestrictDriverInstallationToAdministrators');
|
||
});
|
||
|
||
it('keeps documented Microsoft principals in the Soll value readable', () => {
|
||
const disclosed = discloseResultDbProjection({
|
||
...unsafeScanner(), sourceKind: 'policy-evaluation', sourceTool: 'GPOs', title: 'Privilege Rights · SeServiceLogonRight',
|
||
policyBaselineValue: { kind: 'principal-set', principals: ['*S-1-5-113', '*S-1-5-83-0', 'S-1-5-9',
|
||
'NT SERVICE\\WdiServiceHost', 'NT VIRTUAL MACHINE\\Virtual Machines', 'Window Manager\\Window Manager Group',
|
||
'CONTOSO\\svc-backup', 'S-1-5-21-1-2-3-1105'] }
|
||
});
|
||
expect(disclosed.policyBaselineValue).toEqual({ kind: 'principal-set', principals: ['*S-1-5-113', '*S-1-5-83-0', 'S-1-5-9',
|
||
'NT SERVICE\\WdiServiceHost', 'NT VIRTUAL MACHINE\\Virtual Machines', 'Window Manager\\Window Manager Group',
|
||
'[DOMAIN_PRINCIPAL_1]', 'S-1-5-21-[DOMAIN]-1105'] });
|
||
});
|
||
|
||
it('keeps public benchmark values in CIS-CAT prose', () => {
|
||
const disclosed = discloseResultDbProjection({
|
||
...unsafeScanner(), sourceTool: 'cis-cat', sourceRuleId: '1.1.4',
|
||
title: "(L1) Ensure 'Minimum password length' is set to '14 or more character(s)'",
|
||
recommendation: "Set 'Account lockout duration' to 15 or more minute(s)."
|
||
});
|
||
|
||
expect(disclosed.title).toBe("(L1) Ensure 'Minimum password length' is set to '14 or more character(s)'");
|
||
expect(disclosed.recommendation).toBe("Set 'Account lockout duration' to 15 or more minute(s).");
|
||
});
|
||
|
||
it('never discloses an unprotected number from non-benchmark scanner prose', () => {
|
||
const fingerprints = ['7 DC', '15 accounts', '55 days', '0 day(s) ago', '2026-08-04', '13:37', '1.015', '3,250'];
|
||
for (const tool of ['pingcastle', 'purple-knight', 'maester']) {
|
||
const disclosed = discloseResultDbProjection({
|
||
...unsafeScanner(), sourceTool: tool, title: `Synthetic ${fingerprints.join(', ')}`,
|
||
description: fingerprints.join(' / '), recommendation: `Review ${fingerprints.join(' and ')}`
|
||
});
|
||
const prose = [disclosed.title, disclosed.description, disclosed.recommendation].join('\n');
|
||
expect(prose.replace(/\[(?:count|duration|date|time)\]/g, '')).not.toMatch(/\d/);
|
||
}
|
||
});
|
||
|
||
it('discloses only the baseline family instead of the compared release', () => {
|
||
for (const [label, family] of [
|
||
['Microsoft Security Baseline Windows Server 2025 (2602)', 'Microsoft Security Baseline - Windows Server'],
|
||
['Windows 11 25H2 Security Baseline', 'Microsoft Security Baseline - Windows Client'],
|
||
['Microsoft 365 Apps for enterprise 2512', 'Microsoft Security Baseline - Microsoft 365 Apps'],
|
||
['Private customer template', 'Baseline recommendation']
|
||
]) {
|
||
const disclosed = discloseResultDbProjection({
|
||
...unsafeScanner(), sourceKind: 'policy-evaluation', sourceTool: 'GPOs', title: 'Synthetic setting',
|
||
policyBaselineLabel: label
|
||
});
|
||
expect(disclosed.policyBaselineLabel).toBe(family);
|
||
}
|
||
});
|
||
|
||
it('collapses identical baseline expectations so the compared release count is not disclosed', () => {
|
||
const variant = (id: string) => ({ settingId: id, baselineValues: [], comparator: {
|
||
version: 1 as const, comparator: { kind: 'equality' as const }, relation: 'different' as const,
|
||
actual: { kind: 'text' as const, value: '0' }, baseline: { kind: 'text' as const, value: '1' }, diagnosticCodes: []
|
||
} });
|
||
const disclosed = discloseResultDbProjection({
|
||
...unsafeScanner(), sourceKind: 'policy-evaluation', sourceTool: 'GPOs', title: 'Synthetic setting',
|
||
policySourceCount: 4,
|
||
policyBaselineVariantAssessment: { version: 1, scope: 'backup-configuration', decision: 'all-fail',
|
||
actualValues: [], variants: [variant('server-2019'), variant('server-2022'), variant('server-2025')], diagnosticCodes: [] }
|
||
});
|
||
|
||
expect(disclosed.policyBaselineVariantAssessment?.variants).toHaveLength(1);
|
||
expect(disclosed).not.toHaveProperty('policySourceCount');
|
||
expect(JSON.stringify(disclosed)).not.toMatch(/server-20\d\d/);
|
||
});
|
||
|
||
it('retains Purple Knight native rule identity and remediation text', () => {
|
||
const disclosed = discloseResultDbProjection({
|
||
...unsafeScanner(), sourceTool: 'purple-knight', sourceRuleId: 'SI000055',
|
||
title: 'Privileged accounts have weak delegation settings',
|
||
description: 'Review delegation on PRIVATE\\admin-user and private-company.example.',
|
||
recommendation: '<p>Remove unconstrained delegation from the <b>affected account</b>.</p>',
|
||
affectedAsset: 'PRIVATE\\admin-user'
|
||
});
|
||
|
||
expect(disclosed).toMatchObject({
|
||
sourceTool: 'purple-knight', sourceRuleId: 'SI000055',
|
||
title: 'Privileged accounts have weak delegation settings',
|
||
description: 'Review delegation on [account] and [domain].',
|
||
recommendation: 'Remove unconstrained delegation from the affected account.'
|
||
});
|
||
});
|
||
|
||
it('publishes Purple Knight for Entra ID as its own Result DB source scope', () => {
|
||
const disclosed = discloseResultDbProjection({
|
||
...unsafeScanner(), sourceTool: 'purple-knight', sourceToolScope: 'entra-id', sourceRuleId: 'SI000055',
|
||
title: 'Synthetic Entra control', description: 'Synthetic Entra description.'
|
||
});
|
||
|
||
expect(disclosed).toMatchObject({
|
||
sourceTool: 'purple-knight-entra', sourceToolScope: 'entra-id', sourceRuleId: 'SI000055',
|
||
title: 'Synthetic Entra control',
|
||
categories: [{ namespace: 'source-tool', categoryCode: 'purple-knight-entra', categoryLabel: 'Purple Knight for Entra ID' }]
|
||
});
|
||
});
|
||
|
||
it('shows a publicly documented PingCastle ID in its native spelling and rejects an unknown ID', () => {
|
||
const item = { ...unsafeScanner(), sourceTool: 'pingcastle', sourceRuleId: 'S-oldNtlm',
|
||
title: 'The LAN Manager Authentication Level allows the use of NTLMv1 or LM.' };
|
||
expect(discloseResultDbProjection(item)).toMatchObject({
|
||
sourceRuleId: 'S-oldNtlm', title: item.title
|
||
});
|
||
expect(discloseResultDbProjection({ ...item, sourceRuleId: 'S-PrivateCustomerRule' }).sourceRuleId)
|
||
.toBeUndefined();
|
||
});
|
||
|
||
it('releases documented Maester suite IDs but keeps unknown and instance-specific IDs private', () => {
|
||
for (const sourceRuleId of ['CIS.M365.1.1.1', 'CISA.MS.AAD.2.3', 'EIDSCA.AF01', 'ORCA.100', 'MT.1025']) {
|
||
expect(discloseResultDbProjection({ ...unsafeScanner(), sourceRuleId }).sourceRuleId)
|
||
.toBe(sourceRuleId);
|
||
}
|
||
for (const sourceRuleId of ['CT001', 'MT.INV.001', 'MT.1024.private-customer-key',
|
||
'MT.1024.applicationCredentialExpiry.private-tenant', 'MT.1033.0', 'CIS.M365.99.99.99']) {
|
||
expect(discloseResultDbProjection({ ...unsafeScanner(), sourceRuleId }).sourceRuleId)
|
||
.toBeUndefined();
|
||
}
|
||
});
|
||
|
||
it('keeps a CIS-CAT control number and public policy path while hiding an actual address and account', () => {
|
||
const disclosed = discloseResultDbProjection({
|
||
...unsafeScanner(), sourceTool: 'cis-cat', sourceRuleId: '18.10.36.1',
|
||
title: "18.10.36.1 Ensure 'Turn off location' is set to 'Enabled'",
|
||
recommendation: 'Computer Configuration\\Administrative Templates\\System\\Location and Sensors\\Turn off location; CONTOSO\\jane\n'
|
||
+ 'Use the public policy file Windows.adml.\n'
|
||
+ 'Review CONTOSO\\jane at 10.20.30.40 and jane@contoso.example.'
|
||
});
|
||
|
||
expect(disclosed.sourceRuleId).toBe('18.10.36.1');
|
||
expect(disclosed.title).toContain('18.10.36.1');
|
||
expect(disclosed.title).not.toContain('[ip]');
|
||
expect(disclosed.recommendation).toContain('Computer Configuration\\Administrative Templates\\System\\Location and Sensors\\Turn off location');
|
||
expect(disclosed.recommendation).toContain('Turn off location; [account]');
|
||
expect(disclosed.recommendation).toContain('Windows.adml');
|
||
expect(disclosed.recommendation).toContain('[account] at [ip] and [email]');
|
||
expect(disclosed.recommendation).not.toMatch(/CONTOSO|jane|10\.20\.30\.40/);
|
||
});
|
||
|
||
it('keeps only the GPO Soll value and withholds every customer value and principal', () => {
|
||
const systemSid = 'S-1-5-82-123456789-234567890-345678901-456789012-567890123';
|
||
const disclosed = discloseResultDbProjection({
|
||
...unsafeScanner(), sourceKind: 'policy-evaluation', sourceTool: 'GPOs',
|
||
title: 'System Access · LockoutBadCount',
|
||
policyActualValue: { kind: 'text', value: '5', principals: [systemSid, 'S-1-5-32-544'] },
|
||
policyBaselineValue: { kind: 'text', value: '10' },
|
||
policyBaselineVariantAssessment: {
|
||
version: 1, scope: 'backup-configuration', decision: 'all-fail',
|
||
actualValues: [{ kind: 'principal-set', principals: [systemSid] }],
|
||
variants: [{ settingId: 'private-setting', baselineValues: [], comparator: {
|
||
version: 1, comparator: { kind: 'set', mode: 'exact' }, relation: 'different',
|
||
actual: { kind: 'string-set', values: [systemSid] },
|
||
baseline: { kind: 'string-set', values: ['S-1-5-32-544'] }, diagnosticCodes: []
|
||
}}], diagnosticCodes: []
|
||
}
|
||
});
|
||
|
||
expect(disclosed).not.toHaveProperty('policyActualValue');
|
||
expect(disclosed.policyBaselineValue).toMatchObject({ kind: 'text', value: '10' });
|
||
expect(disclosed.policyBaselineVariantAssessment).toMatchObject({ decision: 'all-fail', actualValues: [],
|
||
variants: [{ comparator: { relation: 'different', baseline: { kind: 'string-set', values: ['S-1-5-32-544'] } } }] });
|
||
expect(disclosed.policyBaselineVariantAssessment?.variants[0].comparator).not.toHaveProperty('actual');
|
||
const serialized = JSON.stringify(disclosed);
|
||
expect(serialized).not.toContain(systemSid);
|
||
expect(serialized).not.toContain('PRINCIPAL');
|
||
expect(serialized).not.toContain('"5"');
|
||
});
|
||
|
||
it('keeps the technical GPO setting, Soll value and relation without customer values or source identity', () => {
|
||
const disclosed = discloseResultDbProjection({
|
||
...unsafeScanner(), sourceKind: 'policy-evaluation', sourceTool: 'GPOs',
|
||
sourceRuleId: 'registry:machine:software\\policies\\microsoft\\windows nt\\printers\\pointandprint:restrictdriverinstallationtoadministrators',
|
||
title: 'Restrict driver installation to administrators',
|
||
description: 'Private Company GmbH configures this setting for dummy.local.',
|
||
recommendation: 'Set RestrictDriverInstallationToAdministrators to 1.',
|
||
policyDeliveryMechanism: 'registry-policy', policyClassification: 'misconfigured',
|
||
policyActualValue: {
|
||
kind: 'registry-policy', operation: 'set-value', key: 'Software\\Policies\\Microsoft\\Windows NT\\Printers\\PointAndPrint',
|
||
valueName: 'RestrictDriverInstallationToAdministrators', typeCode: 4, data: { kind: 'uint32', value: 0 },
|
||
principals: ['S-1-5-18', 'S-1-5-32-544', 'BUILTIN\\Administrators', 'DUMMY\\Domain Admins',
|
||
'DUMMY\\jane.doe', 'S-1-5-21-123456789-234567890-345678901-512'],
|
||
note: 'Configured by {A1B2C3D4-1111-2222-3333-444455556666}',
|
||
gpoName: 'DUMMY – Domain Controllers Security', gpoId: '{A1B2C3D4-1111-2222-3333-444455556666}',
|
||
sourcePath: '\\\\fileserver\\backups\\dummy.local\\gpo.zip'
|
||
},
|
||
policyBaselineValue: {
|
||
kind: 'registry-policy', operation: 'set-value', key: 'Software\\Policies\\Microsoft\\Windows NT\\Printers\\PointAndPrint',
|
||
valueName: 'RestrictDriverInstallationToAdministrators', typeCode: 4, data: { kind: 'uint32', value: 1 }
|
||
},
|
||
policyIdentityKey: 'private-company-policy', policyGpoId: '{A1B2C3D4-1111-2222-3333-444455556666}',
|
||
policyNativeBackupId: '{99999999-1111-2222-3333-444455556666}',
|
||
policyBaselineId: 'private-baseline-id', policyBaselineLabel: 'Private Company Microsoft Security Baseline Windows Server 2022 v1',
|
||
policyDiagnosticCodes: ['policy-safe-code', 'Private Company'], policyComparisonRelation: 'different',
|
||
policyBaselineVariantAssessment: {
|
||
version: 1, scope: 'backup-configuration', decision: 'all-fail',
|
||
actualValues: [{ kind: 'principal-set', principals: ['DUMMY\\named.user'] }],
|
||
variants: [{
|
||
settingId: 'private-company-baseline-setting',
|
||
baselineValues: [{ kind: 'principal-set', principals: ['BUILTIN\\Administrators'] }],
|
||
comparator: { version: 1, comparator: { kind: 'set', mode: 'exact' }, relation: 'different',
|
||
actual: { kind: 'string-set', values: ['DUMMY\\named.user'] },
|
||
baseline: { kind: 'string-set', values: ['BUILTIN\\Administrators'] }, diagnosticCodes: [] }
|
||
}], diagnosticCodes: []
|
||
}
|
||
});
|
||
expect(disclosed).toMatchObject({
|
||
title: 'Restrict driver installation to administrators', sourceTool: 'GPOs',
|
||
sourceRuleId: 'registry:machine:software\\policies\\microsoft\\windows nt\\printers\\pointandprint:restrictdriverinstallationtoadministrators',
|
||
policyBaselineValue: { data: { kind: 'uint32', value: 1 } },
|
||
policyBaselineLabel: 'Microsoft Security Baseline - Windows Server',
|
||
policyComparisonRelation: 'different',
|
||
recommendation: 'Set RestrictDriverInstallationToAdministrators to 1.',
|
||
policyDiagnosticCodes: ['policy-safe-code'],
|
||
policyBaselineVariantAssessment: {
|
||
actualValues: [],
|
||
variants: [{ settingId: 'baseline-expectation-1', comparator: {
|
||
relation: 'different', baseline: { kind: 'string-set', values: ['BUILTIN\\Administrators'] }
|
||
}}]
|
||
}
|
||
});
|
||
expect(disclosed).not.toHaveProperty('policyActualValue');
|
||
expect(disclosed.policyIdentityKey).toBeUndefined();
|
||
expect(disclosed.policyBaselineId).toBeUndefined();
|
||
const serialized = JSON.stringify(disclosed);
|
||
for (const privateValue of ['Private Company', 'dummy.local', 'DUMMY', 'Domain Admins', 'PRINCIPAL', '2022',
|
||
'A1B2C3D4', '99999999', 'fileserver', 'jane.doe', 'named.user', '123456789', '234567890', '345678901']) {
|
||
expect(serialized).not.toContain(privateValue);
|
||
}
|
||
expect(serialized).toContain('Backup comparison; effective endpoint state not verified');
|
||
});
|
||
|
||
it('removes generated GPO identity hashes instead of presenting them as rule IDs', () => {
|
||
const disclosed = discloseResultDbProjection({
|
||
...unsafeScanner(), sourceKind: 'policy-evaluation', sourceTool: 'GPOs',
|
||
sourceRuleId: 'gpo-policy-identity-deadbeef', title: 'Synthetic setting',
|
||
policyBaselineLabel: 'Microsoft Security Baseline', policyActualValue: 0, policyBaselineValue: 1
|
||
});
|
||
expect(disclosed.sourceRuleId).toBeUndefined();
|
||
expect(disclosed.policyIdentityKey).toBeUndefined();
|
||
});
|
||
|
||
it('replaces customer scope handles after query filtering', () => {
|
||
expect(removeResultDbScopeIdentifiers(discloseResultDbProjection(unsafeScanner()))).toMatchObject({
|
||
projectId: 'scoped-project', assessmentId: 'scoped-assessment', collectionRunId: 'scoped-collection',
|
||
scanRunId: 'scoped-scan'
|
||
});
|
||
});
|
||
|
||
it('keeps the complete seven-level CIS number so sibling recommendations do not collide', () => {
|
||
const first = discloseResultDbProjection({ ...unsafeScanner(), sourceTool: 'cis-cat', sourceRuleId: '18.10.42.11.1.1.1' });
|
||
const second = discloseResultDbProjection({
|
||
...unsafeScanner(), sourceTool: 'cis-cat', sourceRuleId: 'xccdf_org.cisecurity.benchmarks_rule_18.10.42.11.1.1.2_L1_Ensure'
|
||
});
|
||
expect(first.sourceRuleId).toBe('18.10.42.11.1.1.1');
|
||
expect(second.sourceRuleId).toBe('18.10.42.11.1.1.2');
|
||
expect(discloseResultDbProjection({ ...unsafeScanner(), sourceTool: 'cis-cat', sourceRuleId: '1.2.3.4.5.6.7.8.9.10' }).sourceRuleId)
|
||
.toBeUndefined();
|
||
});
|
||
|
||
it('does not mistake the ordinary word "limited" for an organization but still redacts legal names', () => {
|
||
const disclosed = discloseResultDbProjection({
|
||
...unsafeScanner(), title: 'Limited number of Global Admins are assigned.',
|
||
description: 'External sharing limited to existing guests.', recommendation: 'Contact Contoso Ltd or Fabrikam GmbH.'
|
||
});
|
||
expect(disclosed.title).toBe('Limited number of Global Admins are assigned.');
|
||
expect(disclosed.description).toBe('External sharing limited to existing guests.');
|
||
expect(disclosed.recommendation).not.toMatch(/Contoso|Fabrikam/);
|
||
expect(disclosed.recommendation).toContain('[organization]');
|
||
});
|
||
|
||
it('keeps public versions, releases and step, option and page references in scanner prose', () => {
|
||
const disclosed = discloseResultDbProjection({
|
||
...unsafeScanner(), sourceTool: 'pingcastle', sourceRuleId: 'A-DCLdapsProtocolAdvanced',
|
||
title: 'At least one DC is using Tls1.0 or Tls1.1.',
|
||
description: 'NTLMv2 works starting Windows 95. See Microsoft Graph v1.0. If using a pre 2012-R2 schema, '
|
||
+ 'divide the value from step 2 by the value from step 3. Option 1 is described on Page 16. 4 DCs were observed.'
|
||
});
|
||
expect(disclosed.title).toBe('At least one DC is using Tls1.0 or Tls1.1.');
|
||
expect(disclosed.description).toBe('NTLMv2 works starting Windows 95. See Microsoft Graph v1.0. If using a pre 2012-R2 schema, '
|
||
+ 'divide the value from step 2 by the value from step 3. Option 1 is described on Page 16. [count] DCs were observed.');
|
||
});
|
||
|
||
it('keeps documented Microsoft GUIDs but redacts every other GUID', () => {
|
||
const disclosed = discloseResultDbProjection({
|
||
...unsafeScanner(),
|
||
description: 'Add Azure DevOps (App ID: 499b84ac-1321-427f-aa17-267ca6975798) and block with ASR rule '
|
||
+ 'd3e037e1-3eb8-44c8-a917-57927947596d. Tenant 11111111-2222-4333-8444-555555555555 is affected.'
|
||
});
|
||
expect(disclosed.description).toBe('Add Azure DevOps (App ID: 499b84ac-1321-427f-aa17-267ca6975798) and block with ASR rule '
|
||
+ 'd3e037e1-3eb8-44c8-a917-57927947596d. Tenant [id] is affected.');
|
||
});
|
||
|
||
it('withholds per-finding scanner scores', () => {
|
||
const disclosed = discloseResultDbProjection({ ...unsafeScanner(), sourceTool: 'purple-knight', sourceRuleId: 'SI000011', score: 73 });
|
||
expect(disclosed).not.toHaveProperty('score');
|
||
});
|
||
|
||
it('lists withheld fields in the trace without revealing client review state', () => {
|
||
const reviewed = discloseResultDbProjectionWithTrace({ ...unsafeScanner(), reviewDisposition: 'accepted', reviewPriority: 'high' });
|
||
const unreviewed = discloseResultDbProjectionWithTrace({
|
||
...unsafeScanner(), reviewStatus: undefined, reviewOwner: undefined, reviewDueDate: undefined
|
||
});
|
||
expect(reviewed.disclosureTrace?.withheldFields).toEqual(unreviewed.disclosureTrace?.withheldFields);
|
||
expect(JSON.stringify(reviewed.disclosureTrace)).not.toMatch(/review/i);
|
||
expect(reviewed.disclosureTrace?.withheldFields).toContain('affectedAsset');
|
||
});
|
||
});
|