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) }, // Non-identifying aggregate counts and configuration state derived from // PingCastle inventory tables. Each value is a plain count or boolean; no // account, host, or directory identifier crosses this boundary. ...([ ['user.account_count', 'User accounts'], ['user.account_enabled_count', 'Enabled user accounts'], ['user.account_disabled_count', 'Disabled user accounts'], ['user.account_active_count', 'Active user accounts'], ['user.password_never_expires_count', 'Users with non-expiring passwords'], ['computer.account_count', 'Computer accounts'], ['computer.account_enabled_count', 'Enabled computer accounts'], ['computer.account_disabled_count', 'Disabled computer accounts'], ['computer.account_active_count', 'Active computer accounts'], ['admin_group.count', 'Administrative groups'], ['trust.count', 'Domain trusts'], ['pki.certificate_template_count', 'Certificate templates'], ['pki.certificate_count', 'Certificates'], ['pki.dc_certificate_count', 'Domain controller certificates'], ['pki.delegation_count', 'PKI delegations'], ['operating_system.count', 'Distinct operating systems'], ['password_policy.minimum_length', 'Password minimum length'], ['password_policy.history_length', 'Password history length'], ['password_policy.lockout_threshold', 'Account lockout threshold'], ['password_policy.maximum_age_days', 'Maximum password age (days)'] ] as const).map(([property, title]): DisclosureRule => ({ toolId: 'pingcastle', property, title, subjectType: 'active-directory-environment', disclosureClass: 'aggregate', disclose: value => integer(value, 0, 1_000_000_000) })), ...([ ['password_policy.complexity_enabled', 'Password complexity enabled'], ['entra.configuration_detected', 'Entra ID configuration detected'], ['laps.legacy.present', 'Legacy LAPS present'], ['laps.windows.present', 'Windows LAPS present'] ] as const).map(([property, title]): DisclosureRule => ({ toolId: 'pingcastle', property, title, subjectType: 'active-directory-environment', disclosureClass: 'configuration-state', disclose: booleanValue })) ]; 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 }; }