11.2.3 Log Processing
Definisi
Log Processing adalah seni mengumpulkan, memproses, menganalisis, dan menyimpan log dari berbagai sumber - server, firewall, aplikasi, SIEM - untuk mendeteksi ancaman, troubleshooting insiden, dan memenuhi kepatuhan (compliance). Dalam keamanan siber, kemampuan memproses log secara efisien membedakan antara SOC analyst yang reaktif dan proaktif. Tools seperti awk, sed, Python, ELK Stack, dan Loki+Grafana menjadi senjata utama untuk mengubah data log mentah menjadi intelligence yang actionable.
1. Format Log Umum
Syslog (RFC 5424)
<34>1 2024-07-22T12:30:45.123Z server01 sshd 1234 - - Failed password for root from 10.0.0.5 port 22 ssh2
Struktur:
<PRI>VERSION TIMESTAMP HOSTNAME APP PID MSGID STRUCTURED_DATA MSG
Apache Combined Log Format
192.168.1.10 - - [22/Jul/2024:12:30:45 +0700] "GET /wp-admin HTTP/1.1" 404 512 "-" "Mozilla/5.0 (compatible; MJ12bot/v1.4)"
Kolom:
IP IDENT AUTH [DATE] "METHOD PATH PROTO" STATUS SIZE "REFERER" "USER_AGENT"
JSON Log
{
"timestamp": "2024-07-22T12:30:45.123Z",
"level": "ERROR",
"logger": "com.company.auth.LoginController",
"message": "Failed login attempt for user admin",
"source_ip": "10.0.0.5",
"user_agent": "Mozilla/5.0",
"attempt_count": 15,
"threat_score": 85
}
CSV Log
timestamp,src_ip,dst_ip,port,protocol,action,rule_id
2024-07-22 12:30:45,10.0.0.5,192.168.1.100,443,tcp,BLOCK,RULE-1001
2024-07-22 12:30:46,10.0.0.5,192.168.1.100,22,tcp,BLOCK,RULE-1001
EVTX (Windows Event Log - XML)
<Event xmlns="http://schemas.microsoft.com/win/2004/08/events/event">
<System>
<EventID>4625</EventID>
<Provider Name="Microsoft-Windows-Security-Auditing"/>
<TimeCreated SystemTime="2024-07-22T12:30:45.123Z"/>
</System>
<EventData>
<Data Name="TargetUserName">Administrator</Data>
<Data Name="IpAddress">10.0.0.5</Data>
<Data Name="LogonType">3</Data>
<Data Name="Status">0xC000006D</Data>
</EventData>
</Event>
2. Parsing Tools - awk, sed, cut
awk - Swiss Army Knife Log Parser
# Struktur dasar awk: awk 'pattern {action}' file.log
# Cetak kolom tertentu - Apache log
awk '{print $1, $7, $9}' /var/log/apache2/access.log
# Filter response code 4xx/5xx
awk '$9 >= 400 {print $1, $7, $9}' /var/log/apache2/access.log
# Filter dengan multiple kondisi
awk '($9 == 404 || $9 == 403) && $1 != "127.0.0.1"' /var/log/apache2/access.log
# Hitung jumlah request per IP
awk '{count[$1]++} END {for (ip in count) print ip, count[ip]}' \
/var/log/apache2/access.log | sort -rn | head -10
# Parse syslog - filter service tertentu
awk '$5 == "sshd:" {print $0}' /var/log/syslog
# Format output dengan printf
awk '{printf "%-15s -> %-50s [%s]\n", $1, $7, $9}' /var/log/apache2/access.log
# Custom delimiter - CSV parsing
awk -F, '{print $2, $5}' /var/log/firewall.csv
# Hitung rata-rata response time
awk '{total += $NF; count++} END {print "Average:", total/count}' response_times.log
# Conditional counter
awk '{
if ($9 ~ /^5/) server_errors++
else if ($9 ~ /^4/) client_errors++
else if ($9 ~ /^2/) success++
} END {
print "2xx:", success, "4xx:", client_errors, "5xx:", server_errors
}' /var/log/apache2/access.log
sed - Stream Editor untuk Transformasi Log
# Anonimisasi IP address
sed -E 's/[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}/XXX.XXX.XXX.XXX/g' \
access.log > access_anonymized.log
# Ekstrak hanya tanggal dan IP
sed -n 's/^\([0-9.]*\).*\[\([^]]*\)\].*/\1 \2/p' access.log
# Hapus baris dengan status code tertentu
sed '/" 404 /d' access.log > no_404.log
# Ubah format timestamp
sed -E 's|\[([0-9]{2})/([A-Za-z]{3})/([0-9]{4})|\3-\2-\1|g' access.log
# Multiple transformasi
sed -e 's/sshd/SSHD/g' -e 's/ssh/SSH/g' /var/log/auth.log
# Hapus baris komentar dan kosong
sed -E '/^#|^$/d' /etc/logrotate.conf
# Tambahkan prefix baris (nomor urut)
sed = access.log | sed 'N;s/\n/\t/'
cut - Parsing Kolom Sederhana
# Ekstrak IP dari Apache log (kolom 1)
cut -d' ' -f1 /var/log/apache2/access.log | sort | uniq -c | sort -rn
# Ekstrak tanggal dari syslog (kolom 1-3)
cut -d' ' -f1-3 /var/log/syslog | sort | uniq -c
# CSV - ambil timestamp dan action
cut -d, -f1,5 firewall.csv | head -20
# Range karakter - ekstrak jam dari timestamp
cut -c12-19 /var/log/syslog | sort | uniq -c
# Multiple fields dengan delimiter berbeda
# Gabung cut dan paste
cut -d' ' -f1,7 access.log | sort | uniq -c | sort -rn | head -10
Pipeline Lengkap: Ekstrak IP Attacker
# Full pipeline: auth.log -> IP attacker -> report
grep "Failed password" /var/log/auth.log | \
awk '{print $(NF-3)}' | \
sort | uniq -c | sort -rn | \
awk '$1 >= 5 {printf "%-4s attempts\t%-15s [ALERT]\n", $1, $2; count++} \
$1 < 5 {printf "%-4s attempts\t%-15s\n", $1, $2}' \
END {print "Total unique attackers:", count}'
3. Parsing dengan Python
Ekstrak IP dan Deteksi Brute Force
#!/usr/bin/env python3
# parse-auth-log.py
# Ekstrak IP attacker dari auth.log dan deteksi brute force
import re
import sys
from collections import Counter, defaultdict
from datetime import datetime, timedelta
class AuthLogParser:
"""Parser untuk /var/log/auth.log - deteksi brute force SSH."""
# Pattern untuk failed password
FAILED_PATTERN = re.compile(
r"Failed password for (.*?) from (\d+\.\d+\.\d+\.\d+)"
)
# Pattern untuk success login
SUCCESS_PATTERN = re.compile(
r"Accepted password for (.*?) from (\d+\.\d+\.\d+\.\d+)"
)
def __init__(self, log_file="/var/log/auth.log"):
self.log_file = log_file
self.failed_attempts = [] # List of (timestamp, username, ip)
self.success_attempts = []
def parse(self):
"""Parse seluruh file auth.log."""
try:
with open(self.log_file, "r") as f:
for line in f:
# Parse failed attempts
match = self.FAILED_PATTERN.search(line)
if match:
username, ip = match.groups()
# Ekstrak timestamp dari syslog
ts_str = " ".join(line.split()[:3])
self.failed_attempts.append((ts_str, username, ip))
# Parse success attempts
match = self.SUCCESS_PATTERN.search(line)
if match:
username, ip = match.groups()
ts_str = " ".join(line.split()[:3])
self.success_attempts.append((ts_str, username, ip))
except FileNotFoundError:
print(f"[ERROR] File {self.log_file} tidak ditemukan")
sys.exit(1)
return self
def detect_brute_force(self, threshold=5):
"""Deteksi IP dengan failed attempts di atas threshold."""
ip_counter = Counter([ip for _, _, ip in self.failed_attempts])
attackers = [
{"ip": ip, "attempts": count}
for ip, count in ip_counter.items()
if count >= threshold
]
attackers.sort(key=lambda x: x["attempts"], reverse=True)
return attackers
def detect_credential_stuffing(self, threshold=3):
"""Deteksi username yang jadi target credential stuffing."""
username_counter = Counter([
user for _, user, _ in self.failed_attempts
])
targets = [
{"username": user, "attempts": count}
for user, count in username_counter.items()
if count >= threshold
]
targets.sort(key=lambda x: x["attempts"], reverse=True)
return targets
def generate_report(self):
"""Generate laporan lengkap."""
attackers = self.detect_brute_force(threshold=5)
targets = self.detect_credential_stuffing(threshold=3)
print("=" * 60)
print(" AUTH LOG ANALYSIS REPORT")
print(f" File: {self.log_file}")
print(f" Total failed: {len(self.failed_attempts)}")
print(f" Total success: {len(self.success_attempts)}")
print("=" * 60)
print("\n[TOP ATTACKERS - IP dengan failed login terbanyak]")
print(f"{'ATTEMPTS':<10} {'IP':<20}")
print("-" * 30)
for a in attackers[:10]:
print(f"{a['attempts']:<10} {a['ip']:<20}")
print("\n[TOP TARGET USERNAMES]")
print(f"{'ATTEMPTS':<10} {'USERNAME':<20}")
print("-" * 30)
for t in targets[:10]:
print(f"{t['attempts']:<10} {t['username']:<20}")
return {"attackers": attackers, "targets": targets}
# Run
if __name__ == "__main__":
parser = AuthLogParser("/var/log/auth.log")
parser.parse()
parser.generate_report()
Parse Apache Log - Aggregasi Error per URL
#!/usr/bin/env python3
# parse-apache-log.py
# Parse Apache combined log format - deteksi error, scanner, bot
import re
from collections import Counter, defaultdict
class ApacheLogParser:
"""Parser untuk Apache/Nginx combined log format."""
# Regex untuk combined log format
LOG_PATTERN = re.compile(
r'(\S+) (\S+) (\S+) \[([^]]+)\] '
r'"(\S+) (\S+) (\S+)" (\d{3}) (\S+) '
r'"([^"]*)" "([^"]*)"'
)
# Daftar bot/user-agent mencurigakan
SUSPICIOUS_BOTS = [
"mj12bot", "ahrefsbot", "semrushbot",
"zgrab", "masscan", "nmap", "python-requests"
]
def __init__(self, log_file):
self.log_file = log_file
self.entries = []
self.errors_4xx = Counter()
self.errors_5xx = Counter()
self.ip_requests = Counter()
self.path_requests = Counter()
self.user_agents = Counter()
def parse(self):
"""Parse log file per line."""
try:
with open(self.log_file, "r") as f:
for line in f:
match = self.LOG_PATTERN.match(line)
if not match:
continue
ip, ident, auth, timestamp, method, path, \
protocol, status, size, referer, ua = match.groups()
entry = {
"ip": ip,
"timestamp": timestamp,
"method": method,
"path": path,
"protocol": protocol,
"status": int(status),
"size": size,
"referer": referer,
"user_agent": ua
}
self.entries.append(entry)
# Counters
self.ip_requests[ip] += 1
self.path_requests[path] += 1
self.user_agents[ua] += 1
if entry["status"] >= 500:
self.errors_5xx[path] += 1
elif entry["status"] >= 400:
self.errors_4xx[path] += 1
except FileNotFoundError:
print(f"[ERROR] File {self.log_file} tidak ditemukan")
return self
def detect_scanners(self, request_threshold=100):
"""Deteksi IP yang melakukan scanning (banyak request ke path berbeda)."""
ip_paths = defaultdict(set)
for entry in self.entries:
ip_paths[entry["ip"]].add(entry["path"])
scanners = [
{"ip": ip, "unique_paths": len(paths), "total_requests": self.ip_requests[ip]}
for ip, paths in ip_paths.items()
if len(paths) >= request_threshold
]
scanners.sort(key=lambda x: x["unique_paths"], reverse=True)
return scanners
def detect_attack_patterns(self):
"""Deteksi pola serangan dari path request."""
attack_patterns = {
"sql_injection": Counter(),
"xss": Counter(),
"path_traversal": Counter(),
"lfi": Counter()
}
for entry in self.entries:
path = entry["path"].lower()
if any(kw in path for kw in ["select", "union", "insert", "drop", "--"]):
attack_patterns["sql_injection"][entry["ip"]] += 1
if any(kw in path for kw in ["script", "onerror", "onload", "alert("]):
attack_patterns["xss"][entry["ip"]] += 1
if ".." in path or "etc/passwd" in path:
attack_patterns["path_traversal"][entry["ip"]] += 1
if "include" in path or "file=" in path:
attack_patterns["lfi"][entry["ip"]] += 1
return attack_patterns
def generate_report(self):
"""Generate analisis report."""
scanners = self.detect_scanners(100)
attacks = self.detect_attack_patterns()
print("=" * 60)
print(" APACHE LOG ANALYSIS REPORT")
print(f" File: {self.log_file}")
print(f" Total requests: {len(self.entries)}")
print(f" Unique IPs: {len(self.ip_requests)}")
print("=" * 60)
print("\n[ERROR 4xx - Top Paths]")
for path, count in self.errors_4xx.most_common(10):
print(f" {count:<6} {path}")
print("\n[ERROR 5xx - Top Paths]")
for path, count in self.errors_5xx.most_common(5):
print(f" {count:<6} {path}")
print("\n[SCANNERS - IP dengan banyak path unik]")
for s in scanners[:5]:
print(f" {s['ip']:<20} {s['unique_paths']:<5} paths "
f"[{s['total_requests']} requests]")
print("\n[ATTACK PATTERNS]")
for attack_type, ips in attacks.items():
if ips:
total = sum(ips.values())
print(f" {attack_type.replace('_', ' ').title()}: {total} attempts "
f"dari {len(ips)} IPs")
# Run
if __name__ == "__main__":
parser = ApacheLogParser("/var/log/apache2/access.log")
parser.parse()
parser.generate_report()
Parse JSON Log - Timeline & Aggregasi
#!/usr/bin/env python3
# parse-json-log.py
# Parse JSON structured log untuk analisis timeline
import json
from datetime import datetime
from collections import Counter, defaultdict
class JSONLogParser:
"""Parser untuk log format JSON (modern app logs)."""
def __init__(self, log_file):
self.log_file = log_file
self.entries = []
self.by_level = Counter()
self.by_logger = Counter()
self.by_hour = Counter()
self.errors_by_message = Counter()
def parse(self):
"""Parse JSON lines (setiap baris adalah JSON object)."""
try:
with open(self.log_file, "r") as f:
for line in f:
line = line.strip()
if not line:
continue
try:
entry = json.loads(line)
self.entries.append(entry)
# Aggregasi
level = entry.get("level", "UNKNOWN")
self.by_level[level] += 1
logger = entry.get("logger", "unknown")
self.by_logger[logger] += 1
# Aggregasi per jam
ts = entry.get("timestamp", "")
if ts:
try:
hour = ts[:13] # "2024-07-22T12"
self.by_hour[hour] += 1
except (IndexError, ValueError):
pass
# Error messages
if level in ("ERROR", "CRITICAL", "FATAL"):
msg = entry.get("message", "")
self.errors_by_message[msg] += 1
except json.JSONDecodeError:
print(f"[WARN] Baris bukan JSON valid: {line[:80]}")
except FileNotFoundError:
print(f"[ERROR] File {self.log_file} tidak ditemukan")
return self
def timeline(self):
"""Generate timeline aktivitas per jam."""
print("[TIMELINE - Requests per Hour]")
print(f"{'HOUR':<20} {'COUNT':<8} {'BAR'}")
print("-" * 60)
max_count = max(self.by_hour.values()) if self.by_hour else 1
for hour in sorted(self.by_hour.keys())[-24:]: # Last 24 hours
count = self.by_hour[hour]
bar_length = int(count / max_count * 40)
bar = "█" * bar_length
print(f"{hour:<20} {count:<8} {bar}")
def top_errors(self, limit=10):
"""Tampilkan error paling sering."""
print(f"\n[TOP {limit} ERRORS]")
print(f"{'COUNT':<8} {'MESSAGE'}")
print("-" * 60)
for msg, count in self.errors_by_message.most_common(limit):
# Truncate message for display
short_msg = msg[:80] + "..." if len(msg) > 80 else msg
print(f"{count:<8} {short_msg}")
def correlation(self, minute_window=5):
"""Cari korelasi temporal antar error."""
error_entries = [
e for e in self.entries
if e.get("level") in ("ERROR", "CRITICAL")
]
# Group errors yang terjadi dalam window yang sama
from itertools import combinations
correlations = []
for i, e1 in enumerate(error_entries):
for e2 in error_entries[i+1:]:
ts1 = e1.get("timestamp", "")
ts2 = e2.get("timestamp", "")
if ts1 and ts2:
try:
dt1 = datetime.fromisoformat(ts1.replace("Z", "+00:00"))
dt2 = datetime.fromisoformat(ts2.replace("Z", "+00:00"))
diff = abs((dt1 - dt2).total_seconds())
if diff <= minute_window * 60:
correlations.append({
"window_minutes": minute_window,
"error1": e1.get("message", "")[:60],
"error2": e2.get("message", "")[:60],
"diff_seconds": diff
})
except (ValueError, TypeError):
pass
if correlations:
print(f"\n[CORRELATION - Errors dalam {minute_window} menit]")
print(f"Ditemukan {len(correlations)} pasang error berkorelasi")
for corr in correlations[:5]:
print(f" [{corr['diff_seconds']:.0f}s gap]")
print(f" A: {corr['error1']}")
print(f" B: {corr['error2']}")
# Run
if __name__ == "__main__":
parser = JSONLogParser("/var/log/app/application.log")
parser.parse()
parser.timeline()
parser.top_errors(10)
parser.correlation(5)
4. logrotate - Manajemen Rotasi Log
Konfigurasi Dasar
# /etc/logrotate.conf
# Konfigurasi global
# Rotasi mingguan
weekly
# Simpan 4 minggu
rotate 4
# Buat file log baru setelah rotasi
create
# Kompres log lama
compress
# Jangan error jika file tidak ada
missingok
# Jangan rotate jika file kosong
notifempty
# Sertakan konfigurasi per-service
include /etc/logrotate.d
Konfigurasi Per-Service
# /etc/logrotate.d/auth
/var/log/auth.log {
weekly
rotate 12
compress
delaycompress # Jangan kompres file paling baru
missingok
notifempty
postrotate
# Reload syslog setelah rotasi
systemctl reload rsyslog > /dev/null 2>&1 || true
endscript
}
# /etc/logrotate.d/apache2
/var/log/apache2/*.log {
daily # Rotasi setiap hari
rotate 30 # Simpan 30 hari
compress
delaycompress
missingok
notifempty
create 640 www-data adm
sharedscripts
postrotate
if /etc/init.d/apache2 status > /dev/null; then
/etc/init.d/apache2 reload > /dev/null
fi
endscript
}
Konfigurasi Security-Focused
# /etc/logrotate.d/security-logs
/var/log/security/*.log {
daily
rotate 365 # Simpan 1 tahun untuk compliance
compress
delaycompress
missingok
notifempty
dateext # Tambahkan tanggal di filename
dateformat -%Y%m%d
extension .log
create 640 security security
sharedscripts
prerotate
# Hitung hash sebelum rotasi untuk integritas
sha256sum "$1" > "$1.sha256"
endscript
postrotate
# Notifikasi ke SIEM
curl -s -X POST -H "Content-Type: application/json" \
-d "{\"text\":\"Log rotated: $1\"}" \
https://hooks.slack.com/services/xxx/xxx/xxx
endscript
}
Test logrotate
# Dry run - lihat apa yang akan dirotasi
sudo logrotate -d /etc/logrotate.conf
# Force rotate semua
sudo logrotate -f /etc/logrotate.conf
# Force rotate service tertentu
sudo logrotate -f /etc/logrotate.d/auth
# Verbose mode
sudo logrotate -v /etc/logrotate.d/apache2
# Cek kapan terakhir rotasi
cat /var/lib/logrotate/status | grep auth
5. ELK Stack - Elasticsearch, Logstash, Kibana
Arsitektur ELK
Log Sources (Server, Firewall, App)
│
▼
Filebeat / Logstash ──► Elasticsearch ──► Kibana
(Ship logs) (Store & Index) (Visualize)
Filebeat - Log Shipper Ringan
# /etc/filebeat/filebeat.yml
filebeat.inputs:
- type: log
enabled: true
paths:
- /var/log/auth.log
- /var/log/syslog
fields:
service: linux-syslog
fields_under_root: true
- type: log
enabled: true
paths:
- /var/log/apache2/access.log
- /var/log/apache2/error.log
fields:
service: apache-web
fields_under_root: true
# Output ke Elasticsearch
output.elasticsearch:
hosts: ["https://elasticsearch.company.com:9200"]
username: "filebeat_user"
password: "${ES_PASSWORD}"
ssl.verification_mode: "none"
# Atau output ke Logstash dulu
# output.logstash:
# hosts: ["logstash.company.com:5044"]
Logstash - Pipeline Processing
# /etc/logstash/conf.d/auth-log.conf
input {
beats {
port => 5044
ssl => true
ssl_certificate => "/etc/logstash/certs/logstash.crt"
ssl_key => "/etc/logstash/certs/logstash.key"
}
}
filter {
# Parse auth.log dengan grok
if [service] == "linux-syslog" {
grok {
match => { "message" => [
# Failed password
"%{SYSLOGTIMESTAMP:timestamp} %{SYSLOGHOST:hostname} sshd\[%{NUMBER:pid}\]: %{DATA:auth_event} for %{DATA:user} from %{IP:src_ip} port %{NUMBER:port} ssh2",
# Accepted password
"%{SYSLOGTIMESTAMP:timestamp} %{SYSLOGHOST:hostname} sshd\[%{NUMBER:pid}\]: Accepted %{DATA:auth_method} for %{DATA:user} from %{IP:src_ip} port %{NUMBER:port} ssh2"
]}
}
# GeoIP enrichment
geoip {
source => "src_ip"
target => "geo"
}
# Add threat intelligence tag
if [auth_event] == "Failed password" {
mutate {
add_tag => ["brute_force_attempt"]
}
}
}
}
output {
elasticsearch {
hosts => ["localhost:9200"]
index => "security-logs-%{+YYYY.MM.dd}"
user => "elastic"
password => "${ES_PASSWORD}"
}
}
Kibana - Visualisasi & Dashboard
# Contoh query KQL (Kibana Query Language) untuk deteksi
# Cari semua failed SSH
auth_event: "Failed password"
# Cari dari IP spesifik
src_ip: 10.0.0.5
# Brute force - lebih dari 10 failed dalam 5 menit
# (Gunakan threshold alert di Kibana)
auth_event: "Failed password" AND tags: "brute_force_attempt"
# Cari IP dengan GeoIP tertentu
geo.country_name: "CN" OR geo.country_name: "RU"
# Aggregasi per IP
# Visualize -> Vertical Bar -> Buckets: Terms (src_ip) -> Metrics: Count
ELK Stack Quick Start
# Docker Compose untuk ELK
version: '3'
services:
elasticsearch:
image: docker.elastic.co/elasticsearch/elasticsearch:8.12.0
environment:
- discovery.type=single-node
- xpack.security.enabled=false
- "ES_JAVA_OPTS=-Xms1g -Xmx1g"
ports:
- "9200:9200"
volumes:
- es_data:/usr/share/elasticsearch/data
logstash:
image: docker.elastic.co/logstash/logstash:8.12.0
volumes:
- ./logstash.conf:/usr/share/logstash/pipeline/logstash.conf
ports:
- "5044:5044"
depends_on:
- elasticsearch
kibana:
image: docker.elastic.co/kibana/kibana:8.12.0
ports:
- "5601:5601"
environment:
- ELASTICSEARCH_HOSTS=http://elasticsearch:9200
depends_on:
- elasticsearch
volumes:
es_data:
6. Loki + Grafana - Logging Ringan & Visualisasi
Loki adalah alternatif ELK yang lebih ringan - tanpa full-text indexing, menggunakan label-based approach.
Arsitektur Loki
Log Sources
│
▼
Promtail ──► Loki ──► Grafana
(Ship) (Store) (Query & Visualize)
Promtail - Log Shipper untuk Loki
# /etc/promtail/config.yml
server:
http_listen_port: 9080
grpc_listen_port: 0
positions:
filename: /tmp/positions.yaml
clients:
- url: http://loki.company.com:3100/loki/api/v1/push
scrape_configs:
- job_name: system-auth
static_configs:
- targets: [localhost]
labels:
job: authlog
host: server01
__path__: /var/log/auth.log
- job_name: apache-access
static_configs:
- targets: [localhost]
labels:
job: apache
host: server01
__path__: /var/log/apache2/access.log
- job_name: security-logs
pipeline_stages:
- json:
expressions:
level: level
source_ip: source_ip
message: message
static_configs:
- targets: [localhost]
labels:
job: security-app
host: server01
__path__: /var/log/security/*.json
Loki - Query LogQL untuk Security
# LogQL - Bahasa Query Loki
# Semua log dari job authlog
{job="authlog"}
# Filter baris yang mengandung "Failed password"
{job="authlog"} |= "Failed password"
# Filter IP spesifik
{job="authlog"} |= "Failed password" |= "10.0.0.5"
# Regex pattern
{job="apache"} |~ "status (4[0-9]{2}|5[0-9]{2})"
# Exclude
{job="apache"} != "healthcheck"
# Parsing JSON
{job="security-app"} | json | level = "ERROR"
# Aggregasi - count per IP (over time)
sum by (source_ip) (
count_over_time({job="authlog"} |= "Failed password" [1h])
)
# Rate - request per second
rate({job="apache"} |~ "(4[0-9]{2}|5[0-9]{2})" [5m])
# Top 10 IP dengan most errors
topk(10, sum by (source_ip) (
count_over_time({job="authlog"} |= "Failed password" [24h])
))
# Timeline - error count per jam
sum by (level) (
count_over_time({job="security-app"} | json | level =~ "ERROR|CRITICAL" [1h])
)
Grafana Dashboard - Security Overview
{
"title": "Security Dashboard",
"panels": [
{
"title": "Failed SSH Attempts per Hour",
"type": "timeseries",
"datasource": "Loki",
"targets": [{
"expr": "sum by (host) (rate({job=\"authlog\"} |= \"Failed password\" [5m]))",
"legendFormat": "{{host}}"
}]
},
{
"title": "Top Attacker IPs (24h)",
"type": "barchart",
"datasource": "Loki",
"targets": [{
"expr": "topk(10, sum by (source_ip) (count_over_time({job=\"authlog\"} |= \"Failed password\" [24h])))",
"legendFormat": "{{source_ip}}"
}]
},
{
"title": "HTTP Error Rate (4xx/5xx)",
"type": "stat",
"datasource": "Loki",
"targets": [{
"expr": "sum(rate({job=\"apache\"} |~ \"(4[0-9]{2}|5[0-9]{2})\" [5m]))"
}]
}
]
}
Docker Compose - Loki + Grafana
version: '3'
services:
loki:
image: grafana/loki:2.9.0
ports:
- "3100:3100"
command: -config.file=/etc/loki/local-config.yaml
volumes:
- loki_data:/loki
promtail:
image: grafana/promtail:2.9.0
volumes:
- /var/log:/var/log
- ./promtail-config.yml:/etc/promtail/config.yml
command: -config.file=/etc/promtail/config.yml
grafana:
image: grafana/grafana:10.2.0
environment:
- GF_SECURITY_ADMIN_PASSWORD=admin
ports:
- "3000:3000"
volumes:
- grafana_data:/var/lib/grafana
volumes:
loki_data:
grafana_data:
7. Contoh Implementasi Nyata
Pipeline Log Processing Lengkap
#!/bin/bash
# log-processing-pipeline.sh
# Pipeline lengkap: collect -> parse -> analyze -> alert -> archive
set -euo pipefail
# Konfigurasi
LOG_DIR="/var/log"
REPORT_DIR="/reports/$(date +%Y%m%d)"
ARCHIVE_DIR="/archive/logs"
THRESHOLD_BRUTE_FORCE=10
THRESHOLD_SCANNER=100
WEBHOOK_URL="${WEBHOOK_URL:-https://hooks.slack.com/services/xxx}"
mkdir -p "$REPORT_DIR" "$ARCHIVE_DIR"
echo "[PIPELINE] Log Processing Pipeline - $(date)"
echo "==========================================="
# Phase 1: Collect - kumpulkan log terbaru
echo "[PHASE 1] Mengumpulkan log..."
cp "$LOG_DIR/auth.log" "$REPORT_DIR/auth.log"
cp "$LOG_DIR/apache2/access.log" "$REPORT_DIR/access.log"
echo "[PHASE 1] Selesai: $(ls -lh $REPORT_DIR/)"
# Phase 2: Parse - ekstrak informasi penting
echo "[PHASE 2] Parsing log..."
# Auth log - failed attempts
grep "Failed password" "$REPORT_DIR/auth.log" > "$REPORT_DIR/failed_attempts.txt"
echo " Failed attempts: $(wc -l < $REPORT_DIR/failed_attempts.txt)"
# Access log - 4xx/5xx errors
awk '$9 ~ /^[45]/ {print $1, $7, $9}' "$REPORT_DIR/access.log" > "$REPORT_DIR/http_errors.txt"
echo " HTTP errors: $(wc -l < $REPORT_DIR/http_errors.txt)"
# Phase 3: Analyze - deteksi anomali
echo "[PHASE 3] Analisis keamanan..."
# Brute force detection
echo " [BRUTE FORCE]" > "$REPORT_DIR/alerts.txt"
awk '{print $(NF-3)}' "$REPORT_DIR/failed_attempts.txt" | \
sort | uniq -c | sort -rn | \
awk -v t="$THRESHOLD_BRUTE_FORCE" '$1 >= t {
print " ALERT: "$2" - "$1" attempts"}' >> "$REPORT_DIR/alerts.txt"
# Scanner detection
echo " [SCANNERS]" >> "$REPORT_DIR/alerts.txt"
awk '{print $1}' "$REPORT_DIR/access.log" | \
sort | uniq -c | sort -rn | \
awk -v t="$THRESHOLD_SCANNER" '$1 >= t {
print " ALERT: "$2" - "$1" requests"}' >> "$REPORT_DIR/alerts.txt"
cat "$REPORT_DIR/alerts.txt"
# Phase 4: Alert - kirim notifikasi jika ada anomali
echo "[PHASE 4] Notifikasi..."
if grep -q "ALERT" "$REPORT_DIR/alerts.txt" 2>/dev/null; then
ALERT_COUNT=$(grep -c "ALERT" "$REPORT_DIR/alerts.txt")
SUMMARY=$(grep "ALERT" "$REPORT_DIR/alerts.txt" | head -5)
curl -s -X POST -H "Content-Type: application/json" \
-d "{
\"text\": \"⚠️ *Security Alert - Log Analysis*\nFound: $ALERT_COUNT anomalies\n\`\`\`$SUMMARY\`\`\`\"
}" "$WEBHOOK_URL" > /dev/null
echo " Alert terkirim: $ALERT_COUNT anomali"
else
echo " Tidak ada anomali"
fi
# Phase 5: Archive - kompres dan simpan
echo "[PHASE 5] Arsip..."
tar -czf "$ARCHIVE_DIR/logs_$(date +%Y%m%d).tar.gz" -C "$REPORT_DIR" .
echo " Archive: $ARCHIVE_DIR/logs_$(date +%Y%m%d).tar.gz"
# Hapus report lebih dari 7 hari
find "$ARCHIVE_DIR" -name "*.tar.gz" -mtime +7 -delete
echo "==========================================="
echo "[DONE] Pipeline selesai: $(date)"
Deteksi Brute Force Real-Time dengan Python
#!/usr/bin/env python3
# brute-force-detector.py
# Deteksi brute force real-time dengan tail -f
import time
import re
import subprocess
from collections import defaultdict, deque
from datetime import datetime, timedelta
class RealTimeBruteForceDetector:
"""Deteksi brute force secara real-time dari log stream."""
def __init__(self, log_file="/var/log/auth.log", window_minutes=10, threshold=5):
self.log_file = log_file
self.window = timedelta(minutes=window_minutes)
self.threshold = threshold
# Ring buffer: IP -> deque of (timestamp, username)
self.attempts = defaultdict(lambda: deque(maxlen=100))
self.failed_pattern = re.compile(
r"Failed password for (\S+) from (\d+\.\d+\.\d+\.\d+)"
)
def follow(self):
"""Tail -f log file real-time."""
with subprocess.Popen(
["tail", "-f", self.log_file],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True
) as proc:
for line in proc.stdout:
self.process_line(line.strip())
def process_line(self, line):
"""Proses satu baris log."""
match = self.failed_pattern.search(line)
if not match:
return
username, ip = match.groups()
now = datetime.now()
# Simpan attempt
self.attempts[ip].append((now, username))
# Hapus attempt yang sudah kadaluarsa dari window
self._cleanup(ip, now)
# Cek threshold
if len(self.attempts[ip]) >= self.threshold:
self.alert(ip, username, len(self.attempts[ip]))
def _cleanup(self, ip, now):
"""Hapus attempt di luar window."""
cutoff = now - self.window
while self.attempts[ip] and self.attempts[ip][0][0] < cutoff:
self.attempts[ip].popleft()
def alert(self, ip, last_username, count):
"""Trigger alert saat threshold terlewati."""
print(
f"[{datetime.now().strftime('%H:%M:%S')}] "
f"🚨 BRUTE FORCE DETECTED!\n"
f" IP: {ip}\n"
f" Username: {last_username}\n"
f" Attempts: {count} in last {self.window.seconds // 60} minutes"
)
# Di production: kirim ke SIEM, block IP, notifikasi
# self.block_ip(ip)
# self.send_alert_to_siem(ip, count)
# Run
if __name__ == "__main__":
detector = RealTimeBruteForceDetector(
log_file="/var/log/auth.log",
window_minutes=10,
threshold=5
)
print("[DETECTOR] Monitoring brute force... (Ctrl+C to stop)")
try:
detector.follow()
except KeyboardInterrupt:
print("\n[DETECTOR] Stopped")
8. Best Practices
Log Management Checklist
- Centralized logging - jangan simpan log hanya di lokal
- Rotasi log - gunakan logrotate, jangan sampai hardisk penuh
- Retention policy - tentukan berapa lama log disimpan (90 hari minimum)
- Format standar - prefer JSON, memudahkan parsing otomatis
- Timestamp UTC - konsisten, hindari zona waktu berbeda
- Checksum - verifikasi integritas log untuk forensik
- Encryption - enkripsi log saat transit dan saat istirahat
- Access control - log hanya bisa dibaca oleh authorized personnel
Tools Comparison
| Tool | Kelebihan | Kekurangan | Use Case |
|---|---|---|---|
| awk/sed/grep | Cepat, universal, tanpa setup | Tidak scalable untuk big data | Quick analysis, ad-hoc |
| Python | Fleksibel, banyak library | Butuh scripting | Custom pipeline, parsing kompleks |
| ELK Stack | Full-featured, powerful search | Berat, butuh resource besar | Enterprise SIEM, full-text search |
| Loki+Grafana | Ringan, cloud-native | Query kurang powerful untuk text | Cloud, microservices, Kubernetes |
| logrotate | Standar Linux, wajib | Hanya rotasi, tidak analisis | Log housekeeping |
Performance Tips
- Gunakan pipeline - jangan baca file besar berkali-kali
- Limit scope - filter dulu sebelum pipe ke awk/sed
- Index strategis - di ELK, index berdasarkan waktu untuk performance
- Batch processing - proses log per batch, bukan per baris
- Compression - kompres log lama, bisa hemat 80-90% space