Neuer Stand Assessor #1
|
|
@ -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).
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "assessor",
|
||||
"version": "2.6.1",
|
||||
"version": "2.6.2",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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 ?? []));
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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';");
|
||||
|
|
|
|||
|
|
@ -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' };
|
||||
|
|
|
|||
|
|
@ -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' };
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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!));
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue