Assessor_2.0/src/evaluation/policy-comparator.ts

123 lines
6.8 KiB
TypeScript

import type { GpoPolicyValue, GpoRegistryPolicyData } from '../domain/gpo-policy';
import type {
PolicyComparableValue,
PolicyComparatorDefinition,
PolicyComparatorOutcome,
PolicyComparatorRelation
} from '../domain/policy-evaluation';
import { canonicalJson } from '../shared/canonical-json';
function sortedUnique(values: string[]): string[] {
return [...new Set(values)].sort((left, right) => left.localeCompare(right));
}
function registryDataValue(data: GpoRegistryPolicyData | undefined): PolicyComparableValue | undefined {
if (!data) return undefined;
if (data.kind === 'none') return { kind: 'none' };
if (data.kind === 'string') return { kind: 'text', value: data.value };
if (data.kind === 'multi-string') return { kind: 'string-set', values: sortedUnique(data.values) };
if (data.kind === 'binary') return { kind: 'binary', hexadecimal: data.hexadecimal.toLocaleLowerCase('en-US') };
if (data.kind === 'uint32') return { kind: 'integer', value: String(data.value) };
if (data.kind === 'uint64' && /^\d+$/.test(data.value)) return { kind: 'integer', value: BigInt(data.value).toString() };
return undefined;
}
export function policyComparableValue(value: GpoPolicyValue | undefined): PolicyComparableValue | undefined {
if (!value) return undefined;
if (value.kind === 'registry-policy') return registryDataValue(value.data);
// Security-template registry values use their INF type code plus raw value.
// Retain the type in the comparable representation so equal text encoded as
// different registry types is not treated as the same recommendation.
if (value.kind === 'registry') return { kind: 'text', value: `${value.typeCode},${value.rawData}` };
if (value.kind === 'scalar') return { kind: 'text', value: value.raw };
if (value.kind === 'principal-set') return { kind: 'string-set', values: sortedUnique(value.principals) };
if (value.kind === 'audit-flags') return { kind: 'audit-flags', success: value.success, failure: value.failure };
return undefined;
}
function integer(value: PolicyComparableValue): bigint | undefined {
if (value.kind !== 'integer' || !/^-?\d+$/.test(value.value)) return undefined;
return BigInt(value.value);
}
function relationByStrength(actual: bigint, baseline: bigint, higherIsStronger: boolean): PolicyComparatorRelation {
if (actual === baseline) return 'match';
return (actual > baseline) === higherIsStronger ? 'stronger' : 'weaker';
}
function stringSet(value: PolicyComparableValue): Set<string> | undefined {
return value.kind === 'string-set' ? new Set(value.values) : undefined;
}
function includesAll(left: Set<string>, right: Set<string>): boolean {
return [...right].every(value => left.has(value));
}
function compareSets(actual: Set<string>, baseline: Set<string>, mode: 'superset' | 'subset' | 'exact'): PolicyComparatorRelation {
const actualContains = includesAll(actual, baseline);
const baselineContains = includesAll(baseline, actual);
if (actualContains && baselineContains) return 'match';
if (mode === 'exact') return 'different';
if (mode === 'superset') return actualContains ? 'stronger' : baselineContains ? 'weaker' : 'different';
return baselineContains ? 'stronger' : actualContains ? 'weaker' : 'different';
}
function compareAudit(
actual: Extract<PolicyComparableValue, { kind: 'audit-flags' }>,
baseline: Extract<PolicyComparableValue, { kind: 'audit-flags' }>,
mode: 'contains-baseline' | 'exact'
): PolicyComparatorRelation {
if (actual.success === baseline.success && actual.failure === baseline.failure) return 'match';
if (mode === 'exact') return 'different';
const actualContains = (!baseline.success || actual.success) && (!baseline.failure || actual.failure);
const baselineContains = (!actual.success || baseline.success) && (!actual.failure || baseline.failure);
return actualContains ? 'stronger' : baselineContains ? 'weaker' : 'different';
}
export function comparePolicyValues(
actualSource: GpoPolicyValue | undefined,
baselineSource: GpoPolicyValue | undefined,
comparator: PolicyComparatorDefinition
): PolicyComparatorOutcome {
const actual = policyComparableValue(actualSource);
const baseline = policyComparableValue(baselineSource);
let relation: PolicyComparatorRelation = 'incomparable';
const diagnosticCodes: string[] = [];
if (!actual || !baseline) diagnosticCodes.push('policy-comparator-value-unsupported');
else if (comparator.kind === 'equality') {
relation = canonicalJson(actual) === canonicalJson(baseline) ? 'match' : 'different';
} else if (comparator.kind === 'minimum' || comparator.kind === 'maximum') {
const actualInteger = integer(actual);
const baselineInteger = integer(baseline);
if (actualInteger === undefined || baselineInteger === undefined) diagnosticCodes.push('policy-comparator-integer-required');
else relation = relationByStrength(actualInteger, baselineInteger, comparator.kind === 'minimum');
} else if (comparator.kind === 'enum') {
const actualText = actual.kind === 'text' ? actual.value : undefined;
const baselineText = baseline.kind === 'text' ? baseline.value : undefined;
const actualIndex = actualText === undefined ? -1 : comparator.orderedValues.indexOf(actualText);
const baselineIndex = baselineText === undefined ? -1 : comparator.orderedValues.indexOf(baselineText);
if (actualIndex < 0 || baselineIndex < 0 || new Set(comparator.orderedValues).size !== comparator.orderedValues.length) {
diagnosticCodes.push('policy-comparator-enum-value-unsupported');
} else relation = relationByStrength(BigInt(actualIndex), BigInt(baselineIndex), comparator.direction === 'higher-is-stronger');
} else if (comparator.kind === 'bitmask') {
const actualInteger = integer(actual);
const baselineInteger = integer(baseline);
if (actualInteger === undefined || baselineInteger === undefined || actualInteger < 0n || baselineInteger < 0n) {
diagnosticCodes.push('policy-comparator-bitmask-required');
} else if (actualInteger === baselineInteger) relation = 'match';
else if (comparator.mode === 'exact') relation = 'different';
else if ((actualInteger & baselineInteger) === baselineInteger) relation = 'stronger';
else if ((actualInteger & baselineInteger) === actualInteger) relation = 'weaker';
else relation = 'different';
} else if (comparator.kind === 'set') {
const actualSet = stringSet(actual);
const baselineSet = stringSet(baseline);
if (!actualSet || !baselineSet) diagnosticCodes.push('policy-comparator-set-required');
else relation = compareSets(actualSet, baselineSet, comparator.mode);
} else {
if (actual.kind !== 'audit-flags' || baseline.kind !== 'audit-flags') diagnosticCodes.push('policy-comparator-audit-flags-required');
else relation = compareAudit(actual, baseline, comparator.mode);
}
return { version: 1, comparator, relation, actual, baseline, diagnosticCodes };
}