diff --git a/.claude/settings.json b/.claude/settings.json new file mode 100644 index 0000000..cb0fb84 --- /dev/null +++ b/.claude/settings.json @@ -0,0 +1,46 @@ +{ + "$schema": "https://json.schemastore.org/claude-code-settings.json", + "permissions": { + "allow": [ + "Bash(make help)", + "Bash(make deps)", + "Bash(make clean)", + "Bash(make test)", + "Bash(make test-all)", + "Bash(make test-integration)", + "Bash(make test-demo)", + "Bash(make fmt)", + "Bash(make fmt-check)", + "Bash(make lint)", + "Bash(make release-snapshot)", + "Bash(go build:*)", + "Bash(go test:*)", + "Bash(go vet:*)", + "Bash(go run ./cmd/example:*)", + "Bash(go run ./cmd/wraith:*)", + "Bash(go doc:*)", + "Bash(go list:*)", + "Bash(go mod tidy)", + "Bash(go mod download)", + "Bash(gofmt:*)", + "Bash(golangci-lint run:*)", + "Bash(osv-scanner --version)", + "Bash(git status:*)", + "Bash(git diff:*)", + "Bash(git log:*)", + "Bash(git show:*)", + "Bash(git branch:*)", + "Bash(gh pr view:*)", + "Bash(gh pr diff:*)", + "Bash(gh pr list:*)", + "Bash(gh run view:*)", + "Bash(gh run list:*)" + ], + "deny": [ + "Bash(goreleaser release:*)", + "Bash(git push --force:*)", + "Read(./.env)", + "Read(./.env.*)" + ] + } +} diff --git a/.github/workflows/lint.go.yml b/.github/workflows/lint.go.yml index 6ee7241..3793528 100644 --- a/.github/workflows/lint.go.yml +++ b/.github/workflows/lint.go.yml @@ -13,5 +13,7 @@ jobs: with: go-version-file: go.mod cache: true + - name: Check gofmt formatting + run: make fmt-check - name: Run Go golangci-lint run: make lint diff --git a/.gitignore b/.gitignore index 002c724..6f03de9 100644 --- a/.gitignore +++ b/.gitignore @@ -22,6 +22,9 @@ !LICENSE !NOTICE !Makefile +!CLAUDE.md +!AGENTS.md +!.claude/settings.json # ...even if they are in subdirectories !*/ diff --git a/AGENTS.md b/AGENTS.md new file mode 120000 index 0000000..681311e --- /dev/null +++ b/AGENTS.md @@ -0,0 +1 @@ +CLAUDE.md \ No newline at end of file diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..a5539ce --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,27 @@ +# Wraith + +Dependency vulnerability scanner that wraps [osv-scanner](https://github.com/google/osv-scanner) behind a friendlier CLI and a Go library. Module path is `github.com/ghostsecurity/wraith`. No third-party Go dependencies, and it must stay that way, since a scanner that pulls in a large dependency tree undercuts its own point. + +## Layout + +- `pkg/wraith.go` mirrors the osv-scanner JSON schema as Go structs. +- `pkg/scanner.go` resolves and shells out to the osv-scanner binary, parses its JSON, and defines the functional `ScanOption` API. +- `pkg/convert.go` reduces the raw result into simplified findings, counts, and severity or license filters. +- `cmd/wraith/main.go` is the CLI with `scan`, `download-db`, and `version` subcommands, plus the text, JSON, and markdown renderers. +- `cmd/example/main.go` shows library usage. + +## Binary resolution + +`NewScanner` looks for osv-scanner in three places in order: an explicit path argument, a binary sitting next to the wraith executable, then `$PATH`. Releases ship the bundled binary alongside wraith, which is why the second case exists. The pinned version lives in three places that have to move together on a bump: `OSV_VERSION` in `scripts/download-osv-scanner.sh`, `.goreleaser.yaml`, and `.github/workflows/test.go.yml`. + +## Working on it + +Run `make test` for unit tests, which is what CI runs and which needs no osv-scanner binary. Anything covering the real scanner belongs behind `-short` so that stays true. `make test-integration` and `make test-demo` exercise the live binary and require osv-scanner on `$PATH`. Run `make lint` before opening a PR. Use `make release-snapshot` to check a goreleaser change without tagging. + +Scanner behavior is verified against the fixtures in `pkg/testdata`, where `lockfiles/` holds real lockfiles and `output/` holds recorded osv-scanner JSON. Prefer extending those fixtures over mocking the binary. + +## Conventions + +Run `make fmt` after touching any Go file. CI runs `make fmt-check` on every pull request and fails the build when a file is not gofmt-formatted, so an unformatted edit blocks the merge rather than getting cleaned up later. + +Commits and PR titles carry the Jira key, as in `GHO-11706: bump bundled osv-scanner to v2.3.8`. Errors wrap with `%w` and read as lowercase phrases. The CLI writes results to stdout and diagnostics to stderr, so keep that split intact for the agents and pipelines consuming this tool. diff --git a/Makefile b/Makefile index 093a24a..54d983c 100644 --- a/Makefile +++ b/Makefile @@ -32,14 +32,29 @@ test: ## Run unit tests only (fast, no OSV-Scanner required) .PHONY: test-integration test-integration: ## Run integration tests (requires OSV-Scanner) - @which osv-scanner || (echo "❌ osv-scanner not found in PATH." && exit 1) + @which osv-scanner || (echo "osv-scanner not found in PATH." && exit 1) go test -v ./pkg/... -run "Integration" .PHONY: test-demo test-demo: ## Run live demo with real lockfiles - @which osv-scanner || (echo "❌ osv-scanner not found in PATH." && exit 1) + @which osv-scanner || (echo "osv-scanner not found in PATH." && exit 1) go test -v ./pkg/... -run "QuickScan" +.PHONY: fmt +fmt: ## Format all Go files with gofmt + gofmt -w . + +.PHONY: fmt-check +fmt-check: ## Fail if any Go file is not gofmt-formatted + @unformatted=$$(gofmt -l .); \ + if [ -n "$$unformatted" ]; then \ + echo "Not gofmt-formatted:"; \ + echo "$$unformatted"; \ + echo "Run 'make fmt' to fix."; \ + exit 1; \ + fi + @echo "All Go files are gofmt-formatted" + .PHONY: lint lint: ## Run linter @which golangci-lint || go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest