Assessor_2.0/src/importers/defender-for-iot-source-ext...

141 lines
6.1 KiB
TypeScript

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<string, JsonValue> {
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 deviceLabel(device: DefenderDevice | undefined, fallback: string): string {
return device?.displayName || device?.ip || fallback;
}
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 den meldenden Sensoren und dem Standort.
function affectedDevicesTable(occurrences: DefenderOccurrence[], deviceById: Map<string, DefenderDevice>): string {
const header = '| Affected Device | Scanner | Site |\n| --- | --- | --- |';
const rows = occurrences.map(occurrence => `| ${cell(deviceLabel(deviceById.get(occurrence.deviceId), occurrence.deviceId))} | ${cell(occurrence.sensors.join(', '))} | ${cell(occurrence.sites.join(', '))} |`);
return [header, ...rows].join('\n');
}
function cveCandidate(
document: DecodedSourceDocument,
cve: DefenderCve,
occurrences: DefenderOccurrence[],
deviceById: Map<string, DefenderDevice>
): 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.1.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,
toolId: 'defender-for-iot-vulnerabilities',
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<string, DefenderOccurrence[]>();
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
}))
};
}
};