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
153 lines • 6.59 kB
JavaScript
/**
* ═══════════════════════════════════════════════════════════════════════════
* Audit Trail — Anonymisation technique (art. 23.1)
* ═══════════════════════════════════════════════════════════════════════════
*
* Techniques d'anonymisation pour empêcher la ré-identification :
* 1. Pseudonymisation — remplace identifiants directs par hash SHA-256 + salt
* 2. Généralisation — tronque timestamps, géoloc
* 3. Bruit sur embeddings — réduit la ré-identification par recherche vectorielle
*/
import crypto from 'crypto';
// ── Salt rotatif ─────────────────────────────────────────────────────────────
/**
* Salt pour la pseudonymisation. En production, devrait être stocké séparément
* des données (key vault, variable d'environnement séparée).
*/
function getAnonymizationSalt() {
return process.env.OVERMIND_AUDIT_SALT || 'overmind-loi25-default-salt-v1';
}
// ── Pseudonymisation ─────────────────────────────────────────────────────────
/**
* Hash un identifiant direct (email, Discord ID, téléphone) en ID pseudonymisé.
* SHA-256 + salt → impossible de remonter à l'original sans le salt.
*
* @param identifier Identifiant direct (email, ID Discord, etc.)
* @returns Pseudonyme stable (32 chars hex)
*/
export function pseudonymize(identifier) {
const salted = `${getAnonymizationSalt()}:${identifier}`;
return crypto.createHash('sha256').update(salted).digest('hex');
}
/**
* Hash court pour les clés de référence (consent_ref, etc.).
*/
export function hashShort(text) {
const salted = `${getAnonymizationSalt()}:${text}`;
return crypto.createHash('sha256').update(salted).digest('hex').slice(0, 16);
}
// ── Généralisation ───────────────────────────────────────────────────────────
/**
* Généralise un timestamp en tronquant à la journée (supprime heures/minutes/secondes).
* Réduit la précision → moins de risque de ré-identification par corrélation.
*
* @param timestampMs Timestamp en millisecondes
* @returns Timestamp tronqué au jour (00:00:00 UTC)
*/
export function generalizeTimestamp(timestampMs) {
const dayMs = 24 * 60 * 60 * 1000;
return Math.floor(timestampMs / dayMs) * dayMs;
}
/**
* Regex pour détecter les renseignements personnels les plus courants.
* Conservateur : mieux vaut faux positif que faux négatif.
*/
const PII_PATTERNS = [
// Email
{
type: 'email',
pattern: /[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/g,
replacement: '[EMAIL_ANONYMIZED]',
},
// Numéro de téléphone QC (format NXX NXX-XXXX ou +1)
{
type: 'phone',
pattern: /(\+?1[-.\s]?)?\(?[2-9]\d{2}\)?[-.\s]?\d{3}[-.\s]?\d{4}/g,
replacement: '[PHONE_ANONYMIZED]',
},
// Numéro d'assurance sociale (NAS) — format 123 456 789 ou 123-456-789
{
type: 'sin',
pattern: /\b\d{3}[-\s]?\d{3}[-\s]?\d{3}\b/g,
replacement: '[SIN_ANONYMIZED]',
},
// Carte de crédit (Visa/MC/Amex — 13-19 chiffres groupés)
{
type: 'credit_card',
pattern: /\b(?:\d[ -]*?){13,19}\b/g,
replacement: '[CARD_ANONYMIZED]',
},
// Code postal QC (H1A 1A1)
{
type: 'postal_code',
pattern: /\b[ABCEGHJKLMNPRSTVXY]\d[A-Z][ -]?\d[A-Z]\d\b/gi,
replacement: '[POSTAL_ANONYMIZED]',
},
// Discord ID (snowflake 17-20 chiffres)
{
type: 'discord_id',
pattern: /\b\d{17,20}\b/g,
replacement: '[DISCORD_ID_ANONYMIZED]',
},
];
/**
* Détecte les renseignements personnels dans un texte.
*
* @param text Texte à analyser
* @param sanitize Si true, remplace les RP détectés par des placeholders
* @returns Résultat de la détection + texte sanitizé si demandé
*/
export function detectPii(text, sanitize = false) {
const foundTypes = new Set();
let sanitized = text;
for (const { type, pattern, replacement } of PII_PATTERNS) {
if (pattern.test(text)) {
foundTypes.add(type);
pattern.lastIndex = 0; // reset regex state
if (sanitize) {
sanitized = sanitized.replace(pattern, replacement);
}
}
}
const types = Array.from(foundTypes);
return {
found: types.length > 0,
types,
sanitized: sanitize ? sanitized : undefined,
};
}
/**
* Sanitize un texte en remplaçant tous les RP détectés.
* Utilisé avant l'envoi à un LLM si OVERMIND_PII_FILTER=true.
*/
export function sanitizeText(text) {
return detectPii(text, true).sanitized || text;
}
// ── Bruit sur embeddings ─────────────────────────────────────────────────────
/**
* Ajoute un bruit gaussien à un embedding pour réduire la ré-identification
* par recherche vectorielle (K-anonymité approximative).
*
* Le bruit est proportionnel à l'écart-type des composantes → préserve
* la structure globale tout en empêchant le matching exact.
*
* @param embedding Vecteur d'embedding
* @param noiseLevel Niveau de bruit (0 = aucun, 0.1 = léger, 0.3 = fort)
* @returns Embedding bruité
*/
export function addEmbeddingNoise(embedding, noiseLevel = 0.1) {
if (noiseLevel <= 0)
return embedding;
// Calculer l'écart-type
const mean = embedding.reduce((sum, v) => sum + v, 0) / embedding.length;
const variance = embedding.reduce((sum, v) => sum + (v - mean) ** 2, 0) / embedding.length;
const stdDev = Math.sqrt(variance);
// Bruit gaussien via Box-Muller transform
return embedding.map((val) => {
const u1 = Math.random() || 1e-10; // éviter log(0)
const u2 = Math.random();
const gaussian = Math.sqrt(-2 * Math.log(u1)) * Math.cos(2 * Math.PI * u2);
return val + gaussian * stdDev * noiseLevel;
});
}
//# sourceMappingURL=anonymize.js.map