Assessor_2.0/src/importers/purple-knight-report-contex...

109 lines
8.0 KiB
TypeScript

import type { JsonValue } from '../domain/canonical-model';
import type { ImportDiagnostic } from '../domain/import-intermediate';
import type { EnvironmentObservationCandidate, ScannerReportSectionCandidate } from '../domain/source-extraction';
import { isJsonObject } from './embedded-report-json';
import { purpleKnightMetricObservation } from './purple-knight-summary-source-extractor';
export interface PurpleKnightIndicator {
value: Record<string, JsonValue>;
path: string;
identity: string;
}
export function purpleKnightStatus(indicator: Record<string, JsonValue>, execution: Record<string, JsonValue>): string {
if (indicator.IsFailedToRun === true) return 'Failed to run';
if (indicator.IsAborted === true) return 'Canceled';
if (indicator.Selected === false) return 'Not selected';
if (indicator.IsEvaluated === false) return 'Not Relevant';
if (indicator.IsFailed === true && typeof indicator.ExposureType === 'string' && /IOE/i.test(indicator.ExposureType)) return 'IOE Found';
if (indicator.IsFailed === true && typeof indicator.ExposureType === 'string' && /IOC/i.test(indicator.ExposureType)) return 'IOC Found';
const status = execution.Status ?? indicator.State;
return typeof status === 'string' ? status.replace(/([a-z])([A-Z])/g, '$1 $2') : '';
}
const METRICS: Record<string, string> = {
TotalScore: 'Security posture score', TotalCount: 'Total indicators', TotalEvaluatedCount: 'Evaluated',
PassedCount: 'Passed', IOEsCount: 'IOEs found', IOCsCount: 'IOCs found', FailedToRunCount: 'Failed to run',
AbortedCount: 'Canceled', UnselectedCount: 'Not selected', NotRelevant: 'Not relevant',
TestExecutionTime: 'Scan duration', TotalSelectedCount: 'Selected indicators'
};
function countDiagnostic(value: JsonValue, actual: number, path: string): ImportDiagnostic[] {
if (typeof value !== 'number' || !Number.isSafeInteger(value) || value < 0) return [{
severity: 'warning', code: 'purple-knight-count-invalid', stage: 'extraction',
message: 'A declared PurpleKnight count is invalid.', details: { objectPath: path }
}];
return value === actual ? [] : [{
severity: 'warning', code: 'purple-knight-count-mismatch', stage: 'extraction',
message: `PurpleKnight declares ${value} results; ${actual} were independently counted.`,
details: { objectPath: path, declared: value, actual }
}];
}
export function purpleKnightReportContext(fileName: string, root: Record<string, JsonValue>, indicators: PurpleKnightIndicator[]) {
const sectionKey = 'purple-knight-assessment-summary';
const rootPath = '$.reportJSON';
// Keep all report metadata, including future nested keys, separate from findings.
const reportSections: ScannerReportSectionCandidate[] = [{
version: 1, sectionKey, label: 'PurpleKnight assessment summary', ordinal: 0, state: 'observed',
tables: [{ name: 'Report metadata', headers: ['Property', 'Original value'], rows: Object.entries(root) }],
source: { fileName, objectPath: rootPath }, attributes: { kind: 'purple-knight-report-context' }
}];
const environmentObservations: EnvironmentObservationCandidate[] = Object.entries(root).map(([key, value]) => ({
version: 1, sectionKey, subject: { type: 'purple-knight-report', identity: rootPath },
property: `purple_knight.source.${key}`, label: key, originalValue: value, normalizedValue: value,
valueType: value !== null && typeof value === 'object' ? 'object' : typeof value === 'number' ? 'number' : typeof value === 'boolean' ? 'boolean' : 'string',
state: value === null || value === '' ? 'empty' : 'observed',
source: { fileName, objectPath: `${rootPath}.${key}` }, attributes: { sourceField: key }
}));
const diagnostics: ImportDiagnostic[] = [];
const metric = (key: string, value: JsonValue | undefined, path: string) => {
if (value !== undefined) environmentObservations.push(purpleKnightMetricObservation(key, value, { fileName, objectPath: path }));
};
metric('Tool version', root.AppVersion, `${rootPath}.AppVersion`);
metric('Tool edition', root.Edition, `${rootPath}.Edition`);
const scopes = Array.isArray(root.reportResultsList) ? root.reportResultsList : [];
const byIdentity = new Map(indicators.map(item => [item.identity, item]));
for (const [index, scope] of scopes.entries()) {
if (!isJsonObject(scope) || !isJsonObject(scope.ReportResults)) continue;
const path = `${rootPath}.reportResultsList[${index}]`;
const prefix = scope.ADType === 'AD' ? 'AD' : /^(?:AAD|Entra)$/i.test(String(scope.ADType)) ? 'Entra' : 'Other';
const results = scope.ReportResults;
metric('Generated date', results.GeneratedOn, `${path}.ReportResults.GeneratedOn`);
for (const [key, label] of Object.entries(METRICS)) metric(key === 'TotalSelectedCount' && scopes.length === 1 ? 'Selected indicators' : `${prefix}: ${label}`, results[key], `${path}.ReportResults.${key}`);
metric(`${prefix}: Forest name`, scope.ForestName, `${path}.ForestName`);
if (Array.isArray(scope.SelectedDomains)) {
environmentObservations.push({
...purpleKnightMetricObservation(`${prefix}: Number of domains`, scope.SelectedDomains, { fileName, objectPath: `${path}.SelectedDomains` }),
normalizedValue: scope.SelectedDomains.length, valueType: 'number', attributes: { derived: true, derivation: 'array-length' }
});
}
metric(`${prefix}: Domains`, scope.SelectedDomains, `${path}.SelectedDomains`);
metric('Requested by', scope.UserName, `${path}.UserName`);
const referenced = new Set<string>();
for (const [categoryIndex, entry] of (Array.isArray(scope.ReportCategories) ? scope.ReportCategories : []).entries()) {
if (!isJsonObject(entry)) continue;
const category = isJsonObject(entry.Category) ? entry.Category : {};
if (typeof category.Name === 'string') metric(`${prefix} category score: ${category.Name}`, category.Score, `${path}.ReportCategories[${categoryIndex}].Category.Score`);
if (Array.isArray(entry.Indicators)) for (const identity of entry.Indicators) if (typeof identity === 'string') referenced.add(identity);
}
const scoped = scopes.length === 1 ? indicators : [...referenced].flatMap(id => byIdentity.has(id) ? [byIdentity.get(id)!] : []);
const counts: Record<string, number> = { TotalCount: scoped.length, PassedCount: 0, IOEsCount: 0, IOCsCount: 0, FailedToRunCount: 0, AbortedCount: 0, UnselectedCount: 0, NotRelevant: 0, TotalEvaluatedCount: 0 };
const statusKeys: Record<string, string> = { Pass: 'PassedCount', Passed: 'PassedCount', 'IOE Found': 'IOEsCount', 'IOC Found': 'IOCsCount', 'Failed to run': 'FailedToRunCount', Canceled: 'AbortedCount', 'Not selected': 'UnselectedCount', 'Not Relevant': 'NotRelevant' };
for (const item of scoped) {
const indicator = isJsonObject(item.value.ResIndicator) ? item.value.ResIndicator : {};
const status = purpleKnightStatus(indicator, isJsonObject(item.value.ExecutionResult) ? item.value.ExecutionResult : {});
if (Object.hasOwn(statusKeys, status)) counts[statusKeys[status]]++;
if (indicator.IsEvaluated === true) counts.TotalEvaluatedCount++;
}
for (const [key, actual] of Object.entries(counts)) if (key in results) diagnostics.push(...countDiagnostic(results[key], actual, `${path}.ReportResults.${key}`));
for (const id of referenced) if (!byIdentity.has(id)) diagnostics.push({ severity: 'warning', code: 'purple-knight-reference-missing', stage: 'extraction', message: 'A report category references a missing indicator.', details: { objectPath: `${path}.ReportCategories` } });
}
for (const item of indicators) {
const rows = Array.isArray(item.value.IndicatorReportObjects) ? item.value.IndicatorReportObjects : [];
if ('TotalResultsCount' in item.value) diagnostics.push(...countDiagnostic(item.value.TotalResultsCount, rows.length, `${item.path}.TotalResultsCount`));
if (isJsonObject(root.IndicatorsTotalCount) && item.identity in root.IndicatorsTotalCount) diagnostics.push(...countDiagnostic(root.IndicatorsTotalCount[item.identity], rows.length, `${rootPath}.IndicatorsTotalCount[${JSON.stringify(item.identity)}]`));
}
return { reportSections, environmentObservations, diagnostics };
}