# Copyright 2024 Chainguard, Inc.
# SPDX-License-Identifier: Apache-2.0

SAMPLES_REPO ?= chainguard-sandbox/malcontent-samples
SAMPLES_COMMIT ?= fca96935d01d3b0a7a2765626279c676bd9d495f

# BEGIN: lint-install ../malcontent
# http://github.com/tinkerbell/lint-install

.PHONY: lint
lint: _lint

LINT_ARCH := $(shell uname -m)
LINT_OS := $(shell uname)
LINT_OS_LOWER := $(shell echo $(LINT_OS) | tr '[:upper:]' '[:lower:]')
LINT_ROOT := $(shell dirname $(realpath $(firstword $(MAKEFILE_LIST))))

# flags required to successfully build malcontent with yara-x's C API
CPPFLAGS ?= "-I$(LINT_ROOT)/out/include"
LDFLAGS :=
PKGCONF_PATH ?= "$(LINT_ROOT)/out/lib/pkgconfig"

ifeq ($(LINT_OS),Darwin)
	LDFLAGS="-L$(LINT_ROOT)/out/lib -Wl,-no_warn_duplicate_libraries,-rpath,$(LINT_ROOT)/out/lib,-lyara_x_capi"
else ifeq ($(LINT_OS),Linux)
	LDFLAGS="-L$(LINT_ROOT)/out/lib -Wl,-rpath,$(LINT_ROOT)/out/lib,-lyara_x_capi,-no-pie"
endif

# yara-x adds an additional string for the platform (apple, unknown)
LINT_PLATFORM :=
ifeq ($(LINT_OS),Darwin)
	LINT_PLATFORM=apple
else
	LINT_PLATFORM=unknown
endif

LINT_PLATFOM_SUFFIX :=
ifeq ($(LINT_OS),Linux)
	LINT_PLATFORM_SUFFIX=-gnu
endif

LINTERS :=
FIXERS :=

GOLANGCI_LINT_CONFIG := $(LINT_ROOT)/.golangci.yml
GOLANGCI_LINT_VERSION ?= v2.15.0
GOLANGCI_LINT_INSTALL_REF := 25c5369a93aacb8cccea0d46e1569ca4a194564d
GOLANGCI_LINT_INSTALL_SHA256 := 1022ddb4d87ed252350ed03fc9677e250a4ae95cc6bcd4658c2a20a8a23d390f
SHA256_CMD := $(shell command -v sha256sum || echo "shasum -a 256")
GOLANGCI_LINT_BIN := $(LINT_ROOT)/out/linters/golangci-lint-$(GOLANGCI_LINT_VERSION)-$(LINT_ARCH)
$(GOLANGCI_LINT_BIN):
	mkdir -p $(LINT_ROOT)/out/linters
	rm -rf $(LINT_ROOT)/out/linters/golangci-lint-*
	curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/$(GOLANGCI_LINT_INSTALL_REF)/install.sh -o $(LINT_ROOT)/out/linters/install-golangci-lint.sh
	printf '%s  %s\n' "$(GOLANGCI_LINT_INSTALL_SHA256)" "$(LINT_ROOT)/out/linters/install-golangci-lint.sh" | $(SHA256_CMD) -c -
	sh $(LINT_ROOT)/out/linters/install-golangci-lint.sh -b $(LINT_ROOT)/out/linters $(GOLANGCI_LINT_VERSION)
	rm -f $(LINT_ROOT)/out/linters/install-golangci-lint.sh
	mv $(LINT_ROOT)/out/linters/golangci-lint $@

YARA_X_REPO ?= virusTotal/yara-x
YARA_X_VERSION ?= v1.22.0
YARA_X_COMMIT ?= e27597f2c05e13fe1c2bb459781aef8899afc485
YARA_X_SHA :=
ifeq ($(LINT_OS),Darwin)
	ifeq ($(shell uname -m),arm64)
		LINT_ARCH = aarch64
		YARA_X_SHA = 79d596da5bf02205de63a1d7844a964daebc7293fa36fcb7da46c3e3c7567229
	else
		YARA_X_SHA = d059da4b91c7d7b43f6f50174a7739e3362b51f349f2ba8b33d46e3334bb3300
	endif
else ifeq ($(LINT_OS),Linux)
	ifneq ($(filter $(shell uname -m),aarch64 arm64),)
		LINT_ARCH = aarch64
		YARA_X_SHA = b1b90f0686ae3e9f607c07b743fd96d6283d9ea5e2fd764919b8505de8b63d4e
	else
		YARA_X_SHA = 7f470770b2c1a0d97f02298fc194978d999074d147d003d33dd62775761439db
	endif
endif
YARA_X_BIN := $(LINT_ROOT)/out/linters/yr-$(YARA_X_VERSION)-$(LINT_ARCH)
YARA_X_RELEASE_EXT := .tar.gz
$(YARA_X_BIN):
	mkdir -p $(LINT_ROOT)/out/linters
	rm -rf $(LINT_ROOT)/out/linters/yr
	curl -sSfL https://github.com/VirusTotal/yara-x/releases/download/$(YARA_X_VERSION)/yara-x-$(YARA_X_VERSION)-$(LINT_ARCH)-$(LINT_PLATFORM)-$(LINT_OS_LOWER)$(LINT_PLATFORM_SUFFIX)$(YARA_X_RELEASE_EXT) -o yara-x$(YARA_X_RELEASE_EXT)
	echo "$(YARA_X_SHA) *yara-x$(YARA_X_RELEASE_EXT)" | shasum -a 256 --check
	tar -xzvf yara-x$(YARA_X_RELEASE_EXT) && mv yr $(LINT_ROOT)/out/linters && rm yara-x$(YARA_X_RELEASE_EXT)
	mv $(LINT_ROOT)/out/linters/yr $@

LINTERS += golangci-lint-lint
golangci-lint-lint: $(GOLANGCI_LINT_BIN)
	find . -maxdepth 1 -name go.mod -print0 | xargs -0 -L1 -I{} /bin/sh -c 'CGO_LDFLAGS=$(LDFLAGS) CGO_CPPFLAGS=$(CPPFLAGS) PKG_CONFIG_PATH=$(PKGCONF_PATH) "$(GOLANGCI_LINT_BIN)" run -c "$(GOLANGCI_LINT_CONFIG)"' \;

FIXERS += golangci-lint-fix
golangci-lint-fix: $(GOLANGCI_LINT_BIN)
	CGO_LDFLAGS=$(LDFLAGS) \
	CGO_CPPFLAGS=$(CPPFLAGS) \
	PKG_CONFIG_PATH=$(PKGCONF_PATH) \
	find . -maxdepth 1 -name go.mod -execdir "$(GOLANGCI_LINT_BIN)" run -c "$(GOLANGCI_LINT_CONFIG)" --fix \;

FIXERS += modernize
modernize:
	CGO_LDFLAGS=$(LDFLAGS) \
	CGO_CPPFLAGS=$(CPPFLAGS) \
	PKG_CONFIG_PATH=$(PKGCONF_PATH) \
	go run golang.org/x/tools/gopls/internal/analysis/modernize/cmd/modernize@latest -fix -test ./...

LINTERS += yara-x-fmt
yara-x-fmt: $(YARA_X_BIN)
	find rules -type f -name "*.yara" -execdir "$(YARA_X_BIN)" fmt {} \;

# Suppress only text_as_hex: the hex patterns are deliberate. Rules are embedded in
# the binary, so a plaintext malware signature in the shipped executable makes macOS
# Gatekeeper/XProtect flag malcontent itself. Every other warning surfaces, and a
# bare -w here would disable all of them.
yara-x-compile: $(YARA_X_BIN)
	"$(YARA_X_BIN)" compile --path-as-namespace --disable-warnings=text_as_hex --define 'file_sha256=""' rules/

# rewrite text patterns of filesize- or header-constrained first-party rules as
# literal regexps (see hack/literal_regexps.pl); third_party/yara/update.sh does
# the same for third-party rules
.PHONY: literal-regexps
literal-regexps:
	find rules -type f -name "*.yara" -print0 | xargs -0 perl hack/literal_regexps.pl

.PHONY: _lint $(LINTERS)
_lint: $(LINTERS)

.PHONY: fix $(FIXERS)
fix: $(FIXERS)

# END: lint-install ../malcontent

# sample checkouts have 3 stages:
# out/samples/.git - I'm a blank git repo
# out/samples/.git/commit-<hash> - I'm checked out to this particular commit
# out/samples/.decompressed-<hash> - I've decompressed to this particular commit

out/${SAMPLES_REPO}/.git/commit-$(SAMPLES_COMMIT):
	mkdir -p out/$(SAMPLES_REPO)
	test -d out/$(SAMPLES_REPO)/.git || git clone --depth 4 https://github.com/$(SAMPLES_REPO).git out/$(SAMPLES_REPO)
	rm out/$(SAMPLES_REPO)/.git/commit-* 2>/dev/null || true
	git -C out/$(SAMPLES_REPO) switch - || true
	git -C out/$(SAMPLES_REPO) pull
	git -C out/$(SAMPLES_REPO) checkout $(SAMPLES_COMMIT)
	touch out/$(SAMPLES_REPO)/.git/commit-$(SAMPLES_COMMIT)

out/$(SAMPLES_REPO)/.decompressed-$(SAMPLES_COMMIT): out/${SAMPLES_REPO}/.git/commit-$(SAMPLES_COMMIT)
	find out/$(SAMPLES_REPO)/ -name "*.xz" -type f -exec xz -dk {} \;
	touch out/$(SAMPLES_REPO)/.decompressed-$(SAMPLES_COMMIT)

out/$(YARA_X_REPO)/.git/commit-$(YARA_X_COMMIT):
	mkdir -p out/$(YARA_X_REPO)
	test -d out/$(YARA_X_REPO)/.git || git clone https://github.com/$(YARA_X_REPO).git out/$(YARA_X_REPO)
	rm out/$(YARA_X_REPO)/.git/commit-* 2>/dev/null || true
	git -C out/$(YARA_X_REPO) switch - || true
	git -C out/$(YARA_X_REPO) pull --rebase --autostash
	git -C out/$(YARA_X_REPO) checkout $(YARA_X_COMMIT)
	touch out/$(YARA_X_REPO)/.git/commit-$(YARA_X_COMMIT)

samples: out/$(SAMPLES_REPO)/.decompressed-$(SAMPLES_COMMIT)

.PHONY: install-yara-x
install-yara-x: out/$(YARA_X_REPO)/.git/commit-$(YARA_X_COMMIT)
	mkdir -p out/lib
	mkdir -p out/include
	cd out/$(YARA_X_REPO) && \
	cargo install cargo-c --locked && \
	RUSTFLAGS="-C target-feature=+crt-static" cargo cinstall -p yara-x-capi --features=native-code-serialization --profile release-lto --prefix="$(LINT_ROOT)/out" --libdir="$(LINT_ROOT)/out/lib" --crt-static --library-type="staticlib"

.PHONY: update-deps
update-deps:
	go get -u ./...
	go mod tidy

# re-pin developer tools, e.g. `make update-golangci-lint VERSION=v2.13.2`
# or `make update-crane VERSION=v0.22.1`
.PHONY: update-golangci-lint update-crane
update-golangci-lint:
	hack/update_tool.sh golangci-lint "$(VERSION)"
update-crane:
	hack/update_tool.sh crane "$(VERSION)"

# unit tests only
.PHONY: test
test:
	CGO_LDFLAGS=$(LDFLAGS) \
	CGO_CPPFLAGS=$(CPPFLAGS) \
	PKG_CONFIG_PATH=$(PKGCONF_PATH) \
	go test -race ./pkg/... ./internal/...

# aggregated quality gate: vet, formatter, linter chain, and the test
# suite. `make test` is invoked (not bare `go test`) because the
# repository's cgo link step requires the LDFLAGS/CPPFLAGS/PKG_CONFIG_PATH
# env injection the `test` target supplies.
.PHONY: verify
verify:
	CGO_LDFLAGS=$(LDFLAGS) \
	CGO_CPPFLAGS=$(CPPFLAGS) \
	PKG_CONFIG_PATH=$(PKGCONF_PATH) \
	go vet ./...
	CGO_LDFLAGS=$(LDFLAGS) \
	CGO_CPPFLAGS=$(CPPFLAGS) \
	PKG_CONFIG_PATH=$(PKGCONF_PATH) \
	gosec -exclude-generated ./...
	gofumpt -w .
	$(MAKE) _lint
	$(MAKE) test

.PHONY: gosec
gosec:
	CGO_LDFLAGS=$(LDFLAGS) \
	CGO_CPPFLAGS=$(CPPFLAGS) \
	PKG_CONFIG_PATH=$(PKGCONF_PATH) \
	gosec -exclude-generated ./...

FUZZ_TIME ?= 10s
.PHONY: fuzz
fuzz:
	@grep -r "^func Fuzz" --include="*_test.go" pkg/ | \
		awk -F'[:(]' '{gsub(/func /, "", $$2); dir=$$1; sub(/\/[^/]+$$/, "/", dir); print $$2, "./" dir}' | \
		while read -r func dir; do \
			echo "--- $$func ($$dir) ---"; \
			CGO_LDFLAGS=$(LDFLAGS) \
			CGO_CPPFLAGS=$(CPPFLAGS) \
			PKG_CONFIG_PATH=$(PKGCONF_PATH) \
			go test -timeout 0 -fuzz="^$$func$$" -fuzztime=$(FUZZ_TIME) "$$dir" || exit 1; \
		done

# fuzz tests - runs continuously (use Ctrl+C to stop)
# Usage: make fuzz-continuous FUZZ_TARGET=FuzzExtractArchive FUZZ_PKG=./pkg/archive/
FUZZ_TARGET ?= FuzzExtractArchive
FUZZ_PKG ?= ./pkg/archive/
.PHONY: fuzz-continuous
fuzz-continuous:
	CGO_LDFLAGS=$(LDFLAGS) \
	CGO_CPPFLAGS=$(CPPFLAGS) \
	PKG_CONFIG_PATH=$(PKGCONF_PATH) \
	go test -fuzz=$(FUZZ_TARGET) $(FUZZ_PKG)

# unit tests only
.PHONY: coverage
coverage: out/mal.coverage

# generate the html report
.PHONY: coverage-html
coverage-html: out/coverage.html

# pop open the html page in a browser directly
.PHONY: coverage-browser
coverage-browser: out/mal.coverage
	CGO_LDFLAGS=$(LDFLAGS) \
	CGO_CPPFLAGS=$(CPPFLAGS) \
	PKG_CONFIG_PATH=$(PKGCONF_PATH) \
	go tool cover -html=$<

# generate the html report
out/coverage.html: out/mal.coverage
	CGO_LDFLAGS=$(LDFLAGS) \
	CGO_CPPFLAGS=$(CPPFLAGS) \
	PKG_CONFIG_PATH=$(PKGCONF_PATH) \
	go tool cover -html=$< -o $@

# we always want to regen the coverage data file
.PHONY: out/mal.coverage
out/mal.coverage:
	mkdir -p out
	CGO_LDFLAGS=$(LDFLAGS) \
	CGO_CPPFLAGS=$(CPPFLAGS) \
	PKG_CONFIG_PATH=$(PKGCONF_PATH) \
	go test -coverprofile $@ -race ./pkg/... ./internal/... -coverpkg ./pkg/...,./internal/...

INTEGRATION_PARALLEL_FLAGS ?= $(if $(CI),,-p 1 -parallel 2)

# integration tests only
.PHONY: integration
integration: out/$(SAMPLES_REPO)/.decompressed-$(SAMPLES_COMMIT)
	MALCONTENT_UPX_PATH=$(shell which upx) \
	CGO_LDFLAGS=$(LDFLAGS) \
	CGO_CPPFLAGS=$(CPPFLAGS) \
	PKG_CONFIG_PATH=$(PKGCONF_PATH) \
	go test -race -timeout 0 $(INTEGRATION_PARALLEL_FLAGS) ./tests/...

.PHONY: bench
bench: out/$(SAMPLES_REPO)/.decompressed-$(SAMPLES_COMMIT)
	CGO_LDFLAGS=$(LDFLAGS) \
	CGO_CPPFLAGS=$(CPPFLAGS) \
	PKG_CONFIG_PATH=$(PKGCONF_PATH) \
	go test -run=^\$$ -bench=. ./... -benchmem

BENCH_CMD := CGO_LDFLAGS=$(LDFLAGS) CGO_CPPFLAGS=$(CPPFLAGS) PKG_CONFIG_PATH=$(PKGCONF_PATH) \
	go test -benchmem -run=^\$$ -bench ^BenchmarkRun\$$ github.com/chainguard-dev/malcontent/tests -args

.PHONY: bench-malcontent
bench-malcontent:
	$(BENCH_CMD) -path="macOS/clean/malcontent"

.PHONY: bench-all-samples
bench-all-samples:
	$(BENCH_CMD) -path=""

.PHONY: bench-does-nothing
bench-does-nothing:
	$(BENCH_CMD) -path="does-nothing"

.PHONY: bench-javascript
bench-javascript:
	$(BENCH_CMD) -path="Javascript"

.PHONY: bench-linux
bench-linux:
	$(BENCH_CMD) -path="Linux"

.PHONY: bench-macos
bench-macos:
	$(BENCH_CMD) -path="macOS"

.PHONY: bench-npm
bench-npm:
	$(BENCH_CMD) -path="NPM"

.PHONY: bench-php
bench-php:
	$(BENCH_CMD) -path="PHP"

.PHONY: bench-python
bench-python:
	$(BENCH_CMD) -path="Python"

.PHONY: bench-typescript
bench-typescript:
	$(BENCH_CMD) -path="TypeScript"

.PHONY: bench-windows
bench-windows:
	$(BENCH_CMD) -path="Windows"

.PHONY: out/mal
out/mal:
	mkdir -p out
	CGO_LDFLAGS=$(LDFLAGS) \
	CGO_CPPFLAGS=$(CPPFLAGS) \
	PKG_CONFIG_PATH=$(PKGCONF_PATH) \
	go build -o out/mal ./cmd/mal

.PHONY: update-third-party
update-third-party:
	./third_party/yara/update.sh

.PHONY: refresh-sample-testdata
refresh-sample-testdata: out/$(SAMPLES_REPO)/.decompressed-$(SAMPLES_COMMIT) out/mal
	MALCONTENT_UPX_PATH=$(shell which upx) ./out/mal refresh

ARCH ?= $(shell uname -m)
CRANE_VERSION=v0.22.1
out/crane-$(ARCH)-$(CRANE_VERSION):
	mkdir -p out
	GOBIN=$(CURDIR)/out go install github.com/google/go-containerregistry/cmd/crane@$(CRANE_VERSION)
	mv out/crane out/crane-$(ARCH)-$(CRANE_VERSION)

export-image: out/crane-$(ARCH)-$(CRANE_VERSION)
	./out/crane-$(ARCH)-$(CRANE_VERSION) \
	export \
	cgr.dev/chainguard/static:latest@sha256:bde549df44d5158013856a778b34d8972cf52bb2038ec886475d857ec7c365ed - | xz > pkg/action/testdata/static.tar.xz
