Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .claude/knowledge/architecture-boundaries.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,15 @@
Key architectural decisions, service boundaries, data flow, integration points, and why things are the way they are.

- **Repository config is an untrusted input.** codeguard runs in CI on PRs, so `codeguard.yaml`/rule packs are attacker-controllable. Command execution and non-allowlisted AI provider base URLs are gated behind `internal/codeguard/trust` (a process-global `Policy`), off by default. The trust anchor is env vars / CLI flags (`--allow-config-commands`, `--allow-config-ai-endpoints`), never config. See `docs/security.md`.
- **All config-derived command execution must go through `trust.GuardConfigCommand`** before `exec`. Existing chokepoints: `runner/support/commands.go:runCommandCheck`, `ai/runtime/provider.go` (command provider), `ai/nlrule/runtime.go`, `ai/semantic/runtime.go`. codeguard's *own* tool invocations (git, govulncheck, the built-in `node -e` TS runner) are NOT gated β€” only commands sourced from config.
- **All config-derived command execution must go through `trust.GuardConfigCommand`** before `exec`. Existing chokepoints: `runner/support/commands.go:runCommandCheck`, `ai/runtime/provider.go` (command provider), `ai/nlrule/runtime.go`, `ai/semantic/runtime.go`, and `runner/govulncheck/govulncheck.go:Run` (gates a config-supplied `security_rules.govulncheck_command` override; the built-in default binary `govulncheck` resolved from PATH stays ungated so default "auto" mode works). codeguard's *own* fixed tool invocations (git, the built-in `node -e` TS runner) are NOT gated β€” only commands (or command names) sourced from config. **NOTE:** the govulncheck override was historically an RCE hole β€” it exec'd the config value with no gate and defaulted on for Go targets; do not remove the guard.
- **Outbound AI HTTP must use `internal/codeguard/ai/safehttp`**: `ValidateProviderURL` (allowlist of public hosts unless opted in) + `Client` (blocks loopback/private/link-local at dial time) + `MaxResponseBytes`. Triage (`ai/triage`) and the runtime providers (`ai/runtime`) both route through it. Triage merges env-then-config base URL; only the env-sourced value is treated as trusted (`BaseURLFromEnv`).
- **OWASP categories are baked into the rule catalog at var-init time**, not via `init()`. The merged `catalog` var calls `withSecurityOWASP(mergeRuleCatalogs(...))` in `rules/catalog.go`; an `init()` would run *after* the `catalog` var initializer copied stale entries. The mapping lives in `rules/catalog_security_owasp.go`. All read paths (`Catalog`, `RuleCatalogForConfig`, SARIF, CLI) flow through `rules.Catalog()`.
- Config-controlled artifact paths (`baseline.path`, `cache.path`, `ai.cache.path`) are resolved relative to the config dir and contained within it by `config.containConfigArtifactPaths` (in `config/io.go`, `LoadFile`). Paths escaping the config directory are rejected.
- The language-agnostic OWASP-gap line rules (CORS, debug, bind-all, weak-hash/cipher, deserialization, Dockerfile root) live in `checks/security/security_owasp_extra.go` and run per-line via `findingsForFile`. NOTE: `findingsForFile` is the **non-TypeScript** path β€” `securityTargetFindings` routes TS/JS targets to a separate `typeScriptTargetFindings` pipeline, so these rules do not run on TS/JS targets. String-literal signals (CORS, bind-all) match the raw line; call/identifier signals match the masked line.
- **Secret/credential scanning (`checks/security/security_secrets.go`) runs as its own repository-wide pass in `securityTargetFindings`, unconditionally for BOTH the TS and non-TS branches** β€” precisely so it covers TS/JS targets that bypass `findingsForFile`. Tiers: `security.hardcoded-credential` (fail; known provider formats + `security_rules.secrets.custom_patterns`), `security.private-key` (fail), `security.hardcoded-secret` (warn; lower-confidence name-based heuristic β€” was fail before, deliberately downgraded). Patterns match the **raw** line (the token lives in a string literal that masking would blank). Placeholder/allowlist filtering is done in Go after the regex match because RE2 has no lookahead (mirrors `isInsecureDeserialization`). Config: `security_rules.secrets` (`enabled`/`allow_paths`/`allow_patterns`/`custom_patterns`), defaulted in `applySecurityDefaults`, validated in `validateSecretsRules`.
- **PERF: the secret scanner is gated by a cheap literal pre-check (`builtinGatePasses`), not a combined regex.** Profiling showed `strings.Contains`/byte scans are ~85x faster than RE2 alternation, and that a leading `\b` or `(?i)` defeats RE2's literal-prefix fast path (those patterns ran ~1.8–7ms per 2000 lines; literal-prefix patterns like the Slack webhook ran ~50Β΅s). The gate is `gateLiterals` (case-sensitive `strings.Contains`) + `gateFoldLiterals` (lowercase, via alloc-free `asciiContainsFold`); when a line contains no marker, the ~17 built-in regexes are skipped. Net: default scan went 3.2β†’99 MB/s, entropy mode 3.1β†’47 MB/s. **INVARIANT: the gate must be a superset of every built-in pattern β€” adding a credential pattern requires adding its required literal to the gate.** `TestBuiltinGateCoversPatterns` enforces this; custom config patterns have arbitrary markers so they bypass the gate and always run.
- **CodeQL `go/regex/missing-regexp-anchor` (High) fires on unanchored regexes containing a host literal (e.g. `hooks\.slack\.com`), even for detection patterns.** The repo's accepted fix (see `quality_ai_style_drift.go:17` and the Slack webhook pattern in `security_secret_patterns.go`) is to bound the host with `(?:^|[^A-Za-z0-9_])(...)(?:$|[^A-Za-z0-9_])` β€” start/non-host-char on both sides β€” which adds the capture group as the matched value. CodeQL accepts this form; a bare `\b` or `(?:^|...)` left-only boundary may not. Apply this whenever adding a regex with an embedded domain literal.
- **HARDENING (codeguard scans untrusted PR content): the scanner bounds its work** β€” skips files > `maxScanFileBytes` (5 MiB), scans only the first `maxScanLineBytes` (64 KiB) of any line, and skips binary files (`looksBinary` = NUL byte in first 8 KiB). The git-history parser uses `bufio.Reader.ReadString` (grows for any line length), NOT `bufio.Scanner` β€” Scanner silently stops on an over-long token, which for a security scan would skip the rest of history undetected.
- **HARDENING (codeguard scans untrusted PR content): the scanner bounds its work** β€” skips files > `maxScanFileBytes` (5 MiB in the secrets/line scanner), scans only the first `maxScanLineBytes` (64 KiB) of any line, and skips binary files (`looksBinary` = NUL byte in first 8 KiB). Separately, the shared per-scan file corpus (`runner/support/utils.go:WalkFiles` + `corpus.go`) skips files above its own `maxScanFileBytes` (32 MiB) at walk time and reads via `readCappedFile` (LimitReader) β€” this is the cap that stops a single multi-GB file from OOMing the in-memory corpus; the two constants share a name but live in different packages. The git-history parser uses `bufio.Reader.ReadString` (grows for any line length), NOT `bufio.Scanner` β€” Scanner silently stops on an over-long token, which for a security scan would skip the rest of history undetected.
- **The secret scanner exposes a pure API for reuse: `security.BuildScanner(cfg) Scanner` + `Scanner.ScanContent(content) []Match` + `Scanner.SkipPath(file)`.** `secretFindingsForFile` wraps `ScanContent` with `env.NewFinding`. Tiers in `scanLine` (one match per line, priority order): private-key β†’ `credentialPatterns` (known formats + DB/Bearer/Azure, fail) β†’ config `custom_patterns` β†’ name-based (warn) β†’ opt-in entropy (`security.high-entropy-string`, warn). Values are masked in messages via `maskSecret` (first4…last4). Entropy = `shannonEntropy` (bits/char) over whitespace-free quoted literals, gated by `secrets.entropy` (off by default; min_length 20, threshold 4.5). GitHub-token CRC32 checksum validation was deliberately NOT added β€” getting the algorithm slightly wrong yields false negatives (worse than a false positive for a security tool).
- **Git-history secret scan lives in its own top-level package `internal/codeguard/history` (shells `git log -p -U0`), exposed via `pkg/codeguard.ScanGitHistory` and the `scan-history` CLI command.** It MUST be a separate package, not `runner/support`: `checks/security` imports `runner/support` (for `MatchPattern`), so `runner/support` importing `checks/security` would cycle. `history` imports `checks/security` (above it) and is consumed by `pkg` + CLI (both top-level). The parser tracks `+++ b/<path>`, hunk headers (`@@ … +c`), and `+`/`-` line prefixes to attribute added lines to commits; dedupes by rule+path+masked-message keeping the newest commit (log is newest-first).
- **GOTCHA: `support.ScanTargetFiles` caches per-file findings keyed by `(sectionID, target.Path, rel)`.** If two passes scan the same files with the same `sectionID`, the second pass gets a cache hit and returns the FIRST pass's findings. The secret pass therefore uses a distinct cache id `"security-secrets"` (not `"security"`) so it doesn't collide with the language pass. `sectionID` here only affects the cache key β€” findings still land in the real section via their RuleID/`FinalizeSection`. When adding any additional `ScanTargetFiles` call to an existing section, give it a unique cache id.
Expand Down
2 changes: 1 addition & 1 deletion .codeguard/codeguard.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ checks:
- RELEASE_PLEASE_TOKEN
- path: .github/workflows/release.yml
required_contains:
- goreleaser/goreleaser-action@v7
- goreleaser/goreleaser-action
- sync-homebrew-formula
- Formula/codeguard.rb
required_release_files:
Expand Down
20 changes: 20 additions & 0 deletions .github/CODEOWNERS
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# Code owners for codeguard. Owners are automatically requested for review on
# any pull request that touches matching paths, and β€” combined with branch
# protection's "Require review from Code Owners" β€” provide the change-management
# evidence (SOC 3 CC8.1) that changes are reviewed by an accountable party.
#
# Replace @alxxjohn with a @devr-tools/<team> handle once a maintainers team
# exists. Owners must have write access to the repo or GitHub ignores the entry.

# Default owner for everything not matched below.
* @alxxjohn

# Security-sensitive surfaces warrant explicit ownership.
/.github/ @alxxjohn
/.github/workflows/ @alxxjohn
/Dockerfile @alxxjohn
/Dockerfile.release @alxxjohn
/.goreleaser.yaml @alxxjohn
/SECURITY.md @alxxjohn
/internal/codeguard/trust/ @alxxjohn
/internal/codeguard/ai/safehttp/ @alxxjohn
12 changes: 8 additions & 4 deletions .github/workflows/cd.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ jobs:
tag: ${{ steps.version.outputs.tag }}
steps:
- name: Check out code
uses: actions/checkout@v7
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
with:
fetch-depth: 0

Expand Down Expand Up @@ -114,7 +114,9 @@ jobs:
with:
tag: ${{ needs.prepare-prerelease.outputs.tag }}
prerelease: true
secrets: inherit
create_missing_tag: true
secrets:
RELEASE_PLEASE_TOKEN: ${{ secrets.RELEASE_PLEASE_TOKEN }}

release-please:
if: github.event_name == 'push' && (github.ref_name == 'main' || github.ref_name == 'master')
Expand Down Expand Up @@ -142,7 +144,7 @@ jobs:

- name: Run release-please
id: release
uses: googleapis/release-please-action@v5
uses: googleapis/release-please-action@45996ed1f6d02564a971a2fa1b5860e934307cf7 # v5
with:
token: ${{ secrets.RELEASE_PLEASE_TOKEN }}
config-file: .github/release-please-config.json
Expand All @@ -160,4 +162,6 @@ jobs:
with:
tag: ${{ needs.release-please.outputs.tag_name }}
prerelease: false
secrets: inherit
create_missing_tag: true
secrets:
RELEASE_PLEASE_TOKEN: ${{ secrets.RELEASE_PLEASE_TOKEN }}
22 changes: 11 additions & 11 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,9 @@ jobs:
name: fmt
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7

- uses: actions/setup-go@v6
- uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6
with:
go-version-file: go.mod
cache: true
Expand All @@ -39,9 +39,9 @@ jobs:
needs: fmt
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7

- uses: actions/setup-go@v6
- uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6
with:
go-version-file: go.mod
cache: true
Expand All @@ -50,7 +50,7 @@ jobs:
run: make lint

- name: Run golangci-lint
uses: golangci/golangci-lint-action@v9
uses: golangci/golangci-lint-action@ba0d7d2ec06a0ea1cb5fa41b2e4a3ab91d21278a # v9
with:
version: v2.12.2

Expand All @@ -59,9 +59,9 @@ jobs:
needs: lint
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7

- uses: actions/setup-go@v6
- uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6
with:
go-version-file: go.mod
cache: true
Expand All @@ -82,9 +82,9 @@ jobs:
- test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7

- uses: actions/setup-go@v6
- uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6
with:
go-version-file: go.mod
cache: true
Expand All @@ -100,9 +100,9 @@ jobs:
- codeguard
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7

- uses: actions/setup-go@v6
- uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6
with:
go-version-file: go.mod
cache: true
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/homebrew-validation.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,10 @@ jobs:

steps:
- name: Check out code
uses: actions/checkout@v7
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7

- name: Set up Homebrew
uses: Homebrew/actions/setup-homebrew@main
uses: Homebrew/actions/setup-homebrew@64bb52ce84da6455bdbd6df88ac86151ba2ff16c # main

- name: Clone Homebrew tap
shell: bash
Expand Down
Loading
Loading