Services: - ja4sentinel: TLS/JA4 fingerprint capture daemon (Go, libpcap) - logcorrelator: JA4 log correlation engine (Go, ClickHouse) - mod_reqin_log: Apache module (C, JSON request logging) - bot_detector: ML bot detection pipeline (Python) - dashboard: FastAPI/Streamlit analytics UI (Python) Shared libraries: - shared/go/ja4common: logger, config, shutdown, ipfilter (Go module) - shared/python/ja4_common: ClickHouseClient, ClickHouseSettings (Python package) - shared/clickhouse/: canonical SQL migrations (10 files) Build & packaging: - Unified 3-stage Dockerfile.package for Go RPMs (el8/el9/el10) - go.work workspace linking sentinel, correlator, ja4common - Makefile with test-all, build-all, rpm-* targets Fixes applied: - go.work: 1.21 → 1.24.6 (required by sentinel) - correlator Dockerfiles: golang:1.21 → golang:1.24 - replace directives in go.mod for ja4common local path - pyproject.toml: setuptools.backends → setuptools.build_meta - Removed static libpcap linking (unavailable on Rocky 9) - Fixed data races in output/writers_test.go (sync.Mutex + atomic.Int32) - Rewrote corrupted test files (logger_test.go × 2) Test coverage: - correlator: 67.1% total (unixsocket 80.5%, config 91.7%, app 83.3%, multi 87.7%, stdout 100%) - sentinel: all 10 packages pass (api, capture, config, fingerprint, ipfilter, logging, output, tlsparse) Documentation: - README.md + docs/ (architecture, development, 5 services, shared libs, DB schema & migrations) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
126 lines
4.2 KiB
Python
126 lines
4.2 KiB
Python
"""
|
|
Endpoint de recherche globale rapide — utilisé par la barre Cmd+K
|
|
"""
|
|
from fastapi import APIRouter, Query
|
|
from ..database import db
|
|
|
|
router = APIRouter(prefix="/api/search", tags=["search"])
|
|
|
|
IP_RE = r"^(\d{1,3}\.){0,3}\d{1,3}$"
|
|
|
|
|
|
@router.get("/quick")
|
|
async def quick_search(q: str = Query(..., min_length=1, max_length=100)):
|
|
"""
|
|
Recherche unifiée sur IPs, JA4, ASN, hosts.
|
|
Retourne jusqu'à 5 résultats par catégorie.
|
|
"""
|
|
q = q.strip()
|
|
pattern = f"%{q}%"
|
|
results = []
|
|
|
|
# ── IPs ──────────────────────────────────────────────────────────────────
|
|
ip_rows = db.query(
|
|
"""
|
|
SELECT
|
|
replaceRegexpAll(toString(src_ip), '^::ffff:', '') AS clean_ip,
|
|
count() AS hits,
|
|
max(detected_at) AS last_seen,
|
|
any(threat_level) AS threat_level
|
|
FROM ml_detected_anomalies
|
|
WHERE ilike(toString(src_ip), %(p)s)
|
|
AND detected_at >= now() - INTERVAL 24 HOUR
|
|
GROUP BY clean_ip
|
|
ORDER BY hits DESC
|
|
""",
|
|
{"p": pattern},
|
|
)
|
|
for r in ip_rows.result_rows:
|
|
ip = str(r[0])
|
|
results.append({
|
|
"type": "ip",
|
|
"value": ip,
|
|
"label": ip,
|
|
"meta": f"{r[1]} détections · {r[3]}",
|
|
"url": f"/detections/ip/{ip}",
|
|
"investigation_url": f"/investigation/{ip}",
|
|
})
|
|
|
|
# ── JA4 fingerprints ─────────────────────────────────────────────────────
|
|
ja4_rows = db.query(
|
|
"""
|
|
SELECT
|
|
ja4,
|
|
count() AS hits,
|
|
uniq(src_ip) AS unique_ips
|
|
FROM ml_detected_anomalies
|
|
WHERE ilike(ja4, %(p)s)
|
|
AND ja4 != ''
|
|
AND detected_at >= now() - INTERVAL 24 HOUR
|
|
GROUP BY ja4
|
|
ORDER BY hits DESC
|
|
""",
|
|
{"p": pattern},
|
|
)
|
|
for r in ja4_rows.result_rows:
|
|
results.append({
|
|
"type": "ja4",
|
|
"value": str(r[0]),
|
|
"label": str(r[0]),
|
|
"meta": f"{r[1]} détections · {r[2]} IPs",
|
|
"url": f"/investigation/ja4/{r[0]}",
|
|
})
|
|
|
|
# ── Hosts ─────────────────────────────────────────────────────────────────
|
|
host_rows = db.query(
|
|
"""
|
|
SELECT
|
|
host,
|
|
count() AS hits,
|
|
uniq(src_ip) AS unique_ips
|
|
FROM ml_detected_anomalies
|
|
WHERE ilike(host, %(p)s)
|
|
AND host != ''
|
|
AND detected_at >= now() - INTERVAL 24 HOUR
|
|
GROUP BY host
|
|
ORDER BY hits DESC
|
|
""",
|
|
{"p": pattern},
|
|
)
|
|
for r in host_rows.result_rows:
|
|
results.append({
|
|
"type": "host",
|
|
"value": str(r[0]),
|
|
"label": str(r[0]),
|
|
"meta": f"{r[1]} hits · {r[2]} IPs",
|
|
"url": f"/detections?search={r[0]}",
|
|
})
|
|
|
|
# ── ASN ───────────────────────────────────────────────────────────────────
|
|
asn_rows = db.query(
|
|
"""
|
|
SELECT
|
|
asn_org,
|
|
asn_number,
|
|
count() AS hits,
|
|
uniq(src_ip) AS unique_ips
|
|
FROM ml_detected_anomalies
|
|
WHERE (ilike(asn_org, %(p)s) OR ilike(asn_number, %(p)s))
|
|
AND asn_org != '' AND asn_number != ''
|
|
AND detected_at >= now() - INTERVAL 24 HOUR
|
|
GROUP BY asn_org, asn_number
|
|
ORDER BY hits DESC
|
|
""",
|
|
{"p": pattern},
|
|
)
|
|
for r in asn_rows.result_rows:
|
|
results.append({
|
|
"type": "asn",
|
|
"value": str(r[1]),
|
|
"label": f"AS{r[1]} — {r[0]}",
|
|
"meta": f"{r[2]} hits · {r[3]} IPs",
|
|
"url": f"/detections?asn={r[1]}",
|
|
})
|
|
|
|
return {"query": q, "results": results}
|