132 lines
5.9 KiB
TypeScript
132 lines
5.9 KiB
TypeScript
import { canonicalJson } from '../../src/shared/canonical-json';
|
|
import type { AggregatedPolicyState, EnvironmentProfile, StoredPolicySnapshotAggregation } from '../../src/domain/gpo-snapshot-aggregation';
|
|
import type { PolicyBaselineState, PolicyComparatorDefinition } from '../../src/domain/policy-evaluation';
|
|
import {
|
|
policyEvaluationRulesForContext,
|
|
type PolicyDependencyRule,
|
|
type PolicyEvaluationRule,
|
|
type PolicyEvaluationRuleCatalog
|
|
} from '../../src/domain/policy-evaluation-rules';
|
|
|
|
export interface PolicyRuleResolution {
|
|
baselineState: PolicyBaselineState;
|
|
comparator: PolicyComparatorDefinition;
|
|
ruleIds: string[];
|
|
dependencyRuleIds: string[];
|
|
riskRuleIds: string[];
|
|
notApplicable: boolean;
|
|
diagnosticCodes: string[];
|
|
}
|
|
|
|
function sortedUnique(values: string[]): string[] {
|
|
return [...new Set(values)].sort((left, right) => left.localeCompare(right));
|
|
}
|
|
|
|
function dependencyTarget(
|
|
aggregation: StoredPolicySnapshotAggregation,
|
|
dependency: PolicyDependencyRule
|
|
): AggregatedPolicyState | undefined {
|
|
return aggregation.states.find(state => state.identity.id === dependency.identityId
|
|
&& state.deliveryMechanism === dependency.deliveryMechanism);
|
|
}
|
|
|
|
function targetIsConfigured(target: AggregatedPolicyState): boolean {
|
|
return target.status === 'configured' && target.completeness === 'complete' && target.targeting === 'none';
|
|
}
|
|
|
|
function dependencyResolution(
|
|
aggregation: StoredPolicySnapshotAggregation,
|
|
dependencies: PolicyDependencyRule[]
|
|
): { notApplicable: boolean; diagnosticCodes: string[] } {
|
|
const diagnostics = new Set<string>();
|
|
let notApplicable = false;
|
|
for (const dependency of dependencies) {
|
|
const target = dependencyTarget(aggregation, dependency);
|
|
if (!target) {
|
|
if (!aggregation.snapshot.permitsSetAbsenceConclusions) {
|
|
diagnostics.add('policy-dependency-target-absence-unknown');
|
|
} else if (dependency.kind === 'prerequisite') {
|
|
notApplicable = true;
|
|
}
|
|
continue;
|
|
}
|
|
if (target.status === 'conflicted' || target.status === 'indeterminate'
|
|
|| target.completeness !== 'complete' || target.targeting !== 'none') {
|
|
diagnostics.add('policy-dependency-target-indeterminate');
|
|
continue;
|
|
}
|
|
const configured = targetIsConfigured(target);
|
|
if (dependency.kind === 'prerequisite' && !configured) notApplicable = true;
|
|
if (dependency.kind === 'override' && configured) notApplicable = true;
|
|
if (dependency.kind === 'mutual-exclusion' && configured) {
|
|
diagnostics.add('policy-dependency-mutual-exclusion-active');
|
|
}
|
|
}
|
|
return { notApplicable, diagnosticCodes: [...diagnostics].sort() };
|
|
}
|
|
|
|
export function resolvePolicyEvaluationRule(
|
|
catalog: PolicyEvaluationRuleCatalog,
|
|
profile: EnvironmentProfile,
|
|
aggregation: StoredPolicySnapshotAggregation,
|
|
state: AggregatedPolicyState
|
|
): PolicyRuleResolution {
|
|
const diagnostics = new Set<string>();
|
|
const selected: PolicyEvaluationRule[] = [];
|
|
for (const contextId of state.applicableContextIds) {
|
|
const context = profile.contexts.find(candidate => candidate.id === contextId);
|
|
if (!context) {
|
|
diagnostics.add('policy-evaluation-context-missing');
|
|
continue;
|
|
}
|
|
const candidates = policyEvaluationRulesForContext(
|
|
catalog, state.identity.id, state.deliveryMechanism, context, state.identity.scope
|
|
);
|
|
if (candidates.length !== 1) {
|
|
diagnostics.add(candidates.length
|
|
? 'policy-evaluation-rule-ambiguous'
|
|
: 'policy-evaluation-rule-catalog-gap');
|
|
continue;
|
|
}
|
|
selected.push(candidates[0]);
|
|
}
|
|
const rules = [...new Map(selected.map(rule => [rule.id, rule])).values()]
|
|
.sort((left, right) => left.id.localeCompare(right.id));
|
|
const dispositions = new Set(rules.map(rule => rule.baselineDisposition));
|
|
if (dispositions.size > 1) diagnostics.add('policy-evaluation-rule-context-conflict');
|
|
let baselineState: PolicyBaselineState = dispositions.size === 1
|
|
? rules[0].baselineDisposition
|
|
: 'catalog-gap';
|
|
const recommendationRules = rules.filter(rule => rule.baselineDisposition === 'explicit-recommendation');
|
|
const comparatorSignatures = new Set(recommendationRules.map(rule => canonicalJson(rule.comparator)));
|
|
if (comparatorSignatures.size > 1) diagnostics.add('policy-evaluation-comparator-consensus-conflict');
|
|
const riskSignatures = new Set(recommendationRules.map(rule => canonicalJson(rule.riskRule)));
|
|
if (riskSignatures.size > 1) diagnostics.add('policy-evaluation-risk-consensus-conflict');
|
|
if (baselineState === 'no-recommendation') diagnostics.add('policy-evaluation-no-recommendation');
|
|
const dependencies = [...new Map(rules.flatMap(rule => rule.dependencies).map(rule => [rule.id, rule])).values()]
|
|
.sort((left, right) => left.id.localeCompare(right.id));
|
|
const conflictingDependencyIds = new Set<string>();
|
|
for (const rule of rules) {
|
|
for (const dependency of rule.dependencies) {
|
|
const sameId = dependencies.find(candidate => candidate.id === dependency.id);
|
|
if (sameId && canonicalJson(sameId) !== canonicalJson(dependency)) conflictingDependencyIds.add(dependency.id);
|
|
}
|
|
}
|
|
if (conflictingDependencyIds.size) diagnostics.add('policy-evaluation-dependency-definition-conflict');
|
|
const dependency = dependencyResolution(aggregation, dependencies);
|
|
dependency.diagnosticCodes.forEach(code => diagnostics.add(code));
|
|
let notApplicable = baselineState === 'explicitly-not-applicable' || dependency.notApplicable;
|
|
if (dependency.notApplicable) baselineState = 'explicitly-not-applicable';
|
|
if (diagnostics.size) notApplicable = false;
|
|
const comparator = recommendationRules[0]?.comparator ?? { kind: 'equality' as const };
|
|
return {
|
|
baselineState,
|
|
comparator,
|
|
ruleIds: rules.map(rule => rule.id),
|
|
dependencyRuleIds: dependencies.map(rule => rule.id),
|
|
riskRuleIds: sortedUnique(recommendationRules.flatMap(rule => rule.riskRule ? [rule.riskRule.id] : [])),
|
|
notApplicable,
|
|
diagnosticCodes: [...diagnostics].sort()
|
|
};
|
|
}
|