Scanner de code “anti‑malware” pour VS Code — Tuto complet
Objectif : détecter du code potentiellement malveillant (eval, powershell encodé, URLs d’exfiltration, etc.) ou des fichiers infectés dans un projet VS Code.
Plan
- Prérequis & périmètre
- Solution rapide (outils existants)
- Créer une extension VS Code (TypeScript) : scanner heuristique
- b Intégrer ClamAV / YARA[/b]
- Automatiser : Tasks VS Code + pre‑commit
- Copilot Chat : prompts utiles
- Limites, bonnes pratiques
1) Prérequis & périmètre
Systèmes : Windows, macOS, Linux
Outils possibles : VS Code, Node.js 18+, ClamAV (facultatif), YARA (facultatif), Git (pour pre‑commit)
Ce que ce tuto couvre :
- Détection par heuristiques (regex & patterns de risque).
- Intégration optionnelle d’un moteur AV (ClamAV) et de signatures (YARA).
- Affichage des alertes dans l’onglet Problèmes de VS Code.
- Automatisation à la demande, à la sauvegarde ou à l’intégration (Git).
2) Solution rapide (outils existants)
A) ClamAV (gratuit, signatures malwares connues)
- Installez ClamAV (via package manager ou installateur).
- Mettez à jour les signatures :
Code:
freshclam - Lancez un scan dans le workspace depuis le terminal VS Code :
Code:
clamscan -r --bell -i . - Les fichiers infectés seront listés dans la sortie.
B) Semgrep & plugins sécurité
- Semgrep (règles personnalisables) :
Code:
semgrep --config auto - Bandit (Python), ESLint security plugins (JS/TS), Snyk/CodeQL selon votre stack.
Astuce : Combinez ClamAV (malwares connus) + Semgrep (anti‑patterns de code) pour une bonne couverture.
3) Créer une extension VS Code (TypeScript) : scanner heuristique
3.1) Bootstrap de l’extension
- Installez les outils :
Code:npm install -g yo generator-code vsce - Générez une extension TypeScript :
Choisissez : New Extension (TypeScript)Code:
yo code
3.2) Déclarez la commande dans package.json
Code:
{ "activationEvents": ["onCommand:malwareScanner.scanWorkspace"], "contributes": { "commands": [ { "command": "malwareScanner.scanWorkspace", "title": "Scanner le workspace (Malware)" } ] } }
3.3) Code du scanner (heuristiques + diagnostics)
Créez/éditez src/extension.ts :
Code:
import * as vscode from 'vscode'; import * as fs from 'fs';
import * as path from 'path';
import { exec } from 'child_process';
interface Finding { file: vscode.Uri; range: vscode.Range; message: string; severity: vscode.DiagnosticSeverity; ruleId: string; }
const suspiciousPatterns: { ruleId: string; regex: RegExp; message: string }[] = [ { ruleId: 'JS-EVAL', regex: /\beval\s*(/i, message: "Usage de 'eval' potentiellement dangereux." }, { ruleId: 'JS-FUNCTION-CALL', regex: /\bnew\s+Function\s*(/i, message: "Construction dynamique de code (new Function)." }, { ruleId: 'JS-CHILD-PROC', regex: /\b(child_process|spawn|exec|execFile)\b/i, message: "Exécution de commandes système." }, { ruleId: 'PS-ENC', regex: /powershell(?:.exe)?\s+-enc\s+[A-Za-z0-9+/=]+/i, message: "Commande PowerShell encodée." }, { ruleId: 'B64-LONG', regex: /'"`['"`]/, message: "Chaîne base64 longue suspecte." }, { ruleId: 'URL-EXFIL', regex: /\bhttps?://[^\s'"`]+/i, message: "Lien externe (potentielle exfiltration)." }, { ruleId: 'WIN-PERSIST', regex: /\b(\Run\|\RunOnce\|Startup\|schtasks.exe)\b/i, message: "Indicateurs de persistance Windows." } ];
export function activate(context: vscode.ExtensionContext) { const diagnostics = vscode.languages.createDiagnosticCollection('malwareScanner'); context.subscriptions.push(diagnostics);
const scanCmd = vscode.commands.registerCommand('malwareScanner.scanWorkspace', async () => { diagnostics.clear(); const folders = vscode.workspace.workspaceFolders; if (!folders) { vscode.window.showWarningMessage('Aucun dossier de workspace ouvert.'); return; }
const findings: Finding[] = [];
for (const folder of folders) {
await scanFolder(folder.uri.fsPath, findings);
}
// Publier les diagnostics
const byFile = new Map<string, vscode.Diagnostic[]>();
for (const f of findings) {
const diag = new vscode.Diagnostic(f.range, \`[\${f.ruleId}] \${f.message}\`, f.severity);
diag.source = 'MalwareScanner';
const key = f.file.toString();
if (!byFile.has(key)) byFile.set(key, []);
byFile.get(key)!.push(diag);
}
byFile.forEach((diags, file) => diagnostics.set(vscode.Uri.parse(file), diags));
vscode.window.showInformationMessage(\`Scan terminé: \${findings.length} alerte(s).\`);
});
context.subscriptions.push(scanCmd);
}
async function scanFolder(dir: string, findings: Finding[]) { const entries = await fs.promises.readdir(dir, { withFileTypes: true }); for (const entry of entries) { const full = path.join(dir, entry.name);
if (entry.isDirectory()) {
if (['.git', 'node_modules', 'dist', 'build', '.venv', '.tox', '.pytest_cache'].includes(entry.name)) continue;
await scanFolder(full, findings);
continue;
}
if (!/\.(js|ts|mjs|cjs|jsx|tsx|py|ps1|bat|cmd|sh|php|rb|go|cs|json|yml|yaml|ini|conf|txt)$/i.test(entry.name)) continue;
await scanFile(full, findings);
} }
async function scanFile(filePath: string, findings: Finding[]) { try { const text = await fs.promises.readFile(filePath, 'utf8'); const lines = text.split(/\r?\n/);
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
for (const rule of suspiciousPatterns) {
const m = rule.regex.exec(line);
if (m) {
const start = m.index;
const end = m.index + m[0].length;
findings.push({
file: vscode.Uri.file(filePath),
range: new vscode.Range(new vscode.Position(i, start), new vscode.Position(i, end)),
message: rule.message,
severity: vscode.DiagnosticSeverity.Warning,
ruleId: rule.ruleId
});
}
}
// Heuristique: détecter une base64 longue et vérifier du contenu décodé potentiellement dangereux
const b64Match = line.match(/(['"\`])([A-Za-z0-9+/]{80,}={0,2})\1/);
if (b64Match) {
try {
const buf = Buffer.from(b64Match[2], 'base64');
const decoded = buf.toString('utf8');
if (/\b(eval|cmd\.exe|powershell|wget|curl|Invoke-WebRequest|nc -e)\b/i.test(decoded)) {
findings.push({
file: vscode.Uri.file(filePath),
range: new vscode.Range(new vscode.Position(i, 0), new vscode.Position(i, line.length)),
message: "Chaîne base64 décodée contenant des indicateurs dangereux.",
severity: vscode.DiagnosticSeverity.Error,
ruleId: 'B64-DECODED-DANGEROUS'
});
}
} catch {
// ignore
}
}
}
} catch { // binaire ou permissions: ignorer } }
export function deactivate() {}
3.4) Tester en mode développement
- Installer les dépendances et compiler :
Code:
npm install npm run compile - Appuyer sur F5 pour lancer un “Extension Development Host”.
- Commande palette → taper :
Code:
Scanner le workspace (Malware) - Voir les alertes dans l’onglet Problèmes.
3.5) (Option) Scan en continu
Ajoutez un FileSystemWatcher pour scanner à la sauvegarde (idée simple) :
Code:
const watcher = vscode.workspace.createFileSystemWatcher('**/.{js,ts,py,ps1,sh}'); watcher.onDidChange(uri => scanFile(uri.fsPath, [] / vous pouvez réémettre les diagnostics ciblés ici /)); watcher.onDidCreate(uri => scanFile(uri.fsPath, [])); watcher.onDidDelete(uri => { / retirer diagnostics pour ce fichier */ }); context.subscriptions.push(watcher);
4) (Option) Intégrer ClamAV / YARA
ClamAV (via CLI) : lancez le moteur et parsez sa sortie.
Code:
function runClamAV(rootPath: string): Promise { return new Promise((resolve) => { exec(`clamscan -r --bell -i "${rootPath}"`, { timeout: 5 * 60_000 }, (err, stdout) => { resolve(stdout || ''); }); }); }
YARA : stockez vos règles dans rules/ et exécutez yara en CLI, puis créez des diagnostics pour chaque match.
Astuce : gardez YARA/ClamAV optionnels (feature flags) pour ne pas bloquer l’extension si non installés.
5) Automatiser : Tasks VS Code + pre‑commit
5.1) Task VS Code (exemple ClamAV + Semgrep) dans [.vscode/tasks.json] :
Code:
{ "version": "2.0.0", "tasks": [ { "label": "Scan ClamAV", "type": "shell", "command": "clamscan -r --bell -i .", "problemMatcher": [] }, { "label": "Scan Semgrep", "type": "shell", "command": "semgrep --config auto", "problemMatcher": [] } ] }
5.2) Hook pre‑commit (Git + Husky)
Code:
npm i -D husky lint-staged npx husky install npx husky add .husky/pre-commit "npm run scan:quick"
Ajoutez un script scan:quick dans package.json :
Code:
"scripts": { "scan:quick": "semgrep --config auto && echo 'OK Semgrep' || (echo 'Semgrep KO' && exit 1)" }
Variante : ajoutez un clamscan si installé localement.
6) Copilot Chat : prompts utiles
Utilisez Copilot Chat dans VS Code pour accélérer la création/examen des règles :
-
Code:
Explique ce fichier et signale tout pattern à risque (eval, exec, powershell -enc, base64 longue). -
Code:
Écris une règle regex pour détecter des commandes PowerShell encodées en base64 dans des scripts .js et .ps1. -
Code:
Propose des améliorations à mon extension VS Code : moins de faux positifs, cache par hash, affichage des extraits suspects. -
Code:
Génère un rapport JSON structuré (fichier, ligne, règle, extrait).
7) Limites & bonnes pratiques
- Faux positifs : eval ou child_process peuvent être légitimes. Autorisez des commentaires d’ignore (ex. // malware-scan: ignore JS-EVAL ) et ajustez la sévérité.
- Faux négatifs : du code très obfusqué peut passer. Multipliez heuristiques + signatures (ClamAV/YARA) + SAST (Semgrep).
- Performance : ignorez node_modules , .git , binaires, archives ; mettez des timeouts ; cachez par hash.
- Sécurité : ne jamais exécuter le code scanné. Ne décompressez pas automatiquement des archives inconnues.
- CI/CD : répliquez le scan en pipeline (GitHub Actions/Azure DevOps) et faites échouer en cas d’alertes critiques.
- Traçabilité : exportez un rapport JSON (règle, fichier, ligne, extrait, SHA256).
Conclusion
Avec ce tuto, vous pouvez :
- Utiliser rapidement ClamAV/Semgrep dans VS Code.
- Développer une extension légère qui met en évidence les patterns suspects.
- Automatiser les contrôles à la sauvegarde et au commit.