Repository navigation
Expand file tree
/
Copy pathMakefile
More file actions
257 lines (224 loc) · 11.4 KB
/
Copy pathMakefile
File metadata and controls
257 lines (224 loc) · 11.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
SHELL := /bin/bash
.DEFAULT_GOAL := help
GO ?= go
GOBIN ?= $(shell go env GOPATH)/bin
PROTOC ?= protoc
BIN_DIR := bin
HUB_BIN := $(BIN_DIR)/recon-hub
AGENT_BIN := $(BIN_DIR)/recon-agent
PROTO_DIR := internal/proto
PROTO_SRC := $(PROTO_DIR)/recon.proto
# DIST_VER is set by the release workflow (extracted from the git tag, no
# leading "v") and read by the dist-* targets. Local `make build` falls back
# to `git describe --tags` so dev binaries still report a meaningful version
# in --version output and the hub's outdated check doesn't blanket-skip them.
DIST_VER ?= $(shell git describe --tags --always --dirty 2>/dev/null | sed 's/^v//' || echo 0.1.0-dev)
LDFLAGS := -s -w \
-X 'github.com/vasyakrg/recon/internal/common/version.Version=$(DIST_VER)' \
-X 'github.com/vasyakrg/recon/internal/common/version.Commit=$(shell git rev-parse --short HEAD 2>/dev/null || echo dev)'
.PHONY: help
help:
@grep -E '^[a-zA-Z_-]+:.*?## ' $(MAKEFILE_LIST) | awk 'BEGIN{FS=":.*?## "}{printf " %-18s %s\n", $$1, $$2}'
.PHONY: tools
tools: ## Install Go-managed dev tools (protoc plugins)
$(GO) install google.golang.org/protobuf/cmd/protoc-gen-go@latest
$(GO) install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest
.PHONY: proto
proto: ## Generate gRPC + protobuf Go code
@which protoc >/dev/null || (echo "protoc not installed (brew install protobuf)" && exit 1)
PATH="$(GOBIN):$$PATH" $(PROTOC) \
--go_out=. --go_opt=module=github.com/vasyakrg/recon \
--go-grpc_out=. --go-grpc_opt=module=github.com/vasyakrg/recon \
$(PROTO_SRC)
.PHONY: build
build: build-hub build-agent ## Build hub and agent binaries
.PHONY: build-hub
build-hub:
CGO_ENABLED=0 $(GO) build -ldflags "$(LDFLAGS)" -o $(HUB_BIN) ./cmd/hub
.PHONY: build-agent
build-agent:
CGO_ENABLED=0 $(GO) build -ldflags "$(LDFLAGS)" -o $(AGENT_BIN) ./cmd/agent
.PHONY: test
test: ## Run unit tests
$(GO) test -race -count=1 ./...
.PHONY: test-integration
test-integration: ## Run integration tests (testcontainers)
$(GO) test -race -count=1 -tags=integration -timeout=10m ./...
.PHONY: lint
lint: ## Run golangci-lint
@which golangci-lint >/dev/null || (echo "golangci-lint not installed (brew install golangci-lint)" && exit 1)
golangci-lint run ./...
.PHONY: run-hub
run-hub: build-hub ## Run hub locally
./$(HUB_BIN) --config ./deploy/dev/hub.yaml
.PHONY: run-agent
run-agent: build-agent ## Run agent locally
./$(AGENT_BIN) --config ./deploy/dev/agent.yaml
.PHONY: dist
dist: dist-hub dist-agent ## Build static dist tarballs (linux/amd64 + arm64)
DIST_DIR := dist
.PHONY: dist-hub
# Tarball is named WITHOUT the version (recon-hub-linux-<arch>.tar.gz).
# The release tag pins the version on the GitHub side, which lets the
# install script use stable URLs:
# https://github.com/.../releases/latest/download/recon-hub-linux-amd64.tar.gz
# https://github.com/.../releases/download/v0.1.0/recon-hub-linux-amd64.tar.gz
# DIST_VER is still stamped into the binary's --version output via LDFLAGS.
dist-hub:
@mkdir -p $(DIST_DIR)
@for arch in amd64 arm64; do \
echo "==> recon-hub linux/$$arch"; \
mkdir -p $(DIST_DIR)/recon-hub-linux-$$arch/{bin,deploy/systemd,deploy/nginx,deploy/docs}; \
CGO_ENABLED=0 GOOS=linux GOARCH=$$arch \
$(GO) build -ldflags "$(LDFLAGS)" \
-o $(DIST_DIR)/recon-hub-linux-$$arch/bin/recon-hub ./cmd/hub; \
cp deploy/systemd/recon-hub.service $(DIST_DIR)/recon-hub-linux-$$arch/deploy/systemd/; \
cp deploy/nginx/recon.conf $(DIST_DIR)/recon-hub-linux-$$arch/deploy/nginx/; \
cp deploy/docs/install.md $(DIST_DIR)/recon-hub-linux-$$arch/deploy/docs/; \
tar czf $(DIST_DIR)/recon-hub-linux-$$arch.tar.gz \
-C $(DIST_DIR) recon-hub-linux-$$arch; \
rm -rf $(DIST_DIR)/recon-hub-linux-$$arch; \
done
.PHONY: dist-agent
dist-agent:
@mkdir -p $(DIST_DIR)
@for arch in amd64 arm64; do \
echo "==> recon-agent linux/$$arch"; \
mkdir -p $(DIST_DIR)/recon-agent-linux-$$arch/{bin,deploy/systemd,deploy/sudoers}; \
CGO_ENABLED=0 GOOS=linux GOARCH=$$arch \
$(GO) build -ldflags "$(LDFLAGS)" \
-o $(DIST_DIR)/recon-agent-linux-$$arch/bin/recon-agent ./cmd/agent; \
cp deploy/systemd/recon-agent.service $(DIST_DIR)/recon-agent-linux-$$arch/deploy/systemd/; \
cp deploy/sudoers/recon $(DIST_DIR)/recon-agent-linux-$$arch/deploy/sudoers/; \
tar czf $(DIST_DIR)/recon-agent-linux-$$arch.tar.gz \
-C $(DIST_DIR) recon-agent-linux-$$arch; \
rm -rf $(DIST_DIR)/recon-agent-linux-$$arch; \
done
.PHONY: clean
clean:
rm -rf $(BIN_DIR) $(DIST_DIR)
# ── release ──────────────────────────────────────────────────────────────────
# Bump the latest vMAJOR.MINOR.PATCH tag, push it to origin so the GitHub
# Actions release workflow picks it up and builds tarballs + ghcr images.
#
# make release → patch bump (v0.1.4 → v0.1.5)
# make release BUMP=minor → minor bump (v0.1.4 → v0.2.0)
# make release BUMP=major → major bump (v0.1.4 → v1.0.0)
# make release VERSION=v0.3.0 → pin an explicit tag
#
# Refuses to run on a dirty tree or if HEAD is not on the default branch tip
# of origin — release builds must be reproducible from a pushed commit.
BUMP ?= patch
RELEASE_REMOTE ?= origin
RELEASE_BRANCH ?= main
.PHONY: release
release: ## Bump version tag and push to origin (BUMP=patch|minor|major or VERSION=vX.Y.Z)
@git diff --quiet || (echo "working tree is dirty — commit or stash first" && exit 1)
@git diff --cached --quiet || (echo "staged changes present — commit or reset first" && exit 1)
@git fetch --tags $(RELEASE_REMOTE) >/dev/null
@current=$$(git describe --tags --abbrev=0 --match 'v[0-9]*' 2>/dev/null || echo v0.0.0); \
if [ -n "$(VERSION)" ]; then \
next="$(VERSION)"; \
case "$$next" in v[0-9]*.[0-9]*.[0-9]*) ;; *) echo "VERSION must look like vMAJOR.MINOR.PATCH"; exit 1;; esac; \
else \
v=$${current#v}; \
major=$${v%%.*}; rest=$${v#*.}; minor=$${rest%%.*}; patch=$${rest#*.}; \
case "$(BUMP)" in \
patch) patch=$$((patch+1));; \
minor) minor=$$((minor+1)); patch=0;; \
major) major=$$((major+1)); minor=0; patch=0;; \
*) echo "BUMP must be patch|minor|major"; exit 1;; \
esac; \
next="v$$major.$$minor.$$patch"; \
fi; \
if git rev-parse "$$next" >/dev/null 2>&1; then \
echo "tag $$next already exists"; exit 1; \
fi; \
upstream=$$(git rev-parse $(RELEASE_REMOTE)/$(RELEASE_BRANCH) 2>/dev/null || echo ''); \
head=$$(git rev-parse HEAD); \
if [ -z "$$upstream" ]; then \
echo "cannot resolve $(RELEASE_REMOTE)/$(RELEASE_BRANCH) — fetch first"; exit 1; \
fi; \
if [ "$$upstream" != "$$head" ]; then \
echo "HEAD ($$head) differs from $(RELEASE_REMOTE)/$(RELEASE_BRANCH) ($$upstream) — push your commit first"; exit 1; \
fi; \
echo "==> $$current → $$next"; \
git tag -a "$$next" -m "release $$next" && \
git push $(RELEASE_REMOTE) "$$next" && \
echo "pushed $$next — GitHub Actions will build artifacts at https://github.com/vasyakrg/reconops/releases/tag/$$next"
.PHONY: release-dry
release-dry: ## Print what `make release` would do, without tagging or pushing
@git fetch --tags $(RELEASE_REMOTE) >/dev/null 2>&1 || true
@current=$$(git describe --tags --abbrev=0 --match 'v[0-9]*' 2>/dev/null || echo v0.0.0); \
if [ -n "$(VERSION)" ]; then next="$(VERSION)"; \
else \
v=$${current#v}; major=$${v%%.*}; rest=$${v#*.}; minor=$${rest%%.*}; patch=$${rest#*.}; \
case "$(BUMP)" in \
patch) patch=$$((patch+1));; \
minor) minor=$$((minor+1)); patch=0;; \
major) major=$$((major+1)); minor=0; patch=0;; \
esac; \
next="v$$major.$$minor.$$patch"; \
fi; \
echo "current: $$current"; \
echo "next: $$next (BUMP=$(BUMP)$(if $(VERSION), VERSION=$(VERSION)))"
# ── docker compose ───────────────────────────────────────────────────────────
COMPOSE ?= docker compose
.PHONY: docker-build
docker-build: ## Build the recon-hub docker image
$(COMPOSE) build hub
.PHONY: compose-up
compose-up: ## Start the hub stack in the background
@test -f .env || (echo "missing .env — copy from .env.example and fill in" && exit 1)
$(COMPOSE) up -d hub
.PHONY: compose-down
compose-down: ## Stop the stack (state volume is preserved)
$(COMPOSE) down
.PHONY: compose-logs
compose-logs: ## Tail hub logs
$(COMPOSE) logs -f hub
.PHONY: compose-gen-hash
compose-gen-hash: ## Generate a bcrypt hash via the hub container; PASSWORD=… required
@test -n "$(PASSWORD)" || (echo "set PASSWORD=…" && exit 1)
$(COMPOSE) run --rm --no-deps \
-e RECON_ADMIN_PASSWORD='$(PASSWORD)' \
--entrypoint /usr/local/bin/recon-hub hub --mode gen-password-hash
.PHONY: compose-gen-token
compose-gen-token: ## Issue a bootstrap token; AGENT_ID=… required, TTL=24h optional
@test -n "$(AGENT_ID)" || (echo "set AGENT_ID=…" && exit 1)
$(COMPOSE) exec hub /usr/local/bin/recon-hub \
--config /etc/recon/hub.yaml --mode gen-token \
--agent-id "$(AGENT_ID)" --token-ttl "$${TTL:-24h}"
.PHONY: compose-reset
compose-reset: ## Wipe ALL hub state (db, artifacts, CA) + agent state, then bring the stack back up
@echo "this wipes recon-state + recon-agent-state volumes — every investigation, host, finding, audit row is gone"
@read -p "type 'yes' to continue: " ok && test "$$ok" = "yes"
@$(COMPOSE) --profile with-agent down -v
@$(COMPOSE) up -d
@echo "stack back up — re-bootstrap each agent with the install one-liner."
.PHONY: compose-rotate-ca
compose-rotate-ca: ## Regenerate the bootstrap CA so SAN changes in hub.yaml take effect (invalidates ALL enrolled agents, keeps db/audit)
@echo "this wipes /var/lib/recon/ca/ and EVERY agent's enrolled identity"
@read -p "type 'yes' to continue: " ok && test "$$ok" = "yes"
@$(COMPOSE) --profile with-agent stop agent >/dev/null 2>&1 || true
@$(COMPOSE) stop hub >/dev/null 2>&1 || true
@docker run --rm -v recon_recon-state:/state alpine sh -c \
'rm -rf /state/ca && echo "CA wiped — old client certs are now stale"'
@$(COMPOSE) up -d --force-recreate hub
@echo "CA regenerated on hub start. Re-bootstrap each agent with the install one-liner."
.PHONY: compose-bootstrap-agent
compose-bootstrap-agent: ## (Re-)bootstrap the local-compose-agent: stop it, wipe state, revoke prior identity on the hub, issue a fresh token, start it
@$(COMPOSE) ps hub | grep -q "Up" || (echo "hub is not running — make compose-up first" && exit 1)
@$(COMPOSE) --profile with-agent stop agent >/dev/null 2>&1 || true
@$(COMPOSE) exec -T hub /usr/local/bin/recon-hub \
--config /etc/recon/hub.yaml --mode revoke \
--agent-id local-compose-agent --revoke-reason "compose-bootstrap-agent rerun" >/dev/null 2>&1 || true
@token=$$($(COMPOSE) exec -T hub /usr/local/bin/recon-hub \
--config /etc/recon/hub.yaml --mode gen-token \
--agent-id local-compose-agent --token-ttl 1h 2>/dev/null | tail -1); \
test -n "$$token" || (echo "failed to obtain bootstrap token" && exit 1); \
echo "$$token" | $(COMPOSE) --profile with-agent run --rm --no-deps -T --entrypoint /bin/sh agent \
-c 'rm -f /var/lib/recon-agent/agent.pem /var/lib/recon-agent/agent.key /var/lib/recon-agent/hub-ca.pem /var/lib/recon-agent/bootstrap.token; \
cat > /var/lib/recon-agent/bootstrap.token && chmod 0600 /var/lib/recon-agent/bootstrap.token' && \
echo "wiped stale cert + revoked prior identity + seeded fresh token; bringing the agent up" && \
$(COMPOSE) --profile with-agent up -d agent