Skip to content

Zeek integration - #11

Open
Dhar01 wants to merge 10 commits into
mainfrom
zeek_integration
Open

Zeek integration#11
Dhar01 wants to merge 10 commits into
mainfrom
zeek_integration

Conversation

@Dhar01

@Dhar01 Dhar01 commented Aug 3, 2026

Copy link
Copy Markdown
Collaborator

name: Pull request
about: Submit a pull request for hexwall
title: Integration of zeek to catch network connections
labels: 'new feature'
assignees: Loknath


Description

The previous version of hexwall was designed to fix the gap pi-hole introduced, but it still fails for alternate resolvers, connecting via CNAME alias. A shared CDN edge address legitimately serves thousands of unrelated domains, an attacker can ride an already trusted IP by setting the TLS SNI to their hostname.

This branch closes that gap by reading the SNI directly off the wire via Zeek, by checking domains against pi-hole's real policy lists rather than inferring from history.

Type of change

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Refactor or internal cleanup
  • Documentation update
  • Breaking change

How has this been tested?

List the commands you ran and any manual verification steps.

go test ./...
golangci-lint run ./...

If you skipped any checks, explain why.

  • go test ./...
  • golangci-lint run ./...
  • Manual runtime verification

Test Configuration:

  • OS name and version: Linux pop-os 7.0.11-76070011-generic
  • Go compiler version: go1.26.1 linux/amd64
  • somo version: 1.3.1
  • Pi-hole DB source: pihole-FTL.db, gravity.db.
  • Mode used (watch or enforce): both

Newly added: zeek version 8.2.1

Checklist

  • My code follows the style guidelines of this project
  • I have performed a self-review of my own code
  • I have made corresponding changes to the documentation
  • My changes generate no new lint or vet issues
  • I have updated or intentionally skipped tests with explanation above

Copilot AI lite review requested due to automatic review settings August 3, 2026 11:42
@Dhar01 Dhar01 self-assigned this Aug 3, 2026

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds a Zeek-based sensing path to hexwall so it can (a) correlate live connections with observed TLS SNI from ssl.log and (b) ingest Zeek DNS-bypass notices from notice.log, then evaluate those signals against Pi-hole policy (allow/deny/gravity) and the existing trust store.

Changes:

  • Introduces internal/zeek tailers for ssl.log (SNI correlation cache) and notice.log (DNS-bypass events).
  • Expands Pi-hole integration to include gravity/policy lookups and adds domain/SNI matching + new store tables for per-IP domain sets and observations.
  • Refactors the monitor into a “decision ladder”, adds domain-based deghost checks, and replaces somo-based enforcement with a stopgap internal/enforcer process terminator.

Reviewed changes

Copilot reviewed 22 out of 23 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
zeek-integration-guide.md Design/ops guide for Zeek DNS-bypass notice ingestion.
SNI_DETECTION_DESIGN.md Design notes for SNI mismatch detection and correlation strategy.
SETUP.md Local setup guide for Pi-hole + tool testing.
OVERVIEW.md High-level product overview and threat-model explanation.
main.go Adds flags, logging setup, Zeek watchers, pruning, and updated monitor wiring.
internal/zeek/zeek.go Implements ssl.log tailer + notice.log watcher + parsers.
internal/zeek/zeek_test.go Unit tests for Zeek log parsing/tailing behavior.
internal/store/store.go Adds schema/tables, migrations, pruning, and new domain/SNI/IP observation APIs.
internal/store/store_test.go Extensive tests for new store schema and behaviors.
internal/somo/somo.go Removes PID kill helper (enforcement moved elsewhere).
internal/pihole/policy_test.go Adds tests for gravity/policy lookup and counts.
internal/pihole/pihole.go Adds gravity.db support, policy lookups, and domain matching helper.
internal/pihole/domain_match_test.go Tests for DomainMatches.
internal/pihole/cache.go Stores full IP→domains sets (not single domain per IP) and writes new store table.
internal/monitor/monitor.go New decision ladder, Zeek event handler, domain policy gating, and enforcement path.
internal/enforcer/enforcer.go Adds stopgap process-based enforcement and PID parsing.
internal/enforcer/enforcer_test.go Tests for enforcer guardrails and PID parsing.
internal/deghost/deghost.go Adds domain reputation endpoint + block predicate.
internal/deghost/deghost_test.go Tests for domain reputation client + predicate.
CLAUDE.md Adds repo guidance doc (currently out of sync with new flow/enforcement).
CHAT_SUMMARY_2026-04-06.md Historical implementation summary (now partially out of date).
.golangci.yml Adds golangci-lint configuration.
.gitignore Adds local DB/inventory ignores.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread internal/zeek/zeek.go
Comment on lines +143 to +153
event := Event{Timestamp: time.Now()}
if ts, ok := payload["ts"].(string); ok {
if parsed, err := time.Parse(time.RFC3339Nano, ts); err == nil {
event.Timestamp = parsed
}
}
if ts, ok := payload["timestamp"].(string); ok {
if parsed, err := time.Parse(time.RFC3339Nano, ts); err == nil {
event.Timestamp = parsed
}
}
Comment on lines +85 to +90
selectedMode := normalizeMode(mode)
blocked, err := checker.IsBlockedByPolicy(event.SNI)
if err != nil {
slog.Warn("zeek policy lookup failed", "sni", event.SNI, "err", err)
return
}
Comment on lines +411 to +426
if err := hexwallStore.LogKill(ipStr, conn.PID, conn.Program); err != nil {
slog.Error("failed to log kill", "address", conn.RAddress, "err", err)
}

// Connection-level termination via `ss -K` (requires CONFIG_INET_DIAG_DESTROY)
// is deferred work. KillProcess terminates the owning process, which is coarser.
pidInt, err := enforcer.ParsePID(conn.PID)
if err != nil {
slog.Warn("invalid PID, skipping kill", "pid", conn.PID, "address", conn.RAddress, "err", err)
return
}
if err := enforcer.KillProcess(pidInt, conn.Program, conn.RAddress); err != nil {
slog.Warn("failed to kill process", "address", conn.RAddress, "pid", conn.PID, "err", err)
} else {
slog.Info("killed connection", "address", conn.RAddress)
}
Comment thread CLAUDE.md Outdated
Comment on lines +29 to +34
**Flow:**
1. `main.go` — entry point; runs `runCheck()` immediately, then on a 10s ticker
2. `runCheck()` calls `somo.GetEstablishedConnections()` to get live TCP/UDP connections
3. Each connection's remote IP is passed to `isVulnerable()` (currently a no-op stub)
4. Vulnerable connections are killed via `somo.KillConnection(pid)`

Comment thread CHAT_SUMMARY_2026-04-06.md Outdated
- `ShouldKill(report) == false` -> log as unrecognized but clean; allow.
- `ShouldKill(report) == true`:
- if mode is `watch`: log "would kill" only.
- if mode is `enforce`: `store.LogKill(...)` then `somo.KillConnection(pid)`.
Comment thread internal/zeek/zeek.go
Comment on lines +206 to +229
scanner := bufio.NewScanner(file)
for scanner.Scan() {
line := strings.TrimSpace(scanner.Text())
if line == "" {
continue
}

event, err := ParseNoticeLine(line)
if err != nil {
continue
}

select {
case events <- event:
default:
}
}
if err := scanner.Err(); err != nil {
slog.Debug("zeek: notice log scan failed", "path", logPath, "err", err)
}
}
_ = file.Close()
offset = fi.Size()
}
Dhar01 added 8 commits August 3, 2026 18:21
Adds a second trust signal alongside Pi-hole's IP history. Zeek reads the
TLS SNI off the wire, giving the domain a connection actually claims rather
than just the IP it dialed — closing the gap where a shared CDN edge IP is
trusted for one domain and abused for another.

- internal/zeek: new package. Tails ssl.log into a short-lived per-connection
  SNI cache keyed on local port, and watches notice.log for DNS-bypass alerts.
  Holds partial trailing lines across polls so records flushed mid-write are
  not silently dropped.
- internal/pihole: IsBlockedByPolicy checks Pi-hole's blocklist directly,
  independent of query history.
- internal/deghost: CheckDomain + ShouldBlockDomain for domain-shaped
  reputation lookups. Unlike the IP endpoint, 400/403 carry a real body and
  are decoded rather than treated as "safe".
- internal/store: domain_checks cache (6h), plus zeek_alerts and
  sni_observations audit tables.
- internal/monitor: SNI-bypass connections escalate through blocklist, then
  cache, then the domain API — never the IP reputation check, which reads
  clean on shared CDN addresses regardless of what is fronted through them.
- main.go: --zeek-log, --zeek-notice-log, --enable-zeek flags; flag parsing
  extracted into parseFlags/runConfig.

Zeek support defaults to off. Watch mode remains the default.
Copilot AI review requested due to automatic review settings August 3, 2026 12:23
@Dhar01
Dhar01 force-pushed the zeek_integration branch from 9f415f8 to 86ee303 Compare August 3, 2026 12:23

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 16 out of 17 changed files in this pull request and generated no new comments.

Suppressed comments (1)

internal/enforcer/enforcer.go:90

  • KillProcess treats any error from syscall.Kill(pid, 0) as meaning the process is gone, but kill(pid, 0) can also fail with EPERM when the process still exists (e.g., insufficient permissions). That would incorrectly report success and skip the SIGKILL escalation. Only ESRCH should be treated as “process no longer exists”; other errors should fall through to the escalation path (or be handled explicitly).
	// Check whether the process still exists.
	if err := syscall.Kill(pid, 0); err != nil {
		// Process is gone — SIGTERM worked.
		slog.Info("process terminated by SIGTERM", "pid", pid, "program", program)
		return nil
	}

@Dhar01
Dhar01 force-pushed the zeek_integration branch from d264331 to 6fe9c88 Compare August 4, 2026 07:50
Copilot AI review requested due to automatic review settings August 4, 2026 07:58

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 16 out of 17 changed files in this pull request and generated no new comments.

Suppressed comments (6)

internal/store/store.go:617

  • RecentZeekAlert queries by (src_ip, sni) using exact equality. Without normalizing inputs to match LogZeekAlert's stored canonical form, lookups can miss equivalent values that differ only in case/whitespace.
	cutoff := time.Now().Add(-time.Hour).Unix()

internal/monitor/monitor.go:413

  • applyBlockedOutcome logs a kill to killed_connections before validating the PID or attempting the termination. With the new enforcer guardrails (protected programs, invalid PID, own PID, etc.), this can record "killed" rows even when nothing was actually killed, reducing audit accuracy. Log the kill only after KillProcess succeeds (or rename/store intent separately).
	if err := hexwallStore.LogKill(ipStr, conn.PID, conn.Program); err != nil {
		slog.Error("failed to log kill", "address", conn.RAddress, "err", err)
	}

internal/monitor/monitor.go:83

  • HandleZeekEvent persists Zeek notices to zeek_alerts, but it only checks event.SNI. The zeek_alerts schema requires src_ip/dst_ip/dst_port/sni NOT NULL, so events missing any of these fields will trigger repeated insert errors and noisy logs. Normalize SNI before policy lookup/persist and skip events missing required fields.
	if event.SNI == "" {
		return
	}

internal/store/store.go:383

  • UpsertAllowedIPDomain lowercases/trims the domain but still allows an empty string, which can create meaningless rows and waste space (and complicate later matching/debugging). Reject empty domains explicitly.

This issue also appears on line 617 of the same file.

func (s *Store) UpsertAllowedIPDomain(ip, domain string) error {
	domain = strings.ToLower(strings.TrimSpace(domain))
	now := time.Now().Unix()

internal/zeek/zeek.go:206

  • WatchNoticeLog uses bufio.Scanner with the default 64K token limit; a long JSON notice line will cause scanner.Err() (typically ErrTooLong) and the event will be skipped. Increase the scanner buffer to avoid silently dropping legitimate Zeek notices.
				scanner := bufio.NewScanner(file)

internal/store/store.go:603

  • LogZeekAlert stores SNI and IP fields without normalization/validation. Because RecentZeekAlert matches on exact sni/src_ip and the table columns are NOT NULL, mixed-case or whitespace in SNI can prevent dedupe/lookups and missing fields will fail at INSERT time. Normalize/validate inputs before writing.
func (s *Store) LogZeekAlert(srcIP, dstIP, dstPort, sni string, blocked bool, confidence, actionTaken string) error {
	blockedInt := 0
	if blocked {
		blockedInt = 1
	}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants