Assessor_2.0/src/domain/result-db-supplemental-disc...

135 lines
5.6 KiB
TypeScript

import type { JsonValue } from './canonical-model';
import type { EnvironmentCollectionState, EnvironmentObservation, EnvironmentValueType } from './environment-context';
export const RESULT_DB_SUPPLEMENTAL_DISCLOSURE_POLICY_VERSION = '1.0.0';
export type ResultDbSupplementalDisclosureClass =
| 'aggregate'
| 'configuration-state'
| 'technical-metadata';
export interface ResultDbSupplementalDisclosure {
policyVersion: typeof RESULT_DB_SUPPLEMENTAL_DISCLOSURE_POLICY_VERSION;
disclosureClass: ResultDbSupplementalDisclosureClass;
property: string;
title: string;
subjectType: string;
value: JsonValue;
valueType: EnvironmentValueType;
state: EnvironmentCollectionState;
}
interface DisclosureRule {
toolId: string;
property: string;
title: string;
subjectType: string;
disclosureClass: ResultDbSupplementalDisclosureClass;
disclose(value: JsonValue): JsonValue | undefined;
}
function finiteNumber(value: JsonValue, minimum: number, maximum: number): number | undefined {
if (typeof value !== 'number' || !Number.isFinite(value) || value < minimum || value > maximum) return undefined;
return value;
}
function integer(value: JsonValue, minimum: number, maximum: number): number | undefined {
const numeric = finiteNumber(value, minimum, maximum);
return numeric !== undefined && Number.isInteger(numeric) ? numeric : undefined;
}
function booleanValue(value: JsonValue): boolean | undefined {
return typeof value === 'boolean' ? value : undefined;
}
function productVersion(value: JsonValue): string | undefined {
if (typeof value !== 'string') return undefined;
const normalized = value.trim();
return /^\d+(?:\.\d+){1,3}(?:[-+][a-z0-9.-]+)?$/i.test(normalized) ? normalized : undefined;
}
const pingCastleRules: DisclosureRule[] = [
{
toolId: 'pingcastle', property: 'report.engine_version', title: 'PingCastle engine version',
subjectType: 'scanner-report', disclosureClass: 'technical-metadata', disclose: productVersion
},
...['risk.domain', 'risk.global', 'risk.stale_objects', 'risk.privileged_accounts', 'risk.trusts', 'risk.anomalies']
.map((property): DisclosureRule => ({
toolId: 'pingcastle', property, title: property === 'risk.domain' || property === 'risk.global'
? 'Domain risk score'
: `${property.slice('risk.'.length).replaceAll('_', ' ')} score`,
subjectType: 'active-directory-environment', disclosureClass: 'aggregate',
disclose: value => finiteNumber(value, 0, 100)
})),
{
toolId: 'pingcastle', property: 'domain.functional_level', title: 'Domain functional level',
subjectType: 'active-directory-environment', disclosureClass: 'technical-metadata',
disclose: value => integer(value, 2000, 9999)
},
{
toolId: 'pingcastle', property: 'forest.functional_level', title: 'Forest functional level',
subjectType: 'active-directory-environment', disclosureClass: 'technical-metadata',
disclose: value => integer(value, 2000, 9999)
},
{
toolId: 'pingcastle', property: 'forest.schema_version', title: 'Forest schema version',
subjectType: 'active-directory-environment', disclosureClass: 'technical-metadata',
disclose: value => integer(value, 0, 10000)
},
{
toolId: 'pingcastle', property: 'domain.controller_count', title: 'Domain controller count',
subjectType: 'active-directory-environment', disclosureClass: 'aggregate',
disclose: value => integer(value, 0, 1000000)
},
{
toolId: 'pingcastle', property: 'domain.recycle_bin_enabled', title: 'AD Recycle Bin enabled',
subjectType: 'active-directory-environment', disclosureClass: 'configuration-state', disclose: booleanValue
},
{
toolId: 'pingcastle', property: 'entra.configuration_detected', title: 'Entra ID configuration detected',
subjectType: 'active-directory-environment', disclosureClass: 'configuration-state', disclose: booleanValue
},
{
toolId: 'pingcastle', property: 'logging.wef.configuration_count', title: 'WEF configuration count',
subjectType: 'active-directory-environment', disclosureClass: 'aggregate',
disclose: value => integer(value, 0, 1000000)
},
{
toolId: 'pingcastle', property: 'kerberos.krbtgt.version', title: 'krbtgt version',
subjectType: 'active-directory-environment', disclosureClass: 'technical-metadata',
disclose: value => integer(value, 0, 1000000)
}
];
const disclosureRules = new Map(
pingCastleRules.map(rule => [`${rule.toolId}\u0000${rule.property}`, rule])
);
/**
* Fail-closed privacy boundary for supplemental scanner context in Result DB.
* Findings use their canonical projection. Environment/details records only
* cross this boundary when a tool/property pair has an explicit rule and its
* normalized value conforms to the narrow non-identifying value contract.
*/
export function discloseSupplementalObservation(
observation: EnvironmentObservation
): ResultDbSupplementalDisclosure | undefined {
const toolId = observation.tool.id.trim().toLocaleLowerCase('en-US');
const rule = disclosureRules.get(`${toolId}\u0000${observation.property}`);
if (!rule) return undefined;
const disclosedValue = rule.disclose(observation.normalizedValue ?? observation.originalValue);
if (disclosedValue === undefined) return undefined;
return {
policyVersion: RESULT_DB_SUPPLEMENTAL_DISCLOSURE_POLICY_VERSION,
disclosureClass: rule.disclosureClass,
property: rule.property,
title: rule.title,
subjectType: rule.subjectType,
value: disclosedValue,
valueType: typeof disclosedValue === 'number' ? 'number'
: typeof disclosedValue === 'boolean' ? 'boolean'
: 'string',
state: observation.state
};
}