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; indicators: PurpleKnightIndicator[]; categoryNames: Map; recognized: boolean; diagnostics: ImportDiagnostic[]; } const cache = new WeakMap(); 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(); const names = new Set(); 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 = /|<(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; }