Some checks failed
SourceFinder CI/CD Pipeline / Code Quality & Linting (push) Has been cancelled
SourceFinder CI/CD Pipeline / Unit Tests (push) Has been cancelled
SourceFinder CI/CD Pipeline / Security Tests (push) Has been cancelled
SourceFinder CI/CD Pipeline / Integration Tests (push) Has been cancelled
SourceFinder CI/CD Pipeline / Performance Tests (push) Has been cancelled
SourceFinder CI/CD Pipeline / Code Coverage Report (push) Has been cancelled
SourceFinder CI/CD Pipeline / Build & Deployment Validation (16.x) (push) Has been cancelled
SourceFinder CI/CD Pipeline / Build & Deployment Validation (18.x) (push) Has been cancelled
SourceFinder CI/CD Pipeline / Build & Deployment Validation (20.x) (push) Has been cancelled
SourceFinder CI/CD Pipeline / Regression Tests (push) Has been cancelled
SourceFinder CI/CD Pipeline / Security Audit (push) Has been cancelled
SourceFinder CI/CD Pipeline / Notify Results (push) Has been cancelled
- Architecture modulaire avec injection de dépendances - Système de scoring intelligent multi-facteurs (spécificité, fraîcheur, qualité, réutilisation) - Moteur anti-injection 4 couches (preprocessing, patterns, sémantique, pénalités) - API REST complète avec validation et rate limiting - Repository JSON avec index mémoire et backup automatique - Provider LLM modulaire pour génération de contenu - Suite de tests complète (Jest) : * Tests unitaires pour sécurité et scoring * Tests d'intégration API end-to-end * Tests de sécurité avec simulation d'attaques * Tests de performance et charge - Pipeline CI/CD avec GitHub Actions - Logging structuré et monitoring - Configuration ESLint et environnement de test 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
975 lines
29 KiB
JavaScript
975 lines
29 KiB
JavaScript
/**
|
|
* AntiInjectionEngine - Système de protection anti-prompt injection
|
|
* Implémente les 4 couches de sécurité selon CDC :
|
|
* Layer 1: Content Preprocessing
|
|
* Layer 2: Pattern Detection
|
|
* Layer 3: Semantic Validation
|
|
* Layer 4: Source Scoring avec pénalités
|
|
*/
|
|
const logger = require('../utils/logger');
|
|
const { setupTracer } = logger;
|
|
|
|
class AntiInjectionEngine {
|
|
constructor() {
|
|
this.tracer = setupTracer('AntiInjectionEngine');
|
|
|
|
// Patterns dangereux - Layer 2
|
|
this.dangerousPatterns = [
|
|
// Instructions directes
|
|
/ignore\s+previous\s+instructions/gi,
|
|
/you\s+are\s+now/gi,
|
|
/forget\s+everything/gi,
|
|
/new\s+instructions?:/gi,
|
|
/system\s+prompt:/gi,
|
|
/override\s+instructions/gi,
|
|
/disregard\s+(all\s+)?previous/gi,
|
|
|
|
// Redirections de contexte
|
|
/instead\s+of\s+writing\s+about/gi,
|
|
/don't\s+write\s+about.*write\s+about/gi,
|
|
/change\s+the\s+topic\s+to/gi,
|
|
/focus\s+on.*instead/gi,
|
|
|
|
// Injections de code/commandes
|
|
/<script[^>]*>/gi,
|
|
/<iframe[^>]*>/gi,
|
|
/javascript:/gi,
|
|
/eval\s*\(/gi,
|
|
/exec\s*\(/gi,
|
|
/system\s*\(/gi,
|
|
/\$\{.*\}/g, // Template literals
|
|
/`.*`/g, // Backticks
|
|
|
|
// Métaprompts et tests
|
|
/this\s+is\s+a\s+test/gi,
|
|
/output\s+json\s+format/gi,
|
|
/return\s+only/gi,
|
|
/respond\s+with\s+only/gi,
|
|
/answer\s+with\s+(yes|no|true|false)(\s+only)?/gi,
|
|
|
|
// Tentatives de manipulation
|
|
/pretend\s+(to\s+be|you\s+are)/gi,
|
|
/act\s+as\s+(if\s+)?you/gi,
|
|
/simulate\s+(being|that)/gi,
|
|
/role\s*play/gi,
|
|
|
|
// Bypass attempts
|
|
/\/\*.*ignore.*\*\//gi,
|
|
/<!--.*ignore.*-->/gi,
|
|
/\\n\\n/g, // Tentatives newlines
|
|
/\n\s*\n\s*---/g // Séparateurs suspects
|
|
];
|
|
|
|
// Patterns de validation sémantique - Layer 3
|
|
this.semanticValidationRules = [
|
|
{
|
|
name: 'dog_breed_context',
|
|
pattern: /(chien|dog|race|breed|canin)/gi,
|
|
minMatches: 1,
|
|
weight: 0.4
|
|
},
|
|
{
|
|
name: 'animal_context',
|
|
pattern: /(animal|pet|élevage|vétérinaire|comportement)/gi,
|
|
minMatches: 1,
|
|
weight: 0.3
|
|
},
|
|
{
|
|
name: 'relevant_topics',
|
|
pattern: /(santé|alimentation|dressage|éducation|soins|exercice)/gi,
|
|
minMatches: 1,
|
|
weight: 0.3
|
|
}
|
|
];
|
|
|
|
// Scores de pénalité - Layer 4
|
|
this.penaltyScores = {
|
|
PROMPT_INJECTION_DETECTED: -50,
|
|
SEMANTIC_INCONSISTENCY: -30,
|
|
UNTRUSTED_SOURCE_HISTORY: -20,
|
|
SUSPICIOUS_CONTENT_STRUCTURE: -15,
|
|
MODERATE_RISK_INDICATORS: -10
|
|
};
|
|
|
|
// Statistiques
|
|
this.stats = {
|
|
totalValidated: 0,
|
|
injectionAttempts: 0,
|
|
semanticFailures: 0,
|
|
falsePositives: 0,
|
|
averageProcessingTime: 0,
|
|
riskLevelDistribution: {
|
|
low: 0,
|
|
medium: 0,
|
|
high: 0,
|
|
critical: 0
|
|
}
|
|
};
|
|
|
|
// Cache des résultats de validation
|
|
this.validationCache = new Map();
|
|
this.cacheTimeout = 300000; // 5 minutes
|
|
}
|
|
|
|
/**
|
|
* Valider le contenu principal - Point d'entrée
|
|
* @param {Object} content - Contenu à valider
|
|
* @param {Object} context - Contexte de validation
|
|
* @returns {Promise<Object>} Résultat de validation complet
|
|
*/
|
|
async validateContent(content, context = {}) {
|
|
return await this.tracer.run('validateContent', async () => {
|
|
const startTime = Date.now();
|
|
this.stats.totalValidated++;
|
|
|
|
try {
|
|
// Générer clé de cache
|
|
const cacheKey = this.generateCacheKey(content, context);
|
|
|
|
// Vérifier cache
|
|
const cachedResult = this.getFromCache(cacheKey);
|
|
if (cachedResult) {
|
|
return cachedResult;
|
|
}
|
|
|
|
logger.debug('Starting content validation', {
|
|
contentLength: content.content?.length || 0,
|
|
source: content.sourceType || 'unknown',
|
|
raceCode: context.raceCode
|
|
});
|
|
|
|
// Layer 1: Préprocessing du contenu
|
|
const preprocessResult = await this.layer1_preprocessContent(content);
|
|
|
|
// Layer 2: Détection de patterns
|
|
const patternResult = await this.layer2_detectPatterns(preprocessResult);
|
|
|
|
// Layer 3: Validation sémantique
|
|
const semanticResult = await this.layer3_semanticValidation(preprocessResult, context);
|
|
|
|
// Layer 4: Calcul des pénalités
|
|
const penaltyResult = await this.layer4_calculatePenalties(patternResult, semanticResult, content);
|
|
|
|
// Construire résultat final
|
|
const validationResult = {
|
|
isValid: this.determineValidityStatus(patternResult, semanticResult, penaltyResult),
|
|
riskLevel: this.calculateRiskLevel(patternResult, semanticResult),
|
|
processingTime: Date.now() - startTime,
|
|
|
|
// Détails par couche
|
|
layers: {
|
|
preprocessing: preprocessResult,
|
|
patternDetection: patternResult,
|
|
semanticValidation: semanticResult,
|
|
penalties: penaltyResult
|
|
},
|
|
|
|
// Contenu nettoyé
|
|
cleanedContent: {
|
|
...content,
|
|
title: preprocessResult.cleanedTitle,
|
|
content: preprocessResult.cleanedContent
|
|
},
|
|
|
|
// Métadonnées de sécurité
|
|
securityMetadata: {
|
|
engine: 'AntiInjectionEngine',
|
|
version: '1.0',
|
|
validatedAt: new Date().toISOString(),
|
|
context: {
|
|
raceCode: context.raceCode,
|
|
sourceType: content.sourceType,
|
|
clientId: context.clientId
|
|
}
|
|
},
|
|
|
|
// Recommandations
|
|
recommendations: this.generateSecurityRecommendations(patternResult, semanticResult, penaltyResult)
|
|
};
|
|
|
|
// Mise en cache
|
|
this.cacheResult(cacheKey, validationResult);
|
|
|
|
// Mise à jour statistiques
|
|
this.updateValidationStats(validationResult);
|
|
|
|
// Logging selon niveau de risque
|
|
this.logValidationResult(validationResult, content, context);
|
|
|
|
return validationResult;
|
|
|
|
} catch (error) {
|
|
logger.error('Content validation failed', error, {
|
|
contentId: content.id,
|
|
raceCode: context.raceCode
|
|
});
|
|
|
|
return {
|
|
isValid: false,
|
|
riskLevel: 'critical',
|
|
error: error.message,
|
|
processingTime: Date.now() - startTime,
|
|
securityMetadata: {
|
|
engine: 'AntiInjectionEngine',
|
|
status: 'error',
|
|
validatedAt: new Date().toISOString()
|
|
}
|
|
};
|
|
}
|
|
}, {
|
|
contentId: content.id,
|
|
raceCode: context.raceCode
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Layer 1: Préprocessing et nettoyage du contenu
|
|
*/
|
|
async layer1_preprocessContent(content) {
|
|
return await this.tracer.run('layer1_preprocessing', async () => {
|
|
const originalTitle = content.title || '';
|
|
const originalContent = content.content || '';
|
|
|
|
// Normalisation de base
|
|
let cleanedTitle = originalTitle.trim();
|
|
let cleanedContent = originalContent.trim();
|
|
|
|
// Supprimer HTML potentiellement dangereux
|
|
cleanedTitle = this.removeHtmlTags(cleanedTitle);
|
|
cleanedContent = this.removeHtmlTags(cleanedContent);
|
|
|
|
// Normaliser espaces et caractères
|
|
cleanedTitle = this.normalizeWhitespace(cleanedTitle);
|
|
cleanedContent = this.normalizeWhitespace(cleanedContent);
|
|
|
|
// Supprimer caractères de contrôle suspects
|
|
cleanedTitle = this.removeControlCharacters(cleanedTitle);
|
|
cleanedContent = this.removeControlCharacters(cleanedContent);
|
|
|
|
// Encoder caractères potentiellement dangereux
|
|
cleanedTitle = this.encodeSpecialCharacters(cleanedTitle);
|
|
cleanedContent = this.encodeSpecialCharacters(cleanedContent);
|
|
|
|
return {
|
|
cleanedTitle,
|
|
cleanedContent,
|
|
originalTitle,
|
|
originalContent,
|
|
changesApplied: {
|
|
htmlRemoved: originalContent !== cleanedContent,
|
|
whitespaceNormalized: true,
|
|
controlCharsRemoved: true,
|
|
specialCharsEncoded: true
|
|
},
|
|
cleaningStats: {
|
|
titleLengthChange: originalTitle.length - cleanedTitle.length,
|
|
contentLengthChange: originalContent.length - cleanedContent.length,
|
|
cleaningScore: this.calculateCleaningScore(originalContent, cleanedContent)
|
|
}
|
|
};
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Layer 2: Détection de patterns dangereux
|
|
*/
|
|
async layer2_detectPatterns(preprocessResult) {
|
|
return await this.tracer.run('layer2_patternDetection', async () => {
|
|
const { cleanedTitle, cleanedContent } = preprocessResult;
|
|
const fullText = `${cleanedTitle} ${cleanedContent}`;
|
|
|
|
const detectedPatterns = [];
|
|
let totalRiskScore = 0;
|
|
|
|
// Analyser chaque pattern dangereux
|
|
for (const [index, pattern] of this.dangerousPatterns.entries()) {
|
|
const matches = fullText.match(pattern);
|
|
|
|
if (matches && matches.length > 0) {
|
|
const patternInfo = {
|
|
patternIndex: index,
|
|
pattern: pattern.source,
|
|
matches: matches,
|
|
matchCount: matches.length,
|
|
riskWeight: this.getPatternRiskWeight(pattern),
|
|
locations: this.findPatternLocations(fullText, pattern)
|
|
};
|
|
|
|
detectedPatterns.push(patternInfo);
|
|
totalRiskScore += patternInfo.riskWeight * patternInfo.matchCount;
|
|
}
|
|
}
|
|
|
|
// Analyser structure suspecte
|
|
const structureAnalysis = this.analyzeContentStructure(fullText);
|
|
if (structureAnalysis.suspicious) {
|
|
totalRiskScore += structureAnalysis.riskScore;
|
|
}
|
|
|
|
return {
|
|
detectedPatterns,
|
|
totalPatterns: detectedPatterns.length,
|
|
totalRiskScore,
|
|
maxIndividualRisk: Math.max(...detectedPatterns.map(p => p.riskWeight), 0),
|
|
structureAnalysis,
|
|
hasHighRiskPatterns: detectedPatterns.some(p => p.riskWeight >= 8),
|
|
hasMediumRiskPatterns: detectedPatterns.some(p => p.riskWeight >= 5),
|
|
summary: this.summarizePatternDetection(detectedPatterns, totalRiskScore)
|
|
};
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Layer 3: Validation sémantique
|
|
*/
|
|
async layer3_semanticValidation(preprocessResult, context) {
|
|
return await this.tracer.run('layer3_semanticValidation', async () => {
|
|
const { cleanedTitle, cleanedContent } = preprocessResult;
|
|
const fullText = `${cleanedTitle} ${cleanedContent}`;
|
|
|
|
const validationResults = [];
|
|
let semanticScore = 0;
|
|
let totalWeight = 0;
|
|
|
|
// Appliquer chaque règle de validation sémantique
|
|
for (const rule of this.semanticValidationRules) {
|
|
const matches = fullText.match(rule.pattern);
|
|
const matchCount = matches ? matches.length : 0;
|
|
|
|
const ruleResult = {
|
|
ruleName: rule.name,
|
|
required: rule.minMatches,
|
|
found: matchCount,
|
|
passed: matchCount >= rule.minMatches,
|
|
weight: rule.weight,
|
|
matches: matches || [],
|
|
score: matchCount >= rule.minMatches ? rule.weight : 0
|
|
};
|
|
|
|
validationResults.push(ruleResult);
|
|
semanticScore += ruleResult.score;
|
|
totalWeight += rule.weight;
|
|
}
|
|
|
|
// Validation spécifique au contexte race
|
|
const raceValidation = await this.validateRaceContext(fullText, context.raceCode);
|
|
|
|
// Détection d'incohérences
|
|
const inconsistencies = this.detectSemanticInconsistencies(fullText, context);
|
|
|
|
// Score sémantique final (0-1)
|
|
const finalSemanticScore = totalWeight > 0 ? semanticScore / totalWeight : 0;
|
|
|
|
return {
|
|
validationResults,
|
|
raceValidation,
|
|
inconsistencies,
|
|
semanticScore: finalSemanticScore,
|
|
passed: finalSemanticScore >= 0.3, // Seuil minimum 30%
|
|
confidence: this.calculateSemanticConfidence(validationResults, inconsistencies),
|
|
contextRelevance: this.assessContextRelevance(fullText, context),
|
|
recommendations: this.generateSemanticRecommendations(validationResults, raceValidation)
|
|
};
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Layer 4: Calcul des pénalités et score final
|
|
*/
|
|
async layer4_calculatePenalties(patternResult, semanticResult, content) {
|
|
return await this.tracer.run('layer4_penalties', async () => {
|
|
let totalPenalty = 0;
|
|
const appliedPenalties = [];
|
|
|
|
// Pénalité injection détectée
|
|
if (patternResult.hasHighRiskPatterns) {
|
|
totalPenalty += this.penaltyScores.PROMPT_INJECTION_DETECTED;
|
|
appliedPenalties.push({
|
|
type: 'PROMPT_INJECTION_DETECTED',
|
|
score: this.penaltyScores.PROMPT_INJECTION_DETECTED,
|
|
reason: `${patternResult.totalPatterns} patterns dangereux détectés`
|
|
});
|
|
}
|
|
|
|
// Pénalité incohérence sémantique
|
|
if (!semanticResult.passed) {
|
|
totalPenalty += this.penaltyScores.SEMANTIC_INCONSISTENCY;
|
|
appliedPenalties.push({
|
|
type: 'SEMANTIC_INCONSISTENCY',
|
|
score: this.penaltyScores.SEMANTIC_INCONSISTENCY,
|
|
reason: `Score sémantique: ${Math.round(semanticResult.semanticScore * 100)}%`
|
|
});
|
|
}
|
|
|
|
// Pénalité source historique
|
|
const sourceHistory = await this.checkSourceHistory(content);
|
|
if (sourceHistory.isUntrusted) {
|
|
totalPenalty += this.penaltyScores.UNTRUSTED_SOURCE_HISTORY;
|
|
appliedPenalties.push({
|
|
type: 'UNTRUSTED_SOURCE_HISTORY',
|
|
score: this.penaltyScores.UNTRUSTED_SOURCE_HISTORY,
|
|
reason: sourceHistory.reason
|
|
});
|
|
}
|
|
|
|
// Pénalité structure suspecte
|
|
if (patternResult.structureAnalysis.suspicious) {
|
|
totalPenalty += this.penaltyScores.SUSPICIOUS_CONTENT_STRUCTURE;
|
|
appliedPenalties.push({
|
|
type: 'SUSPICIOUS_CONTENT_STRUCTURE',
|
|
score: this.penaltyScores.SUSPICIOUS_CONTENT_STRUCTURE,
|
|
reason: patternResult.structureAnalysis.reason
|
|
});
|
|
}
|
|
|
|
// Pénalité risque modéré
|
|
if (patternResult.hasMediumRiskPatterns && !patternResult.hasHighRiskPatterns) {
|
|
totalPenalty += this.penaltyScores.MODERATE_RISK_INDICATORS;
|
|
appliedPenalties.push({
|
|
type: 'MODERATE_RISK_INDICATORS',
|
|
score: this.penaltyScores.MODERATE_RISK_INDICATORS,
|
|
reason: 'Patterns de risque modéré détectés'
|
|
});
|
|
}
|
|
|
|
return {
|
|
totalPenalty,
|
|
appliedPenalties,
|
|
penaltyCount: appliedPenalties.length,
|
|
maxIndividualPenalty: Math.min(...appliedPenalties.map(p => p.score), 0),
|
|
sourceHistory,
|
|
finalRecommendation: this.generateFinalRecommendation(totalPenalty, patternResult, semanticResult)
|
|
};
|
|
});
|
|
}
|
|
|
|
// === Méthodes utilitaires ===
|
|
|
|
removeHtmlTags(text) {
|
|
return text
|
|
.replace(/<script[^>]*>.*?<\/script>/gi, '')
|
|
.replace(/<style[^>]*>.*?<\/style>/gi, '')
|
|
.replace(/<[^>]*>/g, '');
|
|
}
|
|
|
|
normalizeWhitespace(text) {
|
|
return text
|
|
.replace(/\s+/g, ' ')
|
|
.replace(/\n\s*\n/g, '\n')
|
|
.trim();
|
|
}
|
|
|
|
removeControlCharacters(text) {
|
|
return text.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g, '');
|
|
}
|
|
|
|
encodeSpecialCharacters(text) {
|
|
const specialChars = {
|
|
'<': '<',
|
|
'>': '>',
|
|
'"': '"',
|
|
"'": ''',
|
|
'&': '&'
|
|
};
|
|
|
|
return text.replace(/[<>"'&]/g, char => specialChars[char]);
|
|
}
|
|
|
|
calculateCleaningScore(original, cleaned) {
|
|
if (original === cleaned) return 100;
|
|
|
|
const lengthDiff = Math.abs(original.length - cleaned.length);
|
|
const maxLength = Math.max(original.length, cleaned.length);
|
|
|
|
return Math.max(0, 100 - ((lengthDiff / maxLength) * 100));
|
|
}
|
|
|
|
getPatternRiskWeight(pattern) {
|
|
const source = pattern.source.toLowerCase();
|
|
|
|
if (source.includes('ignore') || source.includes('forget')) return 10;
|
|
if (source.includes('script') || source.includes('eval')) return 9;
|
|
if (source.includes('system') || source.includes('exec')) return 8;
|
|
if (source.includes('instead') || source.includes('pretend')) return 7;
|
|
if (source.includes('json') || source.includes('only')) return 6;
|
|
|
|
return 5; // Risque par défaut
|
|
}
|
|
|
|
findPatternLocations(text, pattern) {
|
|
const locations = [];
|
|
let match;
|
|
|
|
pattern.lastIndex = 0; // Reset regex
|
|
while ((match = pattern.exec(text)) !== null) {
|
|
locations.push({
|
|
start: match.index,
|
|
end: match.index + match[0].length,
|
|
context: text.substring(Math.max(0, match.index - 20), match.index + match[0].length + 20)
|
|
});
|
|
|
|
if (!pattern.global) break;
|
|
}
|
|
|
|
return locations;
|
|
}
|
|
|
|
analyzeContentStructure(text) {
|
|
let riskScore = 0;
|
|
let suspicious = false;
|
|
const reasons = [];
|
|
|
|
// Trop de newlines consécutives
|
|
const excessiveNewlines = (text.match(/\n{3,}/g) || []).length;
|
|
if (excessiveNewlines > 3) {
|
|
riskScore += 2;
|
|
suspicious = true;
|
|
reasons.push('Trop de sauts de ligne consécutifs');
|
|
}
|
|
|
|
// Caractères de séparation suspects
|
|
const suspiciousSeparators = (text.match(/---+|===+|\*\*\*+/g) || []).length;
|
|
if (suspiciousSeparators > 2) {
|
|
riskScore += 3;
|
|
suspicious = true;
|
|
reasons.push('Séparateurs suspects détectés');
|
|
}
|
|
|
|
// Ratio majuscules anormal
|
|
const upperCaseRatio = (text.match(/[A-Z]/g) || []).length / text.length;
|
|
if (upperCaseRatio > 0.3) {
|
|
riskScore += 2;
|
|
suspicious = true;
|
|
reasons.push('Ratio majuscules anormal');
|
|
}
|
|
|
|
return {
|
|
suspicious,
|
|
riskScore,
|
|
reasons: reasons.join(', '),
|
|
metrics: {
|
|
excessiveNewlines,
|
|
suspiciousSeparators,
|
|
upperCaseRatio: Math.round(upperCaseRatio * 100)
|
|
}
|
|
};
|
|
}
|
|
|
|
summarizePatternDetection(patterns, totalRiskScore) {
|
|
if (patterns.length === 0) {
|
|
return 'Aucun pattern dangereux détecté';
|
|
}
|
|
|
|
const highRisk = patterns.filter(p => p.riskWeight >= 8).length;
|
|
const mediumRisk = patterns.filter(p => p.riskWeight >= 5 && p.riskWeight < 8).length;
|
|
const lowRisk = patterns.length - highRisk - mediumRisk;
|
|
|
|
return `${patterns.length} patterns détectés (Risque élevé: ${highRisk}, moyen: ${mediumRisk}, faible: ${lowRisk})`;
|
|
}
|
|
|
|
async validateRaceContext(text, raceCode) {
|
|
if (!raceCode) return { passed: true, score: 1, reason: 'Pas de race spécifique à valider' };
|
|
|
|
// Extraire numéro de race
|
|
const raceNumber = raceCode.split('-')[0];
|
|
|
|
// Rechercher mentions de la race
|
|
const racePattern = new RegExp(`(${raceNumber}|race\\s+${raceNumber})`, 'gi');
|
|
const raceMatches = text.match(racePattern);
|
|
|
|
const passed = raceMatches && raceMatches.length > 0;
|
|
const score = passed ? 1 : 0;
|
|
|
|
return {
|
|
passed,
|
|
score,
|
|
matches: raceMatches || [],
|
|
reason: passed ? 'Race mentionnée dans le contenu' : 'Race non mentionnée'
|
|
};
|
|
}
|
|
|
|
detectSemanticInconsistencies(text, context) {
|
|
const inconsistencies = [];
|
|
|
|
// Vérifier cohérence animal/chien
|
|
const hasAnimalMention = /animal|pet/gi.test(text);
|
|
const hasDogMention = /chien|dog|canin/gi.test(text);
|
|
|
|
if (hasAnimalMention && !hasDogMention && context.raceCode) {
|
|
inconsistencies.push({
|
|
type: 'animal_type_mismatch',
|
|
severity: 'medium',
|
|
description: 'Mention d\'animaux mais pas de chiens spécifiquement'
|
|
});
|
|
}
|
|
|
|
// Vérifier langue cohérente
|
|
const frenchWords = (text.match(/\b(le|la|les|de|du|des|et|avec|pour|dans)\b/gi) || []).length;
|
|
const englishWords = (text.match(/\b(the|and|with|for|in|of|to|a|an)\b/gi) || []).length;
|
|
|
|
if (frenchWords > 0 && englishWords > frenchWords) {
|
|
inconsistencies.push({
|
|
type: 'language_inconsistency',
|
|
severity: 'low',
|
|
description: 'Mélange de français et anglais détecté'
|
|
});
|
|
}
|
|
|
|
return inconsistencies;
|
|
}
|
|
|
|
calculateSemanticConfidence(validationResults, inconsistencies) {
|
|
const passedRules = validationResults.filter(r => r.passed).length;
|
|
const totalRules = validationResults.length;
|
|
|
|
const baseConfidence = totalRules > 0 ? passedRules / totalRules : 0;
|
|
const inconsistencyPenalty = inconsistencies.length * 0.1;
|
|
|
|
return Math.max(0, baseConfidence - inconsistencyPenalty);
|
|
}
|
|
|
|
assessContextRelevance(text, context) {
|
|
let relevanceScore = 0;
|
|
const factors = [];
|
|
|
|
// Contexte race
|
|
if (context.raceCode && text.includes(context.raceCode.split('-')[0])) {
|
|
relevanceScore += 0.3;
|
|
factors.push('Race code found');
|
|
}
|
|
|
|
// Contexte produit
|
|
if (context.productContext && text.toLowerCase().includes(context.productContext.toLowerCase())) {
|
|
relevanceScore += 0.2;
|
|
factors.push('Product context relevant');
|
|
}
|
|
|
|
// Mots-clés pertinents
|
|
const relevantKeywords = ['éducation', 'santé', 'comportement', 'alimentation', 'soins'];
|
|
const foundKeywords = relevantKeywords.filter(keyword => text.toLowerCase().includes(keyword));
|
|
relevanceScore += foundKeywords.length * 0.1;
|
|
|
|
if (foundKeywords.length > 0) {
|
|
factors.push(`${foundKeywords.length} keywords found`);
|
|
}
|
|
|
|
return {
|
|
score: Math.min(1, relevanceScore),
|
|
factors,
|
|
foundKeywords
|
|
};
|
|
}
|
|
|
|
generateSemanticRecommendations(validationResults, raceValidation) {
|
|
const recommendations = [];
|
|
|
|
const failedRules = validationResults.filter(r => !r.passed);
|
|
if (failedRules.length > 0) {
|
|
recommendations.push({
|
|
type: 'semantic_improvement',
|
|
priority: 'high',
|
|
message: `Améliorer la pertinence pour: ${failedRules.map(r => r.ruleName).join(', ')}`
|
|
});
|
|
}
|
|
|
|
if (!raceValidation.passed) {
|
|
recommendations.push({
|
|
type: 'race_context',
|
|
priority: 'medium',
|
|
message: 'Mentionner la race spécifique dans le contenu'
|
|
});
|
|
}
|
|
|
|
return recommendations;
|
|
}
|
|
|
|
async checkSourceHistory(content) {
|
|
// Simulation - À intégrer avec le système de stock
|
|
const sourceDomain = content.sourceDomain || content.url;
|
|
|
|
if (!sourceDomain) {
|
|
return { isUntrusted: false, reason: 'Pas de domaine source' };
|
|
}
|
|
|
|
// Sources connues non fiables
|
|
const untrustedDomains = ['example.com', 'test.com', 'spam.com'];
|
|
|
|
if (untrustedDomains.some(domain => sourceDomain.includes(domain))) {
|
|
return {
|
|
isUntrusted: true,
|
|
reason: `Source ${sourceDomain} dans la liste des domaines non fiables`
|
|
};
|
|
}
|
|
|
|
return { isUntrusted: false, reason: 'Source fiable' };
|
|
}
|
|
|
|
generateFinalRecommendation(totalPenalty, patternResult, semanticResult) {
|
|
if (totalPenalty <= -50 || patternResult.hasHighRiskPatterns) {
|
|
return {
|
|
action: 'REJECT',
|
|
reason: 'Risque sécuritaire critique détecté',
|
|
confidence: 'high'
|
|
};
|
|
}
|
|
|
|
if (totalPenalty <= -30 || !semanticResult.passed) {
|
|
return {
|
|
action: 'QUARANTINE',
|
|
reason: 'Contenu suspect nécessitant révision manuelle',
|
|
confidence: 'medium'
|
|
};
|
|
}
|
|
|
|
if (totalPenalty <= -10 || patternResult.hasMediumRiskPatterns) {
|
|
return {
|
|
action: 'ACCEPT_WITH_MONITORING',
|
|
reason: 'Risque faible mais surveillance recommandée',
|
|
confidence: 'medium'
|
|
};
|
|
}
|
|
|
|
return {
|
|
action: 'ACCEPT',
|
|
reason: 'Contenu validé, aucun risque détecté',
|
|
confidence: 'high'
|
|
};
|
|
}
|
|
|
|
generateSecurityRecommendations(patternResult, semanticResult, penaltyResult) {
|
|
const recommendations = [];
|
|
|
|
if (patternResult.hasHighRiskPatterns) {
|
|
recommendations.push({
|
|
type: 'CRITICAL',
|
|
message: 'Patterns d\'injection détectés - Rejeter le contenu',
|
|
patterns: patternResult.detectedPatterns.map(p => p.pattern)
|
|
});
|
|
}
|
|
|
|
if (!semanticResult.passed) {
|
|
recommendations.push({
|
|
type: 'WARNING',
|
|
message: 'Contenu peu pertinent au contexte demandé',
|
|
score: Math.round(semanticResult.semanticScore * 100)
|
|
});
|
|
}
|
|
|
|
if (penaltyResult.sourceHistory.isUntrusted) {
|
|
recommendations.push({
|
|
type: 'INFO',
|
|
message: 'Source historiquement non fiable',
|
|
details: penaltyResult.sourceHistory.reason
|
|
});
|
|
}
|
|
|
|
return recommendations;
|
|
}
|
|
|
|
determineValidityStatus(patternResult, semanticResult, penaltyResult) {
|
|
// Rejet immédiat si patterns critiques
|
|
if (patternResult.hasHighRiskPatterns) return false;
|
|
|
|
// Rejet si pénalités trop élevées
|
|
if (penaltyResult.totalPenalty <= -50) return false;
|
|
|
|
// Rejet si sémantique insuffisante ET patterns suspects
|
|
if (!semanticResult.passed && patternResult.hasMediumRiskPatterns) return false;
|
|
|
|
return true;
|
|
}
|
|
|
|
calculateRiskLevel(patternResult, semanticResult) {
|
|
if (patternResult.hasHighRiskPatterns) return 'critical';
|
|
if (patternResult.totalRiskScore >= 15) return 'high';
|
|
if (!semanticResult.passed || patternResult.hasMediumRiskPatterns) return 'medium';
|
|
return 'low';
|
|
}
|
|
|
|
// === Cache et performances ===
|
|
|
|
generateCacheKey(content, context) {
|
|
const contentHash = this.simpleHash(content.content + content.title);
|
|
const contextHash = this.simpleHash(JSON.stringify(context));
|
|
return `validation:${contentHash}:${contextHash}`;
|
|
}
|
|
|
|
simpleHash(str) {
|
|
let hash = 0;
|
|
for (let i = 0; i < str.length; i++) {
|
|
const char = str.charCodeAt(i);
|
|
hash = ((hash << 5) - hash) + char;
|
|
hash = hash & hash; // Convert to 32-bit integer
|
|
}
|
|
return hash.toString(36);
|
|
}
|
|
|
|
getFromCache(cacheKey) {
|
|
const cached = this.validationCache.get(cacheKey);
|
|
if (!cached) return null;
|
|
|
|
if (Date.now() - cached.timestamp > this.cacheTimeout) {
|
|
this.validationCache.delete(cacheKey);
|
|
return null;
|
|
}
|
|
|
|
return cached.result;
|
|
}
|
|
|
|
cacheResult(cacheKey, result) {
|
|
this.validationCache.set(cacheKey, {
|
|
result,
|
|
timestamp: Date.now()
|
|
});
|
|
|
|
// Nettoyage périodique du cache
|
|
if (this.validationCache.size > 1000) {
|
|
this.cleanupCache();
|
|
}
|
|
}
|
|
|
|
cleanupCache() {
|
|
const now = Date.now();
|
|
for (const [key, cached] of this.validationCache.entries()) {
|
|
if (now - cached.timestamp > this.cacheTimeout) {
|
|
this.validationCache.delete(key);
|
|
}
|
|
}
|
|
}
|
|
|
|
// === Statistiques et monitoring ===
|
|
|
|
updateValidationStats(result) {
|
|
this.stats.averageProcessingTime = this.updateRunningAverage(
|
|
this.stats.averageProcessingTime,
|
|
result.processingTime,
|
|
this.stats.totalValidated
|
|
);
|
|
|
|
this.stats.riskLevelDistribution[result.riskLevel]++;
|
|
|
|
if (result.layers.patternDetection.hasHighRiskPatterns) {
|
|
this.stats.injectionAttempts++;
|
|
}
|
|
|
|
if (!result.layers.semanticValidation.passed) {
|
|
this.stats.semanticFailures++;
|
|
}
|
|
}
|
|
|
|
updateRunningAverage(currentAvg, newValue, totalCount) {
|
|
if (totalCount === 1) return newValue;
|
|
const alpha = 1 / totalCount;
|
|
return alpha * newValue + (1 - alpha) * currentAvg;
|
|
}
|
|
|
|
logValidationResult(result, content, context) {
|
|
const logData = {
|
|
contentId: content.id,
|
|
riskLevel: result.riskLevel,
|
|
isValid: result.isValid,
|
|
processingTime: result.processingTime,
|
|
patternsDetected: result.layers.patternDetection.totalPatterns,
|
|
semanticScore: Math.round(result.layers.semanticValidation.semanticScore * 100),
|
|
totalPenalty: result.layers.penalties.totalPenalty,
|
|
raceCode: context.raceCode
|
|
};
|
|
|
|
switch (result.riskLevel) {
|
|
case 'critical':
|
|
logger.securityEvent('CRITICAL security threat detected', 'PROMPT_INJECTION', logData);
|
|
break;
|
|
case 'high':
|
|
logger.securityEvent('HIGH security risk detected', 'SUSPICIOUS_CONTENT', logData);
|
|
break;
|
|
case 'medium':
|
|
logger.warn('Medium security risk in content', logData);
|
|
break;
|
|
default:
|
|
logger.debug('Content validation completed', logData);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Obtenir statistiques de sécurité
|
|
*/
|
|
getSecurityStats() {
|
|
const cacheStats = {
|
|
size: this.validationCache.size,
|
|
hitRate: this.stats.totalValidated > 0 ?
|
|
(this.stats.totalValidated - this.stats.injectionAttempts - this.stats.semanticFailures) / this.stats.totalValidated : 0
|
|
};
|
|
|
|
return {
|
|
...this.stats,
|
|
cache: cacheStats,
|
|
engine: 'AntiInjectionEngine',
|
|
version: '1.0',
|
|
lastUpdate: new Date().toISOString()
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Réinitialiser statistiques
|
|
*/
|
|
resetStats() {
|
|
this.stats = {
|
|
totalValidated: 0,
|
|
injectionAttempts: 0,
|
|
semanticFailures: 0,
|
|
falsePositives: 0,
|
|
averageProcessingTime: 0,
|
|
riskLevelDistribution: {
|
|
low: 0,
|
|
medium: 0,
|
|
high: 0,
|
|
critical: 0
|
|
}
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Health check du moteur de sécurité
|
|
*/
|
|
async healthCheck() {
|
|
try {
|
|
const testContent = {
|
|
id: 'health-check',
|
|
title: 'Test de santé du système',
|
|
content: 'Contenu de test pour validation du moteur de sécurité',
|
|
sourceType: 'system'
|
|
};
|
|
|
|
const testContext = {
|
|
raceCode: '352-1',
|
|
clientId: 'health-check'
|
|
};
|
|
|
|
const result = await this.validateContent(testContent, testContext);
|
|
|
|
return {
|
|
status: 'healthy',
|
|
engine: 'AntiInjectionEngine',
|
|
testResult: {
|
|
processed: true,
|
|
processingTime: result.processingTime,
|
|
riskLevel: result.riskLevel
|
|
},
|
|
stats: this.getSecurityStats(),
|
|
cache: {
|
|
size: this.validationCache.size,
|
|
enabled: true
|
|
}
|
|
};
|
|
|
|
} catch (error) {
|
|
return {
|
|
status: 'error',
|
|
engine: 'AntiInjectionEngine',
|
|
error: error.message
|
|
};
|
|
}
|
|
}
|
|
}
|
|
|
|
module.exports = AntiInjectionEngine; |