123 lines
6.8 KiB
TypeScript
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 };
|
|
}
|