109 lines
5.7 KiB
TypeScript
109 lines
5.7 KiB
TypeScript
import { describe, expect, it } from 'vitest';
|
|
import type { EnvironmentObservationCandidate } from '../../src/domain/source-extraction';
|
|
import { derivePingCastleEnvironmentAggregates } from '../../src/importers/pingcastle-environment-aggregates';
|
|
|
|
function cell(
|
|
subjectType: string,
|
|
label: string,
|
|
value: string | number | boolean,
|
|
valueType: EnvironmentObservationCandidate['valueType'],
|
|
state: EnvironmentObservationCandidate['state'] = 'observed'
|
|
): EnvironmentObservationCandidate {
|
|
return {
|
|
version: 1,
|
|
subject: { type: subjectType, identity: 'sevenkingdoms:private', label: 'Private tenant' },
|
|
property: `pingcastle.section.tabelle.99.${label.toLowerCase().replace(/[^a-z0-9]+/g, '.')}`,
|
|
label,
|
|
originalValue: value,
|
|
normalizedValue: value,
|
|
valueType,
|
|
state,
|
|
sectionKey: 'sectionSynthetic',
|
|
source: { fileName: 'ad_hc_report.html', objectPath: 'html/section/cell' },
|
|
attributes: {}
|
|
};
|
|
}
|
|
|
|
function byProperty(observations: EnvironmentObservationCandidate[]) {
|
|
return new Map(observations.map(observation => [observation.property, observation.normalizedValue]));
|
|
}
|
|
|
|
describe('PingCastle environment aggregate derivation', () => {
|
|
it('derives stable account, inventory, policy and configuration aggregates from raw cells', () => {
|
|
const input: EnvironmentObservationCandidate[] = [
|
|
cell('user-information.account-analysis', 'Nb User Accounts', 16, 'number'),
|
|
cell('user-information.account-analysis', 'Nb Enabled', 15, 'number'),
|
|
cell('user-information.account-analysis', 'Nb Disabled', 1, 'number'),
|
|
cell('user-information.account-analysis', 'Nb Active', 15, 'number'),
|
|
cell('user-information.account-analysis', 'Nb pwd never Expire', 15, 'number'),
|
|
cell('computer-information.account-analysis', 'Nb Computer Accounts', 1, 'number'),
|
|
cell('computer-information.account-analysis', 'Nb Enabled', 1, 'number'),
|
|
cell('admin-groups.groups', 'Admin groups list row count', 14, 'number'),
|
|
cell('trusts-details.discovered-domains', 'Trusts list row count', 0, 'number'),
|
|
cell('pki.certificate-templates', 'Certificate Templates list row count', 12, 'number'),
|
|
cell('pki.certificate-templates', 'Delegations list row count', 12, 'number'),
|
|
cell('pki.certificates', 'Certificates list row count', 1, 'number'),
|
|
cell('pki.domain-controller-certificate', 'DC Certificates list row count', 1, 'number'),
|
|
cell('password-policies.password-policies', 'Complexity', false, 'boolean'),
|
|
cell('password-policies.password-policies', 'Lockout Threshold', 5, 'number'),
|
|
cell('password-policies.password-policies', 'Min Password Length', 5, 'number'),
|
|
cell('password-policies.password-policies', 'Max Password Age', '37201 day(s)', 'string'),
|
|
cell('anomalies.laps', 'Legacy LAPS installation date', 'Never', 'datetime', 'explicitly-absent'),
|
|
cell('anomalies.laps', 'Windows LAPS installation date', 'Never', 'datetime', 'explicitly-absent')
|
|
];
|
|
// The LAPS presence booleans key off the canonical installation-date facts.
|
|
input.push({
|
|
...cell('anomalies.laps', 'Legacy LAPS installation date', 'Never', 'datetime', 'explicitly-absent'),
|
|
property: 'laps.legacy.installation_date'
|
|
});
|
|
input.push({
|
|
...cell('anomalies.laps', 'Windows LAPS installation date', '2026-05-27T10:00:00Z', 'datetime', 'observed'),
|
|
property: 'laps.windows.installation_date'
|
|
});
|
|
|
|
const derived = derivePingCastleEnvironmentAggregates(input);
|
|
const values = byProperty(derived);
|
|
|
|
expect(values.get('user.account_count')).toBe(16);
|
|
expect(values.get('user.account_enabled_count')).toBe(15);
|
|
expect(values.get('user.account_disabled_count')).toBe(1);
|
|
expect(values.get('user.account_active_count')).toBe(15);
|
|
expect(values.get('user.password_never_expires_count')).toBe(15);
|
|
expect(values.get('computer.account_count')).toBe(1);
|
|
expect(values.get('computer.account_enabled_count')).toBe(1);
|
|
expect(values.get('admin_group.count')).toBe(14);
|
|
expect(values.get('trust.count')).toBe(0);
|
|
expect(values.get('pki.certificate_template_count')).toBe(12);
|
|
expect(values.get('pki.delegation_count')).toBe(12);
|
|
expect(values.get('pki.certificate_count')).toBe(1);
|
|
expect(values.get('pki.dc_certificate_count')).toBe(1);
|
|
expect(values.get('password_policy.complexity_enabled')).toBe(false);
|
|
expect(values.get('password_policy.lockout_threshold')).toBe(5);
|
|
expect(values.get('password_policy.minimum_length')).toBe(5);
|
|
expect(values.get('password_policy.maximum_age_days')).toBe(37201);
|
|
expect(values.get('laps.legacy.present')).toBe(false);
|
|
expect(values.get('laps.windows.present')).toBe(true);
|
|
|
|
// Every derived value is a plain count or boolean under the shared AD subject;
|
|
// no source identity, label text, or raw path is carried through.
|
|
derived.forEach(observation => {
|
|
expect(observation.subject.type).toBe('active-directory-environment');
|
|
expect(['number', 'boolean']).toContain(observation.valueType);
|
|
expect(observation.subject.identity).toBeUndefined();
|
|
});
|
|
expect(JSON.stringify(derived)).not.toMatch(/private|sevenkingdoms/i);
|
|
});
|
|
|
|
it('emits nothing for unrelated observations and never overwrites an existing stable property', () => {
|
|
expect(derivePingCastleEnvironmentAggregates([
|
|
cell('gpo.login', 'Some login row', 3, 'number')
|
|
])).toEqual([]);
|
|
|
|
const preexisting = derivePingCastleEnvironmentAggregates([
|
|
{ ...cell('active-directory-environment', 'User accounts', 99, 'number'), property: 'user.account_count' },
|
|
cell('user-information.account-analysis', 'Nb User Accounts', 16, 'number')
|
|
]);
|
|
expect(preexisting.some(observation => observation.property === 'user.account_count')).toBe(false);
|
|
});
|
|
});
|