"use strict";
/**
 * IncidentNotifier -- Compliance-driven incident notification system.
 *
 * Manages incident classification, deadline tracking, and notification
 * delivery for regulatory compliance.
 *
 * Regulatory deadlines:
 *   - DORA: 4 hours for major ICT incidents (Art. 19)
 *   - GDPR: 72 hours for personal data breaches (Art. 33)
 *   - HIPAA: 60 days for breach notification (45 CFR 164.404)
 *   - PCI DSS: Immediate for suspected compromise
 *   - SOC 2: Per customer SLA (typically 24-72 hours)
 *
 * @connexum/ai-governance
 */
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    var desc = Object.getOwnPropertyDescriptor(m, k);
    if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
      desc = { enumerable: true, get: function() { return m[k]; } };
    }
    Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
    Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
    o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
    var ownKeys = function(o) {
        ownKeys = Object.getOwnPropertyNames || function (o) {
            var ar = [];
            for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
            return ar;
        };
        return ownKeys(o);
    };
    return function (mod) {
        if (mod && mod.__esModule) return mod;
        var result = {};
        if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
        __setModuleDefault(result, mod);
        return result;
    };
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.IncidentNotifier = exports.NoPacksLoadedError = void 0;
exports.deriveSeverityLabels = deriveSeverityLabels;
const crypto = __importStar(require("crypto"));
const fs_1 = require("../util/fs");
const log_1 = require("../util/log");
const path = __importStar(require("path"));
const log = (0, log_1.createLogger)('incident-notifier');
/**
 * HL-1: Thrown when the incident notifier finds zero packs loaded and
 * therefore cannot derive a compliant fallback category list.
 *
 * Failing closed here is intentional: an incident notifier with no pack
 * context should not silently downgrade severity by guessing categories.
 * The operator must ensure packs are loaded before incidents are classified.
 *
 * Reference: W1 directive (Thomas, 2026-04-24): runtime must read from packs,
 * not from hardcoded lists.
 */
class NoPacksLoadedError extends Error {
    constructor() {
        super('IncidentNotifier: no compliance packs are loaded in the PackDrivenClassifier registry. ' +
            'Cannot derive blocking category list for severity classification. ' +
            'Ensure packs are loaded via PackDrivenClassifier.loadFromPacks() before classifying incidents. ' +
            '(HL-1: hardcoded fallback removed; fail-closed policy.)');
        this.name = 'NoPacksLoadedError';
    }
}
exports.NoPacksLoadedError = NoPacksLoadedError;
/**
 * Deadline rules per framework (hours from discovery).
 *
 * F-NEW-MED-15 (2026-05-02): pre-fix this was a hardcoded map of 6
 * frameworks while every pack declares its own
 * `incidentPolicy.notificationDeadlineHours`. Only HIPAA/DORA/GDPR/PCI
 * DSS/SOC 2/FERPA had wired deadlines; the other 93 packs' declared
 * deadlines were dead data on this code path.
 *
 * Now derived from PackDrivenClassifier registry at lookup time. The
 * legacy values below remain only as a hard fallback for packs that
 * are not in the registry (e.g., test stubs without a registered pack
 * binding). Real customer code paths read from pack metadata.
 */
const LEGACY_DEADLINE_RULES = {
    dora: 4,
    'pci-dss': 1,
    'soc2': 24,
    gdpr: 72,
    hipaa: 1440,
    ferpa: 720,
};
/**
 * Resolve a framework's notification deadline in hours.
 *
 * Resolution order:
 *   1. Caller-supplied packDeadlinesByFramework (built from
 *      pack.incidentPolicy.notificationDeadlineHours by the gov-server)
 *   2. Legacy hardcoded map (backward-compat for the 6 originally-wired
 *      frameworks: dora/pci-dss/soc2/gdpr/hipaa/ferpa)
 *   3. undefined (caller should treat as "no deadline")
 *
 * Method on the class to access the per-instance config.
 */
function resolveDeadlineHoursFor(framework, packDeadlines) {
    const key = framework.toLowerCase();
    if (packDeadlines && typeof packDeadlines[key] === 'number' && packDeadlines[key] > 0) {
        return packDeadlines[key];
    }
    return LEGACY_DEADLINE_RULES[key];
}
// ---------------------------------------------------------------------------
// W1 residual: industry-tag to severity-label derivation
//
// Severity labels are derived from CompliancePack.industries[] at incident
// classification time. No category strings (PHI, PII, GDPR, etc.) appear
// here -- all classification is driven by the pack registry.
//
// Maps each industry tag substring (lowercase) to one or more severity label
// buckets. The match is prefix/contains so that e.g. 'health-it' and
// 'telehealth' both map to HEALTHCARE.
// ---------------------------------------------------------------------------
/**
 * Each entry: [industryTagSubstring, severityLabel].
 * Evaluated in order; a single industry tag can trigger multiple labels.
 * Substrings are matched case-insensitively against the pack's declared
 * industries array entries.
 */
const INDUSTRY_LABEL_RULES = [
    // Healthcare
    ['healthcare', 'HEALTHCARE'],
    ['health-it', 'HEALTHCARE'],
    ['telehealth', 'HEALTHCARE'],
    ['pharma', 'HEALTHCARE'],
    ['clinical-research', 'HEALTHCARE'],
    ['ehr-vendor', 'HEALTHCARE'],
    ['biotech', 'HEALTHCARE'],
    ['medical', 'HEALTHCARE'],
    ['business-associates', 'HEALTHCARE'],
    // Education
    ['edtech', 'EDUCATION'],
    ['education', 'EDUCATION'],
    ['k12', 'EDUCATION'],
    ['higher-education', 'EDUCATION'],
    ['learning-management', 'EDUCATION'],
    ['student-information', 'EDUCATION'],
    // Financial
    ['banking', 'FINANCIAL'],
    ['bank', 'FINANCIAL'],
    ['fintech', 'FINANCIAL'],
    ['payments', 'FINANCIAL'],
    ['insurance', 'FINANCIAL'],
    ['mortgage', 'FINANCIAL'],
    ['credit', 'FINANCIAL'],
    ['investment', 'FINANCIAL'],
    ['broker-dealer', 'FINANCIAL'],
    ['asset-management', 'FINANCIAL'],
    ['clearing-house', 'FINANCIAL'],
    ['finance', 'FINANCIAL'],
    ['money-transmitter', 'FINANCIAL'],
    ['virtual-currency', 'FINANCIAL'],
    ['ecommerce', 'FINANCIAL'],
    ['trading', 'FINANCIAL'],
    ['crypto', 'FINANCIAL'],
    // Government / Defense
    ['government', 'GOVERNMENT'],
    ['federal', 'GOVERNMENT'],
    ['govtech', 'GOVERNMENT'],
    ['law-enforcement', 'GOVERNMENT'],
    ['defense', 'GOVERNMENT'],
    ['public-safety', 'GOVERNMENT'],
    ['courts', 'GOVERNMENT'],
    ['corrections', 'GOVERNMENT'],
    ['criminal-justice', 'GOVERNMENT'],
    ['aerospace', 'GOVERNMENT'],
    ['government-contractor', 'GOVERNMENT'],
    // Legal
    ['legal', 'LEGAL'],
    ['law-firm', 'LEGAL'],
    ['ediscovery', 'LEGAL'],
    ['legal-ops', 'LEGAL'],
    ['legal-tech', 'LEGAL'],
    // Privacy (packs that apply cross-industry for privacy regulation)
    ['all-eu', 'PRIVACY'],
    ['all-ca', 'PRIVACY'],
    ['adtech', 'PRIVACY'],
    ['hr-tech', 'PRIVACY'],
    ['any-collecting-biometric', 'PRIVACY'],
    ['biometric', 'PRIVACY'],
    ['consumer-reporting-agency', 'PRIVACY'],
    ['tenant-screening', 'PRIVACY'],
    ['employer', 'PRIVACY'],
];
/**
 * Derive a set of human-readable severity labels from the pack IDs that
 * produced blocking-category matches on an incident.
 *
 * Each matched pack contributes its declared industries[] to the label
 * derivation. The result is the union of all labels that any matched pack's
 * industries trigger via INDUSTRY_LABEL_RULES.
 *
 * W1 directive: no category name strings (PHI, PII, GDPR, ...) appear in
 * this function. The pack's own metadata drives the output.
 *
 * @param matchedPackIds - Pack IDs whose categories matched (BLOCK or above).
 * @param getIndustries  - Lookup function: packId -> industries[]. Supplied
 *                         by the caller from PackDrivenClassifier.getPackIndustries().
 */
function deriveSeverityLabels(matchedPackIds, getIndustries) {
    const labels = new Set();
    for (const packId of matchedPackIds) {
        const industries = getIndustries(packId);
        for (const industry of industries) {
            const lower = industry.toLowerCase();
            for (const [tag, label] of INDUSTRY_LABEL_RULES) {
                if (lower.includes(tag)) {
                    labels.add(label);
                }
            }
        }
    }
    return labels;
}
class IncidentNotifier {
    config;
    incidents = new Map();
    incidentFile;
    constructor(config) {
        this.config = {
            webhooks: { CRITICAL: [], HIGH: [], MEDIUM: [], LOW: [] },
            activeFrameworks: [],
            notificationEmail: '',
            packDeadlinesByFramework: {},
            ...config,
        };
        (0, fs_1.ensureDir)(config.incidentDir);
        this.incidentFile = path.join(config.incidentDir, 'incidents.json');
        this.loadIncidents();
    }
    /**
     * Classify an incident based on data types, volume, and cause.
     */
    classify(params) {
        const reasons = [];
        let severity = 'LOW';
        // Data type escalation.
        // W1: Instead of hardcoding 'PHI', consult the PackDrivenClassifier registry to
        // determine which category IDs are declared by active packs with BLOCK action.
        // Fallback to structural heuristics when no packs are loaded.
        const { getGlobalClassifier } = require('../classification/pack-driven-classifier');
        const pdClassifier = getGlobalClassifier();
        // Build blockingEntries (category + pack pairs) so we can derive severity labels
        // from the packs that contributed BLOCK-level categories.
        const blockingEntries = pdClassifier.listRegisteredEntries().filter(e => e.action === 'BLOCK');
        const blockingCategories = new Set(blockingEntries.map(e => e.categoryId.toUpperCase()));
        // HL-1: If no packs are loaded, fail closed with NoPacksLoadedError.
        // Hardcoded fallback list removed per W1 directive (Thomas, 2026-04-24):
        // the runtime must read from packs, not from hardcoded constants.
        // If blockingCategories is empty here, it signals a configuration error
        // (packs were not loaded before the incident notifier was called), not a
        // situation that should be silently papered over with hardcoded strings.
        if (blockingCategories.size === 0) {
            throw new NoPacksLoadedError();
        }
        // W1 residual (2026-04-24): hasPHI is now purely pack-driven.
        // Any category that a loaded pack declared with BLOCK action counts as PHI-tier.
        // Hardcoded category name strings removed per W1 directive; pack registry drives all detection.
        const hasPHI = params.dataTypes.some(d => blockingCategories.has(d.toUpperCase()));
        // W1 residual (2026-04-24): hasPII is now pack-driven via WARN-or-above categories.
        // Any category that a loaded pack declared with WARN or stricter action counts as
        // PII-tier (lower severity than BLOCK). Hardcoded category name strings removed per W1 directive.
        const warnOrAboveCategories = new Set(pdClassifier.listCategoriesByMinAction('WARN').map(e => e.categoryId.toUpperCase()));
        const hasPII = params.dataTypes.some(d => warnOrAboveCategories.has(d.toUpperCase()));
        // W1 residual: collect pack IDs whose BLOCK categories matched the incident's
        // data types. These are used to derive the human-readable severity label set
        // via the pack's declared industries[].
        const matchedPackIds = new Set();
        for (const dataType of params.dataTypes) {
            for (const entry of blockingEntries) {
                if (entry.categoryId.toUpperCase() === dataType.toUpperCase()) {
                    matchedPackIds.add(entry.packId);
                }
            }
        }
        // Also include packs whose WARN-or-above categories matched, so PII-type
        // packs (GDPR, CCPA etc.) contribute their labels even when action < BLOCK.
        const warnOrAboveEntries = pdClassifier.listCategoriesByMinAction('WARN');
        for (const dataType of params.dataTypes) {
            for (const entry of warnOrAboveEntries) {
                if (entry.categoryId.toUpperCase() === dataType.toUpperCase()) {
                    matchedPackIds.add(entry.packId);
                }
            }
        }
        const severityLabelSet = deriveSeverityLabels(matchedPackIds, (packId) => pdClassifier.getPackIndustries(packId));
        const hasPAN = params.dataTypes.some(d => ['PAN', 'CREDIT_CARD', 'PCI'].includes(d.toUpperCase()));
        const hasCredentials = params.dataTypes.some(d => ['CREDENTIALS', 'API_KEY', 'SECRET'].includes(d.toUpperCase()));
        if (hasPHI) {
            severity = 'CRITICAL';
            reasons.push('Protected Health Information (PHI) involved');
        }
        if (hasPAN) {
            severity = 'CRITICAL';
            reasons.push('Payment card data (PAN) involved');
        }
        if (hasCredentials) {
            severity = severity === 'CRITICAL' ? 'CRITICAL' : 'HIGH';
            reasons.push('Credentials or API keys exposed');
        }
        if (hasPII) {
            severity = severity === 'LOW' ? 'MEDIUM' : severity;
            reasons.push('Personal Identifiable Information (PII) involved');
        }
        // Volume escalation
        if (params.recordsAffected > 500) {
            if (severity === 'LOW')
                severity = 'MEDIUM';
            if (severity === 'MEDIUM')
                severity = 'HIGH';
            reasons.push(`High volume: ${params.recordsAffected} records affected`);
        }
        if (params.recordsAffected > 5000) {
            severity = 'CRITICAL';
            reasons.push(`Mass breach: ${params.recordsAffected} records affected`);
        }
        // Cause escalation
        const maliciousCauses = ['attack', 'exploit', 'injection', 'unauthorized', 'tamper', 'exfiltration'];
        if (maliciousCauses.some(c => params.cause.toLowerCase().includes(c))) {
            if (severity === 'LOW')
                severity = 'HIGH';
            reasons.push(`Potentially malicious cause: ${params.cause}`);
        }
        // Multi-agent escalation
        if (params.affectedAgents.length > 3) {
            if (severity === 'LOW')
                severity = 'MEDIUM';
            reasons.push(`Multiple agents affected: ${params.affectedAgents.length}`);
        }
        // Calculate deadlines
        const now = new Date();
        const deadlines = {};
        for (const framework of this.config.activeFrameworks) {
            const hours = resolveDeadlineHoursFor(framework, this.config.packDeadlinesByFramework);
            if (hours !== undefined) {
                const deadline = new Date(now.getTime() + hours * 3600000);
                deadlines[framework] = {
                    framework,
                    hoursFromDiscovery: hours,
                    deadline: deadline.toISOString(),
                };
            }
        }
        return {
            severity,
            reasons,
            deadlines,
            requiresImmediateAction: severity === 'CRITICAL' || Object.keys(deadlines).some(k => {
                const h = resolveDeadlineHoursFor(k, this.config.packDeadlinesByFramework);
                return h !== undefined && h <= 4;
            }),
            // W1 residual: labels derived from pack.industries, not from hardcoded strings.
            severityLabels: Array.from(severityLabelSet),
        };
    }
    /**
     * Create a new incident.
     */
    create(params) {
        const classification = this.classify(params);
        const now = new Date().toISOString();
        const incident = {
            id: `INC-${Date.now()}-${crypto.randomBytes(3).toString('hex')}`,
            title: params.title,
            description: params.description,
            severity: classification.severity,
            status: 'open',
            dataTypes: params.dataTypes,
            recordsAffected: params.recordsAffected,
            affectedAgents: params.affectedAgents,
            cause: params.cause,
            discoveredAt: params.discoveredAt || now,
            createdAt: now,
            deadlines: {},
            notifications: [],
            timeline: [{
                    timestamp: now,
                    action: 'created',
                    actor: 'system',
                    detail: `Severity: ${classification.severity}. ${classification.reasons.join('; ')}`,
                }],
        };
        // Set deadlines
        for (const [framework, dl] of Object.entries(classification.deadlines)) {
            incident.deadlines[framework] = dl.deadline;
        }
        this.incidents.set(incident.id, incident);
        this.saveIncidents();
        log.warn(`Incident created: ${incident.id} [${incident.severity}] ${incident.title}`, {
            severity: incident.severity,
            dataTypes: incident.dataTypes,
            deadlines: incident.deadlines,
        });
        // Log notification record
        this.logNotification(incident.id, 'system-log', 'audit-trail', 'sent');
        return incident;
    }
    /**
     * Update incident status.
     */
    updateStatus(incidentId, status, actor, detail) {
        const incident = this.incidents.get(incidentId);
        if (!incident)
            throw new Error(`Incident ${incidentId} not found`);
        incident.status = status;
        incident.timeline.push({
            timestamp: new Date().toISOString(),
            action: `status_change:${status}`,
            actor,
            detail,
        });
        this.saveIncidents();
        return incident;
    }
    /**
     * Get an incident by ID.
     */
    get(incidentId) {
        return this.incidents.get(incidentId) || null;
    }
    /**
     * Get all open incidents.
     */
    getOpen() {
        return Array.from(this.incidents.values())
            .filter(i => i.status !== 'closed' && i.status !== 'resolved');
    }
    /**
     * Get incidents with approaching or breached deadlines.
     */
    getUrgent(withinHours = 4) {
        const cutoff = new Date(Date.now() + withinHours * 3600000).toISOString();
        return this.getOpen().filter(incident => {
            return Object.values(incident.deadlines).some(deadline => deadline <= cutoff);
        });
    }
    /**
     * Check if any deadlines have been breached.
     */
    getBreachedDeadlines() {
        const now = new Date().toISOString();
        const breached = [];
        for (const incident of this.getOpen()) {
            for (const [framework, deadline] of Object.entries(incident.deadlines)) {
                if (deadline < now) {
                    breached.push({ incident, framework, deadline });
                }
            }
        }
        return breached;
    }
    /**
     * Get incident statistics.
     */
    getStats() {
        const all = Array.from(this.incidents.values());
        const open = all.filter(i => i.status !== 'closed' && i.status !== 'resolved');
        return {
            total: all.length,
            open: open.length,
            bySeverity: {
                CRITICAL: all.filter(i => i.severity === 'CRITICAL').length,
                HIGH: all.filter(i => i.severity === 'HIGH').length,
                MEDIUM: all.filter(i => i.severity === 'MEDIUM').length,
                LOW: all.filter(i => i.severity === 'LOW').length,
            },
            breachedDeadlines: this.getBreachedDeadlines().length,
        };
    }
    // --- Private ---
    logNotification(incidentId, channel, recipient, status) {
        const incident = this.incidents.get(incidentId);
        if (!incident)
            return;
        incident.notifications.push({
            timestamp: new Date().toISOString(),
            channel,
            recipient,
            status,
        });
        // Also log to JSONL for audit trail
        const logFile = path.join(this.config.incidentDir, 'notifications.jsonl');
        (0, fs_1.appendJsonl)(logFile, {
            timestamp: new Date().toISOString(),
            incidentId,
            channel,
            recipient,
            status,
        });
    }
    saveIncidents() {
        try {
            const data = Array.from(this.incidents.values());
            (0, fs_1.saveJson)(this.incidentFile, data);
        }
        catch (err) {
            log.critical(`Failed to save incidents: ${err.message}`);
        }
    }
    loadIncidents() {
        const data = (0, fs_1.loadJson)(this.incidentFile);
        if (data && Array.isArray(data)) {
            for (const incident of data) {
                this.incidents.set(incident.id, incident);
            }
        }
    }
}
exports.IncidentNotifier = IncidentNotifier;
//# sourceMappingURL=incident-notifier.js.map