Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 12 additions & 7 deletions alerts/webhook/notify.sh
Original file line number Diff line number Diff line change
Expand Up @@ -45,16 +45,21 @@ ALERT_ID="$(date +%s%N | cut -c1-13)"
# --- Output 1: Log to stdout (captured by Docker) ---
echo "[${TIMESTAMP}] ALERT | ${TITLE} | ${BODY}"

# --- Output 2: File-based bridge for host notifications ---
# Write alert to a file that the host-side watcher reads and triggers notify-send
# Escape double quotes for JSON safety
SAFE_ALERT_NAME=$(echo "$ALERT_NAME" | sed 's/"/\\"/g')
SAFE_SEVERITY=$(echo "$SEVERITY" | sed 's/"/\\"/g')
SAFE_DESCRIPTION=$(echo "$DESCRIPTION" | sed 's/"/\\"/g')
SAFE_TITLE=$(echo "$TITLE" | sed 's/"/\\"/g')
SAFE_BODY=$(echo "$BODY" | sed 's/"/\\"/g')

cat > "${ALERT_DIR}/${ALERT_ID}.json" <<EOF
{
"timestamp": "${TIMESTAMP}",
"alert_name": "${ALERT_NAME}",
"severity": "${SEVERITY}",
"description": "${DESCRIPTION}",
"title": "${TITLE}",
"body": "${BODY}"
"alert_name": "${SAFE_ALERT_NAME}",
"severity": "${SAFE_SEVERITY}",
"description": "${SAFE_DESCRIPTION}",
"title": "${SAFE_TITLE}",
"body": "${SAFE_BODY}"
}
EOF

Expand Down
23 changes: 23 additions & 0 deletions docker-compose.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,29 @@ services:
retries: 3
start_period: 10s

rsigma:
image: ghcr.io/timescale/rsigma:v0.19.0
restart: unless-stopped
mem_limit: 128m
memswap_limit: 128m
working_dir: /app
volumes:
- ./rules/sigma:/app/rules/sigma:ro
ports:
- "9090:9090"
command: [
"engine", "daemon",
"--config", "/app/rules/sigma/rsigma.yaml"
]
networks:
- localobserve
healthcheck:
test: ["CMD", "rsigma", "engine", "status", "--addr", "127.0.0.1:9090"]
interval: 30s
timeout: 5s
retries: 3
start_period: 10s

goflow2:
image: netsampler/goflow2:latest
restart: unless-stopped
Expand Down
7 changes: 7 additions & 0 deletions otel-collector-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,11 @@ processors:
- set(body, "") where IsMap(body)

exporters:
otlp_http/rsigma:
endpoint: http://rsigma:9090
tls:
insecure: true

otlp_http/openobserve_osquery:
endpoint: http://openobserve:5080/api/default
headers:
Expand Down Expand Up @@ -293,6 +298,7 @@ service:
- batch
exporters:
- otlp_http/openobserve_osquery
- otlp_http/rsigma

logs/falco:
receivers:
Expand All @@ -305,6 +311,7 @@ service:
- batch
exporters:
- otlp_http/openobserve_falco
- otlp_http/rsigma

logs/clamav:
receivers:
Expand Down
4 changes: 3 additions & 1 deletion rules/sigma/rsigma.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,14 @@ daemon:
api:
addr: "0.0.0.0:9090"
input:
source: stdin
source: http
format: json
output:
sinks:
- stdout
include_event: true
webhooks:
- /app/rules/sigma/webhooks/alert_receiver.yaml

eval:
rules: ./rules/sigma/active_rules
Expand Down
10 changes: 10 additions & 0 deletions rules/sigma/webhooks/alert_receiver.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
webhooks:
- id: localobserve-alerts
kind: detection
url: http://alert-receiver:9000/hooks/security-alert-open
body: |
{
"alert_name": "${detection.rule.title}",
"severity": "${detection.rule.level}",
"description": "Rule ID: ${detection.rule.id} triggered. Tags: ${detection.tags}"
}
49 changes: 49 additions & 0 deletions tests/test_alert_receiver_integration.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,3 +76,52 @@ def test_falco_payload_processing():
newest.unlink()
except OSError:
pass


def test_rsigma_payload_processing():
# Ensure alert dir exists
ALERT_DIR.mkdir(parents=True, exist_ok=True)

assert wait_for_stack(), "Alert receiver did not become reachable in time"

# Capture the files existing BEFORE we trigger the webhook
existing_files = set(ALERT_DIR.glob("*.json"))

url = f"{BASE_URL}/hooks/security-alert-open"
headers = {"Content-Type": "application/json"}
rsigma_payload = {
"alert_name": "Suspicious Namespace Unshare Command",
"severity": "high",
"description": "Rule ID: 718c5dbc-b1a3-419b-a329-e7721d294257 triggered. Tags: ['attack.t1059', 'attack.t1071']"
}

start = time.time()
r = requests.post(url, headers=headers, json=rsigma_payload, timeout=5)
assert r.status_code == 200, f"Unexpected response: {r.status_code} {r.text}"

# Wait for a new alert file to appear
deadline = time.time() + 10
newest = None
while time.time() < deadline:
current_files = set(ALERT_DIR.glob("*.json"))
new_files = current_files - existing_files
if new_files:
sorted_new = sorted(list(new_files), key=lambda p: p.stat().st_mtime, reverse=True)
candidate = sorted_new[0]
if candidate.stat().st_mtime >= start - 1:
newest = candidate
break
time.sleep(0.5)

assert newest is not None, "No alert file created by alert-receiver for RSigma"

data = json.loads(newest.read_text(encoding="utf-8"))
assert data.get("alert_name") == "Suspicious Namespace Unshare Command"
assert data.get("severity") == "high"
assert "Rule ID: 718c5dbc-b1a3-419b-a329-e7721d294257" in data.get("description", "")

# cleanup the alert file created by this test
try:
newest.unlink()
except OSError:
pass
4 changes: 2 additions & 2 deletions tests/test_infrastructure_health.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ def test_compose_has_healthchecks_for_critical_services() -> None:
compose = _load_compose()
services = compose.get("services", {})

critical_services = {"falco", "openobserve", "otel-collector", "alert-receiver"}
critical_services = {"falco", "openobserve", "otel-collector", "alert-receiver", "rsigma"}
for svc_name in critical_services:
svc = services.get(svc_name, {})
assert "healthcheck" in svc, (
Expand Down Expand Up @@ -71,7 +71,7 @@ def test_health_monitor_has_dead_mans_switch() -> None:
def test_health_monitor_checks_all_services() -> None:
"""Health monitor must check required services and account for optional scan services."""
script = (REPO_ROOT / "tools" / "health-monitor.sh").read_text()
for svc in ("falco", "openobserve", "otel-collector"):
for svc in ("falco", "openobserve", "otel-collector", "rsigma"):
assert svc in script, f"Health monitor should check {svc}"
for svc in ("clamav", "clamav-scanner"):
assert svc in script, f"Health monitor should mention optional service {svc}"
Expand Down
2 changes: 1 addition & 1 deletion tools/health-monitor.sh
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ DISK_WARN_PERCENT=85
DISK_CRIT_PERCENT=95
LOG_FILE="${DATA_DIR}/health/health-monitor.log"

SERVICES=("falco" "openobserve" "otel-collector")
SERVICES=("falco" "openobserve" "otel-collector" "rsigma")
OPTIONAL_SERVICES=("clamav" "clamav-scanner")

mkdir -p "$(dirname "$HEARTBEAT_FILE")"
Expand Down
Loading