Assessor_2.0/src/importers/purple-knight-payload.ts

84 lines
5.7 KiB
TypeScript

import type { JsonValue } from '../domain/canonical-model';
import type { DecodedSourceDocument, ImportDiagnostic } from '../domain/import-intermediate';
import { ImportError } from '../shared/errors';
import { isJsonObject } from './embedded-report-json';
import { assignedReportObjects, readStaticReportObject, StaticReportLiteralError, type StaticReportBindingMatcher } from './static-report-literal';
import type { PurpleKnightIndicator } from './purple-knight-report-context';
import { protectHtmlAttributeMarkup } from './html-attribute-markup';
const MAX_INDICATORS = 250_000;
// Inert source syntax, never a dependency on the browser's global object.
const SCRIPT_GLOBAL = 'win' + 'dow';
const ASSIGNMENT = new RegExp(`^${SCRIPT_GLOBAL}\\s*(?:\\.\\s*(reportJSON)|\\[\\s*["'](Category_\\d+)["']\\s*\\]\\s*\\[\\s*["']([\\w-]+)["']\\s*\\])\\s*=\\s*(?!=|>)`);
const reportBinding: StaticReportBindingMatcher = (script, position) => {
if (!script.startsWith(SCRIPT_GLOBAL, position)) return undefined;
let before = position - 1;
while (before >= 0 && /\s/.test(script[before])) before--;
if (script[before] === '.') return undefined;
const match = ASSIGNMENT.exec(script.slice(position));
return match ? { start: position + match[0].length, binding: match[1] ?? `${match[2]}[${JSON.stringify(match[3])}]` } : undefined;
};
interface PurplePayload {
root?: Record<string, JsonValue>;
indicators: PurpleKnightIndicator[];
categoryNames: Map<number, string>;
recognized: boolean;
diagnostics: ImportDiagnostic[];
}
const cache = new WeakMap<DecodedSourceDocument, PurplePayload>();
function invalid(code: string, message: string): never {
throw new ImportError(message, [{ severity: 'error', stage: 'extraction', code, message }]);
}
export function purpleKnightPayload(document: DecodedSourceDocument): PurplePayload {
const cached = cache.get(document);
if (cached) return cached;
const result: PurplePayload = { indicators: [], categoryNames: new Map(), recognized: false, diagnostics: [] };
if (document.format !== 'HTML') return result;
const bindings = new Set<string>();
const names = new Set<string>();
try {
// Respect quoted attributes, comments and other raw-text elements without
// altering the inert script bodies or their native JSON string values.
const markup = protectHtmlAttributeMarkup(document.rawText);
const scripts = /<!--[\s\S]*?-->|<(script|style|textarea|title|xmp|iframe|noembed|noframes)\b[^>]*>([\s\S]*?)(?:<\/\1\s*>|$)/gi;
for (const script of markup.matchAll(scripts)) {
if (script[1]?.toLowerCase() !== 'script') continue;
for (const { start, binding } of assignedReportObjects(script[2], reportBinding)) {
if (binding !== 'reportJSON' && !binding.startsWith('Category_')) continue;
result.recognized = true;
if (bindings.has(binding)) invalid('purple-knight-binding-duplicate', 'PurpleKnight contains conflicting report assignments.');
bindings.add(binding);
const { value } = readStaticReportObject(script[2], start);
if (binding === 'reportJSON') {
if (!Array.isArray(value.reportResultsList) || value.reportResultsList.some(scope => !isJsonObject(scope) || !isJsonObject(scope.ReportResults))) invalid('purple-knight-payload-invalid', 'PurpleKnight report summary is malformed.');
result.root = value;
} else {
if (result.indicators.length >= MAX_INDICATORS) invalid('purple-knight-indicator-limit', 'PurpleKnight indicator limit exceeded; no results were silently truncated.');
if (!isJsonObject(value.ResIndicator) || !isJsonObject(value.ExecutionResult)) invalid('purple-knight-indicator-invalid', 'A PurpleKnight indicator is malformed.');
const name = value.ResIndicator.ShortName;
if (typeof name !== 'string' || !name.trim()) invalid('purple-knight-id-missing', 'A PurpleKnight indicator has no native rule ID.');
if (names.has(name)) result.diagnostics.push({ severity: 'warning', code: 'purple-knight-id-duplicate', stage: 'extraction', message: 'PurpleKnight contains repeated native rule IDs. All occurrences were retained.' });
names.add(name);
if (value.IndicatorReportObjects != null && (!Array.isArray(value.IndicatorReportObjects) || value.IndicatorReportObjects.some(row => !Array.isArray(row)))) invalid('purple-knight-detail-invalid', 'A PurpleKnight result table is malformed.');
const identity = JSON.parse(binding.slice(binding.indexOf('[') + 1, -1)) as string;
result.indicators.push({ value, path: `$.${binding}`, identity });
}
}
}
} catch (error) {
if (!(error instanceof StaticReportLiteralError)) throw error;
if (result.recognized) invalid('purple-knight-payload-invalid', 'PurpleKnight data could not be read completely. Only bounded static report objects are supported.');
}
if (result.recognized && !result.root) invalid('purple-knight-summary-missing', 'PurpleKnight indicator data has no readable report summary.');
if (result.root) {
const scopes = result.root.reportResultsList as JsonValue[];
if (!result.indicators.length && (!scopes.length || scopes.some(scope => !isJsonObject(scope) || !isJsonObject(scope.ReportResults) || scope.ReportResults.TotalCount !== 0))) invalid('purple-knight-results-missing', 'PurpleKnight announces results but no indicators could be read.');
const config = isJsonObject(result.root.GeneralConfig) ? result.root.GeneralConfig : {};
for (const category of Array.isArray(config.Categories) ? config.Categories : []) if (isJsonObject(category) && typeof category.ID === 'number' && typeof category.Name === 'string') result.categoryNames.set(category.ID, category.Name);
}
cache.set(document, result);
return result;
}