🎯 Pourquoi analyser les logs Apache ?
Le problème
Votre serveur web est exposé sur Internet. Chaque jour, des milliers de requêtes automatisées sondent vos applications à la recherche de vulnérabilités. Ces attaques laissent des traces dans vos logs, mais sous forme brute, ces fichiers sont inexploitables.
Un serveur Apache moyen génère 30 000 lignes de logs par jour. Comment distinguer les visiteurs légitimes des scans automatisés ? Comment identifier les patterns d'attaque ? C'est exactement ce que nous allons résoudre.
Les trois objectifs de l'analyse de logs
- Détection d'intrusion : Identifier les tentatives d'exploitation (SQL injection, path traversal, brute-force)
- Cartographie des menaces : Comprendre d'où viennent les attaques et quels outils sont utilisés
- Renseignement actionnable : Transformer du bruit en décisions (bloquer des IPs, patcher des services, ajuster le WAF)
🔍 Anatomie d'une ligne de log Apache
Apache utilise le Combined Log Format, qui enregistre chaque requête HTTP sur une ligne. Avant de parser quoi que ce soit, il faut comprendre ce que contient cette ligne.
Exemple de log réel
203.0.113.42 - - [21/Aug/2025:10:30:01 +0200] "GET /wp-login.php HTTP/1.1" 404 502 "-" "Mozilla/5.0 (compatible; Nmap Scripting Engine)"
Décryptage champ par champ
| Champ | Valeur | Signification |
|---|---|---|
203.0.113.42 |
Adresse IP | L'attaquant. Première information cruciale pour le traçage. |
- (2 fois) |
Identité & Auth | Rarement utilisés. Le premier est l'ident, le second l'utilisateur HTTP. |
[21/Aug/2025:10:30:01 +0200] |
Timestamp | Permet de corréler plusieurs attaques dans le temps. |
"GET /wp-login.php HTTP/1.1" |
Requête HTTP | Le cœur de l'attaque : méthode, URL ciblée, protocole. |
404 |
Code HTTP | Indicateur clé ! Les codes 4xx révèlent souvent des scans. |
502 |
Taille réponse | Taille en octets de la réponse envoyée. |
"-" |
Referer | Page d'origine. "-" signifie accès direct (typique des bots). |
"Mozilla/5.0 (compatible; Nmap...)" |
User-Agent | Signature de l'outil. Ici, Nmap ne se cache même pas ! |
💡 Ce que révèle ce log
- L'IP cherche
/wp-login.php(scan WordPress) - Le code 404 indique que le fichier n'existe pas
- Le User-Agent confirme un scan Nmap
- Referer vide = accès direct, pas de navigation humaine
Verdict : Tentative de reconnaissance automatisée
⚙️ Stratégie d'extraction : pourquoi les expressions régulières ?
Pourquoi pas un simple split() ?
On pourrait être tenté de découper la ligne par espaces, mais cela échoue dès qu'un champ contient lui-même des espaces :
"GET /page?param=hello world HTTP/1.1"
Les guillemets, crochets et caractères spéciaux rendent le format contextuellement structuré. Les regex permettent de capturer ces structures avec précision.
Construction progressive de la regex
Décortiquons la regex par blocs :
| Pattern | Cible | Explication |
|---|---|---|
^(?<ip>[\d\.]+) |
IP | Capture les chiffres et points en début de ligne |
\S+ \S+ |
Ident & Auth | On ignore ces champs (souvent - -) |
\[(?<datetime>[^\]]+)\] |
Timestamp | Tout ce qui est entre crochets |
"(?<method>\S+) (?<url>\S+) \S+" |
Requête | Méthode, URL, version HTTP (entre guillemets) |
(?<status>\d{3}) |
Code HTTP | Exactement 3 chiffres |
"(?<useragent>.*)" |
User-Agent | Capture non-greedy pour gérer les guillemets internes |
⚠️ Piège classique : le greedy matching
Si on utilise ".*" au lieu de "[^"]*" ou ".*?",
la regex capturera du premier au dernier guillemet de la ligne, englobant plusieurs champs !
💻 Implémentation : du parsing à la visualisation
Étape 1 : Parser les logs
Maintenant qu'on comprend la structure, on peut extraire les informations avec PowerShell :
$logFilePath = "C:\Temp\access.log"
# Regex robuste compatible avec User-Agents complexes
$logPattern = '^(?<ip>[\d\.]+) \S+ \S+ \[(?<datetime>[^\]]+)\] "(?<method>\S+) (?<url>\S+) \S+" (?<status>\d{3}) (?<size>\S+) "[^"]*" "(?<useragent>.*)"'
$parsedLogs = Select-String -Path $logFilePath -Pattern $logPattern | ForEach-Object {
[PSCustomObject]@{
IPAddress = $_.Matches[0].Groups['ip'].Value
DateTime = $_.Matches[0].Groups['datetime'].Value
Method = $_.Matches[0].Groups['method'].Value
URL = $_.Matches[0].Groups['url'].Value
Status = [int]$_.Matches[0].Groups['status'].Value
UserAgent = $_.Matches[0].Groups['useragent'].Value
}
}
Write-Host "✅ $($parsedLogs.Count) lignes parsées avec succès"
Étape 2 : Identifier les indicateurs de compromission (IoC)
Les attaquants laissent des signatures caractéristiques. On filtre selon deux critères :
- Codes HTTP 4xx : Erreurs client révélant des tentatives sur des ressources inexistantes
- User-Agents suspects : Outils d'attaque connus (Nmap, SQLmap, Gobuster...)
# Enrichissement avec identification de l'outil
$suspicious = $parsedLogs | Where-Object {
$_.Status -ge 400 -and $_.Status -lt 500
} | ForEach-Object {
$tool = switch -Regex ($_.UserAgent) {
'nmap|Nmap' { 'Nmap' }
'sqlmap' { 'SQLmap' }
'gobuster|dirb' { 'Directory Brute-Force' }
'nikto' { 'Nikto Scanner' }
'masscan' { 'Masscan' }
default { 'Inconnu' }
}
$_ | Add-Member -NotePropertyName Tool -NotePropertyValue $tool -PassThru
}
Write-Host "🚨 $($suspicious.Count) requêtes suspectes détectées"
Étape 3 : Géolocalisation et agrégation
On identifie les top 20 IPs attaquantes et on les géolocalise pour comprendre l'origine géographique des menaces.
🔒 Principe d'anonymisation
On masque le dernier octet de l'IP (203.0.113.42 → 203.0.113.0/24)
pour respecter les bonnes pratiques de vie privée tout en conservant la localisation géographique.
$mapPoints = foreach ($ipGroup in ($suspicious | Group-Object IPAddress | Sort-Object -Descending Count | Select-Object -First 20)) {
$ip = $ipGroup.Name
$ipAnon = ($ip -split '\.')[0..2] -join '.' + '.0/24'
# Rate-limiting : ip-api.com limite à 45 req/min
Start-Sleep -Milliseconds 1500
try {
$geo = Invoke-RestMethod "http://ip-api.com/json/$ip"
if ($geo.status -eq 'success') {
[PSCustomObject]@{
ip = $ipAnon
lat = $geo.lat
lon = $geo.lon
city = $geo.city
country = $geo.country
isp = $geo.isp
errorCnt = $ipGroup.Count
tool = ($ipGroup.Group.Tool | Sort-Object -Unique) -join ', '
topUrls = ($ipGroup.Group.URL | Group-Object | Sort-Object -Descending Count | Select-Object -First 5).Name
}
}
} catch {
Write-Warning "⚠️ IP $ip non géolocalisable"
}
}
Étape 4 : Génération de la carte interactive
On utilise Leaflet.js avec clustering pour visualiser les attaques sur une carte mondiale. Chaque marqueur affiche les détails de l'attaque au clic.
$jsData = $mapPoints | ConvertTo-Json -Compress
$htmlContent = @"
<!DOCTYPE html>
<html>
<head>
<title>Carte des Attaques Web - Analyse Géographique</title>
<meta charset="utf-8" />
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" />
<link rel="stylesheet" href="https://unpkg.com/leaflet.markercluster@1.5.3/dist/MarkerCluster.css" />
<style>
body { margin: 0; font-family: Arial, sans-serif; }
#map { height: 100vh; }
#exportCSV {
position: absolute;
top: 10px;
right: 10px;
z-index: 999;
padding: 10px 16px;
background: #007bff;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-weight: bold;
}
#exportCSV:hover { background: #0056b3; }
</style>
</head>
<body>
<div id="map"></div>
<button id="exportCSV">📥 Exporter CSV</button>
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
<script src="https://unpkg.com/leaflet.markercluster@1.5.3/dist/leaflet.markercluster.js"></script>
<script>
const map = L.map('map').setView([20, 0], 2);
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: '© OpenStreetMap contributors'
}).addTo(map);
const markers = L.markerClusterGroup();
const ipData = $jsData;
ipData.forEach(point => {
const urlList = point.topUrls.map(u => `<li><code>${u}</code></li>`).join('');
const popup = `
<h3 style="margin-top:0">${point.ip}</h3>
<p><strong>📍 ${point.city}, ${point.country}</strong></p>
<p>🌐 ISP : ${point.isp}</p>
<p>🚨 Erreurs 4xx détectées : <strong>${point.errorCnt}</strong></p>
<p>🔧 Outils identifiés : <strong>${point.tool}</strong></p>
<h4>Top URLs ciblées :</h4>
<ul style="margin:0;padding-left:20px">${urlList}</ul>
`;
markers.addLayer(L.marker([point.lat, point.lon]).bindPopup(popup));
});
map.addLayer(markers);
// Export CSV
document.getElementById('exportCSV').onclick = () => {
const csvContent = 'data:text/csv;charset=utf-8,' +
'IP,Latitude,Longitude,Ville,Pays,ISP,NombreErreurs,Outils\\n' +
ipData.map(p =>
`"${p.ip}",${p.lat},${p.lon},"${p.city}","${p.country}","${p.isp}",${p.errorCnt},"${p.tool}"`
).join('\\n');
const link = document.createElement('a');
link.setAttribute('href', encodeURI(csvContent));
link.setAttribute('download', `attaques_web_${new Date().toISOString().split('T')[0]}.csv`);
link.click();
};
</script>
</body>
</html>
"@
$reportPath = "C:\Temp\rapport_attaques_web.html"
$htmlContent | Out-File -FilePath $reportPath -Encoding UTF8
Write-Host "✅ Rapport généré : $reportPath"
Start-Process $reportPath
🚀 Aller plus loin : optimisations et limites
Performance : PowerShell vs outils Unix
| Outil | Avantages | Quand l'utiliser ? |
|---|---|---|
| PowerShell |
• Intégration Windows native • Objets structurés • API REST faciles |
Automatisation Windows, analyses ad-hoc, exports formatés |
| grep/awk/sed |
• Ultra-rapide sur gros fichiers • Pipe Unix efficient • Syntaxe concise |
Serveurs Linux, fichiers >1GB, filtrage simple |
| ELK Stack |
• Indexation temps réel • Dashboards avancés • Scalabilité |
Infrastructure production, multi-serveurs, alerting |
Scalabilité : traiter des fichiers volumineux
Pour des logs >100MB, adaptez le script :
# Traitement par batch de 10000 lignes
Get-Content $logFilePath -ReadCount 10000 | ForEach-Object {
$batch = $_
# Traiter le batch...
}
Sécurité : patterns d'attaque avancés
Enrichissez la détection avec des patterns spécifiques :
- SQL Injection : URLs contenant
' OR 1=1,UNION SELECT - Path Traversal :
../../../etc/passwd - XSS :
<script>,javascript: - Command Injection :
; ls -la,| cat /etc/passwd
Automatisation : surveillance continue
Planifiez l'exécution quotidienne avec Task Scheduler :
$action = New-ScheduledTaskAction -Execute "powershell.exe" -Argument "-File C:\Scripts\AnalyseLogs.ps1"
$trigger = New-ScheduledTaskTrigger -Daily -At 3AM
Register-ScheduledTask -TaskName "AnalyseLogsApache" -Action $action -Trigger $trigger
🎓 Conclusion : de la donnée brute à l'intelligence
Ce tutoriel vous a montré comment transformer du bruit en signal. En comprenant la structure des logs, en identifiant les patterns d'attaque et en cartographiant les menaces, vous disposez maintenant d'un outil d'analyse reproductible.
Ce que vous avez appris :
- ✅ Décoder le Combined Log Format d'Apache
- ✅ Construire des regex robustes pour le parsing
- ✅ Identifier les IoC (codes 4xx, User-Agents suspects)
- ✅ Géolocaliser et anonymiser les sources d'attaque
- ✅ Générer des rapports visuels exploitables
Prochaines étapes :
- 🔄 Automatiser via Task Scheduler ou Azure Runbook
- 📧 Configurer des alertes email pour les pics d'activité
- 🛡️ Intégrer avec un WAF (ModSecurity, Cloudflare)
- 📊 Migrer vers une solution temps réel (ELK, Splunk)