/** * GPO target-role impact measurement — prints NUMBERS ONLY. * * Question: how many current GPO Fail findings would change if every GPO were * compared only with the Microsoft baseline templates of its target role, and * how many GPOs are not linked at all? * * Privacy contract (read before running): * - Reads only the one project whose ID you pass. No project list is printed * unless you run `--list` yourself. * - Everything stays in memory. No file is written. * - Output is a fixed set of counters. No GPO names, GUIDs, paths, setting * names, values, SIDs, OU names or tenant identifiers are printed. * - Any error is reported only as its error type, never with its message. * - Read-only: it calls GET endpoints of the local backend and runs one SELECT * against the canonical local PostgreSQL container. It changes nothing. * * Usage (from the repository root, backend running on 127.0.0.1:4173): * npx vite-node scripts/gpo-role-impact.ts --list * npx vite-node scripts/gpo-role-impact.ts project-xxxxxxxx */ import { execFileSync } from 'node:child_process'; import { readFileSync } from 'node:fs'; import { join } from 'node:path'; import { policyComparisonDisposition } from '../src/domain/policy-comparison-disposition'; import { baselineTemplateContext } from '../src/evaluation/baseline-template-context'; import { policyWorkVerdictPriority } from '../src/evaluation/policy-work-results'; import { readVerifiedZipFiles, inventoryZipArchive } from '../server/gpo/zip-inventory'; const API = 'http://127.0.0.1:4173/api/v1'; const CONTAINER = 'assessor-2_0-postgres-1'; const DEFAULT_DC_POLICY = '{6AC1786C-016F-11D2-945F-00C04FB984F9}'; type Role = 'Domain Controller' | 'all' | 'unlinked' | 'unknown'; async function get(path: string): Promise { const response = await fetch(API + path); if (!response.ok) throw new Error(`HTTP ${response.status}`); return response.json() as Promise; } function sql(query: string): string[] { return execFileSync('docker', ['exec', CONTAINER, 'psql', '-U', 'assessor', '-d', 'assessor', '-At', '-c', query], { encoding: 'utf8', maxBuffer: 512 * 1024 * 1024 }).split('\n').filter(Boolean); } function artifactRoot(): string { const line = readFileSync('.env.postgres', 'utf8').split(/\r?\n/).find(item => item.startsWith('ASSESSOR_ARTIFACT_ROOT=')); if (!line) throw new Error('artifact root not configured'); return line.slice('ASSESSOR_ARTIFACT_ROOT='.length).trim(); } function decode(bytes: Uint8Array): string { if (bytes[0] === 0xff && bytes[1] === 0xfe) return new TextDecoder('utf-16le').decode(bytes); if (bytes[0] === 0xfe && bytes[1] === 0xff) return new TextDecoder('utf-16be').decode(bytes); return new TextDecoder('utf-8').decode(bytes); } /** Role only from unambiguous evidence in GPReport.xml; everything else stays unknown. */ function roleFromReport(report: string): { role: Role; wmiFilter: boolean } { const text = report.replace(/<(\/?)[A-Za-z0-9]+:/g, '<$1'); const guid = /]*>\s*]*>([^<]+)([^<]+)<\/Identifier>/.exec(text)?.[1] ?? ''; const links = [...text.matchAll(/([\s\S]*?)<\/LinksTo>/g)].map(match => ({ path: /([^<]*)<\/SOMPath>/.exec(match[1])?.[1] ?? '', enabled: !/\s*false\s*<\/Enabled>/i.test(match[1]) })).filter(link => link.enabled); const wmiFilter = /[^<]+<\/FilterName>/.test(text); if (guid.trim().toUpperCase() === DEFAULT_DC_POLICY) return { role: 'Domain Controller', wmiFilter }; if (!links.length) return { role: 'unlinked', wmiFilter }; if (links.every(link => /\/Domain Controllers$/i.test(link.path.trim()))) return { role: 'Domain Controller', wmiFilter }; // A link to the domain itself (no OU segment) applies to every role. if (links.some(link => !link.path.includes('/'))) return { role: 'all', wmiFilter }; return { role: 'unknown', wmiFilter }; } async function list(): Promise { // Shown only on your own screen; do not paste this list. const projects = await get>('/projects'); for (const project of projects) console.log(project.id, ' ', project.name); } async function measure(projectId: string): Promise { if (!/^project-[a-z0-9]+$/.test(projectId)) throw new Error('invalid project id'); const overview = await get<{ current: { evaluationRunIds: string[] } }>(`/projects/${projectId}/policy-analysis-overview`); const runIds = new Set(overview.current.evaluationRunIds); const evaluations = (await get<{ records: any[] }>(`/projects/${projectId}/policy-evaluations`)).records.filter(item => runIds.has(item.run.id)); const aggregations = (await get<{ records: any[] } | any[]>(`/projects/${projectId}/policy-snapshot-aggregations`)); const aggregationRecords: any[] = Array.isArray(aggregations) ? aggregations : aggregations.records; // Backup instance -> backup folder, backup folder -> role from its GPReport.xml. const instanceFolder = new Map(); for (const line of sql(`select json_build_object('id', instance_id, 'data', data)::text from assessor_gpo_backup_instances where project_id = '${projectId}'`)) { const item = JSON.parse(line); const folder = JSON.stringify(item.data).match(/\{[0-9A-Fa-f-]{36}\}/)?.[0]; if (folder) instanceFolder.set(item.id, folder.toUpperCase()); } const folderRole = new Map(); const root = artifactRoot(); for (const line of sql(`select storage_key || '|' || coalesce(relative_path, '') from assessor_import_artifacts where project_id = '${projectId}' and storage_kind = 'backend-controlled'`)) { const [key, relativePath] = line.split('|'); if (!/^[0-9a-f-]{36}$/i.test(key)) continue; let bytes: Uint8Array; try { bytes = readFileSync(join(root, 'objects', `${key}.bin`)); } catch { continue; } const reports: Array<{ path: string; bytes: Uint8Array }> = []; if (bytes[0] === 0x50 && bytes[1] === 0x4b) { const paths = inventoryZipArchive(bytes).entries.filter(entry => entry.kind === 'file' && /(^|\/)gpreport\.xml$/i.test(entry.relativePath)).map(entry => entry.relativePath); if (paths.length) for (const file of readVerifiedZipFiles(bytes, paths, undefined, 256 * 1024 * 1024)) reports.push({ path: file.entry.relativePath, bytes: file.bytes }); } else if (/(^|[\\/])gpreport\.xml$/i.test(relativePath)) reports.push({ path: relativePath, bytes }); for (const report of reports) { const folder = report.path.match(/\{[0-9A-Fa-f-]{36}\}/)?.[0]; if (folder) folderRole.set(folder.toUpperCase(), roleFromReport(decode(report.bytes))); } } const templateRoles = new Map(); for (const baselineId of new Set(evaluations.flatMap(item => item.results.map((result: any) => result.baselineId)))) { const response = await get<{ records?: any[] }>(`/reference-data/microsoft-security-baselines/${encodeURIComponent(String(baselineId))}/analysis`); for (const record of response.records ?? [response]) for (const setting of (record.analysis ?? record).settings ?? []) { templateRoles.set(setting.id, baselineTemplateContext([...new Set((setting.template?.names ?? []).map((name: any) => name.value))])?.role); } } const statusOf = (comparison: any) => policyComparisonDisposition(comparison) === 'not-configured' ? 'Not configured' : policyComparisonDisposition(comparison) === 'misconfigured' ? 'Fail' : comparison.baselineVariantAssessment?.decision === 'all-pass' && comparison.classification === 'configured_match' ? 'Pass' : 'Review required'; const rank: Record = { ...policyWorkVerdictPriority, 'No effect': -1, 'Not applicable': -2 }; const top = (items: string[]) => items.reduce((left, right) => (rank[right] ?? 0) > (rank[left] ?? 0) ? right : left); // Per setting: comparison relations grouped by recognized template role (upper bound of role dependence). const relationsByTemplateRole = new Map>>(); const settings = new Map; current: string[]; simulated: string[] }>(); let variants = 0, variantsWithoutRole = 0; for (const evaluation of evaluations) { const aggregation = aggregationRecords.find(item => item.run.id === evaluation.run.aggregationRunId); const observationRoles = new Map>(); for (const state of aggregation?.states ?? []) for (const observation of state.observationIds) { const roles = observationRoles.get(observation) ?? new Set(); for (const instance of state.backupInstanceIds) roles.add(folderRole.get(instanceFolder.get(instance) ?? '')?.role ?? 'unknown'); observationRoles.set(observation, roles); } for (const comparison of evaluation.results) { const key = JSON.stringify([evaluation.run.aggregationRunId, comparison.identity.scope, comparison.identity.id]); const roles = new Set(comparison.observationIds.flatMap((id: string) => [...(observationRoles.get(id) ?? [])])); const row = settings.get(key) ?? { roles: new Set(), current: [], simulated: [] }; roles.forEach(role => row.roles.add(role)); const current = statusOf(comparison); let simulated = current; const byRole = relationsByTemplateRole.get(key) ?? new Map>(); for (const variant of comparison.baselineVariantAssessment?.variants ?? []) { variants++; const templateRole = templateRoles.get(variant.settingId); if (!templateRole) variantsWithoutRole++; const relations = byRole.get(templateRole ?? 'unrecognized') ?? new Set(); relations.add(variant.comparator.relation); byRole.set(templateRole ?? 'unrecognized', relations); } relationsByTemplateRole.set(key, byRole); if (current !== 'Not configured' && roles.size === 1 && roles.has('unlinked')) simulated = 'No effect'; else if (current !== 'Not configured' && roles.size === 1 && roles.has('Domain Controller') && comparison.baselineVariantAssessment) { // Unknown template roles are kept: an unrecognized template could still apply. const kept = comparison.baselineVariantAssessment.variants.filter((variant: any) => { const role = templateRoles.get(variant.settingId); return role === undefined || role === 'Domain Controller'; }); simulated = !kept.length ? 'Not applicable' : kept.every((variant: any) => variant.comparator.relation === 'match') ? 'Pass' : kept.every((variant: any) => variant.comparator.relation === 'different') ? 'Fail' : 'Review required'; } row.current.push(current); row.simulated.push(simulated); settings.set(key, row); } } const rows = [...settings.entries()].map(([key, row]) => { const real = row.simulated.filter(status => status !== 'Not applicable'); // A Fail is role-dependent when every template of at least one role matches (or it has // matching templates of an unrecognized role): the verdict could change with the target role. const byRole = relationsByTemplateRole.get(key) ?? new Map>(); const roleMatches = [...byRole.entries()].filter(([role, relations]) => role !== 'unrecognized' && relations.size === 1 && relations.has('match')).length; const unrecognizedMatch = byRole.get('unrecognized')?.has('match') ?? false; const dependence = roleMatches ? 'role-dependent (a recognized role matches)' : unrecognizedMatch ? 'possibly role-dependent (only unrecognized templates match)' : 'role-independent (no template matches)'; return { roles: row.roles, current: top(row.current), simulated: real.length ? top(real) : 'Not applicable', dependence }; }); const count = (values: string[]) => values.reduce>((result, value) => (result[value] = (result[value] ?? 0) + 1, result), {}); const changes = count(rows.filter(row => row.current !== row.simulated).map(row => `${row.current} -> ${row.simulated}`)); const gpoRoles = count([...new Set(instanceFolder.values())].map(folder => folderRole.get(folder)?.role ?? 'no GPReport.xml')); const failRoles = count(rows.filter(row => row.current === 'Fail').map(row => [...row.roles].sort().join('+') || 'no customer source')); console.log('=== GPO role impact (numbers only) ==='); console.log('GPO backups by derived role ', JSON.stringify(gpoRoles)); console.log('GPO backups with a WMI filter ', [...folderRole.values()].filter(item => item.wmiFilter).length); console.log('Settings (rolled up) ', rows.length); console.log('Current verdicts ', JSON.stringify(count(rows.map(row => row.current)))); console.log('Verdicts with target role ', JSON.stringify(count(rows.map(row => row.simulated)))); console.log('Changed settings ', JSON.stringify(changes)); console.log('Current Fail by contributing roles ', JSON.stringify(failRoles)); console.log('Baseline variants without template role', `${variantsWithoutRole} of ${variants}`); console.log('Current Fail by role dependence ', JSON.stringify(count(rows.filter(row => row.current === 'Fail').map(row => row.dependence)))); } const argument = process.argv[2]; if (!argument) console.log('Usage: npx vite-node scripts/gpo-role-impact.ts --list | '); (argument === '--list' ? list() : argument ? measure(argument) : Promise.resolve()) .catch((error: unknown) => { // Never print an error message: it could contain source content. console.log('Stopped:', error instanceof Error ? error.constructor.name : typeof error); process.exitCode = 1; });