import type { JsonValue } from '../domain/canonical-model'; import { buildDefenderVulnerabilityModel, defenderTableRole, type DefenderCve, type DefenderDevice, type DefenderObservation, type DefenderOccurrence, type DefenderSourceRow, type DefenderSourceTable } from '../domain/defender-for-iot'; import type { DecodedSourceDocument, ImportTable, SourceLocator } from '../domain/import-intermediate'; import type { FindingCandidate, SourceExtractor, SourceFieldCandidate } from '../domain/source-extraction'; function headerFor(table: ImportTable): string[] { return table.headerCandidates[0]?.headers ?? []; } function sourceRows(table: ImportTable): DefenderSourceRow[] { const header = table.headerCandidates[0]; if (!header) return []; return table.rows .filter(row => row.index !== header.rowIndex && row.cells.some(cell => cell.decodedValue.trim())) .map(row => ({ tableIndex: table.index, rowIndex: row.index, fields: Object.fromEntries(row.cells.map((cell, index) => [header.headers[index] || `Column ${index + 1}`, cell.rawValue])) })); } function sourceTables(document: DecodedSourceDocument): DefenderSourceTable[] { return document.tables.map(table => ({ tableIndex: table.index, name: table.name, headers: headerFor(table), rows: sourceRows(table) })); } function observationValue(observation: DefenderObservation): Record { return { name: observation.name, ip: observation.ip, site: observation.site, sensor: observation.sensor, cve: observation.cve, severity: observation.severitySource, description: observation.description, additionalSourceFields: observation.additionalSourceFields, source: { tableIndex: observation.tableIndex, rowIndex: observation.rowIndex } }; } function field(key: string, label: string, originalValue: JsonValue, decodedValue: string, source: SourceLocator): SourceFieldCandidate { return { key, label, originalValue, decodedValue, source }; } function cell(value: string): string { // Zellen einer Markdown-Tabelle: Pipe und Zeilenumbruch wuerden die // Tabellenstruktur brechen, deshalb neutralisiert. return value.replace(/\|/g, '\\|').replace(/\s*\n\s*/g, ' ').trim(); } // Die betroffenen Devices je CVE als Markdown-Tabelle. Sie wird im // Ergebnisfenster als Tabelle ausgewertet (siehe RichTextValue) und ersetzt so // die frueher als Rohtext gezeigten Source Observations. Eine Zeile je Device // (Occurrence) mit Name und IP-Adresse getrennt, den meldenden Sensoren und dem // Standort. function affectedDevicesTable(occurrences: DefenderOccurrence[], deviceById: Map): string { const header = '| Name | IP Address | Scanner | Site |\n| --- | --- | --- | --- |'; const rows = occurrences.map(occurrence => { const device = deviceById.get(occurrence.deviceId); const name = device?.displayName || occurrence.deviceId; return `| ${cell(name)} | ${cell(device?.ip ?? '')} | ${cell(occurrence.sensors.join(', '))} | ${cell(occurrence.sites.join(', '))} |`; }); return [header, ...rows].join('\n'); } function cveCandidate( document: DecodedSourceDocument, cve: DefenderCve, occurrences: DefenderOccurrence[], deviceById: Map ): FindingCandidate { const allObservations = occurrences.flatMap(occurrence => occurrence.observations); const first = allObservations[0]; const severityObservation = allObservations.find(observation => observation.severity === cve.severity) ?? first; const source = { fileName: document.file.name, tableIndex: first.tableIndex, rowIndex: first.rowIndex }; const observations = allObservations.map(observationValue); const devicesTable = affectedDevicesTable(occurrences, deviceById); return { version: 1, id: `defender-cve-${cve.id}`, source, fields: [ field('cve', 'CVE', cve.id, cve.id, source), field('affected-device-count', 'Affected Device Count', cve.deviceIds.length, String(cve.deviceIds.length), source), field('severity', 'Severity', severityObservation.severitySource, severityObservation.severitySource, source), field('description', 'Description', cve.description, cve.description, source), field('observation-count', 'Observation Count', observations.length, String(observations.length), source), field('affected-devices', 'Affected Devices', devicesTable, devicesTable, source), field('source-observations', 'Source Observations', observations, JSON.stringify(observations), source) ] }; } export const defenderForIotSourceExtractor: SourceExtractor = { id: 'defender-for-iot-vulnerability-source-extractor', version: '1.0.0', detect(document) { const roles = document.tables.map(table => defenderTableRole(headerFor(table))).filter(Boolean); if (!roles.length) return { score: 0, evidence: [] }; const both = roles.includes('summary') && roles.includes('detail'); return { score: both ? 1 : 0.92, evidence: [both ? 'A Defender for IoT asset summary and detailed vulnerability table were found.' : `A Defender for IoT ${roles[0]} table was found.`] }; }, extract(document) { const model = buildDefenderVulnerabilityModel(sourceTables(document)); const deviceById = new Map(model.devices.map(device => [device.id, device])); const occurrencesByCve = new Map(); for (const occurrence of model.occurrences) { occurrencesByCve.set(occurrence.cve, [...(occurrencesByCve.get(occurrence.cve) ?? []), occurrence]); } return { version: 1, candidates: model.cves.map(cve => cveCandidate(document, cve, occurrencesByCve.get(cve.id) ?? [], deviceById)), diagnostics: model.diagnostics.map(diagnostic => ({ severity: diagnostic.severity, code: diagnostic.code, message: diagnostic.message, stage: 'extraction', source: diagnostic.tableIndex === undefined ? undefined : { fileName: document.file.name, tableIndex: diagnostic.tableIndex, rowIndex: diagnostic.rowIndex }, details: diagnostic.details })) }; } };