import type { CanonicalFindingStatus } from '../../domain/canonical-model'; import type { ProfileValueMapping, ToolProfileDefinition } from '../../domain/tool-profile-definition'; import { commonSeverityMappings, commonStatusMappings } from '../common-normalization'; import { genericFieldRules } from '../generic/generic-field-rules'; // PingCastle berichtet ausschliesslich ausgeloeste RiskRules; der Source-Extractor // setzt dafuer den Status "Matched". Ohne eine eigene Deklaration greift die // generische Rueckfallregel (matched -> open/warning), die einen Treffer bewusst // als blossen Hinweis liest. Fuer einen Exposure-Scanner ist das falsch: die // Regel hat getroffen, die Exposition ist erkannt. Die Folge war, dass // exposure_state nie "detected" wurde und damit kein einziger PingCastle-Befund // als is_ioe galt (RESULT_CONTRACT: exposure_finding + detected -> IOE). // Nur der exakte Wert "Matched" wird umgedeutet; unspezifische Treffertexte // bleiben weiterhin ein Hinweis. const pingCastleStatusMappings: Array> = commonStatusMappings .map(mapping => mapping.normalizedValue === 'failed' ? { ...mapping, patterns: [...mapping.patterns, /^matched$/i] } : mapping); export const pingCastleDefinition: ToolProfileDefinition = { schemaVersion: 1, id: 'pingcastle', securityScoring: { version: '1.0.0', native: { kind: 'mapped', properties: ['risk.domain', 'risk.global'], scope: 'overall', scale: '0-100', direction: 'lower-is-better' }, targets: ['domain.fqdn'], resultPopulation: 'findings-only' }, name: 'PingCastle', version: '1.6.0', processing: { mappingVersion: '1.2.0', normalizationVersion: '1.1.0', presentationVersion: '1.2.0', compatibilityKey: 'pingcastle-canonical-v1.1', changeClass: 'compatible', reimportRequired: true, affectedDimensions: ['environment-context'] }, sourceSignals: [ { scope: 'file-name', pattern: /ping[\s_.-]*castle|^ad[_-]hc[_-]/i, contribution: 0.5, message: 'Der Dateiname entspricht einem PingCastle-Healthcheck.' }, { scope: 'raw-text', pattern: /ping[\s_-]*castle/i, contribution: 0.7, message: 'Der Dateiinhalt enthält einen PingCastle-Marker.' }, { scope: 'raw-text', pattern: /rule\s+id\s*:/i, contribution: 0.1, message: 'Der Bericht enthält Rule-ID-Details.' }, { scope: 'table-name', pattern: /^healthcheckdata$/i, contribution: 0.7, message: 'Das XML-Wurzelelement entspricht einem PingCastle-Healthcheck.' }, { scope: 'table-name', pattern: /^risk\s+scores?$/i, contribution: 0.2, message: 'Eine Tabelle heißt „Risk Scores“.' }, { scope: 'header', pattern: /^rules?\s+matched$/i, contribution: 0.12, message: 'Die Kopfzeile „Rules matched“ wurde gefunden.' } ], tableRules: { preferredNames: [{ pattern: /risk\s+scores?|rule|finding/i, contribution: 0.8, message: 'PingCastle-Risiko- oder Regeltabelle.' }], discouragedNames: [{ pattern: /domain\s+information|summary/i, contribution: 0.35, message: 'Domänenmetadaten oder Zusammenfassung.' }] }, fieldRules: genericFieldRules, normalization: { severity: commonSeverityMappings, status: pingCastleStatusMappings }, presentation: { titleHeaders: [/^title$/i], severityHeaders: [/^(?:severity|risk\s*level|score)$/i], sortHeaders: [/^(?:points?|score)$/i], sortDirection: 'desc', defaultStatusFilter: 'all', statusFilters: [], showResultSheetTitles: true, resultSheetTitleRules: [{ pattern: /^PingCastle\s*·\s*/i, replacement: '' }] }, comparisonKeys: [['rule-id', 'asset'], ['rule-id']] };