Makefile
Rama main · réplica de lectura del repositorio original.
.PHONY: all bench-all build bump-build check ci-verify clean cli-build compose-validate env-sync doc-health doctor go-build help help-dev help-legacy help-lint help-ops help-scan init install-hooks ja4p-validate link-check lint lint-ansible lint-docs lint-phases lint-opengrep lint-semgrep logs management-down management-logs management-shell management-up rebuild reload remote-bot sbom scan scan-exceptions scorecard-local setup-build start start-poc status stop sync test test-component-suites test-ip test-race test-ratio traffic-off traffic-on tunnel verify-all preflight poc-secrets
PYTHON ?= $(shell command -v python || command -v python3 || echo python)
# Prefer snap Go if present (local dev), else fall back to env/system GOROOT.
GOROOT := $(shell if [ -d /snap/go/current ]; then echo /snap/go/current; elif [ -n "$$GOROOT" ]; then echo "$$GOROOT"; else go env GOROOT 2>/dev/null; fi)
GO := $(shell if [ -x /snap/go/current/bin/go ]; then echo GOROOT=$(GOROOT) /snap/go/current/bin/go; elif [ -n "$$GOROOT" ]; then echo GOROOT=$(GOROOT) go; else echo go; fi)
# ── Phony targets ─────────────────────────────────────────────────────────────
# ── Build Metadata ────────────────────────────────────────────────────────────
VERSION ?= $(shell cat VERSION 2>/dev/null || echo "2.0")
BUILD_NUMBER ?= $(shell git rev-list --count HEAD 2>/dev/null || echo "0")
GIT_COMMIT ?= $(shell git rev-parse --short HEAD 2>/dev/null || echo "unknown")
BUILD_DATE ?= $(shell date -u +"%Y-%m-%dT%H:%M:%SZ")
FULL_VERSION := v$(VERSION).$(BUILD_NUMBER)
TOOLS_IMG := ja4proxy-tools
LDFLAGS := -ldflags="-s -w -buildid= -X github.com/seanpor/ja4proxy/internal/config.Version=$(FULL_VERSION) \
-X github.com/seanpor/ja4proxy/internal/config.GitCommit=$(GIT_COMMIT) \
-X github.com/seanpor/ja4proxy/internal/config.BuildDate=$(BUILD_DATE)"
# ── Environment Configuration ────────────────────────────────────────────────
ifneq (,$(wildcard .env))
-include .env
export
endif
# Makefile for JA4 Proxy
# ── Phony targets
# (alphabetical by group) ──────────────────────────────────
# Load machine-specific worker count (generated by: make test-calibrate)
-include .local/machine.mk
# Set reasonable default test worker count (override with WORKERS=N)
WORKERS ?= 4
# Normalise NAME= variable — accept Name= or name= as common typos
NAME ?= $(or $(Name),$(name))
# Default target
all: help
doctor: ## Phase 147/225/603 — Verify environment and toolchain health
@echo "=== JA4proxy Doctor: Environment Health Check ==="
@echo "── Required on the host (build will fail without these) ──────────────"
@command -v docker > /dev/null && echo " ✓ docker binary" || { echo " ✗ docker NOT found — required to run containerized tools and build images"; exit 1; }
@docker info > /dev/null 2>&1 && echo " ✓ docker daemon responsive" || { echo " ✗ docker daemon NOT running or not accessible by current user"; exit 1; }
@$(GO) version 2>/dev/null | grep -E "go1\.(2[6-9]|[3-9][0-9])" > /dev/null && echo " ✓ Go ($$($(GO) version | awk '{print $$3}'))" || { echo " ✗ Go 1.26+ required, found: $$($(GO) version 2>&1)"; exit 1; }
@command -v gcc > /dev/null && echo " ✓ C compiler ($$(gcc --version 2>&1 | head -n1 | awk '{print $$1, $$3}'))" || { echo " ✗ gcc NOT found — required for hardened static-pie CGO build mode"; exit 1; }
@command -v $(PYTHON) > /dev/null && echo " ✓ python3 ($$($(PYTHON) --version 2>&1 | awk '{print $$2}'))" || { echo " ✗ python3 NOT found"; exit 1; }
@echo "── Containerized Toolchain (hermetic; zero host dependencies) ───────"
@docker image inspect $(TOOLS_IMG) > /dev/null 2>&1 \
&& echo " ✓ $(TOOLS_IMG) image present (Python linters, test runner, pip-audit, doc tools)" \
|| echo " · $(TOOLS_IMG) image absent — will build on demand on first 'make lint' / 'make test'"
@docker image inspect ja4proxy-bandit > /dev/null 2>&1 \
&& echo " ✓ ja4proxy-bandit image present (bandit SAST on Python 3.11)" \
|| echo " · ja4proxy-bandit image absent — will build on demand on first 'make lint'"
@echo " ✓ Official pinned images used on demand: trivy, hadolint, gitleaks, semgrep, shellcheck,"
@echo " golangci-lint, checkmake, luacheck, ansible-lint, promtool, amtool, helm, scorecard"
@echo "── Environment Configuration ────────────────────────────────────────"
@[ -f .env ] && echo " ✓ .env present" || echo " · .env absent — created by 'make start-poc' or 'cp template.env .env'"
@echo "=== Doctor: environment and toolchain OK ==="
# ── Master help ───────────────────────────────────────────────────────────
help: ## Show the essential front-door targets
@echo "======================================================================"
@echo " JA4proxy $(FULL_VERSION) — Essential Operational Commands"
@echo "======================================================================"
@echo ""
@echo "── First time? ───────────────────────────────────────────────────────"
@echo " make doctor - Check local environment for missing tools"
@echo " make init - Run guided setup wizard (generates .env)"
@echo ""
@echo "── Build ─────────────────────────────────────────────────────────────"
@echo " make go-build - Compile Go proxy + CLI binaries (~5s)"
@echo " make compose-validate - Validate env vars in compose files (fast)"
@echo " make build - go-build + all Docker images (requires .env)"
@echo ""
@echo "── Operate ───────────────────────────────────────────────────────────"
@echo " make start - Start full stack (Proxy + Monitoring)"
@echo " make start-poc - Start POC environment only"
@echo " make stop - Stop all services"
@echo " make status - Show cluster health and security state"
@echo " make logs - Stream real-time structured proxy logs"
@echo " make reload - Reload config without restart (SIGHUP)"
@echo ""
@echo "── Quality gates (three tiers) ───────────────────────────────────────"
@echo " make check - FAST: compile + env validate + tests (~3m)"
@echo " Matches CI required checks. Run before push."
@echo " make preflight - FULL: lint + scan + test (~25m, pre-PR)"
@echo " Includes Trivy CVE scan — run 'make build' first."
@echo " make verify-all - RELEASE: preflight + heavy benchmarks (~45m)"
@echo ""
@echo "── Incident response ─────────────────────────────────────────────────"
@echo " make attack-status - Quick security snapshot (bans, block totals)"
@echo " make top-attackers - Top 10 fingerprints by traffic"
@echo " make dial LEVEL=75 - Raise blocking threshold (0=monitor, 100=full)"
@echo " make block-ip IP=x.x.x.x - Hard-block IP for 1 hour"
@echo " make test-ip IP=x.x.x.x - Simulate pipeline decision for an IP"
@echo " make help-ops - Full incident response command reference"
@echo ""
@echo "── More help ─────────────────────────────────────────────────────────"
@echo " make help-ops - Incident response + threat-intel details"
@echo " make help-lint - Granular linter control"
@echo " make help-scan - Granular scanner control"
@echo " make help-dev - Build, benchmark, and agent targets"
@echo ""
@$(MAKE) -s help-lint
help-ops: ## Incident response and threat intelligence help
@echo ""
@echo " Forms being stuffed? Start here:"
@echo ""
@echo "── Step 1: understand what is happening ──────────────────────────────"
@echo " make attack-status - Bans, block totals, top threats"
@echo " make top-attackers - Top 10 fingerprints by traffic"
@echo " make logs - Live structured log stream"
@echo ""
@echo "── Step 2: raise the blocking threshold ──────────────────────────────"
@echo " make dial LEVEL=50 - Start scoring + light blocking"
@echo " make dial LEVEL=75 - Block most bots, pass browsers"
@echo " make dial LEVEL=100 - Full blocking at configured thresholds"
@echo " (dial=0 is monitor-only; raise gradually to avoid false positives)"
@echo ""
@echo "── Step 3: targeted blocks (no restart needed) ───────────────────────"
@echo " make block-ip IP=1.2.3.4 - Hard-block IP for 1 hour"
@echo " make block-ja4 FP=t13d... - Blacklist JA4 fingerprint (instant RST)"
@echo " make unblock-ip IP=1.2.3.4 - Remove all blocks for IP (false positive)"
@echo " make flush-redis - Nuclear option: reset all bans/blocks/rates"
@echo ""
@echo "── Step 4: reload without downtime ───────────────────────────────────"
@echo " make reload - Push config changes via SIGHUP"
@echo " make test-ip IP=1.2.3.4 - Verify decision for a specific IP"
@echo ""
@echo "── Threat Intelligence ───────────────────────────────────────────────"
@echo " make fetch-db - Fetch malicious fingerprints (ja4db/FoxIO)"
@echo " make list-pending - Show fingerprints awaiting approval"
@echo " make approve-all - Approve all pending fingerprints"
@echo " make update-geoip - Download latest GeoIP DB (run monthly)"
@echo " make check-geoip - Check age of current GeoIP database"
@echo " make geoip-report - Full geo-blocking report"
@echo ""
help-lint: ## Show linting commands help
@echo ""
@echo "── Linting ───────────────────────────────────────────────────"
@echo " lint-static - Python: mypy + bandit + ruff + pip-audit"
@echo " lint-go - Go: go-lint + lint-go-full + lint-go-mod"
@echo " go-lint - Go: go vet (fast static analysis)"
@echo " lint-shell - shellcheck all .sh scripts"
@echo " lint-yaml - yamllint config/ and deploy/monitoring/"
@echo " lint-docker - hadolint + docker compose config check"
@echo " lint-prom - promtool alert + recording rule validation"
@echo " lint-alertmanager - amtool Alertmanager config validation"
@echo " lint-secrets - gitleaks scan of git history"
@echo " lint-deps - run govulncheck/gosec (via container) + pip-audit"
@echo " lint-docs - core doc quality: doc-health + link-check + ATT&CK"
@echo " lint-phases - Validate manifest + phase doc integrity"
@echo " lint-all - Run every linter in one shot"
# ── Scan sub-help ────────────────────────────────────────────────────────
help-scan: ## Show security scanning commands help
@echo ""
@echo "── Security Scanning ─────────────────────────────────────────"
@echo " scan - Run ALL security + container scans (full gate)"
@echo " scan-all - Alias of 'scan'"
@echo " scan-container - Go SAST (gosec) in a container"
@echo " scan-local - gosec locally (view output)"
@echo " scan-exceptions - List .trivyignore.first-party / .trivyignore.third-party + days-to-expiry"
@echo ""
@echo "── Container Scanning ────────────────────────────────────────"
@echo " scan-images - Trivy scan of production images"
@echo " scan-dockerfiles - Trivy scan of Dockerfiles (IaC)"
@echo " scan-first-party - Trivy scan of 1st-party Python/Go deps"
@echo " check-image-versions - Check base image tags against Dockerfiles"
@echo " check-updates - Check all deps (Docker, Go, Python, Node) for updates"
@echo ""
@echo "── CVE baseline: PR gate vs scheduled monitor (Phase 847c) ────"
@echo " scan-regression - PR gate: scans only the surfaces this branch touches"
@echo " scan-regression-plan - Print those surfaces as JSON (no scanning; CI helper)"
@echo " scan-baseline - Record security/baseline.json (run on main)"
@echo " scan-monitor - Full scan + drift report (APPLY=1 files deduplicated issues)"
@echo " medium-inventory - Measure the MEDIUM surface (Phase 847e)"
# ── Legacy (Python) sub-help ─────────────────────────────────────────────
help-legacy: ## Show legacy Python proxy sub-help
@echo ""
@echo "── Signal-Score Audit ──────────────────────────────────────"
@echo " check-scores - Audit Go/Python scores vs signal registry"
# ── Dev sub-help ─────────────────────────────────────────────────────────
help-dev: ## Show developer commands sub-help
@echo ""
@echo "── Build ─────────────────────────────────────────────────────"
@echo " build - Build Go binaries + all Docker images"
@echo " rebuild - Wipe volumes/images, rebuild, start"
@echo " clean - Stop + remove all containers/volumes"
@echo ""
@echo "── Compose file map ──────────────────────────────────────────"
@echo " docker-compose.yml Quick start / CI smoke test"
@echo " 3 containers, sensible defaults"
@echo " deploy/docker/docker-compose.poc.yml Full local dev (make start)"
@echo " HAProxy+proxy+Redis+analytics"
@echo " +management. Needs .env"
@echo " deploy/docker/docker-compose.prod.yml Single-instance production"
@echo " deploy/docker/docker-compose.monitoring.yml Prometheus/Grafana overlay"
@echo " deploy/docker/docker-compose.scale.yml 4× proxy workers overlay"
@echo " deploy/docker/docker-compose.test.yml Go integration/chaos test env"
@echo " deploy/docker/docker-compose.redis-tls.yml Redis-TLS test overlay"
@echo ""
@echo "── Startup / Shutdown ────────────────────────────────────────"
@echo " start - Start full stack (POC + monitoring)"
@echo " start-monitoring - Start monitoring stack only"
@echo " start-scaled - Start 4-worker scaled + HAProxy"
@echo " stop - Stop all services (keep Redis data)"
@echo " stop-clean - Stop all + wipe volumes (fresh slate)"
@echo " status - Show health + security state"
@echo " logs - Stream proxy container logs"
@echo " ssh-tunnels - Print SSH tunnel command for remote UI"
@echo " remote-bot - Run the TLS test bot: make remote-bot HOST=<ip> [PORT=<port>]"
@echo ""
@echo "── Testing ───────────────────────────────────────────────────"
@echo " test - Run all tests locally (fast, no Docker)"
@echo " test-unit - Run unit tests only"
@echo " test-chaos - Run chaos/resilience tests only"
@echo " test-adversarial - Run adversarial/fuzz tests only"
@echo " test-calibrate - Benchmark machine, store worker count"
@echo " test-docker - Run tests inside Docker (CI env)"
@echo " test-component-suites - Run every component test suite"
@echo " smoke-test - Quick sanity check"
@echo " smoke-docker - Docker Compose lifecycle smoke test"
@echo " smoke-k8s - Helm/kind smoke test (skips if absent)"
@echo ""
@echo "── Go Proxy ──────────────────────────────────────────────────"
@echo " go-build - Build Go proxy binary to bin/ja4p"
@echo " go-test - Run all Go unit tests"
@echo " go-lint - Run go vet on Go code"
@echo ""
@echo "── Benchmarking ──────────────────────────────────────────────"
@echo " bench - Run all Go benchmarks (micro + macro)"
@echo " bench-micro - Go internal component micro-benchmarks"
@echo " bench-macro - End-to-end load test (requires: make start)"
@echo " ARGS= - Extra flags for macro-bench e.g. ARGS='--duration 60'"
@echo ""
@echo "── Documentation ─────────────────────────────────────────────"
@echo " lint-docs - Validate doc frontmatter"
@echo " link-check - Check internal Markdown links"
@echo " doc-health - Run all doc quality checks"
@echo " test-ratio - Show test-to-code ratio"
@echo " sync - Sync roadmap from manifest.yaml"
@echo ""
@echo "── Multi-Agent ───────────────────────────────────────────────"
@echo " agent-up NAME=.. - Start isolated agent env"
@echo " agent-down [NAME=] - Stop agent env"
@echo " agent-status - List running agent environments"
@echo " tunnel NAME=.. - SSH tunnels for agent UIs"
@echo ""
@echo " After agent-up, these auto-target active agent:"
@echo " stop stop-clean status logs health-check"
@echo " flush-redis clean rebuild"
@echo " management-up/down/logs/shell"
@echo ""
# ── Startup / Shutdown ────────────────────────────────────────────────────────
# Start full stack (POC + monitoring)
start: ## Start full stack (POC + Prometheus/Grafana)
@./scripts/start-all.sh
# Start monitoring stack only (Prometheus / Grafana / Loki)
start-monitoring: ## Start monitoring stack only
@./scripts/start-monitoring.sh
# Start scaled configuration with 4 workers and HAProxy (Phase 26d)
start-scaled: ## Start 4-worker scaled config with HAProxy
@echo "Starting JA4Proxy with 4-worker scaling..."
@docker compose -f deploy/docker/docker-compose.poc.yml -f deploy/docker/docker-compose.scale.yml --env-file .env up -d
@echo "✓ HAProxy load balancer started on port 443"
@echo "✓ 4 worker processes started on ports 8080, 8083, 8084, 8085"
@echo "✓ HAProxy stats available at http://localhost:8404/stats (admin/admin123)"
# Stop all services (keep Redis data)
stop: ## Stop all services (keep Redis data)
@./scripts/stop-all.sh
# Stop all services AND remove all volumes (fresh slate)
stop-clean: ## Stop all + wipe volumes (fresh slate)
@./scripts/stop-all.sh --clean
# Show health of all services + security state
status: ## Show health of all services + security state
@./scripts/status.sh
# ── Remote Manual Testing (Phase 220) ─────────────────────────────────────────
# Remote access needs no special stack: the poc + monitoring compose files
# already bind every service to AGENT_BIND_IP (default 127.0.0.1), and
# start-poc.sh validates it via check_bind_address.py. To test from a laptop:
# set AGENT_BIND_IP=<server-private-LAN-ip> in .env, run `make start`, then the
# test bot below. See docs/runbooks/REMOTE_TESTING.md.
#
# Run the TLS test bot against a (remote) proxy. Usage: make remote-bot HOST=10.0.0.5 PORT=443
remote-bot: ## Run test bot against remote proxy (HOST=... PORT=...)
@[ -n "$(HOST)" ] || { echo "Usage: make remote-bot HOST=<proxy-ip> [PORT=<port>]"; exit 1; }
@PYTHONPATH=. $(PYTHON) scripts/test-bot.py --proxy "$(HOST)" --port $(or $(PORT),443)
# ── Multi-Agent ───────────────────────────────────────────────────────────────
# Start an isolated agent environment (auto-generates .env.<name> if missing).
# Writes .current-agent so subsequent commands default to this agent.
# Usage: make agent-up NAME=claude
agent-up: ## Start an isolated agent environment (NAME=<agent>)
@[ -n "$(NAME)" ] || (echo "Usage: make agent-up NAME=<agent> (agents: gemini|claude|ollama|mistral)"; exit 1)
@[ -f ".env.$(NAME)" ] || (echo "→ No .env.$(NAME) found — generating..."; ./scripts/agent-env.sh $(NAME))
docker compose -f deploy/docker/docker-compose.poc.yml --project-name ja4_$(NAME) --env-file .env.$(NAME) up -d
@echo "$(NAME)" > .current-agent
@echo "✓ Agent $(NAME) started (saved to .current-agent)"
@IP=$$(grep '^AGENT_BIND_IP=' .env.$(NAME) | cut -d= -f2); \
MGMT=$$(grep '^HOST_PORT_MANAGEMENT=' .env.$(NAME) | cut -d= -f2); MGMT=$${MGMT:-8090}; \
ANL=$$(grep '^HOST_PORT_ANALYTICS=' .env.$(NAME) | cut -d= -f2); ANL=$${ANL:-8080}; \
echo " Ingress: https://$$IP:443"; \
echo " Analytics: http://$$IP:$$ANL"; \
echo " Management:http://$$IP:$$MGMT"; \
echo " Metrics: http://$$IP:9090/metrics"
@echo " Admin: ./scripts/ja4-admin.sh status (uses .current-agent automatically)"
@echo " Tunnels: make tunnel NAME=$(NAME) HOST=user@this-server"
@echo ""
@echo "Agent-aware targets (all read .current-agent automatically):"
@echo " make stop / stop-clean / status / logs / health-check"
@echo " make flush-redis / clean / rebuild"
@echo " make management-up/down/logs/shell"
# Stop an isolated agent environment.
# If NAME is not given, reads .current-agent (set by the last agent-up).
# Clears .current-agent if it matches the stopped agent.
# Usage: make agent-down NAME=claude OR make agent-down
agent-down: ## Stop an isolated agent environment (NAME=<agent>)
$(eval _NAME := $(or $(NAME),$(shell cat .current-agent 2>/dev/null)))
@[ -n "$(_NAME)" ] || (echo "Usage: make agent-down NAME=<agent> (or run make agent-up first to set .current-agent)"; exit 1)
@[ -f ".env.$(_NAME)" ] || (echo "No .env.$(_NAME) — is agent $(_NAME) configured?"; exit 1)
docker compose -f deploy/docker/docker-compose.poc.yml --project-name ja4_$(_NAME) --env-file .env.$(_NAME) down
@if [ "$$(cat .current-agent 2>/dev/null)" = "$(_NAME)" ]; then rm -f .current-agent; echo "✓ Cleared .current-agent"; fi
# List all running agent environments and show which is current.
agent-status: ## List all running agent environments
@echo "Running ja4_* agent environments:"
@docker compose ls 2>/dev/null | grep '^ja4_' || echo " (none running)"
@if [ -f .current-agent ]; then echo "Current (.current-agent): $$(cat .current-agent)"; fi
# ── Build ──────────────────────────────────────────────────────────────────────
# Build Docker images
build: bump-build ## Build all Docker images (Go compiled inside Docker — no local Go required)
@$(MAKE) env-sync
@$(MAKE) compose-validate
@echo "Building Docker images (BuildKit enabled)..."
DOCKER_BUILDKIT=1 COMPOSE_DOCKER_CLI_BUILD=1 \
VERSION=$(FULL_VERSION) GIT_COMMIT=$(GIT_COMMIT) BUILD_DATE=$(BUILD_DATE) \
docker compose -f deploy/docker/docker-compose.poc.yml --env-file .env build
build-native: bump-build ## Build host-native Go binaries (requires local Go 1.26+)
@mkdir -p bin
@$(MAKE) go-build cli-build
@$(MAKE) env-sync
@$(MAKE) compose-validate
@echo "Building Docker images (BuildKit enabled)..."
DOCKER_BUILDKIT=1 COMPOSE_DOCKER_CLI_BUILD=1 \
VERSION=$(FULL_VERSION) GIT_COMMIT=$(GIT_COMMIT) BUILD_DATE=$(BUILD_DATE) \
docker compose -f deploy/docker/docker-compose.poc.yml --env-file .env build
# Validate compose files without building images (fast — no Docker daemon build step)
# Catches missing required env vars before you waste time on a full 'make build'.
# Safe to run without a running Docker daemon; used by CI as a merge gate.
env-sync: ## Add any newly-required vars to an existing .env (idempotent, never overwrites)
$(eval _AGENT := $(shell cat .current-agent 2>/dev/null))
@ENVFILE=$$([ -n "$(_AGENT)" ] && echo ".env.$(_AGENT)" || echo ".env"); \
./scripts/env-sync.sh "$$ENVFILE"
compose-validate: ## Validate docker-compose files and required env vars (fast, no image build)
@if [ ! -f .env ]; then \
echo "ERROR: .env not found. Run 'make init' to generate it."; \
exit 1; \
fi
@docker compose -f deploy/docker/docker-compose.poc.yml --env-file .env config > /dev/null \
|| { echo ""; \
echo "If the error above says a variable 'is missing a value', your .env"; \
echo "predates it — start-poc.sh only generates a .env when none exists,"; \
echo "so an existing checkout never picks up newly-required vars."; \
echo "Fix: make env-sync"; \
exit 1; }
@echo "✓ compose config valid — all required env vars present"
# Run all tests locally in parallel (fast — no Docker required)
# Skips tests marked @pytest.mark.live_services (require Go/Python proxy + Redis stack)
test: tools-image cli-build ## Phase 146 — Run the full test suite
@echo "=== Running Go Native Tests ==="
@if command -v go >/dev/null 2>&1 || [ -x /snap/go/current/bin/go ]; then \
GOROOT=$(GOROOT) $(GO) test -v -coverprofile=coverage.txt -covermode=atomic ./...; \
else \
docker run --rm -v "$$PWD":/src -w /src golang:1.27.3-alpine go test -v -coverprofile=coverage.txt -covermode=atomic ./...; \
fi
@echo "=== Running Python Unit Tests (containerized: $(TOOLS_IMG)) ==="
@$(TOOLS_RUN) pytest tests/unit/ -n $(WORKERS) --dist=loadfile --timeout=60 --tb=short
@echo "=== Running management/tests/ (phase-808 — was silently excluded, see manifest) ==="
@$(TOOLS_RUN) pytest management/tests/ -n $(WORKERS) --dist=loadfile --timeout=60 --tb=short
@echo "=== Running Integration Smoke Tests (containerized) ==="
@$(TOOLS_RUN) pytest tests/integration/ -k "not docker_stack" -x -q --timeout=60
@echo "=== Running CI/workflow guardrail tests (containerized) ==="
@$(TOOLS_RUN) pytest tests/test_workflow_pinning.py -q --timeout=60
@echo "✓ Full test suite passed"
@$(TOOLS_RUN) python scripts/pipeline_summary.py test
# Run unit tests only (containerized — pinned Python 3.14, no host venv)
test-unit: tools-image ## Run unit tests only
@$(TOOLS_RUN) pytest tests/unit/ -n $(WORKERS) --dist=loadfile --timeout=60 --tb=short $(ARGS)
@$(TOOLS_RUN) pytest management/tests/ -n $(WORKERS) --dist=loadfile --timeout=60 --tb=short $(ARGS)
# Run chaos/resilience tests only (containerized)
test-chaos: tools-image ## Run chaos/resilience tests only
@$(TOOLS_RUN) pytest tests/chaos/ -n $(WORKERS) --dist=loadfile --timeout=60 --tb=short $(ARGS)
# Run adversarial/fuzz tests only (containerized; no parallelism — some are stateful)
test-adversarial: tools-image ## Run adversarial/fuzz tests only
@$(TOOLS_RUN) pytest tests/adversarial/ --timeout=60 --tb=short $(ARGS)
# Run multi-core concurrent preemption stress tests (Go -race with elevated GOMAXPROCS)
test-race-stress: ## Run Go concurrency stress harness (Phase 842)
@./scripts/run-concurrency-stress.sh
# Benchmark this machine and store worker count in .local/machine.mk
test-calibrate: ## Benchmark this machine, store worker count
@$(PYTHON) scripts/detect_workers.py
# Run tests inside Docker (CI / clean environment)
test-docker: ## Run tests inside Docker (CI env)
@./scripts/run-tests.sh
# Run quick smoke test
smoke-test: ## Quick sanity check
@./scripts/smoke-test.sh
# Run linting
lint: ## Phase 146 — Run all linters (Python, Go, Infra, Docs)
@$(MAKE) lint-all
@python3 scripts/pipeline_summary.py lint
@python3 scripts/ci_summary.py lint
# Security scanning with bandit (medium/high severity, skip B104 bind-all)
lint-security: bandit-image ## bandit SAST (medium/high severity)
@$(BANDIT_RUN) bandit -r src/analytics/ -ll --skip B104 && echo " ✓ lint-security passed"
# Type checking with mypy (suppress output)
lint-types: ## Run mypy type checker on src/
docker run --rm -v $(PWD):/app python:3.14.0-slim sh -c "cd /app && pip install mypy && mypy src/ 2>/dev/null || echo 'Mypy warnings above, see baseline docs/reports/MYPY_BASELINE.md'"
# Phase 16f static analysis gates — runs locally (no Docker required).
# All three tools must pass cleanly; failures block the CI build.
# mypy: type-check src/ and proxy.py using mypy.ini baseline
# bandit: SAST scan, medium/high severity only (-ll), ignore B104 (bind 0.0.0.0 intentional)
# ruff: fast linter/formatter-check (replaces flake8/isort for new code)
# pip-audit: CVE scan of requirements.txt; urllib3 CVEs acknowledged (transitive, tracked backlog)
lint-static: tools-image bandit-image ## mypy + bandit + ruff + pip-audit
@echo "=== mypy: type checking ==="
@$(TOOLS_RUN) mypy src/analytics/ && echo " ✓ mypy passed"
@echo ""
@echo "=== bandit: SAST (medium/high, Python 3.11 — see Dockerfile.bandit) ==="
@$(BANDIT_RUN) bandit -r src/analytics/ -ll -q --skip B104 && echo " ✓ bandit passed"
@echo ""
@echo "=== ruff: linting ==="
@$(TOOLS_RUN) ruff check src/analytics/ && echo " ✓ ruff passed (tests advisory only)"
@echo ""
@echo "=== pip-audit: CVE dependency scan ==="
@# phase-311: resilient wrapper — retries transient PyPI/OSV outages and
@# soft-passes (with a CI warning) if the service is unreachable, but still
@# fails on a real vulnerability. Stops upstream-outage flakes reddening the gate.
@# phase-847a: waived CVEs live in security/waivers.yaml and are rendered into
@# the ignorefiles by scripts/render_waivers.py, so this gate and the ci.yml
@# dependency-audit-python job can no longer drift apart. The old inline list
@# here (6 entries) had already diverged from ci.yml's (2 entries).
@# Fail fast if a derived file is stale or a hand edit slipped in (the pytest
@# drift test covers the same ground; this just fails earlier in the gate).
@$(TOOLS_RUN) python scripts/render_waivers.py --check
@test -s .pip-audit-ignores || { echo "✗ .pip-audit-ignores missing — run: python3 scripts/render_waivers.py"; exit 1; }
@# phase-847f: the registry is coherent — but is it still TRUE? Every waiver's
@# reachability premise is bound to a check and executed here, so a service that
@# gained a published port (or a dev dependency promoted into a runtime image)
@# fails the gate and demands re-justification instead of drifting silently.
@$(TOOLS_RUN) python scripts/reachability_checks.py >/dev/null && echo " ✓ waiver reachability premises hold"
@$(TOOLS_RUN) bash scripts/pip-audit-resilient.sh -r requirements.txt \
$$(grep -v '^[[:space:]]*#' .pip-audit-ignores) \
&& echo " ✓ pip-audit passed (waived CVEs: see security/waivers.yaml)"
@echo ""
@echo "✓ All static analysis gates passed"
# Code quality with flake8 (suppress output)
lint-quality: ## flake8 code quality
docker run --rm -v $(PWD):/app python:3.14.0-slim sh -c "cd /app && pip install flake8 && flake8 src/analytics/ scripts/ tests/ 2>/dev/null || echo 'Flake8 warnings above, see baseline docs/QUICK_REFERENCE.md'"
# Coverage reporting with pytest-cov (Phase 16c gate: ≥80% all modules)
lint-coverage: ## pytest-cov coverage reporting (≥80% gate)
$(PYTHON) -m pytest tests/ --ignore=tests/integration/test_docker_stack.py \
--cov=src --cov=proxy \
--cov-fail-under=80 \
--cov-report=term-missing \
--cov-report=html:reports/coverage/html
# Lint Dockerfiles (hadolint) and validate docker-compose files (docker compose config).
# Ignored rules are consciously accepted — see .hadolint.yaml for rationale.
# deploy/docker/docker-compose.test.yml is the full Go test environment.
# Ignored rules live in .hadolint.yaml (with per-rule rationale), NOT in flags
# here. Phase 800: they used to be duplicated as `--ignore` flags because the
# container had no volume mount, so .hadolint.yaml could never be read — and the
# file additionally used the wrong top-level key (`ignore:` instead of
# `ignored:`), which hadolint accepts silently while applying nothing. Both are
# fixed; the mount + --config below makes .hadolint.yaml the single source of
# truth. Do not reintroduce --ignore flags: they would drift from the file.
HADOLINT_DOCKERFILES := deploy/docker/Dockerfile.management \
deploy/docker/Dockerfile.mockbackend deploy/docker/Dockerfile.test \
deploy/docker/Dockerfile.trafficgen deploy/docker/Dockerfile.go-proxy deploy/docker/Dockerfile.ja4-tap \
src/analytics/Dockerfile src/tarpit/Dockerfile \
tests/docker/Dockerfile.recorder \
tests/docker/Dockerfile.test-runner tests/docker/Dockerfile.tls-backend
lint-docker: ## hadolint + `docker compose config --quiet` (all overlays)
@echo "=== hadolint: Dockerfiles ==="
@# Pre-pull so Docker's "Unable to find image / Downloaded" messages (stderr,
@# emitted on a cold runner) don't land in $$result below and false-FAIL the
@# first file. (We keep 2>&1 to still capture genuine hadolint errors.)
@docker pull -q hadolint/hadolint:v2.14.0 >/dev/null 2>&1 || true
@for f in $(HADOLINT_DOCKERFILES); do \
printf " %-50s" "$$f"; \
result=$$(docker run --rm -i -v "$(PWD):/src:ro" -w /src hadolint/hadolint:v2.14.0 hadolint --config .hadolint.yaml --no-color - < "$$f" 2>&1); \
if [ -n "$$result" ]; then echo "FAIL"; echo "$$result"; exit 1; fi; \
echo "OK"; \
done
@echo ""
@echo "=== docker compose config: compose files ==="
@# Phase-202: the `:?` required-credential syntax in poc/monitoring compose
@# files forces operators to supply real secrets. For `config --quiet` lint
@# we inject harmless placeholders so the static validation still runs.
@docker compose -f deploy/docker/docker-compose.test.yml config --quiet \
&& echo " deploy/docker/docker-compose.test.yml OK"
@REDIS_PASSWORD=lint-placeholder GRAFANA_PASSWORD=lint-placeholder \
HAPROXY_STATS_USER=lint-placeholder HAPROXY_STATS_PASSWORD=lint-placeholder \
EXPORTER_REDIS_PASSWORD=lint-placeholder \
docker compose -f deploy/docker/docker-compose.monitoring.yml config --quiet \
&& echo " deploy/docker/docker-compose.monitoring.yml OK"
@BACKEND_HOST=lint-placeholder ANALYTICS_REDIS_PASSWORD=lint-placeholder TAP_INTERFACE=lint-placeholder \
docker compose -f deploy/docker/docker-compose.prod.yml config --quiet \
&& echo " deploy/docker/docker-compose.prod.yml OK"
@BACKEND_HOST=lint-placeholder REDIS_PASSWORD=lint-placeholder ANALYTICS_REDIS_PASSWORD=lint-placeholder \
MANAGEMENT_REDIS_PASSWORD=lint-placeholder ANALYTICS_HMAC_SECRET=lint-placeholder \
MANAGEMENT_JWT_SECRET=lint-placeholder MANAGEMENT_ADMIN_USER=lint-placeholder MANAGEMENT_ADMIN_PASSWORD=lint-placeholder \
docker compose -f deploy/docker/docker-compose.poc.yml -f deploy/docker/docker-compose.scale.yml config --quiet \
&& echo " deploy/docker/docker-compose.scale.yml (overlay) OK"
@echo ""
@echo "✓ Docker lint passed"
# Pinned lint toolchain (Phase 313). The Python linters + project Python scripts
# run inside $(TOOLS_IMG) (built from Dockerfile.tools) via $(TOOLS_RUN), so CI
# and local share one reproducible env with no host `pip install`. Tools with
# their own official images (hadolint, shellcheck, trivy, gitleaks, gosec,
# promtool, amtool, scorecard, lychee, semgrep) are run from those directly.
tools-image: ## Build containerized tools image (Dockerfile.tools)
@docker build -q -t $(TOOLS_IMG) -f Dockerfile.tools . >/dev/null
# Docker socket mounted so tests/integration/test_container_config.py,
# test_dockerfile_coverage.py, and test_go_proxy_image.py (all gated on
# `shutil.which("docker")` + a reachable daemon) actually run instead of
# silently skipping (phase-514). The tools image runs as root (no USER
# directive), so no socket-permission workaround is needed.
TOOLS_RUN = docker run --rm -v $(PWD):/src -w /src -v /var/run/docker.sock:/var/run/docker.sock $(TOOLS_IMG)
# Bandit runs on Python 3.11 (see Dockerfile.bandit): its B502/B604/B607 plugins
# use ast.Num, removed in 3.12, which makes bandit silently skip whole files on
# the 3.14 tools image. Separate image keeps full SAST coverage.
BANDIT_IMG := ja4proxy-bandit
bandit-image: ## Build containerized bandit SAST image (Dockerfile.bandit)
@docker build -q -t $(BANDIT_IMG) -f Dockerfile.bandit . >/dev/null
BANDIT_RUN = docker run --rm -v $(PWD):/src -w /src $(BANDIT_IMG)
# Opengrep SAST toolchain (Phase 865: replaces Semgrep with pinned binary & local rules)
OPENGREP_IMG := ja4proxy-opengrep:1.33.1
opengrep-image: ## Build containerized Opengrep SAST image
@if [ -z "$$(docker images -q $(OPENGREP_IMG) 2>/dev/null)" ]; then \
echo "Building $(OPENGREP_IMG)..."; \
docker build -t $(OPENGREP_IMG) -f deploy/docker/Dockerfile.opengrep deploy/docker/ >/dev/null; \
fi
SEMGREP_IMG := $(OPENGREP_IMG)
# checkmake publishes no semantic version tags — only `latest` and per-commit
# tags, all from 2022-02-18. Pinned to the newest immutable commit tag rather
# than `latest` (digest sha256:eb6919b20b22d1701a976856e4a224627df0a74b118246101fb6cf5c2e03049f).
# The image's ENTRYPOINT is `./checkmake /Makefile`, which ignores our args, so
# the recipe overrides it with --entrypoint /checkmake.
CHECKMAKE_IMG := mrtazz/checkmake:2c59d1f0939900ca3a4208fb9b9300de90ecee8a
# Lint shell scripts with shellcheck (error-level only; warnings are advisory).
# SC2154 suppressed: variables sourced from .env are referenced but not assigned in-script.
# `.local/` holds container-written caches (Trivy's fanal DB among them) owned
# by root, which this find cannot traverse. It was printing a "Permission
# denied" warning on EVERY make invocation — this variable is expanded at
# parse time, so the noise appeared before any target ran and looked like a
# failure of whatever target was asked for. Nothing there is a shell script.
SHELL_SCRIPTS := $(shell find . \( -path "./.git" -o -path "./.local" -o -path "./node_modules" -o -path "./.claude" \) -prune -o -name "*.sh" -print | sort)
lint-shell: ## shellcheck all `.sh` scripts (error-level)
@echo "=== shellcheck: shell scripts ==="
@fail=0; \
for f in $(SHELL_SCRIPTS); do \
printf " %-60s" "$$f"; \
result=$$(docker run --rm -i koalaman/shellcheck:v0.11.0 \
--severity=error --exclude=SC2154 - < "$$f" 2>/dev/null); \
if [ -n "$$result" ]; then echo "FAIL"; echo "$$result"; fail=1; \
else echo "OK"; fi; \
done; \
[ $$fail -eq 0 ] || exit 1
@echo ""
@echo "✓ Shell lint passed"
# Scan third-party Docker images for HIGH/CRITICAL CVEs (Trivy).
# Third-party image list for CVE scanning. Keep in sync with docs/reference/DOCKER_IMAGES.md.
# Fails only on CRITICAL findings; HIGH findings are reported but non-blocking.
# Scans only pinned third-party images (not images we build ourselves — use scan-first-party for those).
# Shared Trivy cache (Phase 227): mounted into every trivy container so the
# vulnerability DB is downloaded ONCE per `make scan` instead of re-fetched for
# each image. In CI this dir is also restored/saved via actions/cache.
TRIVY_CACHE ?= $(HOME)/.cache/trivy
# Covers images deployed in deploy/docker/docker-compose.prod.yml + deploy/docker/docker-compose.monitoring.yml.
# Phase 810: derived from those compose files via
# scripts/check_image_versions.py --list-third-party instead of hand-copied
# here — a hand-maintained duplicate is exactly how this list went stale
# (one image's tag) and silently incomplete (three images added to
# docker-compose.monitoring.yml over time were never added here, so they went
# unscanned indefinitely). The compose files are now the only place a
# third-party image version is written down. Lazily-expanded (`=`, not `:=`)
# so the script only runs when a recipe references $(TRIVY_IMAGES), not on
# every `make` invocation. See scan-images below for the failure-mode guard
# (an empty or error'd list must not silently scan zero images).
TRIVY_IMAGES = $(shell $(PYTHON) scripts/check_image_versions.py --list-third-party)
# Manifest consistency check. Verifies: TODO.md + PROJECT_STATUS.md generate
# cleanly from manifest.yaml; every COMPLETE phase has a CHANGELOG entry (or
# is in the frozen HISTORICAL_CHANGELOG_GAPS baseline — see the script).
# Phase 800: wired into lint-docs-all (below) so this is a real, CI-enforced
# gate, not an easy-to-forget manual step before a phase close-out. It had
# drifted to 544 permanent failures (240 CHANGELOG + a since-retired,
# CLAUDE.md-contradicting 304-failure TABLE check) before nobody running it
# regularly let that happen — see the script's own docstring.
check-manifest: ## Verify manifest.yaml / TODO.md / CHANGELOG.md stay consistent
@$(PYTHON) scripts/check_manifest.py
scan-images: ## Trivy scan of third-party images (fails on HIGH or CRITICAL)
@mkdir -p "$(TRIVY_CACHE)"
@echo "=== Trivy: third-party image CVE scan (HIGH + CRITICAL) ==="
@echo " Fails on HIGH or CRITICAL. Waivers are authored in security/waivers.yaml"
@echo " and generated into .trivyignore.third-party by scripts/render_waivers.py."
@echo ""
@images="$$($(PYTHON) scripts/check_image_versions.py --list-third-party)" || { \
echo "✗ Could not derive the third-party image list from the compose"; \
echo " files (version drift between them?) — fix that before scanning."; \
exit 1; \
}; \
[ -n "$$images" ] || { echo "✗ scripts/check_image_versions.py --list-third-party returned no images — check compose file paths"; exit 1; }; \
fail=0; \
for img in $$images; do \
echo " Scanning $$img ..."; \
TRIVY_CACHE="$(TRIVY_CACHE)" $(PYTHON) scripts/trivy_image_report.py --ignorefile /scan/.trivyignore.third-party "$$img" || fail=1; \
done; \
[ $$fail -eq 0 ] || { \
echo "✗ HIGH/CRITICAL CVEs found, or a scan could not be run — Phase 847b: a"; \
echo " scan that fails to produce JSON is treated as a FAILURE, never a pass."; \
echo " Waivers are authored in security/waivers.yaml and generated into"; \
echo " .trivyignore.third-party by scripts/render_waivers.py — see"; \
echo " docs/runbooks/security_scan_exceptions.md"; \
exit 1; \
}; \
echo "✓ Image scan complete — no findings"
# Trivy misconfiguration scan of Dockerfiles and compose files.
# Does NOT require building images — analyses file content only.
# Catches: running as root, missing HEALTHCHECK, ADD with URL, exposed secrets, missing no-new-privileges.
# Policy: HIGH + CRITICAL → exit 1 (we own these files; zero tolerance).
scan-dockerfiles: ## Trivy config scan of Dockerfiles + compose files (HIGH/CRITICAL → fail)
@mkdir -p "$(TRIVY_CACHE)"
@echo "=== Trivy: Dockerfile/compose misconfiguration scan (HIGH + CRITICAL) ==="
@echo " Fails on HIGH or CRITICAL findings."
@fail=0; \
for f in deploy/docker/Dockerfile.go-proxy deploy/docker/Dockerfile.management \
deploy/docker/Dockerfile.mockbackend deploy/docker/Dockerfile.test \
deploy/docker/Dockerfile.trafficgen deploy/docker/Dockerfile.cli \
deploy/docker/Dockerfile.ja4-tap \
; do \
if [ -f "$$f" ]; then \
echo ""; \
echo " ── Scanning $$f ──"; \
docker run --rm \
-v "$(PWD):/scan:ro" \
-v "$(TRIVY_CACHE):/root/.cache/trivy" \
aquasec/trivy:0.71.0 config \
--severity HIGH,CRITICAL --exit-code 1 \
--skip-version-check \
"/scan/$$f" || fail=1; \
fi; \
done; \
[ $$fail -eq 0 ] || { echo ""; echo "✗ Dockerfile scan failed — fix HIGH/CRITICAL findings above"; exit 1; }
@echo ""
@echo "✓ All Dockerfiles passed"
@echo " (compose files validated separately via 'docker compose config' in CI)"
# Build every first-party image the CVE gates cover. Split out of
# `scan-first-party` (Phase 847c) so the PR regression gate's CI job can build
# exactly the prerequisites it needs — including the profile-gated
# test/trafficgen/tap images that 'make build' skips — without duplicating these
# build lines into workflow YAML, where they would silently drift from this file.
# Builds every image `scan-first-party` / the baseline model scans.
#
# Phase 847c follow-up (found by the first CI run of scan-baseline-monitor.yml):
# the five `make build`-produced images used to be built only when NOT in CI
# (`[ -z "$CI" ]`), because the one CI caller at the time — the `Security Scan`
# job — had already built them via `make build`, and a --no-cache rebuild of
# five images is minutes of redundant CI time. But 847c added two callers that
# do NOT run `make build` first: the monitor workflow, and the scan-regression
# PR gate when a first-party surface is in scope. Under the old guard both got
# "unable to find the specified image" and exit 2 — fail-closed, but a monitor
# that can never scan and a PR gate that reddens any PR touching a first-party
# Dockerfile. The guard now keys on *presence in this job* instead of on `$CI`:
# local runs still rebuild --no-cache (Phase 800's stale-layer-cache rationale),
# the Security Scan job still pays nothing (the images are present), and any
# other caller — CI or local — gets the images it needs.
build-first-party-images:
@if [ -z "$$CI" ]; then \
echo " Rebuilding all first-party images with --no-cache (local run)..."; \
DOCKER_BUILDKIT=1 docker build --no-cache -q -f deploy/docker/Dockerfile.go-proxy -t ja4proxy:2.0.0 . >/dev/null; \
DOCKER_BUILDKIT=1 docker build --no-cache -q -f src/analytics/Dockerfile -t ja4proxy-analytics:1.0.0 . >/dev/null; \
DOCKER_BUILDKIT=1 docker build --no-cache -q -f src/tarpit/Dockerfile -t ja4proxy-tarpit:1.0.0 src/tarpit >/dev/null; \
DOCKER_BUILDKIT=1 docker build --no-cache -q -f deploy/docker/Dockerfile.mockbackend -t ja4proxy-mockbackend:1.0.0 . >/dev/null; \
DOCKER_BUILDKIT=1 docker build --no-cache -q -f deploy/docker/Dockerfile.management -t ja4proxy-management:1.0.0 . >/dev/null; \
else \
echo " CI: ensuring each first-party image exists in this job (no rebuild if present)..."; \
for spec in \
"deploy/docker/Dockerfile.go-proxy ja4proxy:2.0.0 ." \
"src/analytics/Dockerfile ja4proxy-analytics:1.0.0 ." \
"src/tarpit/Dockerfile ja4proxy-tarpit:1.0.0 src/tarpit" \
"deploy/docker/Dockerfile.mockbackend ja4proxy-mockbackend:1.0.0 ." \
"deploy/docker/Dockerfile.management ja4proxy-management:1.0.0 ."; do \
set -- $$spec; df=$$1; tag=$$2; ctx=$$3; \
if docker image inspect "$$tag" >/dev/null 2>&1; then \
echo " $$tag: already present, not rebuilding"; \
else \
DOCKER_BUILDKIT=1 docker build -q -f "$$df" -t "$$tag" "$$ctx" >/dev/null; \
fi; \
done; \
fi
@echo " Building the profile-gated CI-only test/trafficgen images (skipped by 'make build')..."
@DOCKER_BUILDKIT=1 docker build --no-cache -q -f deploy/docker/Dockerfile.test -t ja4proxy-test:1.0.0 . >/dev/null
@DOCKER_BUILDKIT=1 docker build --no-cache -q -f deploy/docker/Dockerfile.trafficgen -t ja4proxy-trafficgen:1.0.0 . >/dev/null
@echo " Building ja4proxy-tap (deploy/docker/docker-compose.prod.yml's 'tap' profile, skipped by 'make build')..."
@DOCKER_BUILDKIT=1 docker build --no-cache -q -f deploy/docker/Dockerfile.ja4-tap -t ja4proxy-tap:1.0.0 . >/dev/null
# CVE scan of first-party images we build.
# Policy (Phase 317): HIGH + CRITICAL → exit 1. We build/own these images, so a
# HIGH is actionable (base bump / dep bump / dated .trivyignore.first-party). The profile-gated
# CI-only test + trafficgen images are skipped by 'make build', so this target
# builds them explicitly — otherwise they would be scanned-as-absent (a vacuous pass).
scan-first-party: build-first-party-images ## Trivy CVE scan of built images (fails on HIGH or CRITICAL)
@mkdir -p "$(TRIVY_CACHE)"
@echo "=== Trivy: first-party image CVE scan (HIGH + CRITICAL) ==="
@echo " Phase 317: fails on HIGH or CRITICAL. .trivyignore.first-party holds dated exceptions."
@echo " Phase 800: a cached apk/apt layer can sit for weeks and silently hide a"
@echo " since-fixed OS CVE (observed: ja4proxy-test carried a 5-week-stale c-ares"
@echo " layer that a plain 'make build' never re-executed). CI runners are always"
@echo " cache-free, so this only bites local runs — rebuild --no-cache here to match."
@echo ""
@fail=0; \
for img in ja4proxy:2.0.0 ja4proxy-analytics:1.0.0 ja4proxy-tarpit:1.0.0 ja4proxy-mockbackend:1.0.0 ja4proxy-management:1.0.0 ja4proxy-test:1.0.0 ja4proxy-trafficgen:1.0.0 ja4proxy-tap:1.0.0; do \
echo " Scanning $$img ..."; \
TRIVY_CACHE="$(TRIVY_CACHE)" $(PYTHON) scripts/trivy_image_report.py \
--ignorefile /scan/.trivyignore.first-party --docker-socket "$$img" || fail=1; \
done; \
[ $$fail -eq 0 ] || { \
echo "✗ HIGH/CRITICAL CVEs found, or a scan could not be run — Phase 847b:"; \
echo " a scan that fails to produce JSON is a FAILURE, never a pass."; \
echo " Fix it by bumping the base image or dependency, or add a justified,"; \
echo " dated .trivyignore.first-party entry — authored in"; \
echo " security/waivers.yaml (scripts/render_waivers.py), see"; \
echo " docs/runbooks/security_scan_exceptions.md"; \
exit 1; \
}
@echo "✓ First-party image scan complete (HIGH + CRITICAL gate)"
# Phase 228: human-readable rollup of the image CVE scans. Reporting only —
# `make scan` remains the authoritative gate. First-party rows require `make build`.
FIRST_PARTY_IMAGES := ja4proxy:2.0.0 ja4proxy-analytics:1.0.0 ja4proxy-tarpit:1.0.0 ja4proxy-mockbackend:1.0.0 ja4proxy-management:1.0.0 ja4proxy-test:1.0.0 ja4proxy-trafficgen:1.0.0 ja4proxy-tap:1.0.0
scan-summary: ## Phase 228 — compact CRIT/HIGH/MED rollup of all scans (images + misconfig + gosec; reporting only)
@mkdir -p "$(TRIVY_CACHE)"
@# Phase 847a: pass the correct scope-split ignorefile (Phase 822 deleted the
@# single .trivyignore this used to hard-code, so waivers had stopped applying).
@$(PYTHON) scripts/scan_summary.py --ignorefile .trivyignore.third-party $(TRIVY_IMAGES)
@$(PYTHON) scripts/scan_summary.py --ignorefile .trivyignore.first-party $(FIRST_PARTY_IMAGES)
@# gosec rollup (best-effort; builds the pinned security-scan image)
@docker build -q -t ja4proxy-security-scan -f deploy/docker/security-scan/Dockerfile . >/dev/null 2>&1 \
&& docker run --rm -v "$(PWD):/app" ja4proxy-security-scan \
"gosec -fmt=json -exclude-dir=.claude ./... 2>/dev/null" \
| $(PYTHON) scripts/scan_summary.py gosec \
|| echo " ! gosec summary skipped (build/docker unavailable)"
scan-exceptions: ## List .trivyignore.first-party / .trivyignore.third-party exceptions with days-to-expiry
@$(PYTHON) scripts/scan_exceptions.py
# Phase 847c: the PR regression gate. Scans ONLY the surfaces this branch
# touches and fails only on findings absent from security/baseline.json, so an
# upstream CVE disclosed overnight cannot redden an unrelated PR — while a PR
# that bumps an image or a dependency still gets that surface scanned.
BASE_REF ?= origin/main
scan-regression: ## PR gate: scan only the surfaces this branch touches (fails on NEW unwaived findings)
@echo "=== Phase 847c regression gate (base: $(BASE_REF)) ==="
@$(PYTHON) scripts/scan_regression.py --base "$(BASE_REF)"
scan-regression-plan: ## Print the surfaces this branch touches as JSON (no scanning; used by CI)
@$(PYTHON) scripts/scan_regression.py --base "$(BASE_REF)" --plan
# Phase 847f: every waiver's `reachability:` claim names a registered check, and
# the check is executed against the live compose files and source tree. A
# premise that has stopped being true — a service that was internal-only gains
# a published port, a dev dependency gets promoted into a runtime image — fails
# here and asks for re-justification. It never deletes the waiver: deleting the
# waiver removes the *record* of the exposure, not the exposure.
#
# `tests/unit/test_reachability_checks.py` runs the same checks under `make
# test`; this target is the fast, standalone signal for humans and for the
# pre-PR path.
reachability-check: tools-image ## Verify every waiver's reachability premise still holds
@echo "=== Phase 847f reachability checks ==="
@$(TOOLS_RUN) python scripts/reachability_checks.py
# Phase 847c: record the baseline the regression gate judges against. Run from
# main after images/deps change. First-party images must be built first
# ('make build', plus 'make scan-first-party' for the profile-gated ones); a
# missing image fails the recording rather than being silently omitted, because
# an omitted surface would read as "nothing known here" at gate time.
scan-baseline: ## Record security/baseline.json (third-party + first-party images + Python deps)
@mkdir -p "$(TRIVY_CACHE)"
@echo "=== Recording the CVE baseline ==="
@TRIVY_CACHE="$(TRIVY_CACHE)" $(PYTHON) scripts/scan_baseline.py --record --images $(FIRST_PARTY_IMAGES)
# Phase 847c: the scheduled monitor (the other half of the split). Full scan,
# drift vs the baseline, one deduplicated issue per unwaived finding. Never
# blocks a PR — it is deliberately not a required check. Needs the first-party
# images built, same as scan-baseline.
scan-monitor: ## Full CVE scan, drift report, and (APPLY=1) deduplicated issues
@mkdir -p "$(TRIVY_CACHE)"
@TRIVY_CACHE="$(TRIVY_CACHE)" $(PYTHON) scripts/scan_baseline_monitor.py --images $(FIRST_PARTY_IMAGES) $(if $(APPLY),--apply,) $(if $(STATE_OUT),--state-out "$(STATE_OUT)",)
# Phase 849 (issue #622): GitHub does not dispatch workflows for events caused by
# the default GITHUB_TOKEN, so a PR merged by the Actions app lands on main with
# no `push` run — the commit that is now the tip was never built. This asks the
# one question, and `APPLY=1` repairs a "no" by dispatching ci.yml against main.
verify-main: ## Ensure main's tip has a CI run (APPLY=1 dispatches CI when an app-token merge skipped it)
$(PYTHON) scripts/ensure_main_verified.py $(if $(APPLY),--apply,)
# Phase 847e: measure the MEDIUM vulnerability surface, and record it. The
# measurement is what the MEDIUM gating scope was decided from, so it has to be
# re-runnable and reproducible: same images, same database, same numbers.
# Needs the first-party images built, same as scan-baseline.
medium-inventory: ## Measure the MEDIUM surface and record security/baseline-medium.json
@mkdir -p "$(TRIVY_CACHE)"
@TRIVY_CACHE="$(TRIVY_CACHE)" $(PYTHON) scripts/medium_inventory.py
# Detect :latest tags and version drift between compose files.
# Runs in < 5 seconds — no external calls, no Docker required.
check-image-versions: ## Detect `:latest` tags and version drift across compose files
@$(PYTHON) scripts/check_image_versions.py
# Lint YAML config and monitoring files with yamllint.
# Line-length disabled (long Prometheus expressions are unavoidable).
# See .yamllint.yaml for full config.
# `monitoring/` moved to `deploy/monitoring/` in phase-205's repo-root cleanup;
# this reference was never updated, so on a clean checkout (no repo-root
# `monitoring/` dir) `lint-yaml` always crashed with FileNotFoundError. Only
# ever passed locally here because of a stray, untracked, root-owned
# `monitoring/` directory left over on this host from an unrelated Docker run
# — never present in git, so CI always hit this (phase-514).
YAML_DIRS := config deploy/monitoring
# Containerised in phase-800: this used to call a bare host `yamllint` with no
# `command -v` guard — the only linter in the repo not running from a pinned
# image. It hard-failed anywhere yamllint was absent, and silently used whatever
# version the host happened to have otherwise. Now pinned in Dockerfile.tools.
lint-yaml: tools-image ## yamllint `config/` and `monitoring/`
@echo "=== yamllint: config and monitoring YAML ==="
@$(TOOLS_RUN) yamllint -c .yamllint.yaml $(YAML_DIRS) && echo "✓ YAML lint passed"
# Validate Prometheus alert and recording rule files with promtool.
# Uses the prometheus container so promtool version matches the deployed version.
PROM_RULES := /monitoring/prometheus/alerts.yml \
/monitoring/prometheus/recording_rules.yml \
/monitoring/prometheus/slo_recording_rules.yml \
/monitoring/alertmanager/rules/proxy.rules.yml \
/monitoring/alertmanager/rules/redis.rules.yml \
/monitoring/alertmanager/rules/security.rules.yml \
/monitoring/alertmanager/rules/backup.rules.yml \
/monitoring/alertmanager/rules/ebpf_attack.yml \
/monitoring/alertmanager/rules/management_ui_rules.yml \
/monitoring/alertmanager/rules/slo_alerts.yml \
/monitoring/alertmanager/rules/ti_feed.yml \
/monitoring/alertmanager/rules/tls_alerts.yml \
/monitoring/alertmanager/rules/tap.yml \
/monitoring/alertmanager/rules/performance.rules.yml
lint-prom: ## promtool check rules (alerts + recording)
@echo "=== promtool check rules: Prometheus alert + recording rules ==="
@docker run --rm --entrypoint promtool \
-v "$(PWD)/deploy/monitoring:/monitoring" \
prom/prometheus:v2.48.0 \
check rules $(PROM_RULES) \
&& echo "✓ Prometheus rules valid"
# Check Python and Go dependencies for known CVEs.
# pip-audit scans requirements.txt inside $(TOOLS_IMG); scan-container checks Go.
lint-deps: tools-image scan-container ## pip-audit (Python in container) + gosec/govulncheck
@echo "=== pip-audit: Python dependency CVEs (containerized) ==="
@$(TOOLS_RUN) pip-audit -r requirements.txt || true
@$(TOOLS_RUN) pip-audit -r requirements-test.txt || true
@$(TOOLS_RUN) pip-audit -r requirements-analytics.txt || true
@echo " (CVEs above are advisory — pinned versions may lag; update requirements.txt to remediate)"
@echo ""
@echo ""
@echo "✓ Dependency scan complete"
# Scan git history for accidentally committed secrets (gitleaks).
# Scans committed files only — working-tree secrets are handled by .gitignore.
# False positives are suppressed in .gitleaks.toml with rationale.
lint-secrets: ## gitleaks scan of git history
@echo "=== gitleaks: scan git history for secrets ==="
@docker run --rm \
-v "$(PWD):/repo" \
zricethezav/gitleaks:v8.30.1 detect \
--source /repo --no-banner --config /repo/.gitleaks.toml \
&& echo "✓ No secrets found in git history"
# Comprehensive Go linting with golangci-lint (errcheck, ineffassign, staticcheck, govet, unused).
# Uses the container's own Go — do not pass GOROOT. See .golangci.yaml for config and rationale.
# Pinned (was `:latest`, which drifts independently of any code change — a
# newer release enabling a new check turned this red with nothing in the diff
# to explain it; see phase-514). v1.64.5 (ci.yml's old `go install` pin) can't
# run at all here: its own toolchain (go1.24) is older than this repo's target
# (go1.26.4) and it refuses to load the config. Pin to v2.11.4 instead — the
# same version `:latest` currently resolves to, built with go1.26.1. Bump this
# and ci.yml's SAST/CI action pins together when intentionally upgrading.
lint-go-full: ## golangci-lint comprehensive
@echo "=== golangci-lint: Go code ==="
@docker run --rm \
-v "$(PWD):/app" -w /app \
golangci/golangci-lint:v2.12.0 \
golangci-lint run --config .golangci.yaml ./... \
&& echo "✓ Go lint passed"
# Lint the Redis Lua script with luacheck.
# KEYS, ARGV, and redis globals are declared in .luacheckrc (Redis runtime injects them).
LUACHECK_SCRIPTS := scripts/sliding_window.lua
lint-lua: ## luacheck Redis Lua scripts
@echo "=== luacheck: Lua/Redis scripts ==="
@docker run --rm \
-v "$(PWD):/repo" \
pipelinecomponents/luacheck \
luacheck --config /repo/.luacheckrc $(addprefix /repo/,$(LUACHECK_SCRIPTS)) \
&& echo "✓ Lua lint passed"
# Validate JSON files: Grafana dashboards and API spec.
# Phase 805: switched the dashboard portion to a glob (was a hand-maintained
# list missing 04_capacity.json and ja4proxy-infrastructure.json — neither
# had ever been JSON-lint-checked here) so a new dashboard file can't
# silently skip this gate again the way tap_sensor.json and this phase's
# ci_benchmark_trend.json would have.
JSON_FILES := $(wildcard deploy/monitoring/grafana/dashboards/*.json) \
docs/api/openapi.json
lint-json: ## JSON syntax validation
@echo "=== JSON syntax validation ==="
@fail=0; \
for f in $(JSON_FILES); do \
[ -f "$$f" ] || continue; \
printf " %-60s" "$$f"; \
if $(PYTHON) -m json.tool "$$f" > /dev/null 2>&1; then echo "OK"; \
else echo "FAIL"; fail=1; fi; \
done; \
[ $$fail -eq 0 ] || exit 1
@echo "✓ JSON lint passed"
# Validate Alertmanager configuration with amtool.
# Uses the alertmanager container to match the deployed version.
lint-alertmanager: ## amtool check-config
@echo "=== amtool check-config: Alertmanager ==="
@docker run --rm \
--entrypoint amtool \
-v "$(PWD)/deploy/monitoring:/monitoring" \
prom/alertmanager:v0.26.0 \
check-config /monitoring/alertmanager/alertmanager.yml \
&& echo "✓ Alertmanager config valid"
# Dummy values for the compose ${VAR:?required} interpolations so `down` can
# parse the file even when .env is missing/incomplete (the values are irrelevant
# for `down` — it only needs to identify containers by project + service name).
# Fixes: `make clean` aborting with "MANAGEMENT_JWT_SECRET is required" in a
# freshly-cloned worktree whose .env is incomplete.
CLEAN_DUMMY_ENV := MANAGEMENT_JWT_SECRET=_ MANAGEMENT_ADMIN_USER=_ MANAGEMENT_ADMIN_PASSWORD=_ REDIS_PASSWORD=_ GRAFANA_PASSWORD=_ BACKEND_HOST=_ HAPROXY_STATS_USER=_ HAPROXY_STATS_PASSWORD=_
# Clean up (agent-aware: uses .current-agent if set)
clean: ## Stop + remove all containers and volumes
$(eval _AGENT := $(shell cat .current-agent 2>/dev/null))
@echo "Cleaning up containers and volumes..."
@if [ -n "$(_AGENT)" ]; then \
$(CLEAN_DUMMY_ENV) docker compose -f deploy/docker/docker-compose.poc.yml --project-name ja4_$(_AGENT) $$([ -f .env.$(_AGENT) ] && echo --env-file .env.$(_AGENT)) down -v --remove-orphans; \
else \
$(CLEAN_DUMMY_ENV) docker compose -f deploy/docker/docker-compose.poc.yml $$([ -f .env ] && echo --env-file .env) down -v --remove-orphans; \
fi
BACKEND_HOST=_ docker compose -f deploy/docker/docker-compose.prod.yml down -v --remove-orphans 2>/dev/null || true
# Remove reports and cache directories, ignoring errors (e.g., from container-created files with restrictive permissions)
rm -rf reports/ test-results/ __pycache__/ .pytest_cache/ .mypy_cache/ .ruff_cache/ .hypothesis/ .qwen/ 2>/dev/null || true
rm -f ja4proxy ja4check ja4proxy-cli bin/ja4p bin/ja4check bin/ja4p-cli 2>/dev/null || true
# Full clean rebuild from scratch — wipes volumes, removes built images, rebuilds, starts.
# If .current-agent is set (i.e. an agent stack is active), rebuilds and restarts that
# agent's stack. Otherwise rebuilds and restarts the default ja4proxy stack.
rebuild: clean build ## Wipe volumes/images, rebuild from scratch, start fresh
$(eval _AGENT := $(shell cat .current-agent 2>/dev/null))
@echo "Starting clean rebuild..."
@if [ -n "$(_AGENT)" ]; then \
echo " (active agent: $(_AGENT))"; \
docker compose -f deploy/docker/docker-compose.poc.yml --project-name ja4_$(_AGENT) --env-file .env.$(_AGENT) down -v --remove-orphans --rmi local; \
else \
docker compose -f deploy/docker/docker-compose.poc.yml --env-file .env down -v --remove-orphans --rmi local; \
fi
docker compose -f deploy/docker/docker-compose.monitoring.yml down -v --remove-orphans --rmi local 2>/dev/null || true
@echo "Rebuilding all images (no cache, but preserving apt/pip build caches)..."
DOCKER_BUILDKIT=1 COMPOSE_DOCKER_CLI_BUILD=1 \
VERSION=$(FULL_VERSION) GIT_COMMIT=$(GIT_COMMIT) BUILD_DATE=$(BUILD_DATE) \
docker compose -f deploy/docker/docker-compose.poc.yml --env-file .env build --no-cache
@echo "Starting full stack..."
@if [ -n "$(_AGENT)" ]; then \
docker compose -f deploy/docker/docker-compose.poc.yml --project-name ja4_$(_AGENT) --env-file .env.$(_AGENT) up -d; \
else \
./scripts/start-all.sh; \
fi
# Deploy PoC environment
deploy-poc: ## Deploy PoC environment
@./scripts/start-poc.sh
# Deploy enterprise environment
deploy-enterprise: ## Deploy enterprise environment (sudo)
@echo "Running enterprise deployment script..."
@sudo ./scripts/deploy.sh production
# Health checks (agent-aware: uses .current-agent if set)
health-check: ## Run health checks against metrics + Redis
$(eval _AGENT := $(shell cat .current-agent 2>/dev/null))
@ENVFILE=$$([ -n "$(_AGENT)" ] && echo ".env.$(_AGENT)" || echo ".env"); \
IP=$$(grep '^AGENT_BIND_IP=' "$$ENVFILE" 2>/dev/null | cut -d= -f2); IP=$${IP:-localhost}; \
RPASS=$$(grep '^REDIS_PASSWORD=' "$$ENVFILE" 2>/dev/null | cut -d= -f2); RPASS=$${RPASS:-changeme}; \
FLAGS=$$([ -n "$(_AGENT)" ] && echo "--project-name ja4_$(_AGENT) --env-file $$ENVFILE" || true); \
echo "Running health checks ($$IP)..."; \
curl -sf "http://$$IP:9090/metrics" > /dev/null && echo "✓ Proxy metrics OK" || echo "✗ Proxy metrics failed"; \
curl -sk "https://$$IP:8443/api/health" > /dev/null && echo "✓ Backend OK" || echo "✗ Backend failed"; \
docker compose -f deploy/docker/docker-compose.poc.yml --env-file .env $$FLAGS exec -T redis redis-cli -a "$$RPASS" ping > /dev/null 2>&1 && echo "✓ Redis OK" || echo "✗ Redis failed"
# View logs (agent-aware: uses .current-agent if set)
logs: ## Stream proxy container logs
$(eval _AGENT := $(shell cat .current-agent 2>/dev/null))
@if [ -n "$(_AGENT)" ]; then \
docker compose -f deploy/docker/docker-compose.poc.yml --project-name ja4_$(_AGENT) --env-file .env.$(_AGENT) logs -f proxy; \
else \
docker compose -f deploy/docker/docker-compose.poc.yml --env-file .env logs -f proxy; \
fi
# Flush all transient security state from Redis (bans, blocks, rate windows, audit logs)
# Preserves ja4:whitelist and ja4:blacklist so config survives the flush.
# Agent-aware: uses .current-agent if set.
flush-redis: ## Reset bans/blocks/rates (keeps whitelist/blacklist)
$(eval _AGENT := $(shell cat .current-agent 2>/dev/null))
@ENVFILE=$$([ -n "$(_AGENT)" ] && echo ".env.$(_AGENT)" || echo ".env"); \
REDIS_PASS=$$(grep '^REDIS_PASSWORD=' "$$ENVFILE" 2>/dev/null | cut -d= -f2); \
if [ -z "$$REDIS_PASS" ]; then echo "✗ No REDIS_PASSWORD in $$ENVFILE"; exit 1; fi; \
FLAGS=$$([ -n "$(_AGENT)" ] && echo "--project-name ja4_$(_AGENT) --env-file $$ENVFILE" || true); \
echo "Flushing Redis security state$$([ -n "$(_AGENT)" ] && echo " (agent: $(_AGENT))")..."; \
COUNT=$$(docker compose -f deploy/docker/docker-compose.poc.yml --env-file .env $$FLAGS exec -T redis redis-cli -a "$$REDIS_PASS" --no-auth-warning \
EVAL "local n=0; \
for _,p in ipairs({'rate:*','banned:*','blocked:*','suspicious:*','enforcement:*','audit:*','repeat_block:*'}) do \
for _,k in ipairs(redis.call('keys',p)) do redis.call('del',k); n=n+1 end \
end; return n" 0 2>/dev/null); \
echo "✓ Cleared $$COUNT keys (whitelist/blacklist preserved)"
# ── ja4db feed management ─────────────────────────────────────────────────────
# Fetch new malicious fingerprints from FoxIO GitHub / ja4db.com, queue for review
fetch-db: ## Fetch new malicious fingerprints from ja4db/FoxIO
@./scripts/fetch-ja4db.sh
# Show fingerprints awaiting approval
list-pending: ## Show fingerprints awaiting admin approval
@./scripts/ja4-admin.sh list-pending
# Approve all pending fingerprints (prompts for confirmation)
approve-all: ## Approve all pending fingerprints
@./scripts/ja4-admin.sh approve-all
# ── GeoIP monitoring ───────────────────────────────────────────────────────────
# Full blocking report (countries, CIDRs, fingerprints, Prometheus summary)
geoip-report: ## Full blocking report
@./scripts/ja4-admin.sh report
# Run the GeoIP monitor once — auto-blocks attacking countries, respects safe list
geoip-monitor: ## Auto-block attacking countries (run once)
@./scripts/geoip-monitor.sh
# Run geoip-monitor in watch mode (loops every 60s, Ctrl-C to stop)
geoip-watch: ## Auto-block attacking countries (continuous loop)
@./scripts/geoip-monitor.sh --watch
# Download the latest IP2Location LITE country database (run monthly)
# Requires proxy restart after update: make stop && make start
update-geoip: ## Download latest IP2Location LITE DB (monthly)
@./scripts/update-geoip.sh
# Check how old the current GeoIP database is (no download)
check-geoip: ## Check age of current GeoIP database
@./scripts/update-geoip.sh --check
# ── Incident response shortcuts (wrappers for scripts/ja4-admin.sh) ──────────
# Quick security snapshot: active bans, block totals, top threats
attack-status: ## Quick security snapshot
@./scripts/ja4-admin.sh status
# Top 10 fingerprints by traffic (red = blocked, green = allowed)
top-attackers: ## Top 10 fingerprints by traffic
@./scripts/ja4-admin.sh top 10
# Blacklist a JA4 fingerprint (instant TCP RST, permanent)
# Usage: make block-ja4 FP=t13d190900_9dc949149365_97f8aa674fd9
block-ja4: ## Blacklist a JA4 fingerprint (FP=...)
@[ -n "$(FP)" ] || (echo "Usage: make block-ja4 FP=<fingerprint>"; exit 1)
@./scripts/ja4-admin.sh block-ja4 $(FP)
# Hard-block an IP for 1 hour
# Usage: make block-ip IP=203.0.113.42
block-ip: ## Hard-block an IP address for 1 hour (IP=...)
@[ -n "$(IP)" ] || (echo "Usage: make block-ip IP=<address>"; exit 1)
@./scripts/ja4-admin.sh block-ip $(IP) 3600
# Remove all blocks/bans for an IP
# Usage: make unblock-ip IP=203.0.113.42
unblock-ip: ## Remove blocks/bans for an IP (IP=...)
@[ -n "$(IP)" ] || (echo "Usage: make unblock-ip IP=<address>"; exit 1)
@./scripts/ja4-admin.sh unblock-ip $(IP)
# Run performance tests
# poc-secrets (phase-800): docker-compose.poc.yml bind-mounts five secret files
# that nothing in the repo created. On a machine that had not been hand-primed,
# every target raising the PoC stack died with a bare Docker error —
# "bind source path does not exist: .../analytics_redis_password.txt" — which is
# what aborted `make bench-all` before test-go-perf, load-test and measure-mttr
# ever ran. Names are derived from the compose file, so adding a secret there
# cannot silently break this again. Existing files are never overwritten.
poc-secrets: ## Generate any missing deploy/secrets/*.txt the PoC stack needs
@echo "=== PoC secrets ==="
@scripts/ensure-poc-secrets.sh
perf-test: ## (Retired) Formerly ran Locust performance tests; use make bench-macro
@echo "perf-test: Locust performance tests have been retired (Phase 819b)."
@echo "Use 'make bench-macro' or 'make bench-hostnative' for truthful proxy load benchmarking."
# ── Configuration ─────────────────────────────────────────────────────────────
# Set the blocking dial (0 = monitor only, 100 = full blocking)
# Usage: make dial LEVEL=50
dial: ## Set blocking dial 0-100 (LEVEL=...)
@[ -n "$(LEVEL)" ] || (echo "Usage: make dial LEVEL=<0-100>"; exit 1)
@$(PYTHON) scripts/set_dial.py $(LEVEL)
# ── Operations ────────────────────────────────────────────────────────────────
# Print SSH tunnel command for the default (non-agent) stack.
# For agent stacks use: make tunnel NAME=<agent> [HOST=user@server]
ssh-tunnels: ## Print SSH tunnel command for default stack
@echo ""
@echo "NOTE: For agent stacks use: make tunnel NAME=<agent> HOST=user@server"
@echo ""
@echo "Default stack — run this on your laptop:"
@echo ""
@echo " ssh -L 9091:localhost:9091 -L 9093:localhost:9093 -L 8404:localhost:8404 USER@HOST"
@echo ""
@echo "Then browse to:"
@echo " http://localhost:9091 — Prometheus"
@echo " http://localhost:9093 — Alertmanager"
@echo " http://localhost:8404/stats — HAProxy stats"
@echo " http://localhost:3001 — Grafana"
@echo ""
# ── Proxy Operations ──────────────────────────────────────────────────────────
# Prefer snap Go if present (local dev), else fall back to env/system GOROOT.
GOROOT := $(shell if [ -d /snap/go/current ]; then echo /snap/go/current; elif [ -n "$$GOROOT" ]; then echo "$$GOROOT"; else go env GOROOT 2>/dev/null; fi)
GO := GOROOT=$(GOROOT) go
# Build the Go proxy binary into bin/ja4p
# Run all Go unit tests
go-test: ## Run all Go unit tests
$(GO) test ./... -count=1
# Run all Go tests under the race detector (phase-518). This is the enforcement
# arm for the concurrency regression tests — several of them (config hot-reload
# snapshot JA4PROXY-2026-0088, forward() config capture 0068, worker lifecycle
# 0090) only *detect* a regression when run with -race, and the plain `make test`
# gate does not pass -race. Requires cgo (a C toolchain); CI runners have one.
test-race: ## Run Go unit tests under the race detector
$(GO) test -race ./... -count=1
# Run go vet (static analysis)
go-lint: ## Run `go vet` on Go code
@if command -v go >/dev/null 2>&1 || [ -x /snap/go/current/bin/go ]; then \
$(GO) vet ./...; \
else \
docker run --rm -v "$$PWD":/src -w /src golang:1.27.3-alpine go vet ./...; \
fi
# Audit Python and Go signal scores against registry (Phase 65)
check-scores: tools-image ## Audit Python and Go signal scores against registry
@$(TOOLS_RUN) python scripts/check-signal-scores.py
# ── Security Scans ────────────────────────────────────────────────────────────
# Run OpenSSF Scorecard locally via Docker (Phase 131)
scorecard-local: ## OpenSSF Scorecard (local, advisory — the gate is scorecard.yml)
@echo "=== OpenSSF Scorecard (Local — advisory; the required gate is the scorecard.yml workflow) ==="
@echo "Note: Full checks require a GitHub token with repo scope (make scorecard-local GITHUB_AUTH_TOKEN=...)."
@if [ -z "$$GITHUB_AUTH_TOKEN" ]; then \
echo "Warning: GITHUB_AUTH_TOKEN not set — some checks will be skipped or fail."; \
docker run --rm -v $$(pwd):/workspace gcr.io/openssf/scorecard:latest --local /workspace --format default \
|| echo " ! scorecard reported issues or could not read the tree (advisory)"; \
else \
docker run --rm -e GITHUB_AUTH_TOKEN=$$GITHUB_AUTH_TOKEN -v $$(pwd):/workspace gcr.io/openssf/scorecard:latest --local /workspace --format default \
|| echo " ! scorecard reported issues (advisory)"; \
fi
scan-container: ## Run Go SAST (gosec) in a container — gates on high-severity/high-confidence findings
@docker build -q -t ja4proxy-security-scan -f deploy/docker/security-scan/Dockerfile .
@docker run --rm -v "$(PWD):/app" ja4proxy-security-scan \
"gosec -severity high -confidence high -exclude-dir=.claude -quiet ./..."
scan-local: ## Run gosec Go SAST scanner locally
@(gosec -fmt=text -exclude-dir=.claude ./... || true)
scan: ## Phase 146 — Run all security and container scans
@$(MAKE) scan-all
@python3 scripts/pipeline_summary.py scan
scan-all: scan-container scan-dockerfiles scan-first-party scan-images ## Run all security scans (container, dockerfiles, 1st-party, images)
@echo "✓ All blocking scan gates passed (HIGH=0, CRITICAL=0)"
# Analyze Docker containers and dependencies for version discrepancies
check-updates-container: ## Run dependency update checker in container
@echo "=== Containerized Update Check (Go, Python, Docker, Node) ==="
@docker build -q -t ja4proxy-update-checker -f deploy/docker/update-checker/Dockerfile .
@docker run --rm -v "$(PWD):/app" ja4proxy-update-checker
check-updates-local: ## Run dependency update checker locally
@echo "=== Local Update Check (Go, Python, Docker, Node) ==="
@$(PYTHON) scripts/check_updates.py
check-updates: check-updates-container ## Check Python/Go/Docker dependency versions
# Start Python legacy proxy alongside the Go proxy for parity comparison.
# Both proxies share the same Redis instance.
# Go proxy (primary): host port 8081 / metrics 9090
# Python proxy (legacy): host port 8083 / metrics 9094
# Stop Python legacy proxy (Go proxy unaffected)
# Confirm Go proxy is HAProxy primary (Go is the default — verify config is correct).
# See docs/runbooks/go_proxy_migration.md for background.
# Emergency rollback: switch HAProxy to Python legacy proxy.
# Run cross-language parity tests.
# Both proxies must be running: Python on :8080, Go on :8082.
# ── Docker test harness ────────────────────────────────────────────────────────
# Generate ClientHello fixtures (curl + openssl + synthetic)
capture-fixtures: ## Generate ClientHello `.bin` fixtures (curl + openssl)
@bash scripts/generate_fixtures.sh
# Build ja4check utility (reads .bin file, prints JA4 fingerprint)
go-build-ja4check: ## Build `bin/ja4check` utility
@echo "Building ja4check..."
@mkdir -p bin
$(GO) build -o bin/ja4check ./cmd/ja4check
@echo "✓ bin/ja4check"
# Run Go integration tests inside Docker (full test harness, self-contained)
test-go-docker: ## Go integration tests inside Docker (self-contained)
@echo "Starting Go proxy test stack..."
docker compose -f deploy/docker/docker-compose.test.yml up --build --abort-on-container-exit --exit-code-from test-runner test-runner
# Run Go integration tests locally (requires Go proxy running on GO_PROXY_PORT)
test-go-integration: ## Go integration tests locally
$(PYTHON) -m pytest tests/integration/test_go_python_parity.py -v
# Run Go chaos tests locally (requires Go proxy running on GO_PROXY_PORT)
test-go-chaos: ## Go chaos tests locally
$(PYTHON) -m pytest tests/chaos/test_go_proxy_chaos.py -v
# Run Go performance benchmarks locally (requires Go proxy running on GO_PROXY_PORT / HOST_PORT_DIRECT)
test-go-perf: tools-image ## Go performance benchmarks
@docker run --rm --net=host -v $(PWD):/src -w /src $(TOOLS_IMG) pytest tests/performance/test_bench_go_proxy.py -v -s
# Run all Go tests: build binaries, generate fixtures, then run test suite
test-go: ## Build + test all Go (build binaries, generate fixtures, run suite)
$(MAKE) go-build
$(MAKE) go-build-ja4check
$(MAKE) capture-fixtures
$(PYTHON) -m pytest \
tests/integration/test_go_python_parity.py \
tests/chaos/test_go_proxy_chaos.py \
tests/performance/test_bench_go_proxy.py \
-v --tb=short
# Phase 201: smoke-test the Go Redis client against a real TLS-terminated Redis.
# Generates a self-signed cert, brings up the redis-tls compose overlay, runs the
# in-package TLS unit suite (which dials real TLS listeners via the harness),
# then tears down. Real TLS handshake; no shortcuts.
test-go-redis-tls: ## Go Redis TLS smoke test
@bash deploy/docker/redis-tls/generate-certs.sh
@docker compose -f deploy/docker/docker-compose.redis-tls.yml up -d --wait
@trap 'docker compose -f deploy/docker/docker-compose.redis-tls.yml down -v' EXIT; \
GOROOT=$(GOROOT) go test -race -count=1 ./internal/redis/... -run 'TLS|HealthCheck'
test-go-fuzz-smoke: ## Run Go native fuzz smoke tests across all targets
$(GO) test -run='^$$' -fuzz='^FuzzClientHello$$' -fuzztime=5s ./cmd/ja4pd/
$(GO) test -run='^$$' -fuzz='^FuzzReadProxyProtocol$$' -fuzztime=5s ./cmd/ja4pd/
$(GO) test -run='^$$' -fuzz='^FuzzReadProxyProtocolV2$$' -fuzztime=5s ./cmd/ja4pd/
$(GO) test -run='^$$' -fuzz='^FuzzFragmentation$$' -fuzztime=5s ./cmd/ja4pd/
$(GO) test -run='^$$' -fuzz='^FuzzProtocolSmuggling$$' -fuzztime=5s ./cmd/ja4pd/
$(GO) test -run='^$$' -fuzz='^FuzzParseClientHello$$' -fuzztime=5s ./internal/tls/
$(GO) test -run='^$$' -fuzz='^FuzzDifferentialClientHello$$' -fuzztime=5s ./internal/tls/
$(GO) test -run='^$$' -fuzz='^FuzzParseClientHelloFeatures$$' -fuzztime=5s ./internal/quic/
$(GO) test -run='^$$' -fuzz='^FuzzReadVarint$$' -fuzztime=5s ./internal/quic/
$(GO) test -run='^$$' -fuzz='^FuzzParseCRYPTOFrames$$' -fuzztime=5s ./internal/quic/
$(GO) test -run='^$$' -fuzz='^FuzzDecodeInitialHeader$$' -fuzztime=5s ./internal/quic/
$(GO) test -run='^$$' -fuzz='^FuzzComputeHMAC$$' -fuzztime=5s ./internal/webhook/
$(GO) test -run='^$$' -fuzz='^FuzzWebhookEndpointRedaction$$' -fuzztime=5s ./internal/webhook/
$(GO) test -run='^$$' -fuzz='^FuzzIsPrivateIP$$' -fuzztime=5s ./internal/webhook/
$(GO) test -run='^$$' -fuzz='^FuzzWebhookEventDelivery$$' -fuzztime=5s ./internal/webhook/
$(GO) test -run='^$$' -fuzz='^FuzzLoadConfigYAML$$' -fuzztime=5s ./internal/config/
$(GO) test -run='^$$' -fuzz='^FuzzLoadYAML$$' -fuzztime=5s ./internal/config/
$(GO) test -run='^$$' -fuzz='^FuzzFlexInt$$' -fuzztime=5s ./internal/config/
$(GO) test -run='^$$' -fuzz='^FuzzExpandEnvVars$$' -fuzztime=5s ./internal/config/
$(GO) test -run='^$$' -fuzz='^FuzzLRUOperations$$' -fuzztime=5s ./internal/cache/
$(GO) test -run='^$$' -fuzz='^FuzzLRUKeyFormatting$$' -fuzztime=5s ./internal/cache/
$(GO) test -run='^$$' -fuzz='^FuzzSNIAnalyzer$$' -fuzztime=5s ./internal/security/
$(GO) test -run='^$$' -fuzz='^FuzzBuildRanger$$' -fuzztime=5s ./internal/security/
$(GO) test -run='^$$' -fuzz='^FuzzEffectiveThresholdAndDecider$$' -fuzztime=5s ./internal/security/
$(GO) test -run='^$$' -fuzz='^FuzzRiskScorer$$' -fuzztime=5s ./internal/security/
$(GO) test -run='^$$' -fuzz='^FuzzPipelineBypassesAndHardBlocks$$' -fuzztime=5s ./internal/security/
$(GO) test -run='^$$' -fuzz='^FuzzVerifyPubSubHMAC$$' -fuzztime=5s ./internal/redis/
$(GO) test -run='^$$' -fuzz='^FuzzPubSubEnvelopeRoundtrip$$' -fuzztime=5s ./internal/redis/
$(GO) test -run='^$$' -fuzz='^FuzzPubSubHandlerDispatch$$' -fuzztime=5s ./internal/redis/
$(GO) test -run='^$$' -fuzz='^FuzzPacketDecoder$$' -fuzztime=5s ./internal/tap/
$(GO) test -run='^$$' -fuzz='^FuzzTCPReassembly$$' -fuzztime=5s ./internal/tap/
$(GO) test -run='^$$' -fuzz='^FuzzParsePortList$$' -fuzztime=5s ./internal/tap/
$(GO) test -run='^$$' -fuzz='^FuzzComputeJA4T$$' -fuzztime=5s ./internal/tap/
$(GO) test -run='^$$' -fuzz='^FuzzCLIArgumentParsing$$' -fuzztime=5s ./internal/cli/engine/
$(GO) test -run='^$$' -fuzz='^FuzzOutputFormatter$$' -fuzztime=5s ./internal/cli/output/
.PHONY: test-go-fuzz-smoke
# ── Benchmarking ─────────────────────────────────────────────────────────────
bench: ## Run all benchmarks (micro + macro)
@$(MAKE) bench-micro
@$(MAKE) bench-macro
# The trailing `|| true` was removed in phase-800. It had been masking three
# permanently-failing benchmarks in ./internal/tls (invalid ClientHello fixtures,
# fixed in #408): the package exited 1 on every run and this target still
# reported success. `go test` does not run benchmarks without -bench, so nothing
# else in the suite could see it either.
bench-micro: ## Run Go native micro-benchmarks
@echo "=== Go Proxy Micro-benchmarks ==="
@GOROOT=$(GOROOT) go test -bench=. -run=^$$ -benchmem ./cmd/ja4pd/ ./internal/tls/
bench-macro: go-build cli-build ## Run end-to-end load test (requires: make start)
@echo "=== JA4proxy High-Speed Go Macro-benchmark ==="
@PORT=$$(grep "^HOST_PORT_DIRECT=" .env | cut -d= -f2); \
PORT=$${PORT:-8081}; \
echo " Targeting Port: $$PORT"; \
./bin/ja4p test benchmark --host 127.0.0.1:$$PORT --duration 30 $(ARGS)
# Generate SBOM (Software Bill of Materials) for the Go proxy
sbom: ## Generate CycloneDX SBOM for Go proxy binary
@echo "Generating SBOM for Go proxy..."
@if command -v syft &>/dev/null; then \
syft bin/ja4pd -o cyclonedx-json > reports/sbom-proxy.json; \
echo "✓ SBOM generated: reports/sbom-proxy.json"; \
else \
echo "✗ syft not found. Install with: curl -sSfL https://raw.githubusercontent.com/anchore/syft/main/install.sh | sh"; \
fi
validation-report: ## Generate validation report
$(PYTHON) scripts/generate_validation_report.py
## Phase 63 targets — SLO validation and reporting
validate-slo-rules: ## Validate SLO recording/alert rules (promtool or YAML)
@command -v promtool >/dev/null 2>&1 && { \
promtool check rules deploy/monitoring/prometheus/slo_recording_rules.yml; \
promtool check rules deploy/monitoring/alertmanager/rules/slo_alerts.yml; \
echo "SLO recording rules and alert rules are syntactically valid."; \
} || { \
$(PYTHON) -c "import yaml; yaml.safe_load(open('deploy/monitoring/prometheus/slo_recording_rules.yml')); yaml.safe_load(open('deploy/monitoring/alertmanager/rules/slo_alerts.yml')); print('YAML structurally valid (promtool not installed)')"; \
}
slo-report: ## Show SLO report from live Prometheus
@echo "=== JA4proxy SLO Report ==="
@curl -sg 'http://localhost:9090/api/v1/query?query=job:ja4proxy_availability:ratio_rate5m' \
| $(PYTHON) -c "import sys,json; d=json.load(sys.stdin); v=d['data']['result']; print('Availability (5m):', round(float(v[0]['value'][1])*100, 4) if v else 'NO DATA', '%')"
@curl -sg 'http://localhost:9090/api/v1/query?query=job:ja4proxy_latency_p99_good:ratio_rate5m' \
| $(PYTHON) -c "import sys,json; d=json.load(sys.stdin); v=d['data']['result']; print('Latency (5m):', round(float(v[0]['value'][1])*100, 4) if v else 'NO DATA', '%')"
@curl -sg 'http://localhost:9090/api/v1/query?query=job:ja4proxy_redis_correctness:ratio_rate5m' \
| $(PYTHON) -c "import sys,json; d=json.load(sys.stdin); v=d['data']['result']; print('Redis (5m):', round(float(v[0]['value'][1])*100, 4) if v else 'NO DATA', '%')"
test-slo: ## SLO validation tests
GOROOT=$(GOROOT) go test ./internal/metrics/... ./internal/redis/... -count=1
$(PYTHON) -c "import yaml; yaml.safe_load(open('deploy/monitoring/prometheus/slo_recording_rules.yml')); yaml.safe_load(open('deploy/monitoring/alertmanager/rules/slo_alerts.yml')); print('YAML OK')"
test-soak: ## High-concurrency connection leak and stream table soak suite (Phase 854)
$(GO) test -v -tags=soak -timeout=180s ./cmd/ja4pd ./internal/tap
.PHONY: test-soak
## CI local targets
ci-local: ## Run the same fast checks the CI workflow runs (Go + Python tests)
GOROOT=$(GOROOT) go test -v -coverprofile=coverage.txt -covermode=atomic ./...
$(PYTHON) -m pytest tests/ --ignore=tests/integration/test_docker_stack.py -x -q --timeout=60
## Phase 64a targets
smoke-docker: ## Run Docker Compose smoke test
bash scripts/smoke/test_docker_compose.sh
## Phase 64b targets
smoke-k8s: ## Run Helm + kind smoke test
bash scripts/smoke/test_helm_kind.sh
## Phase 64h targets
measure-mttr: ## Measure MTTR for DR scenarios
bash scripts/measure_mttr.sh
## Phase 86b targets — Load testing harness
LOAD_TEST_TARGET ?= localhost:8080
LOAD_TEST_DURATION ?= 60
LOAD_TEST_RPS ?= 1000
LOAD_TEST_SCENARIO ?= mixed
load-test: tools-image ## Run JA4proxy load test
@docker run --rm --net=host -v $(PWD):/src -w /src $(TOOLS_IMG) python scripts/load_test.py \
--target $(LOAD_TEST_TARGET) \
--duration $(LOAD_TEST_DURATION) \
--rps $(LOAD_TEST_RPS) \
--scenario $(LOAD_TEST_SCENARIO)
load-test-baseline: ## Run baseline load test (localhost:8080, 60s, 1000 rps)
LOAD_TEST_TARGET=localhost:8080 \
LOAD_TEST_DURATION=60 \
LOAD_TEST_RPS=1000 \
LOAD_TEST_SCENARIO=mixed \
$(MAKE) load-test
load-test-report: ## Show latest load test reports
@echo "Latest benchmark reports:"
@ls -lt test-results/load-test/ 2>/dev/null | head -5 || echo "No reports found"
## Phase 86h targets
lint-alert-urls: ## Verify Alertmanager runbook_url values are up to date
@echo "Checking alertmanager runbook URLs..."
@$(PYTHON) scripts/fix_runbook_urls.py \
--rules-dir deploy/monitoring/alertmanager/rules/ \
--mapping docs/phases/PHASE_86h_runbook_mapping.yml \
--check
# Quick-start and performance test targets
quick-start: ## Start the proxy with default config (builds if needed)
bash scripts/quick-start.sh
perf-test-basic: ## Run basic performance test against a local proxy
bash scripts/basic_perf_test.sh
# Single quality gate — linters + coverage thresholds
quality: lint-all lint-coverage ## Run all linters + coverage checks in one shot
@echo ""
@echo "=== Go coverage check ==="
@GOROOT=$(GOROOT) go test -v -coverprofile=coverage.txt -covermode=atomic ./... -coverprofile=/tmp/go_cover_quality.out -count=1 > /dev/null 2>&1 \
&& GOROOT=$(GOROOT) go tool cover -func=/tmp/go_cover_quality.out \
| tail -1 | awk '{gsub(/%/,"",$$NF); if($$NF+0 < 80) {print "FAIL: Go coverage "$$NF"% < 80%"; exit 1} else print " ✓ Go coverage "$$NF"%"}'
@echo ""
@echo "✓ quality complete — all checks passed"
verify-finding: ## Run two-state revert proof for a single finding (usage: make verify-finding FINDING=JA4PROXY-2026-0042)
@if [ -z "$(FINDING)" ]; then echo "Error: FINDING is required (e.g. make verify-finding FINDING=JA4PROXY-2026-0042)" >&2; exit 1; fi
@scripts/verify_revert.sh $(FINDING)
verify-findings-all: ## Run two-state revert proof for all findings with recorded regression tests
@scripts/verify_revert.sh --all
verify-findings: ## Validate docs/security/findings.yaml schema and referential integrity
@$(PYTHON) scripts/findings_register.py validate
verify-findings-green: ## Run only the regression tests backing findings.yaml entries (fast signal)
@$(PYTHON) scripts/findings_register.py verify-regression-tests --stub-green
findings-render: ## Regenerate docs/security/FINDINGS_REGISTER.md from findings.yaml
@$(PYTHON) scripts/findings_register.py render
findings-list: ## List open findings (add FINDINGS_ARGS=... to pass flags, e.g. --severity HIGH)
@$(PYTHON) scripts/findings_register.py list $(FINDINGS_ARGS)
verify-manifest-closeout: ## Manifest close-out gate — validate register, required docs, ADRs, manifest
@$(PYTHON) scripts/phase_121_verify.py
# phase-107h: regulatory-conformance regression guards
test-compliance-language: ## Phase 107h.1 — fail if "certified"/"compliant" appears in self-assessed compliance docs
@$(PYTHON) -m pytest tests/test_compliance_language.py -v
test-evidence-paths: ## Phase 107h.2 — fail if conformance docs cite repo paths that don't exist
@$(PYTHON) -m pytest tests/test_compliance_evidence_paths.py -v
test-compliance: test-compliance-language test-evidence-paths ## Phase 107h — all regulatory-conformance regression guards
# phase-107f: ATT&CK mapping CI gate
test-attack-mapping: tools-image ## Phase 107f.4 — fail if ATT&CK mapping rows lack confidence labels or cite missing source files
@$(TOOLS_RUN) python3 -m pytest tests/test_attack_mapping.py -v
# phase-107w.3: doc-link check (requires lychee installed locally — same tool the CI workflow runs)
test-doc-links: ## Phase 107w.3 — lychee-check all docs for broken internal links (advisory; gated by docs-link-check.yml)
@echo "=== lychee: internal link check (containerised; advisory in make lint) ==="
@docker run --rm -v $(PWD):/input -w /input lycheeverse/lychee:0.24.2 \
--no-progress --accept 200,202,204,301,302,403,429 \
--exclude-path /input/archive \
--exclude-path /input/node_modules \
--exclude-path /input/docs/reports/archive \
--exclude-path /input/docs/phases/cancelled \
--exclude-path /input/docs/reports/LINK_AUDIT_2026-06-03.md \
--exclude-path /input/docs/phases/TODO.md \
--exclude-path /input/docs/reference/PROJECT_STATUS.md \
--exclude 'http://localhost' \
--exclude 'http://127\.0\.0\.1' \
"/input/**/*.md" \
|| echo " ! lychee reported link issues (advisory — see the docs-link-check workflow for the gate)"
lint-meta: tools-image ## Phase 147 — Verify Makefile and automation script health
@$(TOOLS_RUN) python3 scripts/meta_lint.py
@$(TOOLS_RUN) python3 scripts/check_toolchain_alignment.py
@$(TOOLS_RUN) python3 scripts/sync_reference_docs.py --check
@$(TOOLS_RUN) python3 scripts/check_deps_resolution.py
lint-resolution: tools-image ## Phase 862 — Verify independent Python requirements resolution and parity
@$(TOOLS_RUN) python3 scripts/check_deps_resolution.py
hygiene: tools-image ## Phase 862 — Fast on-demand local pre-check for CVEs, doc sync, and dependency parity
@bash scripts/local_hygiene.sh
check-toolchain: tools-image ## Audit toolchain alignment across go.mod, Dockerfiles, and linter capabilities
@$(TOOLS_RUN) python3 scripts/check_toolchain_alignment.py
reload: ## Reload proxy configuration without restart (SIGHUP)
@docker compose -f deploy/docker/docker-compose.poc.yml kill -s SIGHUP proxy 2>/dev/null || kill -s SIGHUP $$(pgrep ja4p) 2>/dev/null || echo "Proxy not running"
test-ip: ## Alias for simulating IP decision
@./bin/ja4p test ip $(IP)
# ── Lint Hierarchy (Phase 92) ─────────────────────────────────────────────────
# Prerequisite-based dependency graph — every aggregate lists its leaves as
# PREREQUISITES so that `make -n`, dependency parsers, and test suites all see
# the correct graph. Advisory targets do not block the gate but report issues.
# ── Individual linters (Phase 92 additions) ───────────────────────────────────
lint-pylint: tools-image ## Run pylint errors-only on Python source (advisory)
@echo "=== pylint: Python source ==="
@$(TOOLS_RUN) python3 -m pylint --errors-only --output-format=colorized \
src/ management/ 2>&1 | tail -30 \
|| echo " ! pylint reported errors (advisory — see output above)"
@echo " ✓ pylint done"
lint-checkov: ## Run Checkov IaC security scan (containerised; advisory)
@echo "=== checkov: IaC security scan ==="
@docker run --rm -v $(PWD):/tf bridgecrew/checkov:latest \
--quiet --directory /tf --framework dockerfile 2>/dev/null \
|| echo " ! checkov reported issues or is unavailable (advisory)"
@echo " ✓ checkov done"
lint-haproxy: ## Validate HAProxy configuration syntax (advisory)
@echo "=== haproxy: config validation ==="
@if [ -f deploy/haproxy/haproxy.cfg ]; then \
docker run --rm -v $(PWD):/src haproxy:2.8-alpine \
haproxy -c -f /src/deploy/haproxy/haproxy.cfg 2>&1; \
echo " ✓ HAProxy config valid"; \
else \
echo " ! deploy/haproxy/haproxy.cfg not found — skipping (advisory)"; \
fi
lint-helm: ## Run helm lint on Helm charts (advisory)
@echo "=== helm lint: Helm charts ==="
@if [ -d deploy/helm ]; then \
docker run --rm -v $(PWD):/src alpine/helm:3.17.3 \
lint /src/deploy/helm/ 2>&1 \
|| echo " ! helm lint reported issues (advisory)"; \
echo " ✓ helm lint done"; \
else \
true; \
fi
lint-markdown: tools-image ## Lint Markdown files for formatting issues (advisory)
@echo "=== markdownlint: Markdown files ==="
@$(TOOLS_RUN) python3 -m pymarkdown --config /src/.pymarkdown scan docs/ README.md 2>&1 \
|| echo " ! markdownlint reported issues (advisory)"
@echo " ✓ Markdown lint done"
lint-spelling: tools-image ## Spell-check documentation (advisory)
@echo "=== codespell: spelling check ==="
@$(TOOLS_RUN) codespell docs/ README.md \
--skip="*.po,*.pot,docs/phases,docs/reference,docs/pdf" \
--ignore-words=/src/.codespellignore 2>&1 \
|| echo " ! codespell reported issues (advisory)"
@echo " ✓ Spelling check done"
lint-toml: ## Validate TOML files (pyproject.toml, .gitleaks.toml) using tomllib
@echo "=== lint-toml: TOML syntax validation ==="
@python3 scripts/lint_toml.py
@echo " ✓ TOML lint passed"
# checkmake does NOT auto-discover .checkmake.ini from the working directory —
# verified 2026-08-10 (Phase 800): with the file in cwd and no flag, maxbodylength
# is still reported; passing --config suppresses it. Without --config this repo's
# Makefile yields 13 spurious maxbodylength violations; with it, 0. The
# docs/phases/complete/PHASE_802.md row claiming cwd auto-discovery was wrong.
#
# Containerised in phase-800. It previously ran a bare host `checkmake` behind a
# `command -v` guard, and nothing in this repo ever installed it — no CI step, no
# tools image — so the guard always took the else branch and this step silently
# checked nothing, everywhere, for its whole existence. Now it always runs.
# checkmake's exit code is the violation count (0 = clean), so no output parsing
# is needed; the recipe simply fails on non-zero.
lint-makefiles: ## Lint Makefile for common issues (checkmake)
@echo "=== Makefile lint ==="
@docker run --rm -v "$(PWD):/src:ro" -w /src --entrypoint /checkmake \
$(CHECKMAKE_IMG) --config=.checkmake.ini Makefile
@echo " ✓ checkmake passed"
lint-go-mod: ## Verify go.mod and go.sum are consistent (go mod verify)
@echo "=== go mod verify ==="
@if command -v go >/dev/null 2>&1 || [ -x /snap/go/current/bin/go ]; then \
GOROOT=$(GOROOT) $(GO) mod verify; \
else \
docker run --rm -v "$$PWD":/src -w /src golang:1.27.3-alpine go mod verify; \
fi
@echo " ✓ go mod verify passed"
# ── Aggregate linters ─────────────────────────────────────────────────────────
lint-python: lint-static lint-security lint-pylint ## Run all Python linters (ruff, mypy, bandit, pylint)
@echo "✓ lint-python complete"
lint-go: go-lint lint-go-full lint-go-mod ## Run all Go linters (fmt, vet, golangci-lint, mod verify)
@echo "✓ lint-go complete"
lint-sast: lint-opengrep lint-checkov ## Run cross-language SAST (Opengrep, Checkov)
@echo "✓ lint-sast complete"
lint-infra: lint-docker lint-shell lint-yaml lint-lua lint-json lint-haproxy lint-makefiles lint-toml lint-ansible lint-helm ## Run all infrastructure linters
@echo "✓ lint-infra complete"
lint-observability: lint-prom lint-alertmanager ## Run observability linters (promtool, amtool)
@echo "✓ lint-observability complete"
lint-supply-chain: lint-secrets lint-deps ## Run supply-chain linters (Gitleaks, govulncheck)
@echo "✓ lint-supply-chain complete"
doc-health: tools-image ## Validate documentation frontmatter
@$(TOOLS_RUN) python3 scripts/check_doc_frontmatter.py
lint-docs: doc-health link-check test-attack-mapping ## Core documentation quality checks
@echo "✓ lint-docs complete"
link-check: ## Alias for the internal-link checker (test-doc-links)
@$(MAKE) test-doc-links
lint-phases: tools-image ## Validate phase docs (frontmatter, numbering, manifest sync)
@$(TOOLS_RUN) python3 scripts/lint-phases.py
lint-docs-all: lint-docs lint-phases check-manifest link-check lint-markdown lint-spelling ## Run all documentation quality checks
@echo "✓ lint-docs-all complete"
lint-opengrep: opengrep-image ## Run Opengrep SAST using local rulesets (containerised)
@echo "=== opengrep: SAST (pinned binary v1.33.1 in container) ==="
@docker run --rm -u $$(id -u):$$(id -g) -e HOME=/tmp -v $(PWD):/src -w /src $(OPENGREP_IMG) \
scan --quiet --error --disable-version-check --config security/sast/rules/ --config security/sast/custom-rules/ .
lint-semgrep: lint-opengrep ## Backward-compatible alias for lint-opengrep (Phase 865)
lint-ansible: ## Lint the Ansible playbooks/roles under deploy/ansible (containerised; advisory)
@echo "=== ansible-lint: deploy/ansible (advisory — molecule role-path syntax-checks are pre-existing) ==="
@docker run --rm -v $(PWD):/src -w /src pipelinecomponents/ansible-lint:0.8.0 \
ansible-lint deploy/ansible/ \
|| echo " ! ansible-lint reported issues or is unavailable (advisory)"
lint-all: lint-meta lint-python lint-go lint-sast lint-infra lint-observability lint-supply-chain lint-docs-all ## Run every linter in one shot
@echo ""
@echo "✓ lint-all complete"
@python3 scripts/ci_summary.py lint
test-lint-hierarchy: tools-image ## Run Phase 92 lint hierarchy structural tests
@$(TOOLS_RUN) python3 -m pytest tests/lint-hierarchy/ -v
start-poc: deploy-poc ## Alias for starting the POC environment
surface-inventory: tools-image ## Rebuild docs/security/ATTACK_SURFACE.md from static AST inspection
@$(TOOLS_RUN) python scripts/surface_inventory.py
sync: tools-image ## Sync roadmap from manifest.yaml, attack surface, and generated reference docs
@$(TOOLS_RUN) python scripts/sync-roadmap.py
@$(TOOLS_RUN) python scripts/sync_reference_docs.py
@$(TOOLS_RUN) python scripts/surface_inventory.py
changelog-assemble: tools-image ## Fold docs/fragments/*.md into CHANGELOG.md (run at release, not per-phase)
@$(TOOLS_RUN) python scripts/assemble-changelog.py
ja4p-validate: ## Validate proxy configuration YAML
@./bin/ja4p config validate
@mkdir -p bin
@$(GO) build -o bin/ja4p-setup ./cmd/setup
setup-build: cli-build ## Build the ja4p CLI the setup wizard needs
@true
init: setup-build ## Start the guided setup wizard
@./bin/ja4p init
@echo "✓ bin/ja4ps"
bump-build: ## Show build number (derived from git commit count — no file needed)
@echo "✓ Build number: $$(git rev-list --count HEAD 2>/dev/null || echo 0)"
# Keep in sync: F2 hardened flags — see docs/phases/PHASE_863.md flag matrix
go-build: ## Build the Go proxy daemon into bin/ja4pd
@mkdir -p bin
@command -v gcc >/dev/null 2>&1 || { echo "✗ gcc NOT found — required for hardened build mode (Phase 863)"; exit 1; }
VERSION=$(FULL_VERSION) GIT_COMMIT=$(GIT_COMMIT) BUILD_DATE=$(BUILD_DATE) ./scripts/go_build_hardened.sh bin/ja4pd ./cmd/ja4pd
@echo "✓ bin/ja4pd"
# Keep in sync: F2 hardened flags — see docs/phases/PHASE_863.md flag matrix
go-build-foss: ## Build royalty-free Go proxy daemon into bin/ja4pd-foss (-tags no_ja4plus)
@mkdir -p bin
@command -v gcc >/dev/null 2>&1 || { echo "✗ gcc NOT found — required for hardened build mode (Phase 863)"; exit 1; }
VERSION=$(FULL_VERSION) GIT_COMMIT=$(GIT_COMMIT) BUILD_DATE=$(BUILD_DATE) ./scripts/go_build_hardened.sh bin/ja4pd-foss ./cmd/ja4pd no_ja4plus
@echo "✓ bin/ja4pd-foss (pure FOSS, no FoxIO JA4+ methods)"
# Keep in sync: F2 hardened flags — see docs/phases/PHASE_863.md flag matrix
cli-build: ## Build the unified ja4p CLI tool
@mkdir -p bin
@command -v gcc >/dev/null 2>&1 || { echo "✗ gcc NOT found — required for hardened build mode (Phase 863)"; exit 1; }
VERSION=$(FULL_VERSION) GIT_COMMIT=$(GIT_COMMIT) BUILD_DATE=$(BUILD_DATE) ./scripts/go_build_hardened.sh bin/ja4p ./cmd/ja4p
@echo "✓ bin/ja4p"
ci-verify: ## CI mirror: full lint, security scans, and test suite gate
@$(MAKE) preflight
install-hooks: ## Install shared git hooks (pre-push runs `make preflight`)
@git config core.hooksPath .githooks
@echo "✓ git hooks installed: core.hooksPath=.githooks (pre-push runs 'make preflight')"
tunnel: ## Print the SSH local-forward command for an agent stack (NAME=, HOST=)
@[ -n "$(NAME)" ] || { echo "Usage: make tunnel NAME=<agent> HOST=user@server"; exit 1; }
@[ -n "$(HOST)" ] || { echo "Usage: make tunnel NAME=<agent> HOST=user@server"; exit 1; }
@[ -f ".env.$(NAME)" ] || { echo "No .env.$(NAME) found — run 'make agent-up NAME=$(NAME)' first"; exit 1; }
@MGMT=$$(grep '^HOST_PORT_MANAGEMENT=' .env.$(NAME) | cut -d= -f2); MGMT=$${MGMT:-8090}; \
ANL=$$(grep '^HOST_PORT_ANALYTICS=' .env.$(NAME) | cut -d= -f2); ANL=$${ANL:-8080}; \
echo "Run on your laptop to tunnel agent '$(NAME)':"; \
echo " ssh -L $$MGMT:localhost:$$MGMT -L $$ANL:localhost:$$ANL -L 9090:localhost:9090 $(HOST)"
# management-* operate on the 'management' service of the current agent stack (.current-agent)
management-up: ## Start the management UI for the current agent
$(eval _AGENT := $(shell cat .current-agent 2>/dev/null))
@if [ -n "$(_AGENT)" ]; then \
docker compose -f deploy/docker/docker-compose.poc.yml --project-name ja4_$(_AGENT) --env-file .env.$(_AGENT) up -d management; \
else \
docker compose -f deploy/docker/docker-compose.poc.yml --env-file .env up -d management; \
fi
management-down: ## Stop the management UI for the current agent
$(eval _AGENT := $(shell cat .current-agent 2>/dev/null))
@if [ -n "$(_AGENT)" ]; then \
docker compose -f deploy/docker/docker-compose.poc.yml --project-name ja4_$(_AGENT) --env-file .env.$(_AGENT) stop management; \
else \
docker compose -f deploy/docker/docker-compose.poc.yml --env-file .env stop management; \
fi
management-logs: ## Tail the management UI logs for the current agent
$(eval _AGENT := $(shell cat .current-agent 2>/dev/null))
@if [ -n "$(_AGENT)" ]; then \
docker compose -f deploy/docker/docker-compose.poc.yml --project-name ja4_$(_AGENT) --env-file .env.$(_AGENT) logs -f management; \
else \
docker compose -f deploy/docker/docker-compose.poc.yml --env-file .env logs -f management; \
fi
management-shell: ## Open a shell in the management UI container for the current agent
$(eval _AGENT := $(shell cat .current-agent 2>/dev/null))
@if [ -n "$(_AGENT)" ]; then \
docker compose -f deploy/docker/docker-compose.poc.yml --project-name ja4_$(_AGENT) --env-file .env.$(_AGENT) exec management sh; \
else \
docker compose -f deploy/docker/docker-compose.poc.yml --env-file .env exec management sh; \
fi
test-ratio: ## Show the test-to-code ratio
@$(PYTHON) scripts/test_ratio.py
test-component-suites: ## Run every component test suite (unit, chaos, adversarial)
@$(MAKE) test-unit test-chaos test-adversarial
bench-all: ## Run every heavy benchmark (load, go-perf, hostnative) — slow, runs alone
@echo "=== bench-all: heavy benchmarks (excluded from 'make lint scan test') ==="
@$(MAKE) bench
@$(MAKE) test-go-perf
@$(MAKE) load-test
@$(MAKE) bench-hostnative
@echo "✓ bench-all complete"
verify-all: ## Full release gate: lint + scan + test + bench-all — slow
@$(MAKE) lint
@$(MAKE) scan
@$(MAKE) test
@$(MAKE) bench-all
@echo "✓ verify-all complete"
bench-hostnative: go-build cli-build ## End-to-end throughput, ja4pd host-native (no docker-proxy; ~4.5x the bridge port)
@chmod +x scripts/bench-hostnative.sh
@./scripts/bench-hostnative.sh
scan-js: ## Scan the vendored Management-UI JS for known CVEs (retire.js)
@echo "-> retire.js CVE scan of management/static (vendored front-end JS)"
@npx --yes retire@5 --path management/static
lane: ## Show this worktree's dev lane (collision-free host ports + Grafana URL)
@chmod +x scripts/lane-env.sh && ./scripts/lane-env.sh
open: lane ## Open a lane service: make open SVC=grafana|management|metrics|prometheus
@set -a; . ./.env; set +a; \
svc="$(or $(SVC),grafana)"; bind="$${AGENT_BIND_IP:-127.0.0.1}"; \
case "$$svc" in \
grafana) port=$$HOST_PORT_GRAFANA;; management) port=$$HOST_PORT_MANAGEMENT;; \
metrics) port=$$HOST_PORT_METRICS;; prometheus) port=$$HOST_PORT_PROMETHEUS;; \
*) echo "unknown SVC=$$svc (grafana|management|metrics|prometheus)"; exit 1;; esac; \
url="http://$$bind:$$port"; echo "opening $$svc -> $$url"; \
(xdg-open "$$url" >/dev/null 2>&1 || open "$$url" >/dev/null 2>&1 || echo " open it manually: $$url")
loadtest: go-build cli-build ## Lane-isolated good/bad load test -> watch Grafana (knobs: GOOD_RATE BAD_RATE DURATION WORKERS DIAL)
@chmod +x scripts/lane-env.sh && ./scripts/lane-env.sh >/dev/null
@$(MAKE) --no-print-directory start-poc
@set -a; . ./.env; set +a; \
g="$(or $(GOOD_RATE),10)"; b="$(or $(BAD_RATE),190)"; d="$(or $(DURATION),300)"; \
w="$(or $(WORKERS),16)"; dial="$(or $(DIAL),100)"; bind="$${AGENT_BIND_IP:-127.0.0.1}"; \
echo "lane $$JA4_LANE: raising dial to $$dial (best-effort via Management API) ..."; \
JA4PROXY_URL="http://$$bind:$$HOST_PORT_MANAGEMENT" JA4PROXY_TOKEN="$${JA4PROXY_TOKEN:-$$UI_API_KEY}" \
./bin/ja4p management dial set $$dial --confirm 2>/dev/null \
|| echo " (could not auto-set dial; scored/risk panels still populate. Set it via 'ja4p management dial' for block/ban panels)"; \
echo "driving good=$$g/s bad=$$b/s for $${d}s at 127.0.0.1:$$HOST_PORT_DIRECT ..."; \
./bin/ja4p test benchmark --host "127.0.0.1:$$HOST_PORT_DIRECT" --good-rate $$g --bad-rate $$b --workers $$w --duration $$d; \
echo "watch it: http://$$bind:$$HOST_PORT_GRAFANA (Grafana)"
# ── Phase 332: pre-PR gate ───────────────────────────────────────────────────
# Run the full required-check set locally before opening a PR. This is the
# shift-left half of the CI trial: heavy checks (lint, scan) run here on the
# author's machine so the PR path can stay fast. All three must be green before
# you push a branch and open a PR. See docs/phases/complete/PHASE_332.md.
check: go-build cli-build compose-validate test ## Fast gate: compile + env validate + tests (~3 min, no image builds or CVE scans)
@echo "✓ check passed — matches CI required checks"
preflight: ## Full local gate before opening a PR: lint + scan + test + test-race (~25 min)
@echo "NOTE: scan-first-party scans Docker images — run 'make build' first to ensure they are current."
@echo "=== preflight [1/4] make lint ==="
@$(MAKE) lint
@echo "=== preflight [2/4] make scan ==="
@$(MAKE) scan
@echo "=== preflight [3/4] make test ==="
@$(MAKE) test
@echo "=== preflight [4/4] make test-race ==="
@$(MAKE) test-race
@echo "✓ preflight passed — safe to open a PR"
tap-build: ## Build standalone TAP sensor binary
GOBIN=$(GOPATH)/bin go build -o bin/ja4-tap ./cmd/ja4-tap
# ── Phase 245: First-class admin CLI + minimal init ──────────────────────────
admin: ## Run the ja4-admin incident response CLI (pass ARGS for commands)
@./scripts/ja4-admin.sh $(ARGS)
init-minimal: setup-build ## Emergency init: prompt for BACKEND_HOST, generate .env, done
@./bin/ja4p init --minimal
# ── Phase 511: Emergency traffic insertion / rollback ────────────────────────
# Routes incoming :443 through JA4proxy :8443 using iptables PREROUTING.
# Requires sudo. Rollback is instant — see docs/runbooks/dashboard_access.md.
TRAFFIC_PUBLIC_PORT ?= 443
TRAFFIC_PROXY_PORT ?= 8443
traffic-on: ## Insert JA4proxy into the traffic path (iptables redirect :443→:8443; needs sudo)
@echo "=== Routing port $(TRAFFIC_PUBLIC_PORT) → JA4proxy :$(TRAFFIC_PROXY_PORT) ==="
@sudo iptables -t nat -I PREROUTING -p tcp --dport $(TRAFFIC_PUBLIC_PORT) -j REDIRECT --to-port $(TRAFFIC_PROXY_PORT)
@echo "✓ Live traffic now flowing through JA4proxy."
@echo " Monitor: docker compose logs -f ja4proxy"
@echo " Rollback at any time: make traffic-off"
traffic-off: ## Remove JA4proxy from the traffic path (instant rollback; needs sudo)
@echo "=== Removing JA4proxy from traffic path ==="
@sudo iptables -t nat -D PREROUTING -p tcp --dport $(TRAFFIC_PUBLIC_PORT) -j REDIRECT --to-port $(TRAFFIC_PROXY_PORT) 2>/dev/null \
&& echo "✓ Traffic bypassing JA4proxy. Original service restored." \
|| echo "Rule not found — JA4proxy was not in the traffic path."
# ── Phase 92: PHONY declarations for lint hierarchy targets ──────────────────
.PHONY: lint-pylint lint-checkov lint-haproxy lint-helm lint-markdown lint-spelling
.PHONY: lint-toml lint-makefiles lint-go-mod test-lint-hierarchy
.PHONY: lint-python lint-go lint-sast lint-infra lint-observability
.PHONY: lint-supply-chain lint-docs-all lint-all lint-docs doc-health link-check
# ── Phase 814a: penetration-testing range ────────────────────────────────────
pentest-range: ## Bring up the isolated pentest range (zero egress, verified) and print provenance
@./scripts/start-pentest-range.sh
pentest-range-down: ## Tear down the pentest range (evidence is kept)
@./scripts/start-pentest-range.sh --down
pentest-range-verify: ## Re-run the range isolation assertions without rebuilding
@./scripts/start-pentest-range.sh --verify-only
pentest-shell: ## Open a shell on the attacker workstation inside the range
@docker compose -p ja4range --env-file .pentest-range/range.env \
-f deploy/docker/docker-compose.poc.yml \
-f deploy/docker/docker-compose.pentest.yml exec attacker bash
.PHONY: pentest-range pentest-range-down pentest-range-verify pentest-shell
# Phase 824 — journey tests. Assert a PERSON can do the thing, not that a unit
# returns a value. Require a running stack, so they are not part of `make test`.
# TIER: 1=static/contract only, 2=live stack (default), 3=fault injection / restart
TIER ?= 2
test-journeys: ## Phase 824 — run customer-journey checks against a live stack (TIER=2 default)
@set -a; [ -f .env ] && . ./.env; set +a; \
fail=0; \
req_tier="$(TIER)"; \
req_num="$${req_tier#T}"; req_num="$${req_num#t}"; \
echo "Running customer journey checks for TIER <= $$req_num..."; \
for j in tests/integration/journeys/check_*.sh; do \
[ -x "$$j" ] || continue; \
script_tier=$$(grep -E '^# Tier: T[0-9]+' "$$j" | head -1 | sed -E 's/.*T([0-9]+).*/\1/' || echo "2"); \
[ -n "$$script_tier" ] || script_tier=2; \
if [ "$$script_tier" -le "$$req_num" ]; then \
echo "── $$j (Tier: T$$script_tier)"; "$$j" || fail=1; \
fi; \
done; \
[ $$fail -eq 0 ] || { echo "✗ journey check(s) failed"; exit 1; }; \
echo "✓ all journey checks for TIER=$$req_num passed"
test-ui-contract: tools-image ## Phase 824f — run Tier 1 UI route contract, regression, and health tests
@echo "=== Running Tier 1 UI tests (containerized: $(TOOLS_IMG)) ==="
@$(TOOLS_RUN) pytest \
management/tests/test_partial_contract.py \
management/tests/test_ui_regression.py \
management/tests/test_ui_health.py \
-v --timeout=60 --tb=short
# ── phase-826: demo support ───────────────────────────────────────────────────
# Pre-flight before showing the product to anyone. Checks the failures that
# have actually happened and were invisible: analytics unable to read the
# stream, events rejected on an HMAC mismatch, the proxy never loading its
# blacklist, an empty Intelligence panel. See docs/DEMO_RUNBOOK.md.
.PHONY: demo-check demo-bot
demo-check: ## Pre-flight health check for demo environment
@scripts/demo-check.sh
# One deliberately non-browser TLS connection, for the "now something that
# isn't a browser" step of the demo.
demo-bot: ## Send test non-browser TLS connection for demo
@scripts/demo-bot.sh
# ── Phase 816: End-to-end management console showcase demo ──────────────────
.PHONY: demo demo-verify demo-stop
demo: poc-secrets ## Phase 816 — Launch showcase demo environment with API seeding and traffic
@bash scripts/demo-mgmt.sh
demo-verify: ## Phase 816 — Run end-to-end data flow and policy assertions on demo stack
@bash scripts/demo-verify.sh
demo-stop: ## Phase 816 — Gracefully stop all demo environment services
@bash scripts/stop-all.sh