feat(defender-for-iot): add vulnerability finding result type and backfill logic
This commit is contained in:
parent
24893f965a
commit
524a3da3b7
|
|
@ -95,3 +95,17 @@ Haupttabelle. Der unveränderte externe Vertragstext liegt unter
|
||||||
und ihre Begründung stehen in
|
und ihre Begründung stehen in
|
||||||
[`RESULT_CONTRACT.md`](RESULT_CONTRACT.md#abweichungen-der-assessor-realisierung).
|
[`RESULT_CONTRACT.md`](RESULT_CONTRACT.md#abweichungen-der-assessor-realisierung).
|
||||||
Dieses Dokument bleibt der fachlich verbindliche, implementierte Stand.
|
Dieses Dokument bleibt der fachlich verbindliche, implementierte Stand.
|
||||||
|
|
||||||
|
### ASSESSOR-Erweiterung: Vulnerability Finding
|
||||||
|
|
||||||
|
Seit 2026-09-11 erweitert ASSESSOR den geschlossenen externen
|
||||||
|
`result_type`-Katalog additiv um `vulnerability_finding`. Der Typ bezeichnet
|
||||||
|
einen Scannerbefund zu einer konkreten Schwachstelle, ohne ihn als bestaetigten
|
||||||
|
Indicator of Exposure zu klassifizieren. Microsoft-Defender-for-IoT-CVEs werden
|
||||||
|
als `vulnerability_finding` mit `outcome = warning`, `is_ioe = false` und damit
|
||||||
|
`is_deviation = true` projiziert. Severity bleibt eine unabhaengige Achse.
|
||||||
|
|
||||||
|
`environment-observation` bleibt dagegen ein `source_kind`, kein weiterer
|
||||||
|
`result_type`; diese Kontext- und Inventardaten werden als `informational`
|
||||||
|
projiziert. Der unveraenderte externe Referenzvertrag 1.0.0 wird durch diese
|
||||||
|
ASSESSOR-spezifische Erweiterung nicht rueckwirkend umgeschrieben.
|
||||||
|
|
|
||||||
|
|
@ -2580,6 +2580,7 @@ Verifiziert 2026-09-09 (Phase 7): Bereichswechsel beschleunigt und Performance-V
|
||||||
Verifiziert 2026-09-08 (Phase 7/13B): GPO-Quellenkorrektur fuer Work und Client Info. Aktuelle Evaluationen aus dem vorhandenen autoritativen Readmodell statt ergebnisabhaengiger Backupgruppierung; Import- und Auswertungszeit getrennt, Source-Scan-Provenienz auch auf GPO-only-Blaettern. 1.014 Tests bestanden, acht optionale PostgreSQL-Tests uebersprungen; Local-only-/Designpruefung und beide Builds erfolgreich. Dummy lesend bestaetigt: eine statt drei GPO-Quellenoptionen, 3.805 aktuelle Zeilen, elf historische Evaluationen unveraendert. Keine Persistenzmutation oder Neuanalyse.
|
Verifiziert 2026-09-08 (Phase 7/13B): GPO-Quellenkorrektur fuer Work und Client Info. Aktuelle Evaluationen aus dem vorhandenen autoritativen Readmodell statt ergebnisabhaengiger Backupgruppierung; Import- und Auswertungszeit getrennt, Source-Scan-Provenienz auch auf GPO-only-Blaettern. 1.014 Tests bestanden, acht optionale PostgreSQL-Tests uebersprungen; Local-only-/Designpruefung und beide Builds erfolgreich. Dummy lesend bestaetigt: eine statt drei GPO-Quellenoptionen, 3.805 aktuelle Zeilen, elf historische Evaluationen unveraendert. Keine Persistenzmutation oder Neuanalyse.
|
||||||
|
|
||||||
| Datum | Änderung |
|
| Datum | Änderung |
|
||||||
|
| 2026-09-11 | Phasen 2/6/7/11 um `vulnerability_finding` erweitert. Das Defender-Profil 1.1.0 setzt fuer statuslose CVE-Findings `outcome=warning`; Work und Unified Result projizieren `result_type=vulnerability_finding`, `is_ioe=false` und `deviation=true`. Migration 023 aktualisiert bestehende abgeleitete Result-Type-/Outcome-Spalten, ohne Originalfelder, Occurrence-JSON oder Profilprovenienz zu veraendern. Profil 1.0.0 benoetigt fuer einen vollstaendig aktuellen Datensatz Reimport/Reprocessing. |
|
||||||
| 2026-09-10 | Separate globale CVE-Anreicherung nach dem Import umgesetzt: Das tenantunabhaengige CVE-Modell und Migration 021 speichern normalisierte NVD-Beschreibung, CVSS-Metriken, CWE, Referenzen, betroffene Konfigurationen, Zeitpunkte und Provenienz einmal je CVE. Local- und PostgreSQL-Repository, Assessment-/Export- und Unified-Result-Projektionen verwenden denselben Bestand fuer passende Defender-Vorkommen. Same-Origin-API und Results-Browser bieten manuelle Enrich-/Refresh-Aktionen; der NVD-Client verwendet den festen CVE-2.0-Endpunkt mit Timeout und Antwortgrenze. Keine Scanner-Originalfelder werden veraendert. |
|
| 2026-09-10 | Separate globale CVE-Anreicherung nach dem Import umgesetzt: Das tenantunabhaengige CVE-Modell und Migration 021 speichern normalisierte NVD-Beschreibung, CVSS-Metriken, CWE, Referenzen, betroffene Konfigurationen, Zeitpunkte und Provenienz einmal je CVE. Local- und PostgreSQL-Repository, Assessment-/Export- und Unified-Result-Projektionen verwenden denselben Bestand fuer passende Defender-Vorkommen. Same-Origin-API und Results-Browser bieten manuelle Enrich-/Refresh-Aktionen; der NVD-Client verwendet den festen CVE-2.0-Endpunkt mit Timeout und Antwortgrenze. Keine Scanner-Originalfelder werden veraendert. |
|
||||||
| 2026-09-10 | Defender-Betroffenheitszahl von der Rolle `score` auf `asset` umgestellt: In ASSESSOR ist `score` die Risikomagnitude, waehrend die Anzahl betroffener Devices eine Verbreitungskennzahl ist. Das aggregierte CVE traegt die Anzahl daher als `affectedAsset`; der Results-Browser zeigt sie unter der bestehenden Betroffenheitszeile, und `score` bleibt fuer echte Risikoscores frei. Kein Vergleichsschluessel betroffen (weiter `rule-id`). |
|
| 2026-09-10 | Defender-Betroffenheitszahl von der Rolle `score` auf `asset` umgestellt: In ASSESSOR ist `score` die Risikomagnitude, waehrend die Anzahl betroffener Devices eine Verbreitungskennzahl ist. Das aggregierte CVE traegt die Anzahl daher als `affectedAsset`; der Results-Browser zeigt sie unter der bestehenden Betroffenheitszeile, und `score` bleibt fuer echte Risikoscores frei. Kein Vergleichsschluessel betroffen (weiter `rule-id`). |
|
||||||
| 2026-09-10 | Results-Browser zeigt fuer Defender-CVEs die aggregierte Betroffenheit als `Affected Devices`-Anzahl statt eines leeren `Affected Object`. Andere Werkzeuge behalten die `Affected Object`-Zeile unveraendert. |
|
| 2026-09-10 | Results-Browser zeigt fuer Defender-CVEs die aggregierte Betroffenheit als `Affected Devices`-Anzahl statt eines leeren `Affected Object`. Andere Werkzeuge behalten die `Affected Object`-Zeile unveraendert. |
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,7 @@ import nativePolicyTargets from './migrations/019_native_policy_targets.sql?raw'
|
||||||
import baselineDrivenPolicyEvaluation from './migrations/020_baseline_driven_policy_evaluation.sql?raw';
|
import baselineDrivenPolicyEvaluation from './migrations/020_baseline_driven_policy_evaluation.sql?raw';
|
||||||
import cveEnrichment from './migrations/021_cve_enrichment.sql?raw';
|
import cveEnrichment from './migrations/021_cve_enrichment.sql?raw';
|
||||||
import vulnerabilityFindingResultType from './migrations/022_vulnerability_finding_result_type.sql?raw';
|
import vulnerabilityFindingResultType from './migrations/022_vulnerability_finding_result_type.sql?raw';
|
||||||
|
import defenderVulnerabilityBackfill from './migrations/023_defender_vulnerability_backfill.sql?raw';
|
||||||
|
|
||||||
export interface DatabaseMigration {
|
export interface DatabaseMigration {
|
||||||
version: string;
|
version: string;
|
||||||
|
|
@ -49,5 +50,6 @@ export const databaseMigrations: readonly DatabaseMigration[] = [
|
||||||
{ version: '019', name: 'native-policy-targets', sql: nativePolicyTargets },
|
{ version: '019', name: 'native-policy-targets', sql: nativePolicyTargets },
|
||||||
{ version: '020', name: 'baseline-driven-policy-evaluation', sql: baselineDrivenPolicyEvaluation },
|
{ version: '020', name: 'baseline-driven-policy-evaluation', sql: baselineDrivenPolicyEvaluation },
|
||||||
{ version: '021', name: 'cve-enrichment', sql: cveEnrichment },
|
{ version: '021', name: 'cve-enrichment', sql: cveEnrichment },
|
||||||
{ version: '022', name: 'vulnerability-finding-result-type', sql: vulnerabilityFindingResultType }
|
{ version: '022', name: 'vulnerability-finding-result-type', sql: vulnerabilityFindingResultType },
|
||||||
|
{ version: '023', name: 'defender-vulnerability-backfill', sql: defenderVulnerabilityBackfill }
|
||||||
];
|
];
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,12 @@
|
||||||
|
-- Backfill ausschliesslich der abgeleiteten, indizierten Read-Model-Spalten
|
||||||
|
-- fuer bereits importierte Defender-CVEs. Originalfelder, Occurrence-JSON und
|
||||||
|
-- Importprovenienz bleiben unveraendert.
|
||||||
|
|
||||||
|
UPDATE assessor_finding_occurrences
|
||||||
|
SET
|
||||||
|
result_type = 'vulnerability_finding',
|
||||||
|
outcome_normalized = CASE
|
||||||
|
WHEN outcome_normalized = 'unknown' THEN 'warning'
|
||||||
|
ELSE outcome_normalized
|
||||||
|
END
|
||||||
|
WHERE lower(btrim(tool_id)) = 'defender-for-iot-vulnerabilities';
|
||||||
|
|
@ -8,8 +8,9 @@ export type JsonPrimitive = string | number | boolean | null;
|
||||||
export type JsonValue = JsonPrimitive | JsonValue[] | { [key: string]: JsonValue };
|
export type JsonValue = JsonPrimitive | JsonValue[] | { [key: string]: JsonValue };
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* result_type aus RESULT_CONTRACT 1.0.0 (docs/RESULT_CONTRACT.md). Additive
|
* result_type aus RESULT_CONTRACT 1.0.0 plus dokumentierte ASSESSOR-Erweiterungen
|
||||||
* Facette, ausschließlich über src/domain/result-contract-mapping.ts abgeleitet.
|
* (docs/CANONICAL_MODEL.md). Additive Facette, ausschließlich über
|
||||||
|
* src/domain/result-contract-mapping.ts abgeleitet.
|
||||||
*/
|
*/
|
||||||
export type CanonicalResultType = ResultContractResultType;
|
export type CanonicalResultType = ResultContractResultType;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -359,10 +359,19 @@ function mappingConfidence(occurrence: FindingOccurrence): number {
|
||||||
export function createScannerUnifiedResult(source: ScannerUnifiedResultSource): UnifiedResultProjection {
|
export function createScannerUnifiedResult(source: ScannerUnifiedResultSource): UnifiedResultProjection {
|
||||||
const review = matchingReview(source);
|
const review = matchingReview(source);
|
||||||
const dimensions = reviewDimensions(review);
|
const dimensions = reviewDimensions(review);
|
||||||
const outcomeNormalized = source.occurrence.outcome?.normalizedValue
|
|
||||||
?? outcomeFromLegacyStatus(source.occurrence.status.normalizedValue ?? 'unknown');
|
|
||||||
const resultType = source.occurrence.resultType
|
const resultType = source.occurrence.resultType
|
||||||
?? deriveResultType(source.occurrence.sourceIdentity.tool.id);
|
?? deriveResultType(source.occurrence.sourceIdentity.tool.id);
|
||||||
|
const recordedOutcome = source.occurrence.outcome?.normalizedValue
|
||||||
|
?? outcomeFromLegacyStatus(source.occurrence.status.normalizedValue ?? 'unknown');
|
||||||
|
const originalOutcome = source.occurrence.outcome?.originalValue ?? source.occurrence.status.originalValue;
|
||||||
|
const hasOriginalOutcome = typeof originalOutcome === 'string'
|
||||||
|
? Boolean(originalOutcome.trim())
|
||||||
|
: originalOutcome !== null && originalOutcome !== undefined;
|
||||||
|
const outcomeNormalized = resultType === 'vulnerability_finding'
|
||||||
|
&& recordedOutcome === 'unknown'
|
||||||
|
&& !hasOriginalOutcome
|
||||||
|
? 'warning'
|
||||||
|
: recordedOutcome;
|
||||||
const exposureState = source.occurrence.exposureState
|
const exposureState = source.occurrence.exposureState
|
||||||
?? deriveExposureState(resultType, outcomeNormalized);
|
?? deriveExposureState(resultType, outcomeNormalized);
|
||||||
const isIoe = source.occurrence.isIoe
|
const isIoe = source.occurrence.isIoe
|
||||||
|
|
|
||||||
|
|
@ -178,6 +178,23 @@ describe('Microsoft Defender for IoT vulnerability model', () => {
|
||||||
isIoe: false,
|
isIoe: false,
|
||||||
isDeviation: true
|
isDeviation: true
|
||||||
});
|
});
|
||||||
|
expect(createScannerUnifiedResult({
|
||||||
|
project: canonical.project,
|
||||||
|
assessment: canonical.assessment,
|
||||||
|
scanRun: canonical.scanRuns[0],
|
||||||
|
importFile: canonical.importFiles[0],
|
||||||
|
definition: canonical.findingDefinitions[0],
|
||||||
|
occurrence: {
|
||||||
|
...canonical.findingOccurrences[0],
|
||||||
|
outcome: { ...canonical.findingOccurrences[0].outcome!, normalizedValue: 'unknown' }
|
||||||
|
},
|
||||||
|
reviews: canonical.reviews
|
||||||
|
})).toMatchObject({
|
||||||
|
resultType: 'vulnerability_finding',
|
||||||
|
outcomeNormalized: 'warning',
|
||||||
|
isIoe: false,
|
||||||
|
isDeviation: true
|
||||||
|
});
|
||||||
// The aggregated CVE carries its affected-device count as the affected asset (two devices here); the device list lives in the evidence table.
|
// The aggregated CVE carries its affected-device count as the affected asset (two devices here); the device list lives in the evidence table.
|
||||||
expect(canonical.findingOccurrences[0].affectedAsset?.normalizedValue).toBe('2');
|
expect(canonical.findingOccurrences[0].affectedAsset?.normalizedValue).toBe('2');
|
||||||
expect(canonical.findingOccurrences[0].score).toBeUndefined();
|
expect(canonical.findingOccurrences[0].score).toBeUndefined();
|
||||||
|
|
|
||||||
|
|
@ -12,19 +12,40 @@ const migrationPath = resolve(
|
||||||
'migrations',
|
'migrations',
|
||||||
'022_vulnerability_finding_result_type.sql'
|
'022_vulnerability_finding_result_type.sql'
|
||||||
);
|
);
|
||||||
|
const backfillPath = resolve(
|
||||||
|
import.meta.dirname,
|
||||||
|
'..',
|
||||||
|
'..',
|
||||||
|
'server',
|
||||||
|
'postgres',
|
||||||
|
'migrations',
|
||||||
|
'023_defender_vulnerability_backfill.sql'
|
||||||
|
);
|
||||||
|
|
||||||
describe('PostgreSQL vulnerability finding result type migration', () => {
|
describe('PostgreSQL vulnerability finding result type migration', () => {
|
||||||
it('registers migration 022 after CVE enrichment', () => {
|
it('registers the immutable catalog extension before the Defender backfill', () => {
|
||||||
expect(databaseMigrations.at(-1)).toMatchObject({
|
expect(databaseMigrations.at(-2)).toMatchObject({
|
||||||
version: '022',
|
version: '022',
|
||||||
name: 'vulnerability-finding-result-type'
|
name: 'vulnerability-finding-result-type'
|
||||||
});
|
});
|
||||||
|
expect(databaseMigrations.at(-1)).toMatchObject({
|
||||||
|
version: '023',
|
||||||
|
name: 'defender-vulnerability-backfill'
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('extends the closed result type constraint without rewriting occurrences', async () => {
|
it('keeps migration 022 limited to the closed result type constraint', async () => {
|
||||||
const sql = await readFile(migrationPath, 'utf8');
|
const sql = await readFile(migrationPath, 'utf8');
|
||||||
expect(sql).toContain("'vulnerability_finding'");
|
expect(sql).toContain("'vulnerability_finding'");
|
||||||
expect(sql).toContain('assessor_occurrences_result_type_check');
|
expect(sql).toContain('assessor_occurrences_result_type_check');
|
||||||
expect(sql).not.toMatch(/UPDATE\s+assessor_finding_occurrences/i);
|
expect(sql).not.toMatch(/UPDATE\s+assessor_finding_occurrences/i);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('backfills only derived Defender columns in migration 023', async () => {
|
||||||
|
const sql = await readFile(backfillPath, 'utf8');
|
||||||
|
expect(sql).toMatch(/UPDATE\s+assessor_finding_occurrences/i);
|
||||||
|
expect(sql).toContain("outcome_normalized = 'unknown' THEN 'warning'");
|
||||||
|
expect(sql).toContain("lower(btrim(tool_id)) = 'defender-for-iot-vulnerabilities'");
|
||||||
|
expect(sql).not.toMatch(/jsonb_set|\bdata\s*=/i);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue