UNPKG

overmind-mcp

Version:

Orchestrateur universel agents IA multi-modeles via MCP. Inclut le protocole 'Custom-Nickname' pour identifier vos agents avec des surnoms originaux (The Chaos Prophet, Shadow Sniper, etc.), l'isolation mémoire (Private Memory Context) et le support pour

180 lines • 8.63 kB
/** * ═══════════════════════════════════════════════════════════════════════════ * Audit Trail — Outil MCP : Notification d'incident (art. 3.5-3.8) * ═══════════════════════════════════════════════════════════════════════════ * * Signale un incident de confidentialité, liste les incidents ouverts, * ou résout un incident. Table `incident_log` dans `overmind_core`. * * ⚠️ Les incidents de sévérité "high" déclenchent un avertissement : * notification à la Commission d'accès à l'information (CAI) requise * dans les 30 jours (art. 3.5). */ import { z } from 'zod'; import { randomUUID } from 'crypto'; import { getPool } from 'overmind-postgres-mcp'; // ── Schéma Zod ──────────────────────────────────────────────────────────────── export const auditReportIncidentSchema = z.object({ action: z .enum(['report', 'list', 'resolve']) .optional() .default('list') .describe('report: signale un nouvel incident | ' + 'list: liste les incidents (plus récents d\'abord) | ' + 'resolve: marque un incident comme résolu'), severity: z .enum(['low', 'moderate', 'high']) .describe("Gravité de l'incident. 'high' → notification CAI obligatoire sous 30 jours."), category: z .enum([ 'data_leak', 'unauthorized_access', 'breach', 'retention_violation', 'transfer_violation', 'consent_violation', ]) .describe("Catégorie d'incident."), description: z .string() .min(1) .describe("Description factuelle de l'incident (cause, périmètre, données touchées)."), data_subjects_affected: z .number() .int() .min(0) .optional() .default(0) .describe('Nombre de sujets de données affectés (0 si inconnu).'), incident_id: z .string() .optional() .describe("ID de l'incident à résoudre (requis pour action='resolve')."), }); // ── Helpers ────────────────────────────────────────────────────────────────── function tsNow() { return Date.now(); } function formatIncident(row) { const detected = new Date(row.detected_at).toISOString().replace('T', ' ').slice(0, 19); const resolved = row.resolved_at !== null && row.resolved_at !== undefined ? new Date(row.resolved_at).toISOString().replace('T', ' ').slice(0, 19) : null; const status = resolved ? `✅ résolu (${resolved})` : '🟡 ouvert'; const sevLabel = row.severity === 'high' ? '🔴' : row.severity === 'moderate' ? '🟠' : '🟡'; return [ `${sevLabel} **[${row.severity.toUpperCase()}]** \`${row.category}\` — \`${row.id.slice(0, 8)}\``, ` - Détecté: ${detected} | ${status}`, ` - Sujets affectés: ${row.data_subjects_affected}`, ` - Description: ${row.description ?? '—'}`, ].join('\n'); } // ── Tool ───────────────────────────────────────────────────────────────────── export async function auditReportIncident(args) { try { const pool = getPool(); const action = args.action ?? 'list'; // ── REPORT ─────────────────────────────────────────────────────────────── if (action === 'report') { const id = randomUUID(); const now = tsNow(); await pool.query(`INSERT INTO incident_log (id, detected_at, severity, category, description, data_subjects_affected, cai_notified, subjects_notified, resolved_at) VALUES ($1, $2, $3, $4, $5, $6, 0, 0, NULL)`, [ id, now, args.severity, args.category, args.description, args.data_subjects_affected ?? 0, ]); const lines = [ `🚨 **Incident signalé** \`${id}\``, ` - Sévérité: \`${args.severity}\` | Catégorie: \`${args.category}\``, ` - Sujets affectés: ${args.data_subjects_affected ?? 0}`, ` - Détecté: ${new Date(now).toISOString().replace('T', ' ').slice(0, 19)}`, ]; if (args.severity === 'high') { lines.push(''); lines.push('⚠️ **AVERTISSEMENT — Notification CAI requise (art. 3.5-3.8)**', `Un incident de sévérité \`high\` nécessite une notification à la ` + `**Commission d'accès à l'information (CAI)** dans les **30 jours** suivant la détection,`, `avec notification des sujets concernés lorsque le risque de préjudice sérieux est élevé.`, `Référence: https://www.cai.gouv.qc.ca/notification-dincident`); } return { content: [{ type: 'text', text: lines.join('\n') }], }; } // ── LIST ───────────────────────────────────────────────────────────────── if (action === 'list') { const res = await pool.query(`SELECT * FROM incident_log ORDER BY detected_at DESC`); if (res.rows.length === 0) { return { content: [ { type: 'text', text: '📭 Aucun incident enregistré.' }, ], }; } const open = res.rows.filter((r) => r.resolved_at === null || r.resolved_at === undefined); const block = res.rows.map(formatIncident).join('\n\n'); return { content: [ { type: 'text', text: `🚨 **${res.rows.length} incident(s)** — ${open.length} ouvert(s)\n\n${block}`, }, ], }; } // ── RESOLVE ────────────────────────────────────────────────────────────── if (action === 'resolve') { if (!args.incident_id) { return { content: [ { type: 'text', text: '❌ `incident_id` est requis pour action="resolve".', }, ], isError: true, }; } const res = await pool.query(`UPDATE incident_log SET resolved_at = $1 WHERE id = $2 AND resolved_at IS NULL`, [tsNow(), args.incident_id]); if (res.rowCount === 0) { return { content: [ { type: 'text', text: `❌ Incident \`${args.incident_id}\` introuvable ou déjà résolu.`, }, ], isError: true, }; } return { content: [ { type: 'text', text: `✅ **Incident résolu**: \`${args.incident_id}\``, }, ], }; } // unreachable return { content: [{ type: 'text', text: `❌ Action inconnue: ${action}` }], isError: true, }; } catch (err) { return { content: [ { type: 'text', text: `❌ Erreur audit_report_incident: ${err instanceof Error ? err.message : String(err)}`, }, ], isError: true, }; } } //# sourceMappingURL=audit_report_incident.js.map