Neuer Stand Assessor #1

Merged
bika merged 33 commits from AI-integration into experimental 2026-09-25 09:34:44 +00:00
12 changed files with 232 additions and 64 deletions
Showing only changes of commit 0444db1e4b - Show all commits

View File

@ -2753,6 +2753,8 @@ Festgelegtes Betriebsrisiko vom 2026-09-06: Verzeichnisabhaengige Compose-Projek
## 12. Änderungsprotokoll
2026-09-24 (Phase 7, GPO-Registry-Zustaende lesbar): Registry-Sollzustaende erscheinen im Finding Reader, in Remediation und Baseline-Empfehlungen nicht mehr als interne Codes `State: absent/key-empty/key-present`, sondern als `Value not set`, `No values under this key` und `Registry key exists` (Nutzerwortlaut). Ein fehlender Kundenzustand zeigt immer `Not configured`, auch wenn der Loeschvergleich intern einen synthetischen `absent`-Istwert fuehrt; `Registry type` entfaellt fuer Schluesselanweisungen (delete-all-values, create-key) und fehlende Typcodes (vorher `0`/`undefined`). Reine Darstellung ueber bestehenden Fakten: Urteile, Filter, Score und gespeicherte Vergleiche unveraendert, keine Reevaluation und kein Reimport. Offen und unentschieden bleibt, ob fehlende leere GPMC-Schluessel (z. B. `SrpV2\Dll`) weiter als Fail zaehlen. Im selben Batch zeigt `Expected baseline value` Baseline-Varianten lesbar je Baseline und Template (`<Template>: <Wert> · Match/Differs`, Fallback `Variant N`) statt interner `Setting Id`/`Relation`-JSON; leere Kontenlisten heissen `Empty account list` statt `Not set`; Kontenlisten-Differenzen stehen dort als `Additional`/`Missing`. Einzelne Konsenswerte bleiben typisiertes JSON. Die GPO-Description nennt ADML-Erklaerung und Geltungsgrenzen nur noch einmal, dazu das Gesamturteil und die Baselines gruppiert nach Urteil, ohne je Template wiederholte Boilerplate, `Setting reference`-IDs und Sollwertbloecke. Remediation nennt die Template-Klaerung einmal und identische konkrete Empfehlungen einmal mit ihren Baselines; Comparison ist eine Zeile mit Urteilszaehlung. Result DB behaelt seine Variantendetails unveraendert. Offen: Ob reine Client-Baselines (Windows 10/11) eine auf Domain Controller passende Einstellung zum Gesamt-Fail machen duerfen (Beispiel `SeEnableDelegationPrivilege`), bleibt eine separate Anwendbarkeitsentscheidung. Synthetische Regressionen fuer Delete-all ohne Kundenzustand (Pass, `Not configured`, Klartext, kein Registry type), Create-key-Remediation und die Variantenanzeige ueber mehrere Baselines; Gesamtlauf 1.654 bestanden/16 uebersprungen (zwei lastabhaengige Timeouts in `client-report-buttons` und `sheet-selection-cost` bestanden isoliert und im Wiederholungslauf), beide Builds, Local-only- und Designcheck gruen. Patchversion 2.6.2.
2026-09-24 (Phase 7, Workspace-Ladezeit): Der erste Timeslot-Load wurde lokal mit autorisiertem Dummy gemessen und beschleunigt. (1) Data Hub laedt den kompletten Workspace fuer seine Donuts; Open lud denselben Scope erneut. Die Ladung wird jetzt genau einmal an das naechste Open desselben normalisierten Projekt-/Run-/Assessment-Scopes uebergeben, nur wenn sie hoechstens 60 s alt ist und seit Beginn keine mutierende API-Anfrage startete oder endete; sonst wird frisch geladen. (2) Mit explizitem Assessment-Scope laeuft die GPO-Kette (Observations, Evaluations/Overview, Baseline-Analysen) parallel zum Scannermodell. (3) Baseline-Analysen (je 2-4 MB) laden mit hoechstens drei gleichzeitigen Anfragen statt strikt sequenziell; jede Baseline schreibt nur eigene Result-IDs, Fehler bleiben je Baseline markiert. (4) Zeilenanalyse: Kopfzeilenordnung und -merkmale einmal je Kopfzeilenliste (Identitaets-Cache mit Kopie, erkennt Mutation), jede Zelle einmal bereinigt, per-Zelle toLowerCase() statt toLocaleLowerCase(de-DE) (fuer alle 1.112.064 Codepoints in Browser und Node identisch; Deutsch hat keine sprachspezifischen Mappings). Ausgabe bytegleich verifiziert (Browser-Loader-Fingerprint unveraendert, 53-MB-Sheet-Serialisierung alt/neu identisch); Record-IDs und Reviews bleiben stabil, kein Reimport. Beobachtungen: Open nach Data-Hub-Auswahl 8,6 s -> 0,7 s; Tenant-Klick bis Client Info (kalt) ~5,2 s statt Data-Hub-Load plus 8,6 s; GPO-Anfuegen in Node 643-653 -> 423-461 ms. Offen: verbleibende ~1,9 s Main-Thread-Block beim Datasetaufbau (GPO-Zeilen werden fuer GPO- und Aggregatblatt analysiert); ein Worker waere eine eigene Architekturentscheidung. Tests: synthetische Nebenlaeufigkeits-, Handoff-, Cache- und Aequivalenzregressionen; Gesamtlauf 1.642 bestanden/16 uebersprungen, beide Builds erfolgreich.
2026-09-24 (Phase 13D, AI-Uebersicht Ladezeit): Der GET der AI-Uebersicht baut fuer die Stale-Pruefung die komplette Timeslot-Eingabe; mit 500er-Seiten projizierte das Repository alle Ergebnisse neunmal (lokal gemessen ~5,5 s, Beobachtung). Die AI-Eingabe liest jetzt mit der Repository-Maximalseite von 10.000 (~0,9 s beobachtet); Konsistenzpruefung und Paging fuer groessere Timeslots bleiben. Client Info wird beim Bereichswechsel entladen; die letzte Antwort je Projekt-/Timeslot-Scope liegt in einem auf acht Scopes begrenzten Modulcache, wird beim Wiedereintritt sofort gezeigt und bei jedem Mount ohne Ladeanzeige revalidiert; Antworten und Generierungsstarts ersetzen den Eintrag. Synthetischer Remount-Test (schlaegt ohne Cache fehl) ergaenzt, Seitengroessen-Erwartung angepasst; Gesamtlauf 1.631 bestanden/16 uebersprungen, beide Builds erfolgreich. Offen: erster Workspace-Load ~8,6 s (Netzwerk-Wasserfall inkl. elf sequenzieller Baseline-Analysen plus ~3,5 s Main-Thread).

4
package-lock.json generated
View File

@ -1,12 +1,12 @@
{
"name": "assessor",
"version": "2.6.1",
"version": "2.6.2",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "assessor",
"version": "2.6.1",
"version": "2.6.2",
"dependencies": {
"@fontsource-variable/inter": "^5.3.0",
"jsdom": "^29.1.1",

View File

@ -1,6 +1,6 @@
{
"name": "assessor",
"version": "2.6.1",
"version": "2.6.2",
"private": true,
"type": "module",
"scripts": {

View File

@ -5,7 +5,7 @@ import { buildPolicyWorkResults } from '../../evaluation/policy-work-results';
import { baselineVariantSummary } from '../../evaluation/policy-baseline-variants';
import { rowsToCsv } from '../../exports/csv-export';
import { makeSheet } from '../../importers/sheet-factory';
import { buildPolicyGuidance, type PolicyGuidanceMap } from '../../profiles/group-policy/guidance';
import { buildPolicyGuidance, formatBaselineVariants, type PolicyGuidanceMap } from '../../profiles/group-policy/guidance';
import { formatPolicySources } from '../../profiles/group-policy/source-presentation';
import { policySourceEvidence } from '../../profiles/group-policy/source-evidence';
import { policyBackupImpactSources } from './policy-backup-impact-sources';
@ -70,9 +70,9 @@ export function appendPolicyObservationsToDataset(
: '';
const conflict = buildPolicyConflictPresentation(entry);
const duplicate = buildPolicyDuplicatePresentation(entry);
const configured = comparison?.comparator.actual
?? (comparison?.customerState === 'missing' ? 'Not configured'
: entry.sources.flatMap(source => source.policyValues ?? []));
// A missing customer state is shown as such, even where a deletion comparison records a synthetic absent state.
const configured = comparison?.customerState === 'missing' ? 'Not configured'
: comparison?.comparator.actual ?? entry.sources.flatMap(source => source.policyValues ?? []);
const target = comparison?.identity.target;
const rawRegistry = entry.sources.flatMap(source => source.policyValues ?? [])
.find(value => value && typeof value === 'object' && !Array.isArray(value) && value.kind === 'registry-policy');
@ -82,15 +82,17 @@ export function appendPolicyObservationsToDataset(
return {
entry,
historical: Boolean(previousVersion),
explanation: comparison ? guidance.get(comparison.id)?.explanation : undefined,
importedAt: entry.importedAt,
failingBackupSources: policyBackupImpactSources(entry),
sourceEvidence: policySourceEvidence(entry.sources,
previousVersion ? undefined : comparison?.baselineVariantAssessment),
row: [
entry.title, 'GPOs', previousVersion ? 'Review required' : entry.status, entry.scope, display(configured),
display(comparison?.comparator.baseline ?? comparison?.baselineVariantAssessment?.variants.map(variant => ({
settingId: variant.settingId, value: variant.comparator.baseline, relation: variant.comparator.relation
}))), comparison?.baselineLabel ?? '',
comparison?.comparator.baseline ? display(comparison.comparator.baseline)
: comparison?.baselineVariantAssessment?.variants.length
? formatBaselineVariants(comparison, guidance.get(comparison.id)?.variantTemplates) : '',
comparison?.baselineLabel ?? '',
previousVersion ? 'Historical comparison — re-evaluation required'
: comparison?.baselineVariantAssessment ? baselineVariantSummary(comparison.baselineVariantAssessment)
: comparison?.baselineTemplateSelection?.status === 'no-match'

View File

@ -2,6 +2,7 @@ import type { FindingRecord } from '../../domain/finding';
import type { PolicyWorkResult, PolicyWorkStatus } from '../../evaluation/policy-work-results';
import { policyWorkVerdictPriority } from '../../evaluation/policy-work-results';
import { policySourceEvidence } from '../../profiles/group-policy/source-evidence';
import { baselineComparisonLine, MIXED_TEMPLATE_REMEDIATION, policyScopeNote } from '../../profiles/group-policy/guidance';
import { buildPolicyConflictPresentation, buildPolicyDuplicatePresentation, serializePolicyConflictPresentation,
POLICY_CONFLICT_DETAILS, POLICY_DUPLICATE_DETAILS } from '../../profiles/group-policy/conflict-presentation';
@ -12,6 +13,8 @@ export interface PolicyWorkRow {
failingBackupSources: NonNullable<FindingRecord['policyFailingBackupSources']>;
sourceEvidence: NonNullable<FindingRecord['policySourceEvidence']>;
baselineLabels?: string[];
/** Original ADML explanation of this comparison's baseline, used once in the rolled-up description. */
explanation?: string;
reviewMembers?: Array<{ baseline: string; row: string[] }>;
row: string[];
}
@ -52,6 +55,27 @@ export function rollupPolicyWorkRows(rows: PolicyWorkRow[], headers: string[]):
const content = value(item, header);
return content ? `${item.entry.comparison?.baselineLabel ?? 'Comparison'}\n${content}` : '';
}).filter(Boolean).join('\n\n');
const verdictGroups = [...group.reduce((map, item) => {
const verdict = value(item, 'Status');
return map.set(verdict, [...(map.get(verdict) ?? []), item]);
}, new Map<string, PolicyWorkRow[]>())].sort(([left], [right]) =>
(policyWorkVerdictPriority[right as PolicyWorkStatus] ?? 0) - (policyWorkVerdictPriority[left as PolicyWorkStatus] ?? 0));
// Current variant comparisons: explanation once, the verdict per baseline group, the scope limits once.
const variantDescription = group.every(item => item.entry.comparison?.baselineVariantAssessment) ? (() => {
const explanations = [...group.reduce((map, item) => item.explanation
? map.set(item.explanation, [...(map.get(item.explanation) ?? []), item.entry.comparison!.baselineLabel]) : map,
new Map<string, string[]>())];
const explanation = explanations.length === 1 ? explanations[0][0]
: explanations.map(([text, labels]) => `${labels.join(', ')}\n${text}`).join('\n\n');
const verdicts = verdictGroups.map(([verdict, items]) => {
const mixed = items.every(item => item.entry.comparison!.baselineVariantAssessment!.decision === 'mixed');
return `${verdict} (${items.length})${mixed ? ' — templates differ' : ''}\n`
+ items.map(item => item.entry.comparison!.baselineLabel).join('\n');
});
const scopes = unique(group.map(item => policyScopeNote(item.entry.comparison!.baselineTemplateSelection?.status)));
return [explanation, `Result across ${packageCount} baseline packages: ${status}`, ...verdicts, ...scopes]
.filter(Boolean).join('\n\n');
})() : undefined;
const merged = headers.map(header => {
const values = unique(group.map(item => value(item, header)));
if (header === 'Status') return status;
@ -63,21 +87,30 @@ export function rollupPolicyWorkRows(rows: PolicyWorkRow[], headers: string[]):
if (header === 'Conflict' && conflict) return conflict.summary;
if (header === 'Duplicates' && duplicate) return duplicate.summary;
if (header === 'Baseline') return baselineLabels.join('\n');
if (header === 'Comparison') return `Overall verdict: ${status} · ${packageCount} baseline packages\n`
+ group.map(item => `${item.entry.comparison?.baselineLabel ?? 'Comparison'}: ${value(item, 'Status')} — ${value(item, header)}`).join('\n');
if (header === 'Description') return byBaseline(header);
if (header === 'Baseline value' && values.length && (values.length > 1 || group.some(item => !value(item, header)))) return JSON.stringify(group.map(item => ({
baseline: item.entry.comparison?.baselineLabel,
// Retain each typed value, never synthesize a consensus requirement.
expected: item.entry.comparison?.comparator.baseline
?? item.entry.comparison?.baselineVariantAssessment?.variants.map(variant => ({
settingId: variant.settingId, value: variant.comparator.baseline, relation: variant.comparator.relation
})) ?? 'Unavailable'
})));
if (header === 'Comparison') return `Overall verdict: ${status} · ${packageCount} baseline packages (`
+ verdictGroups.map(([verdict, items]) => `${items.length} ${verdict}`).join(', ') + ')';
if (header === 'Description') return variantDescription ?? byBaseline(header);
if (header === 'Baseline value' && values.length && (values.length > 1 || group.some(item => !value(item, header)))) {
// Retain each typed value per baseline, never synthesize a consensus requirement.
return group.map(item => {
const comparison = item.entry.comparison;
const expected = comparison?.comparator.baseline
? baselineComparisonLine(comparison.comparator, comparison.customerState === 'missing')
: value(item, header) || 'Unavailable';
return `${comparison?.baselineLabel ?? 'Comparison'}\n${expected}`;
}).join('\n\n');
}
if (header === 'Remediation' && !values.length) return '';
if (header === 'Remediation' && (values.length > 1 || group.some(item => !value(item, header)))) {
return 'Establish the applicable baseline template before changing this setting. The recommendations below are separate comparisons.\n\n'
+ byBaseline(header);
// Identical concrete recommendations are stated once with the baselines that share them.
const concrete = new Map<string, string[]>();
for (const item of group) {
const text = value(item, header);
if (!text || text === MIXED_TEMPLATE_REMEDIATION) continue;
concrete.set(text, [...(concrete.get(text) ?? []), item.entry.comparison?.baselineLabel ?? 'Comparison']);
}
return [MIXED_TEMPLATE_REMEDIATION,
...[...concrete].map(([text, labels]) => `${text}\nBaselines: ${labels.join(', ')}`)].join('\n\n');
}
// Multiple structured payloads cannot be joined into invalid JSON.
if (header === 'Configured Value' && values.length > 1) return JSON.stringify(sources.flatMap(source => source.policyValues ?? []));

View File

@ -4,15 +4,60 @@ import { formatPolicyComparisonValue } from './presentation';
import { policyComparableValue } from '../../evaluation/policy-comparator';
import { baselineVariantSummary } from '../../evaluation/policy-baseline-variants';
export interface PolicyGuidance { description: string; remediation: string; source: string; descriptionLoadFailed?: boolean }
export interface PolicyGuidance {
description: string; remediation: string; source: string; descriptionLoadFailed?: boolean;
/** Original ADML explanation of the policy, when the exact analysis provides one. */
explanation?: string;
/** Baseline template name per variant setting ID, when the exact analysis provides one. */
variantTemplates?: Record<string, string>;
}
export type PolicyGuidanceMap = Map<string, PolicyGuidance>;
/** Scope limits of a variant comparison, stated once per finding. */
export function policyScopeNote(selection?: 'selected' | 'no-match' | 'unresolved'): string {
const context = selection === 'selected'
? 'Only baseline templates matching the explicit comparison context were assessed; this does not prove GPO application to a device.'
: selection === 'no-match'
? 'No baseline template matches the explicit comparison context, so individual template comparisons do not establish Pass or Fail.'
: 'The GPO\'s target role (Domain Controller, member server, client) is not known, so each baseline template was compared separately; no role was inferred from OS inventory or domain functional level.';
return `Scope\nCompares imported GPO backup configuration with the referenced baselines, not effective device security. ${context} Different templates are not a conflict in the customer GPO.`;
}
/** Relation of one baseline value to the customer value, including exact account-list differences. */
export function baselineComparisonLine(comparator: PolicyEvaluationResult['comparator'], customerMissing = false): string {
const value = comparator.baseline;
const actual = comparator.actual;
const relation = comparator.relation === 'match' ? 'Match'
: comparator.relation === 'different' ? customerMissing ? 'Missing' : 'Differs' : 'Unavailable';
const extra = actual?.kind === 'string-set' && value?.kind === 'string-set' ? actual.values.filter(item => !value.values.includes(item)) : [];
const missing = actual?.kind === 'string-set' && value?.kind === 'string-set' ? value.values.filter(item => !actual.values.includes(item)) : [];
return `${inlineBaselineValue(value)} · ${relation}`
+ (extra.length ? ` · Additional: ${extra.join(', ')}` : '') + (missing.length ? ` · Missing: ${missing.join(', ')}` : '');
}
/** One readable line for a typed baseline value; an empty account list stays explicit. */
export function inlineBaselineValue(value: PolicyEvaluationResult['comparator']['baseline']): string {
if (!value) return 'Unavailable';
if (value.kind === 'string-set') return value.values.length ? value.values.join(', ') : 'Empty account list';
if (value.kind === 'text' && value.value === '') return 'Empty string';
return formatPolicyComparisonValue(JSON.stringify(value)).split('\n').map(line => line.trim()).filter(Boolean).join(', ');
}
/** Lists each retained baseline variant separately; never synthesizes a consensus value. */
export function formatBaselineVariants(result: PolicyEvaluationResult, templates: Record<string, string> = {}): string {
return (result.baselineVariantAssessment?.variants ?? []).map((variant, index) =>
`${templates[variant.settingId] ?? `Variant ${index + 1}`}: ${baselineComparisonLine(variant.comparator, result.customerState === 'missing')}`
).join('\n');
}
export const MIXED_TEMPLATE_REMEDIATION = 'Establish the applicable baseline template before changing this setting. Each template under Expected baseline value is a separate recommendation; do not combine their values into a new baseline.';
export function baselineRemediation(result: PolicyEvaluationResult): string {
const baseline = result.comparator.baseline;
if (result.baselineVariantAssessment) {
if (result.baselineVariantAssessment.decision === 'unavailable') return '';
if (!baseline || result.baselineVariantAssessment.decision === 'mixed') {
return 'Establish the applicable baseline template before changing this setting. Compare the individual recommendations below; do not combine their values into a new baseline.';
return MIXED_TEMPLATE_REMEDIATION;
}
}
if (result.baselineState !== 'explicit-recommendation' || !baseline || baseline.kind === 'none'
@ -67,40 +112,20 @@ export function buildPolicyGuidance(
if (description && resource) source = `Imported baseline ADML · ${resource.locale} · ${resource.admxFileName}`;
}
}
const explanation = description;
let variantTemplates: Record<string, string> | undefined;
if (result.baselineVariantAssessment) {
// Per-template values are shown once under Expected baseline value; the description keeps
// the policy explanation, the verdict and the scope limits.
const assessment = result.baselineVariantAssessment;
const recommendations = assessment.variants.map(variant => {
for (const variant of assessment.variants) {
const setting = catalog?.baselineId === result.baselineId ? catalog.settings.get(variant.settingId) : undefined;
const names = setting?.identity.status === 'resolved' && setting.identity.identity.id === result.identity.id
? [...new Set(setting.template?.names.map(item => item.value) ?? [])] : [];
const template = names.length ? names.join(' / ') : 'Template name unavailable';
const value = variant.comparator.baseline;
const expected = value?.kind === 'string-set' && !value.values.length ? 'Empty account list'
: value?.kind === 'text' && value.value === '' ? 'Empty string'
: value ? formatPolicyComparisonValue(JSON.stringify(value)) : 'Unavailable';
const compared = variant.comparator.relation === 'match' ? 'Match'
: variant.comparator.relation === 'different' ? result.customerState === 'missing'
? 'Missing required value or key' : 'Different'
: 'Unavailable';
const actual = variant.comparator.actual;
const extra = actual?.kind === 'string-set' && value?.kind === 'string-set'
? actual.values.filter(item => !value.values.includes(item)) : [];
const missing = actual?.kind === 'string-set' && value?.kind === 'string-set'
? value.values.filter(item => !actual.values.includes(item)) : [];
const registry = variant.baselineValues[0];
const type = registry?.kind === 'registry-policy' || registry?.kind === 'registry' ? `\nRegistry type: ${registry.typeCode}` : '';
return `${result.baselineLabel} — ${template}\nSetting reference: ${variant.settingId}\nValue comparison: ${compared}\nExpected: ${expected}${type}`
+ (extra.length ? `\nAdditional entries: ${extra.join(', ')}` : '')
+ (missing.length ? `\nMissing entries: ${missing.join(', ')}` : '');
});
const context = result.baselineTemplateSelection?.status === 'selected'
? 'Only templates matching the explicit comparison context were assessed. This does not prove GPO application to a device.'
: result.baselineTemplateSelection?.status === 'no-match'
? 'No template matches the explicit comparison context. Individual value comparisons do not establish Pass or Fail.'
: 'The customer GPO target role is not established. All retained candidate recommendations were compared independently; no role was inferred from OS inventory or domain functional level.';
description = [description, baselineVariantSummary(assessment), context,
'This assesses imported backup configuration against the referenced baseline set, not effective device security. Different baseline variants are not evidence of a customer GPO conflict.',
`Baseline recommendations\n\n${recommendations.join('\n\n')}`].filter(Boolean).join('\n\n');
if (names.length) variantTemplates = { ...variantTemplates, [variant.settingId]: names.join(' / ') };
}
description = [explanation, baselineVariantSummary(assessment), policyScopeNote(result.baselineTemplateSelection?.status)]
.filter(Boolean).join('\n\n');
}
else if (result.baselineTemplateSelection || result.diagnosticCodes.includes('policy-evaluation-baseline-consensus-conflict')) {
const recommendations = new Set<string>();
@ -135,7 +160,8 @@ export function buildPolicyGuidance(
description += '\n\nNo template matches the specified comparison context. No Pass or Fail has been assigned.';
}
}
guidance.set(result.id, { description, source, remediation: baselineRemediation(result) });
guidance.set(result.id, { description, source, remediation: baselineRemediation(result),
...(explanation ? { explanation } : {}), ...(variantTemplates ? { variantTemplates } : {}) });
}
return guidance;
}

View File

@ -13,6 +13,10 @@ function scalar(value: unknown): string {
return '';
}
const registryStateLabels: Record<string, string> = {
absent: 'Value not set', 'key-empty': 'No values under this key', 'key-present': 'Registry key exists'
};
function linesFor(value: unknown, depth = 0): string[] {
const direct = scalar(value);
if (direct) return (formatAppLockerXml(direct) ?? direct).split('\n');
@ -24,6 +28,10 @@ function linesFor(value: unknown, depth = 0): string[] {
const record = value as Record<string, unknown>;
const keys = Object.keys(record);
if (record.kind === 'registry-state' && typeof record.state === 'string' && record.state in registryStateLabels
&& keys.every(key => key === 'kind' || key === 'state')) return [registryStateLabels[record.state]];
// An empty account list is an explicit requirement ("no one"), not an unset value.
if (record.kind === 'string-set' && Array.isArray(record.values) && !record.values.length) return ['Empty account list'];
if ('value' in record && keys.every(key => key === 'kind' || key === 'value')) return linesFor(record.value, depth + 1);
return keys

View File

@ -9,7 +9,7 @@ const components = readFileSync(new URL('../../src/styles/components.css', impor
describe('ASSESSOR version in the brand header', () => {
it('shows the centrally maintained product version and opens About from the brand', () => {
expect(ASSESSOR_PRODUCT_VERSION).toBe('2.6.1');
expect(ASSESSOR_PRODUCT_VERSION).toBe('2.6.2');
expect(app).toContain("import { ASSESSOR_PRODUCT_VERSION } from '../domain/assessor-release';");
expect(app).toContain('aria-label="About ASSESSOR"');
expect(app).toContain("import assessorLogo from '../assets/branding/assessor-icon-transparent.png';");

View File

@ -18,6 +18,7 @@ import { makeSheet } from '../../src/importers/sheet-factory';
import { buildPolicyWorkResults, countPolicyWorkResults } from '../../src/evaluation/policy-work-results';
import { hydrateWorkspaceReviews, previousPolicyReviews } from '../../src/features/review-workspace/database-review';
import { defaultReview } from '../../src/domain/review';
import { baselineRemediation, MIXED_TEMPLATE_REMEDIATION, policyScopeNote } from '../../src/profiles/group-policy/guidance';
vi.mock('../../src/features/project-import/load-canonical-project-workspace', () => ({ loadCanonicalProjectWorkspace: vi.fn() }));
afterEach(() => { vi.restoreAllMocks(); vi.unstubAllGlobals(); localStorage.clear(); });
@ -165,7 +166,8 @@ describe('one Work finding per technical setting across baseline packages', () =
expect(container.querySelector('[data-policy-diagnostic="duplicate"]')).toBeNull();
expect(container.querySelectorAll('.policy-source-deviation-value')).toHaveLength(1);
for (let i = 0; i < 9; i++) {
expect(container.querySelector('.finding-reader')?.textContent).toContain(`Setting reference: setting-${i}`);
expect(container.querySelector('.finding-reader')?.textContent).toContain(`Synthetic baseline ${i}`);
expect(container.querySelector('.finding-reader')?.textContent).not.toContain('Setting reference');
await select('Baseline', `synthetic baseline ${i}`);
expect(container.querySelectorAll('[data-testid="findings"] li')).toHaveLength(1);
expect(container.querySelector('.outcome-mark')?.textContent).toBe('Fail');
@ -188,15 +190,64 @@ describe('one Work finding per technical setting across baseline packages', () =
const dataset = project(observation, [evaluation]);
expect(dataset.sheets[0].analysis.records).toHaveLength(1);
expect(field(dataset, 'Status')).toBe('Fail');
const expected = JSON.parse(field(dataset, 'Baseline value'));
expect(expected.map((item: { expected: { value: string } }) => item.expected.value)).toEqual(['1', '0']);
expect(field(dataset, 'Baseline value')).toBe('Synthetic baseline 0\n1 · Differs\n\nSynthetic baseline 1\n0 · Match');
expect(field(dataset, 'Remediation')).toContain('Establish the applicable baseline template');
expect(field(dataset, 'Comparison')).toContain('Synthetic baseline 1: Pass');
expect(field(dataset, 'Comparison')).toBe('Overall verdict: Fail · 2 baseline packages (1 Fail, 1 Pass)');
expect(field(dataset, 'Description')).toContain('Pass (1)\nSynthetic baseline 1');
expect(dataset.sheets[0].analysis.records[0].policySourceEvidence?.[0].deviationFields).toBeUndefined();
expect(project(observation, [{ ...evaluation, results: [...evaluation.results].reverse() }]).sheets[0].analysis.records[0].id)
.toBe(dataset.sheets[0].analysis.records[0].id);
});
it('shows per-template account lists readably, keeps empty lists explicit and hides internal variant fields', () => {
const { observation, evaluation } = fixture(2);
const administrators = { kind: 'string-set' as const, values: ['*S-1-5-32-544'] };
const empty = { kind: 'string-set' as const, values: [] as string[] };
const variant = (settingId: string, baseline: typeof administrators) => ({ settingId, baselineValues: [],
comparator: { version: 1, comparator: { kind: 'equality' }, relation: baseline.values.length ? 'match' : 'different', diagnosticCodes: [], baseline } });
const [server, client] = evaluation.results;
server.classification = 'review_required';
server.comparator = { ...server.comparator, relation: 'incomparable', baseline: undefined };
server.baselineVariantAssessment = { ...server.baselineVariantAssessment!, decision: 'mixed',
variants: [variant('dc', administrators), variant('member', empty)] } as never;
client.comparator = { ...client.comparator, actual: administrators, baseline: empty };
client.baselineVariantAssessment!.variants = [variant('client', empty)] as never;
const before = JSON.stringify(evaluation);
const explanation = 'Synthetic policy explanation.';
const guidance = new Map([
[server.id, { description: '', remediation: MIXED_TEMPLATE_REMEDIATION, source: '', explanation,
variantTemplates: { dc: 'Synthetic - Domain Controller', member: 'Synthetic - Member Server' } }],
[client.id, { description: '', remediation: baselineRemediation(client), source: '', explanation }]
]);
const dataset = appendPolicyObservationsToDataset(null, 'Synthetic', [observation], [evaluation], guidance)!;
expect(field(dataset, 'Baseline value')).toBe('Synthetic baseline 0\n'
+ 'Synthetic - Domain Controller: *S-1-5-32-544 · Match\nSynthetic - Member Server: Empty account list · Differs\n\n'
+ 'Synthetic baseline 1\nEmpty account list · Differs · Additional: *S-1-5-32-544');
// Description: explanation and scope once, verdicts grouped by baseline, no per-template repetition.
const description = field(dataset, 'Description');
expect(description).toBe([explanation, 'Result across 2 baseline packages: Fail', 'Fail (1)\nSynthetic baseline 1',
'Review required (1) — templates differ\nSynthetic baseline 0', policyScopeNote()].join('\n\n'));
expect(field(dataset, 'Comparison')).toBe('Overall verdict: Fail · 2 baseline packages (1 Fail, 1 Review required)');
expect(field(dataset, 'Remediation')).toBe(`${MIXED_TEMPLATE_REMEDIATION}\n\n${baselineRemediation(client)}\nBaselines: Synthetic baseline 1`);
expect(field(dataset, 'Remediation').match(/Establish the applicable baseline template/g)).toHaveLength(1);
const record = dataset.sheets[0].analysis.records[0];
const html = renderToStaticMarkup(<FindingReader record={record} title={record.title} linkedSheets={[]} resultReferences={[]}
search="" position={0} recordCount={1} toolId="group-policy" onPrevious={() => {}} onNext={() => {}} onReview={() => {}}/>);
expect(html).toContain('Empty account list · Differs');
expect(html).not.toMatch(/Setting Id|Relation:|Not set/);
// A single baseline without template metadata never exposes the internal setting ID.
const single = appendPolicyObservationsToDataset(null, 'Synthetic', [observation], [{ ...evaluation, results: [server] }])!;
expect(field(single, 'Baseline value')).toBe('Variant 1: *S-1-5-32-544 · Match\nVariant 2: Empty account list · Differs');
// A consensus empty list stays typed data and reads as an explicit empty list.
const consensus = appendPolicyObservationsToDataset(null, 'Synthetic', [observation], [{ ...evaluation, results: [client] }])!;
expect(JSON.parse(field(consensus, 'Baseline value'))).toEqual(empty);
const consensusRecord = consensus.sheets[0].analysis.records[0];
expect(renderToStaticMarkup(<FindingReader record={consensusRecord} title={consensusRecord.title} linkedSheets={[]} resultReferences={[]}
search="" position={0} recordCount={1} toolId="group-policy" onPrevious={() => {}} onNext={() => {}} onReview={() => {}}/>))
.toMatch(/Expected baseline value<\/h4><p[^>]*>Empty account list</);
expect(JSON.stringify(evaluation)).toBe(before);
});
it('preserves historical readiness and never fabricates a current red source marker', () => {
const { observation, evaluation } = fixture(2);
evaluation.run.evaluator = { ...POLICY_EVALUATION_ENGINE, version: '1.8.0' };

View File

@ -5,7 +5,7 @@ import type { FindingRecord } from '../../src/domain/finding';
import type { CanonicalAssessmentModel } from '../../src/domain/canonical-model';
import { POLICY_EVALUATION_ENGINE, type PolicyEvaluationResult, type StoredPolicyEvaluation } from '../../src/domain/policy-evaluation';
import type { StoredMicrosoftSecurityBaselineAnalysis } from '../../src/domain/microsoft-security-baseline-analysis';
import { baselineRemediation, buildPolicyGuidance } from '../../src/profiles/group-policy/guidance';
import { baselineRemediation, buildPolicyGuidance, formatBaselineVariants } from '../../src/profiles/group-policy/guidance';
import { policyWebSearch } from '../../src/profiles/group-policy/web-search';
import { FindingReader } from '../../src/features/finding-reader/FindingReader';
import { appendPolicyObservationsToDataset } from '../../src/features/project-import/policy-observations-to-dataset';
@ -141,6 +141,48 @@ describe('GPO baseline guidance', () => {
expect(baselineRemediation(item)).toBe('');
});
it('shows a missing customer state as Not configured and registry states in plain language', () => {
const item = result();
item.identity.admx = { status: 'not-found' };
item.identity.target = { kind: 'registry-key', hive: 'HKEY_LOCAL_MACHINE', key: 'Software\\Policies\\Synthetic\\Blocklist' } as never;
item.classification = 'configured_match';
const keyEmpty = { kind: 'registry-state', state: 'key-empty' } as const;
const synthesizedAbsent = { kind: 'registry-state', state: 'absent' } as const;
item.comparator = { version: 1, comparator: { kind: 'equality' }, relation: 'match', diagnosticCodes: [],
actual: synthesizedAbsent, baseline: keyEmpty } as PolicyEvaluationResult['comparator'];
item.baselineVariantAssessment = { version: 1, scope: 'backup-configuration', decision: 'all-pass', diagnosticCodes: [], actualValues: [],
variants: [{ settingId: 'setting', comparator: item.comparator,
baselineValues: [{ kind: 'registry-policy', key: 'Software\\Policies\\Synthetic\\Blocklist', operation: 'delete-all-values' }] }]
} as unknown as PolicyEvaluationResult['baselineVariantAssessment'];
const before = JSON.stringify(item);
const guidance = buildPolicyGuidance([evaluation(item)], []);
const [headers, row] = appendPolicyObservationsToDataset(null, 'Synthetic', [], [evaluation(item)], guidance)!.sheets[0].rows;
const fields = Object.fromEntries(headers.map((header, index) => [String(header), String(row[index])]));
expect(fields.Status).toBe('Pass');
expect(fields['Configured Value']).toBe('Not configured');
expect(fields.Description).toContain('Matches every assessed baseline variant');
expect(fields.Description).not.toContain('Registry type');
expect(fields.Description).not.toContain('State:');
const html = render(record(fields));
expect(html).toMatch(/Configured customer value<\/h4><p[^>]*>Not configured</);
expect(html).toMatch(/Expected baseline value<\/h4><p[^>]*>No values under this key</);
expect(html).not.toContain('State:');
expect(JSON.stringify(item)).toBe(before);
const required = result();
required.identity.target = { kind: 'registry-key', hive: 'HKEY_LOCAL_MACHINE', key: 'Software\\Policies\\Synthetic\\Dll' } as never;
required.comparator.baseline = { kind: 'registry-state', state: 'key-present' };
expect(baselineRemediation(required)).toBe('Computer policy: HKEY_LOCAL_MACHINE\\Software\\Policies\\Synthetic\\Dll\nExpected: Registry key exists');
required.comparator.baseline = { kind: 'registry-state', state: 'absent' };
expect(baselineRemediation(required)).toContain('Expected: Value not set');
required.baselineVariantAssessment = { version: 1, scope: 'backup-configuration', decision: 'all-fail', diagnosticCodes: [], actualValues: [],
variants: [{ settingId: 'dll', comparator: { ...required.comparator, relation: 'different', baseline: { kind: 'registry-state', state: 'key-present' } },
baselineValues: [{ kind: 'registry-policy', key: 'Software\\Policies\\Synthetic\\Dll', operation: 'create-key', typeCode: 0, data: { kind: 'none' } }] }]
} as unknown as PolicyEvaluationResult['baselineVariantAssessment'];
expect(formatBaselineVariants(required)).toBe('Variant 1: Registry key exists · Missing');
expect(buildPolicyGuidance([evaluation(required)], []).get(required.id)!.description).not.toMatch(/Registry type|Setting reference/);
});
it('retains minimum/maximum comparison semantics in remediation', () => {
const item = result();
item.comparator.comparator = { kind: 'minimum' };

View File

@ -52,7 +52,7 @@ describe('local ASSESSOR HTTP server', () => {
const health = await fetch(`${baseUrl}/api/v1/health`);
expect(await health.json()).toEqual({
status: 'ok', application: 'ASSESSOR', productVersion: '2.6.1', apiVersion: 1, repositoryRevision: 0, persistence: 'memory'
status: 'ok', application: 'ASSESSOR', productVersion: '2.6.2', apiVersion: 1, repositoryRevision: 0, persistence: 'memory'
});
expect(health.headers.get('access-control-allow-origin')).toBeNull();
});

View File

@ -11,6 +11,7 @@ import { storedPolicyEvaluationValidationErrors, type StoredPolicyEvaluation } f
import { createPolicyEvaluationUnifiedResult, type UnifiedResultProjection, selectCurrentPolicyEvaluations } from '../../src/domain/unified-result';
import { appendPolicyObservationsToDataset } from '../../src/features/project-import/policy-observations-to-dataset';
import { buildPolicyGuidance } from '../../src/profiles/group-policy/guidance';
import { baselineVariantSummary } from '../../src/evaluation/policy-baseline-variants';
import { filterAndSortRecords } from '../../src/features/search-filter/filter-records';
import { policyComparisonDisposition } from '../../src/domain/policy-comparison-disposition';
import { selectWorkFacets } from '../../src/features/search-filter/work-facets';
@ -444,7 +445,10 @@ describe('baseline-driven policy evaluation', () => {
expect(fields.Status).toBe(expected);
expect(fields.Comparison).toContain(decision === 'mixed' ? 'Review required' : 'every assessed baseline variant');
expect(fields.Description).toContain('not effective device security');
expect(fields.Description.match(/Value comparison:/g)).toHaveLength(3);
expect(fields.Description).not.toMatch(/Setting reference|Value comparison/);
const expectedField = String(fields['Baseline value']);
if (saved.results[0].comparator.baseline) expect(JSON.parse(expectedField)).toEqual(saved.results[0].comparator.baseline);
else expect(expectedField.split('\n')).toHaveLength(3);
expect(fields.Conflict).toBe('');
const matching = (status: string) => filterAndSortRecords(sheet, { activeProfile: genericProfile, dataset,
reviews: {}, search: '', statusFilter: status, sortColumn: '', sortDirection: 'asc', actionOnly: false });
@ -466,7 +470,7 @@ describe('baseline-driven policy evaluation', () => {
expect(html).toContain('Configured Group Policy value compared with the retained baseline recommendation.');
// The saved per-variant values and verdicts remain available consistently in Work guidance
// and in the Result DB comparison without claiming effective endpoint state.
expect(buildPolicyGuidance([saved]).get(saved.results[0].id)?.description).toContain('Value comparison:');
expect(buildPolicyGuidance([saved]).get(saved.results[0].id)?.description).toContain(baselineVariantSummary(saved.results[0].baselineVariantAssessment!));
}
});