No description
  • Go 99.3%
  • Shell 0.6%
  • Dockerfile 0.1%
Find a file
2026-03-31 21:49:31 -06:00
.github/workflows Fix GHA workflows: set CGO_ENABLED=0 globally 2026-03-26 22:01:49 +00:00
cmd/trapline Fix resource leak by ensuring watcher is closed properly and update SSH_CONNECTION environment variable formatting in tests 2026-03-31 09:12:10 -06:00
deploy Add Ansible playbook files for Trapline configuration and management 2026-03-31 21:49:31 -06:00
dev Fix playground duplicate findings: bump cooldown to 1h 2026-03-26 22:33:21 +00:00
e2e Add exhaustive documentation to every package and module 2026-03-26 21:21:06 +00:00
internal Fix resource leak by ensuring watcher is closed properly and update SSH_CONNECTION environment variable formatting in tests 2026-03-31 09:12:10 -06:00
pkg/finding Document pkg/finding - the universal data model 2026-03-26 21:22:30 +00:00
.gitignore Fix 8 critical issues from security audit 2026-03-26 20:22:45 +00:00
.goreleaser.yml Add installer script and update README with installation instructions 2026-03-30 10:52:22 -06:00
CLAUDE.md Many fixes 2026-03-30 21:43:42 -06:00
go.mod Many fixes 2026-03-30 21:43:42 -06:00
go.sum Add inotify file watching, benchmarks, dashboard server, deploy config 2026-03-26 20:56:26 +00:00
install.sh Many fixes 2026-03-30 21:43:42 -06:00
mise.toml Many fixes 2026-03-30 21:43:42 -06:00
README.md Implement SSH session monitoring with allowed users whitelist and add tests 2026-03-30 21:47:17 -06:00
trapline.png Replace idea.md with comprehensive README, add logo 2026-03-26 19:34:47 +00:00

Trapline

Trapline

A lightweight, modular host integrity and security monitoring daemon written in Go.

Trapline replaces Tripwire with something that actually helps. It runs continuously, watches what matters, ignores what doesn't, and reports cleanly to structured logging. No database rebuilds. No passphrase-protected policy files. No 4,000-line email diffs.

The name: a trapline is a route a trapper walks regularly, checking each trap along the way. That's what this does — walks a defined route across your system, checking each point for something wrong.

This project was vibe coded with Claude Code.


Install

One command:

curl -sSL https://raw.githubusercontent.com/jclement/trapline/main/install.sh | sudo bash

Or manually — download the binary and run the installer yourself:

curl -sL https://github.com/jclement/trapline/releases/latest/download/trapline_linux_amd64 -o trapline && chmod +x trapline
sudo ./trapline install

The binary is the installer. trapline install copies itself to /usr/local/bin, creates config directories, writes a default config, installs the systemd unit, enables the service, and captures initial baselines. Done.

$ sudo ./trapline install
Installing trapline v0.1.0 ...
  ✓ Binary installed to /usr/local/bin/trapline
  ✓ Directories created
  ✓ Config written to /etc/trapline/trapline.yml
  ✓ Systemd unit installed
  ✓ Apt hook installed
  ✓ Service enabled
  ✓ Service started

Trapline v0.1.0 is installed. Check status with: trapline status

Uninstall is just as clean:

sudo trapline uninstall           # removes everything
sudo trapline uninstall --keep-config  # preserves /etc/trapline/

What It Watches

Trapline ships with 13 scanner modules. Each runs independently on its own schedule.

Module What it does Default interval
file-integrity SHA-256 hashes of critical files (/etc/passwd, sshd_config, sudoers, etc.) 5m
packages dpkg --verify — detects files modified outside the package manager 1h
ports Monitors listening TCP/UDP ports via /proc/net/tcp 60s
processes Process allowlist/denylist with resource monitoring 30s
users User accounts, groups, sudoers, authorized_keys 15m
containers Docker container inventory via the Docker socket 60s
cron Cron jobs across all sources (crontab, cron.d, user crontabs) 15m
suid SUID/SGID binary detection 1h
ssh sshd_config security validation 15m
permissions World-writable files, bad ownership, shadow file permissions 1h
rootkit Kernel module baselining, hidden files, /dev anomalies, promiscuous NICs, deleted-exe processes 30m
malware Smart ClamAV integration — only scans new/modified files in high-risk dirs 15m
network Outbound connection monitoring with process correlation — baselines known remote IPs, identifies owning process, supports process allowlisting (disabled by default) 60s

How It Avoids Tripwire's Noise

  • Package-manager-aware. If apt installed it, it's not suspicious. The packages module uses dpkg --verify — after apt upgrade, no findings. Only files changed outside of apt are flagged.
  • Specific, not exhaustive. Watches ~50-100 specific files, not entire /usr/lib trees.
  • Automatic rebaseline. An apt hook (/etc/apt/apt.conf.d/99trapline) rebaselines after package operations. Ansible deploys end with trapline rebaseline. No manual database rebuilds.
  • Cooldown deduplication. Same finding isn't emitted again within a configurable window (default 1h). You don't get 500 identical alerts every day.

CLI

# Lifecycle
trapline install              # install binary, config, systemd unit, start
trapline uninstall            # stop service, remove everything
trapline update               # check for and apply updates from GitHub
trapline doctor               # validate installation health

# Operations
trapline run                  # start daemon (foreground, for systemd)
trapline status               # show module status
trapline scan                 # run all modules once, print results, exit
trapline scan --module ports  # run one module
trapline rebaseline           # capture current state as known-good
trapline findings             # list active findings (JSON)
trapline findings --format table

# Configuration
trapline config check         # validate config
trapline config show          # dump effective config

# Info
trapline version              # print version, commit, build date
trapline version --json

All commands support --config /path/to/trapline.yml and --quiet / --verbose.

Exit codes: 0 = clean, 1 = findings present, 2 = config error, 3 = runtime error.


Configuration

Config lives at /etc/trapline/trapline.yml. Hot-reloaded on change (SIGHUP).

state_dir: /var/lib/trapline

output:
  console:
    enabled: true
    format: json          # json | text
    level: warn
  file:
    enabled: true
    path: /var/log/trapline/trapline.log
    format: json
    level: info
    max_size_mb: 50
  tcp:
    enabled: true
    address: 127.0.0.1:51888    # Fluent Bit
    format: json
    level: info
  webhook:
    enabled: false
    url: ""                      # Teams/Slack incoming webhook
    level: high
    cooldown: 1h

update:
  enabled: true
  repo: jclement/trapline
  channel: stable
  check_interval: 6h
  auto_apply: true

defaults:
  interval: 5m
  cooldown: 1h

modules:
  file-integrity:
    enabled: true
    interval: 5m
    watch_extra:
      - /docker/*/docker-compose.yml
      - /usr/local/bin/*
  packages:
    enabled: true
    interval: 1h
  ports:
    enabled: true
    interval: 60s
  processes:
    enabled: true
    interval: 30s
    exclude:                    # glob patterns for process names to ignore (default: ["kworker/*"])
      - "kworker/*"
      - "kthreadd"
    deny:
      - name: xmrig
      - name: nc
        min_uptime: 300
  containers:
    enabled: true
    interval: 60s
  users:
    enabled: true
    interval: 15m
  cron:
    enabled: true
    interval: 15m
  suid:
    enabled: true
    interval: 1h
  ssh:
    enabled: true
    interval: 15m
    # allowed_users:              # optional: alert CRITICAL on SSH sessions by users not in this list
    #   - admin
    #   - deploy
  permissions:
    enabled: true
    interval: 1h
  rootkit:
    enabled: true
    interval: 30m
  malware:
    enabled: true
    interval: 15m
    # watch_dirs:                # override default high-risk paths
    #   - /tmp
    #   - /var/tmp
    #   - /dev/shm
  network:
    enabled: false                  # disabled by default — noisy on systems with Docker/dynamic outbound traffic
    interval: 60s
    allowed_processes:              # connections from these processes are silently ignored
      - apt
      - dpkg
      - freshclam

Output & Findings

Every finding is a structured JSON event:

{
  "timestamp": "2026-03-26T14:32:01.883Z",
  "hostname": "nyc1",
  "module": "file-integrity",
  "finding_id": "file-modified:/etc/ssh/sshd_config",
  "severity": "high",
  "status": "new",
  "summary": "sshd_config modified outside of package manager",
  "detail": {
    "path": "/etc/ssh/sshd_config",
    "baseline_hash": "a1b2c3d4...",
    "current_hash": "e5f6a7b8..."
  },
  "trapline_version": "0.1.0",
  "scan_id": "a7f3b291"
}

Severity levels:

Level Meaning Examples
critical Active compromise indicators unexpected SUID in /tmp, sshd replaced outside apt, new UID-0 user
high Security-relevant changes sshd_config modified, new listening port, sudoers changed
medium Notable changes, possibly legitimate new cron job, container image updated, user shell changed
info Informational rebaseline completed, config reloaded

Output sinks:

  • Console (stdout) — captured by systemd journal
  • File — rotated JSON log
  • TCP — for Fluent Bit / Axiom (Trapline -> TCP:51888 -> Fluent Bit -> Axiom)
  • Webhook — Teams/Slack notifications for critical findings

Self-Update

Trapline checks GitHub Releases for new versions and updates itself:

trapline update --check    # just check
trapline update            # download and apply

Updates are verified with SHA-256 checksums. Release binaries are signed with cosign via GitHub Actions OIDC (keyless Sigstore). The old binary is preserved at /usr/local/bin/trapline.bak for rollback. Major version bumps require --allow-major.


Doctor

trapline doctor validates the entire installation:

$ sudo trapline doctor
Trapline Doctor — checking installation health...

Binary
  ✓ Binary at /usr/local/bin/trapline (v0.1.0)
  ✓ Running as root

Config
  ✓ Config file exists at /etc/trapline/trapline.yml
  ✓ Config is valid YAML
  ✓ Config permissions 0600

Systemd
  ✓ Unit file at /usr/lib/systemd/system/trapline.service
  ✓ Service is enabled
  ✓ Service is active (running)

Directories
  ✓ /etc/trapline exists (0700)
  ✓ /var/lib/trapline exists (0700)
  ✓ /var/lib/trapline/baselines exists (0700)
  ✓ /var/log/trapline exists (0750)

Baselines
  ✓ 10 baseline files

Output
  ✓ TCP sink at 127.0.0.1:51888 reachable

Apt
  ✓ Apt hook at /etc/apt/apt.conf.d/99trapline

Summary: 15 passed, 0 warnings, 0 errors

Architecture

trapline (single binary)
├── cmd/trapline/          # CLI entrypoint
├── internal/
│   ├── config/            # YAML config loader, defaults, validation
│   ├── baseline/          # Baseline state management (JSON on disk)
│   ├── engine/            # Module scheduler, lifecycle, scan orchestration
│   ├── output/            # Output sinks (stdout, file, tcp, webhook)
│   ├── updater/           # Self-update from GitHub releases
│   ├── install/           # Install, uninstall, doctor
│   └── modules/
│       ├── fileintegrity/ # File hash monitoring
│       ├── packages/      # dpkg --verify
│       ├── ports/         # Listening port monitoring
│       ├── processes/     # Process allowlist/denylist
│       ├── users/         # User/group/sudoers monitoring
│       ├── containers/    # Docker container inventory
│       ├── cron/          # Cron job monitoring
│       ├── suid/          # SUID/SGID binary detection
│       ├── ssh/           # sshd_config validation
│       ├── permissions/   # Filesystem permission checks
│       ├── rootkit/       # Rootkit indicator detection
│       ├── malware/       # ClamAV integration (smart scanning)
│       └── network/       # Outbound connection monitoring
├── pkg/
│   └── finding/           # Shared Finding type, severity levels
└── e2e/                   # Docker-based end-to-end tests

Every scanner module implements a single interface:

type Module interface {
    Name() string
    Init(cfg ModuleConfig) error
    Scan(ctx context.Context) ([]finding.Finding, error)
    Rebaseline(ctx context.Context) error
}

Modules run in independent goroutines. A slow module (SUID scan on a large filesystem) doesn't block fast modules (port check every 30s).

Dependencies are minimal:

  • gopkg.in/yaml.v3 — config parsing
  • Standard library for everything else (crypto, net, os, encoding/json)

No CGO. Static binary. Cross-compiles for amd64/arm64.


Development

Prerequisites

mise manages all tooling. One command:

mise install

Build & Test

mise run build              # build for current platform
mise run build-linux        # cross-compile for linux
mise run test               # unit tests (fast, runs anywhere)
mise run lint               # golangci-lint
mise run test-e2e           # Docker e2e tests (requires Docker)
mise run snapshot           # goreleaser local build

Developing on macOS, Targeting Linux

Trapline is Linux-only (reads /proc, uses systemd, calls dpkg). Development on macOS works because:

  • Unit tests run anywhere — all Linux-specific reads are behind interfaces, faked in tests
  • E2E tests run in Docker — real Ubuntu containers, real /proc, real dpkg
  • Build cross-compiles with CGO_ENABLED=0 GOOS=linux

E2E Tests

17 tests that spin up Ubuntu 24.04 containers and verify real behavior:

mise run test-e2e

Tests cover: version/config commands, baseline capture, detecting new users, modified /etc/passwd, new cron jobs, SSH config changes, sudoers modifications, new SUID binaries, new listening ports, rebaseline resolving findings, multi-change detection, findings output formats.

Playground

Interactive Ubuntu environment for manual testing:

mise run playground

This cross-compiles the binary, drops it into a fresh Ubuntu container with aggressive scan intervals (5-10s), and starts a TCP sink that prints findings to stdout.

# Shell into the playground
docker exec -it trapline-playground bash

# Watch findings stream in (another terminal)
docker logs -f trapline-playground-sink

# Break things and watch trapline react:
useradd -m hacker                                    # user-added in ~10s
echo "* * * * * root curl evil.com" > /etc/cron.d/x  # cron-added
echo "PermitRootLogin yes" >> /etc/ssh/sshd_config    # ssh-insecure-setting
python3 -m http.server 8080 &                         # port-new in ~5s

# Tear down
mise run playground-down

CI/CD

  • CI (every push/PR): lint, unit tests — via GitHub Actions + jdx/mise-action
  • Release (on v* tags): goreleaser builds linux/amd64 + linux/arm64, cosign signs via Sigstore keyless (GitHub Actions OIDC), creates GitHub release
git tag v0.1.0
git push origin v0.1.0
# GitHub Actions handles the rest

Release assets:

trapline_linux_amd64        (~8-12 MB static binary)
trapline_linux_arm64        (~8-12 MB static binary)
checksums.txt               (SHA-256)
checksums.txt.sig           (cosign keyless signature)
checksums.txt.pem           (Fulcio certificate)

Ansible Integration

# Deploy trapline
- name: Download trapline
  get_url:
    url: "https://github.com/jclement/trapline/releases/latest/download/trapline_linux_amd64"
    dest: /usr/local/bin/trapline
    mode: "0755"

- name: Install trapline
  command: trapline install --no-start
  args:
    creates: /usr/lib/systemd/system/trapline.service

- name: Deploy trapline config
  template:
    src: trapline.yml.j2
    dest: /etc/trapline/trapline.yml
    mode: "0600"
  notify: restart trapline

- name: Start trapline
  systemd:
    name: trapline
    state: started
    enabled: true

# At end of playbook:
- name: Rebaseline trapline after deploy
  command: trapline rebaseline
  tags: ["trapline"]

After the first install, trapline update handles binary updates itself.


License

MIT