141 lines
6.1 KiB
TypeScript
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 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, DefenderDevice>): 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<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.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<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
|
|
}))
|
|
};
|
|
}
|
|
};
|