diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b5adfd3..ed5e976 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -51,8 +51,8 @@ jobs: with: go-version: ${{ env.GO_VERSION }} cache: true - - name: Clone hawk for shared/types - run: git clone --depth=1 https://github.com/GrayCodeAI/hawk.git ../hawk + - name: Boundary guard + run: bash ./scripts/check-ecosystem-boundaries.sh - name: gofumpt diff run: | go install mvdan.cc/gofumpt@v0.10.0 @@ -77,8 +77,8 @@ jobs: with: go-version: ${{ env.GO_VERSION }} cache: true - - name: Clone hawk for shared/types - run: git clone --depth=1 https://github.com/GrayCodeAI/hawk.git ../hawk + - name: Boundary guard + run: bash ./scripts/check-ecosystem-boundaries.sh - uses: golangci/golangci-lint-action@4afd733a84b1f43292c63897423277bb7f4313a9 # v7.0.0 with: version: v2.1.0 @@ -98,8 +98,8 @@ jobs: with: go-version: ${{ env.GO_VERSION }} cache: true - - name: Clone hawk for shared/types - run: git clone --depth=1 https://github.com/GrayCodeAI/hawk.git ../hawk + - name: Boundary guard + run: bash ./scripts/check-ecosystem-boundaries.sh - name: Tidy check run: | go mod tidy @@ -139,8 +139,6 @@ jobs: with: go-version: ${{ env.GO_VERSION }} cache: true - - name: Clone hawk for shared/types - run: git clone --depth=1 https://github.com/GrayCodeAI/hawk.git ../hawk - name: govulncheck run: | go install golang.org/x/vuln/cmd/govulncheck@v1.1.4 @@ -165,8 +163,6 @@ jobs: with: go-version: ${{ env.GO_VERSION }} cache: true - - name: Clone hawk for shared/types - run: git clone --depth=1 https://github.com/GrayCodeAI/hawk.git ../hawk - name: deadcode run: | go install golang.org/x/tools/cmd/deadcode@latest @@ -210,8 +206,6 @@ jobs: with: go-version: ${{ env.GO_VERSION }} cache: true - - name: Clone hawk for shared/types - run: git clone --depth=1 https://github.com/GrayCodeAI/hawk.git ../hawk - name: Build env: GOOS: ${{ matrix.goos }} diff --git a/.gitignore b/.gitignore index 784e66e..ca5a2f5 100644 --- a/.gitignore +++ b/.gitignore @@ -8,7 +8,12 @@ basic # Local state .claude/ .codegraph/ +.gocache/ coverage.out +# Go workspace (local dev only — each developer creates their own) +go.work +go.work.sum + # macOS .DS_Store diff --git a/AGENTS.md b/AGENTS.md index b8e1cd2..aa3224a 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -46,7 +46,7 @@ gofumpt -w . # Format - **Types are domain nouns**: `Finding`, `Report`, `Stats`, `Page`, `PageLink`, `Checker`, `RuleCheck` - **Option functions use `With` prefix**: `WithChecks()`, `WithDepth()`, `WithConcurrency()`, `WithAllowPrivateIPs()` - **Preset options are bare vars**: `Quick`, `Standard`, `Deep`, `SecurityOnly`, `CI` — exported `var Option` values -- **Severity is a type alias**: `type Severity = types.Severity` from `hawk/shared/types` — shared across hawk-eco +- **Severity is a type alias**: `type Severity = types.Severity` from `hawk-core-contracts/types` — shared across hawk-eco - **Internal adapters use `Adapter` suffix**: `ruleCheckAdapter`, `customCheckAdapter` — bridge public to internal interfaces - **Check names are lowercase strings**: `"security"`, `"links"`, `"forms"`, `"a11y"`, `"performance"` — used in `WithChecks()` - **Error handling**: `Scan()` returns `(*Report, error)` — validation errors for empty URL, nil errors for success @@ -94,7 +94,7 @@ gofumpt -w . # Format | Scanner implementation | `scanner.go` (crawler orchestration, check execution) | | Configuration & presets | `options.go` (`config` struct, `With*` functions, presets) | | Config file loading | `config.go` (`.inspect.toml` parsing, `LoadConfig()`) | -| Severity type alias | `severity.go` (re-exports from `hawk/shared/types`) | +| Severity type alias | `severity.go` (re-exports from `hawk-core-contracts/types`) | | SARIF output | `sarif.go` | | CI output formatting | `ci_output.go` | | Built-in checks | `checks/` directory | diff --git a/Makefile b/Makefile index 7dbfa4a..1a7c9cc 100644 --- a/Makefile +++ b/Makefile @@ -27,9 +27,12 @@ GOVULNCHECK := $(GOBIN_DIR)/govulncheck # --------------------------------------------------------------------------- # Phony declarations (alphabetical). # --------------------------------------------------------------------------- -.PHONY: all bench build ci clean cover fmt help lint lint-fix \ +.PHONY: all bench boundaries build ci clean cover fmt help lint lint-fix \ security test test-10x test-race tidy version vet +boundaries: ## Enforce support-repo import boundaries. + bash ./scripts/check-ecosystem-boundaries.sh + # --------------------------------------------------------------------------- # Default target. # --------------------------------------------------------------------------- @@ -93,7 +96,7 @@ tidy: ## Tidy go.mod / go.sum. # --------------------------------------------------------------------------- # Composite gate used by CI and pre-push. # --------------------------------------------------------------------------- -ci: tidy fmt vet lint test-race security ## Run everything CI runs. +ci: tidy fmt vet lint boundaries test-race security ## Run everything CI runs. @echo "All CI checks passed." # --------------------------------------------------------------------------- diff --git a/README.md b/README.md index 7f3526e..d02634d 100644 --- a/README.md +++ b/README.md @@ -31,6 +31,14 @@ handling, and SSRF protection), runs each check against the discovered pages, and returns findings with severity levels. Results can be emitted as SARIF for the GitHub Security tab. +## Ecosystem Boundaries + +Inspect is a Hawk support engine. Keep the dependency edge one-way: + +- use `hawk-core-contracts` for any cross-repo shared contracts +- do not import `hawk/internal/*` +- do not import removed legacy path `hawk/shared/types`; use `hawk-core-contracts/types` + ## Quick Start ```go diff --git a/contracts.go b/contracts.go new file mode 100644 index 0000000..f21597f --- /dev/null +++ b/contracts.go @@ -0,0 +1,53 @@ +package inspect + +import verifycontracts "github.com/GrayCodeAI/hawk-core-contracts/verify" + +// ToContractFinding converts an inspect finding into the shared verification contract. +func ToContractFinding(f Finding) verifycontracts.Finding { + return verifycontracts.Finding{ + Check: f.Check, + Severity: f.Severity, + URL: f.URL, + Element: f.Element, + Message: f.Message, + Fix: f.Fix, + Evidence: f.Evidence, + } +} + +// ToContractFindings converts inspect findings into shared verification contracts. +func ToContractFindings(findings []Finding) []verifycontracts.Finding { + if len(findings) == 0 { + return nil + } + out := make([]verifycontracts.Finding, len(findings)) + for i, f := range findings { + out[i] = ToContractFinding(f) + } + return out +} + +func toContractStats(s Stats) verifycontracts.Stats { + return verifycontracts.Stats{ + PagesScanned: s.PagesScanned, + FindingsTotal: s.FindingsTotal, + BySeverity: s.BySeverity, + ByCheck: s.ByCheck, + DurationPerCheck: s.DurationPerCheck, + } +} + +// ToContractReport converts an inspect report into the shared verification contract. +func ToContractReport(r *Report) *verifycontracts.Report { + if r == nil { + return nil + } + return &verifycontracts.Report{ + Target: r.Target, + Findings: ToContractFindings(r.Findings), + Stats: toContractStats(r.Stats), + CrawledURLs: r.CrawledURLs, + Duration: r.Duration, + FailOn: r.FailOn, + } +} diff --git a/contracts_test.go b/contracts_test.go new file mode 100644 index 0000000..bf7f881 --- /dev/null +++ b/contracts_test.go @@ -0,0 +1,41 @@ +package inspect + +import "testing" + +func TestToContractReport(t *testing.T) { + t.Parallel() + + report := &Report{ + Target: "https://example.com", + CrawledURLs: 3, + Findings: []Finding{ + { + Check: "security", + Severity: SeverityHigh, + URL: "https://example.com/login", + Message: "missing header", + }, + }, + Stats: Stats{ + PagesScanned: 2, + FindingsTotal: 1, + BySeverity: map[Severity]int{SeverityHigh: 1}, + ByCheck: map[string]int{"security": 1}, + }, + FailOn: SeverityMedium, + } + + got := ToContractReport(report) + if got == nil { + t.Fatal("expected non-nil contract report") + } + if got.Target != report.Target { + t.Fatalf("Target = %q, want %q", got.Target, report.Target) + } + if got.Stats.PagesScanned != 2 { + t.Fatalf("PagesScanned = %d, want 2", got.Stats.PagesScanned) + } + if len(got.Findings) != 1 || got.Findings[0].URL != "https://example.com/login" { + t.Fatalf("unexpected findings conversion: %+v", got.Findings) + } +} diff --git a/docs/architecture.md b/docs/architecture.md index c58ea54..b40348d 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -34,7 +34,7 @@ inspect/ ├── options.go ⚙️ config, With* options, presets (Quick/Standard/Deep/…) ├── check.go 🛡️ Checker interface, RuleCheck, RegisterCheck/RegisterRule ├── config.go 📋 .inspect config loading -├── severity.go 🎚️ Severity (aliased from hawk/shared/types) +├── severity.go 🎚️ Severity (aliased from hawk-core-contracts/types) ├── sarif.go 📊 GenerateSARIF — SARIF 2.1.0 output ├── browser.go 🌐 BrowserEngine interface + page-data types (no rod import) ├── browser_fetcher.go 🔌 Adapts a BrowserEngine into the crawler's fetcher diff --git a/go.mod b/go.mod index 8ad22db..0b08e07 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,7 @@ module github.com/GrayCodeAI/inspect go 1.26.4 require ( - github.com/GrayCodeAI/hawk v0.1.0 + github.com/GrayCodeAI/hawk-core-contracts v0.1.0 github.com/mark3labs/mcp-go v0.49.0 golang.org/x/net v0.55.0 golang.org/x/time v0.15.0 diff --git a/go.sum b/go.sum index a9c0dad..6c2c58b 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,7 @@ github.com/GrayCodeAI/hawk v0.1.0 h1:xqFcH05JEZrSB5grybanyAzkRf8GaFjPQxDV/d+ARrE= github.com/GrayCodeAI/hawk v0.1.0/go.mod h1:JIiKVFiFJL52OKNW/ndHRtjzVbVy6CX3HBydPpDHkwA= +github.com/GrayCodeAI/hawk-core-contracts v0.1.0 h1:w4Y6jBFzjXJ8zmAtfCWuKtODlHKosn9TStADxHE/LsU= +github.com/GrayCodeAI/hawk-core-contracts v0.1.0/go.mod h1:Oq7h+CGkmI3HFTHBVGaWHQuhvtGt6e8UYz5zwF4cM2Q= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= diff --git a/go.work b/go.work deleted file mode 100644 index de65690..0000000 --- a/go.work +++ /dev/null @@ -1,10 +0,0 @@ -go 1.26.4 - -use ( - . - ./browser -) - -// Local development overrides for unpublished modules. -// go.work is gitignored — each developer creates their own. -replace github.com/GrayCodeAI/hawk => ../hawk diff --git a/internal/check/registry.go b/internal/check/registry.go index 920138d..5e5d966 100644 --- a/internal/check/registry.go +++ b/internal/check/registry.go @@ -4,7 +4,7 @@ package check import ( "context" - "github.com/GrayCodeAI/hawk/shared/types" + "github.com/GrayCodeAI/hawk-core-contracts/types" "github.com/GrayCodeAI/inspect/internal/crawler" ) diff --git a/internal/report/format.go b/internal/report/format.go index f4ad290..591195b 100644 --- a/internal/report/format.go +++ b/internal/report/format.go @@ -7,7 +7,7 @@ import ( "strings" "time" - "github.com/GrayCodeAI/hawk/shared/types" + "github.com/GrayCodeAI/hawk-core-contracts/types" ) // Severity mirrors the public type. diff --git a/lefthook.yml b/lefthook.yml index ba5700d..7d5bdaf 100644 --- a/lefthook.yml +++ b/lefthook.yml @@ -110,3 +110,18 @@ commit-msg: echo " full guide: https://www.conventionalcommits.org/" exit 1 fi + + strip-co-authored-by: + run: | + # Strip Co-authored-by: trailers that AI tools (Claude, Cursor, etc.) add. + # This enforces the rule that commits list only the human author. + sed '/^[Cc]o-[Aa]uthored-[Bb]y:/d' "{1}" > "{1}.tmp" && mv "{1}.tmp" "{1}" + +# --------------------------------------------------------------------------- +# prepare-commit-msg — strip AI co-author trailers after tools inject them. +# --------------------------------------------------------------------------- +prepare-commit-msg: + commands: + strip-co-authored-by: + run: | + sed '/^[Cc]o-[Aa]uthored-[Bb]y:/d' "{1}" > "{1}.tmp" && mv "{1}.tmp" "{1}" diff --git a/scripts/check-ecosystem-boundaries.sh b/scripts/check-ecosystem-boundaries.sh new file mode 100644 index 0000000..04bb187 --- /dev/null +++ b/scripts/check-ecosystem-boundaries.sh @@ -0,0 +1,21 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +cd "$ROOT_DIR" + +violations="$( + rg -n 'github\.com/GrayCodeAI/hawk/(internal/|shared/types)' \ + --glob '*.go' \ + . || true +)" + +if [[ -n "${violations}" ]]; then + echo "forbidden Hawk imports found:" + echo "${violations}" + echo + echo "support repos must use hawk-core-contracts or local contracts, not hawk/internal or removed hawk/shared/types" + exit 1 +fi + +echo "ecosystem boundary guard passed" diff --git a/severity.go b/severity.go index 6ba3021..d70d28d 100644 --- a/severity.go +++ b/severity.go @@ -1,6 +1,6 @@ package inspect -import "github.com/GrayCodeAI/hawk/shared/types" +import "github.com/GrayCodeAI/hawk-core-contracts/types" // Severity represents the impact level of a finding. // Aliased from shared types for cross-module compatibility.