Assessor_2.0/src/profiles/pingcastle/pingcastle-definition.ts

82 lines
4.5 KiB
TypeScript

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<ProfileValueMapping<CanonicalFindingStatus>> = 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'
},
priorityBasis: {
version: '1.0.0',
// PingCastle carries no native severity per rule; the canonical severity is
// derived from Points via `pingcastle-severity-from-points` and is capped at
// `high` by construction. Points is therefore the faithful basis and keeps
// the granularity the capped severity loses.
basis: {
kind: 'numeric', properties: ['rule.points'], scale: '0-50+', direction: 'higher-is-worse'
},
excludedProperties: [
{ property: 'rule.points_scale', reason: 'Static scoring-scheme description, not the awarded value.' },
{ property: 'risk.domain', reason: 'Category or domain risk total, not a per-finding rank.' }
]
},
timeslotComparison: {
version: '1.0.0', kind: 'available', granularity: 'scope',
scope: { kind: 'ad-domain', properties: ['domain.fqdn'] },
observationTimeProperties: ['report.generated_at'],
resultEvidence: 'findings-only', negativeEvidence: 'unsupported',
diagnosticPolicy: 'fail-closed-by-impact'
},
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']]
};