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
11 changes: 11 additions & 0 deletions .claude/knowledge/architecture-boundaries.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# Architecture & System Boundaries

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.
- **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.
- The taint engine emits a single rule ID per language, chosen by sink: `goTaintRuleID`/`pyTaintRuleID` return `security.ssrf.{go,python}` when the sink is an outbound-HTTP callee (`goHTTPSinkArgIndex`/`pyHTTPSinkArgIndex`), else the generic `security.taint.{go,python}`. To add an SSRF sink, add it to the arg-index map; the rule routing and OWASP A10 mapping follow automatically.
7 changes: 0 additions & 7 deletions .claude/knowledge/local-dev-setup.md

This file was deleted.

7 changes: 7 additions & 0 deletions .claude/knowledge/testing-patterns.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# Testing Patterns

Testing strategies, test infrastructure quirks, how to run/debug specific test suites, mocking conventions.

- The trust policy (`internal/codeguard/trust`) is a process-global. Tests that exercise command-driven checks or local (127.0.0.1) AI endpoints must enable it. `tests/checks` and `tests/codeguard` each have a `TestMain` (`trust_main_test.go`) that sets `Policy{AllowConfigCommands: true, AllowConfigAIEndpoints: true}`. If you add a new test package that runs config commands or hits a loopback test server, add a similar `TestMain` or the tests will fail with "refusing to run config-supplied command" / "ssrf guard" errors.
- `tests/security` deliberately has NO trust `TestMain`: it verifies the secure *default*. Tests there set/restore the policy locally with `trust.Set(...)` + `t.Cleanup(trust.ResetFromEnv)`. Do not add a package-wide opt-in there.
- All tests live under `tests/` as external (`_test`) packages; there are no white-box tests inside `internal/`/`pkg/`. Internal packages are still importable from `tests/` because they are within the module.
3 changes: 3 additions & 0 deletions .codeguard/codeguard.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ waivers:
- rule: quality.max-file-lines
path: tests/checks/features_test.go
reason: consolidated feature coverage is intentionally broad and should still be scanned by other checks
- rule: ci.test-without-assertion
path: tests/**/trust_main_test.go
reason: Go TestMain functions bootstrap package-wide trust policy and are not assertion-bearing tests
targets:
- name: repository
path: ..
Expand Down
30 changes: 30 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
version: 2
updates:
# Go module dependencies.
- package-ecosystem: gomod
directory: "/"
schedule:
interval: weekly
open-pull-requests-limit: 10
groups:
go-dependencies:
patterns:
- "*"

# GitHub Actions used by the workflows (pin updates + security fixes).
- package-ecosystem: github-actions
directory: "/"
schedule:
interval: weekly
open-pull-requests-limit: 10
groups:
github-actions:
patterns:
- "*"

# Base images used by the release/runtime Dockerfiles.
- package-ecosystem: docker
directory: "/"
schedule:
interval: weekly
open-pull-requests-limit: 5
33 changes: 33 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,16 @@ on:
permissions:
contents: write
packages: write
# Required for keyless cosign signing and SLSA provenance (OIDC token).
id-token: write

jobs:
build-release:
runs-on: ubuntu-latest
outputs:
prerelease: ${{ steps.release.outputs.prerelease }}
tag: ${{ steps.release.outputs.tag }}
hashes: ${{ steps.hashes.outputs.hashes }}
steps:
- name: Resolve release context
id: release
Expand Down Expand Up @@ -141,6 +144,12 @@ jobs:
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3

- name: Install cosign
uses: sigstore/cosign-installer@v3

- name: Install syft (SBOM generation)
uses: anchore/sbom-action/download-syft@v0

- name: Log in to GHCR
uses: docker/login-action@v3
with:
Expand All @@ -156,13 +165,37 @@ jobs:
args: release --clean --config .goreleaser.yaml
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
COSIGN_YES: "true"

- name: Compute artifact hashes for provenance
id: hashes
shell: bash
run: |
set -euo pipefail
# The SHA256SUMS file already covers every released artifact.
hashes="$(base64 -w0 dist/SHA256SUMS)"
echo "hashes=$hashes" >>"$GITHUB_OUTPUT"

- name: Upload release bundle
uses: actions/upload-artifact@v7
with:
name: dist-${{ steps.release.outputs.tag }}
path: dist/*

# SLSA build provenance for the released artifacts. The generic generator
# produces a signed in-toto attestation over the artifact hashes and uploads
# it to the GitHub release.
provenance:
needs: build-release
permissions:
actions: read
id-token: write
contents: write
uses: slsa-framework/slsa-github-generator/.github/workflows/generator_generic_slsa3.yml@v2.1.0
with:
base64-subjects: ${{ needs.build-release.outputs.hashes }}
upload-assets: true

sync-homebrew-formula:
needs: build-release
if: needs.build-release.outputs.prerelease != 'true'
Expand Down
31 changes: 31 additions & 0 deletions .goreleaser.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,37 @@ archives:
checksum:
name_template: SHA256SUMS

# Generate a CycloneDX SBOM per archive (requires `syft` on PATH in CI).
sboms:
- id: archive
artifacts: archive

# Keyless cosign signing of the checksum file. Signing the checksums covers
# every released artifact. Requires `cosign` on PATH and an OIDC token
# (id-token: write) in CI; COSIGN_YES is set in the release workflow.
signs:
- cmd: cosign
certificate: "${artifact}.pem"
signature: "${artifact}.sig"
args:
- sign-blob
- "--output-certificate=${certificate}"
- "--output-signature=${signature}"
- "${artifact}"
- "--yes"
artifacts: checksum
output: true

# Keyless cosign signing of the published container images.
docker_signs:
- cmd: cosign
artifacts: images
args:
- sign
- "${artifact}"
- "--yes"
output: true

dockers:
- id: codeguard-amd64
ids:
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,7 @@ func main() {

- [Getting started](/Users/alex/Documents/GitHub/codeguard/docs/getting-started.md:1)
- [Features](/Users/alex/Documents/GitHub/codeguard/docs/features.md:1)
- [Security & OWASP](/Users/alex/Documents/GitHub/codeguard/docs/security.md:1)
- [AI-generated code quality](/Users/alex/Documents/GitHub/codeguard/docs/ai-quality.md:1)
- [Agent-native features](/Users/alex/Documents/GitHub/codeguard/docs/agent-native.md:1)
- [Integrations](/Users/alex/Documents/GitHub/codeguard/docs/integrations.md:1)
Expand Down
2 changes: 1 addition & 1 deletion docs/checks.md
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,7 @@ codeguard scan -config codeguard.yaml -profile strict

## Rule metadata

SDK and catalog discovery surfaces return both `execution_model` and `language_coverage` for each rule via `codeguard.Rules()`, `codeguard.RulesForConfig(...)`, `codeguard.ExplainRule(...)`, and `codeguard.ExplainRuleForConfig(...)`.
SDK and catalog discovery surfaces return `execution_model`, `language_coverage`, and (for security rules) `owasp_category` for each rule via `codeguard.Rules()`, `codeguard.RulesForConfig(...)`, `codeguard.ExplainRule(...)`, and `codeguard.ExplainRuleForConfig(...)`. The OWASP Top 10 (2021) mapping and per-category coverage are documented in [Security & OWASP](/Users/alex/Documents/GitHub/codeguard/docs/security.md:1) and reported by `codeguard owasp`.

`execution_model` values:
- `go-native`: built-in logic that currently depends on Go-specific source structure or Go-only integrations
Expand Down
123 changes: 123 additions & 0 deletions docs/security.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
# Security & OWASP

This page documents codeguard's own trust model and its OWASP Top 10 (2021)
coverage. For the catalogue of checks codeguard runs against *your* repository,
see [Checks reference](/Users/alex/Documents/GitHub/codeguard/docs/checks.md:1).

## Trust model: repository config is untrusted by default

codeguard is frequently run in CI against pull requests, including from
untrusted contributors. Its behavior is driven by `codeguard.yaml` and rule
packs, which live in the repository and are therefore controllable by whoever
opens the PR. To prevent a code-review tool from becoming a remote-code-execution
or credential-exfiltration vector, the following capabilities are **disabled by
default** and must be explicitly enabled by the trusted operator (via the
process environment or a CLI flag — never from the repo config itself):

| Capability | Default | Opt-in env | Opt-in flag |
| --- | --- | --- | --- |
| Run commands defined in config (`*_rules.language_commands`, `license_commands`, `ai.autofix.test_commands`, the `command` AI provider, nlrule/semantic command runtimes) | refused | `CODEGUARD_ALLOW_CONFIG_COMMANDS=1` | `--allow-config-commands` |
| Use an AI provider `baseURL` outside the built-in allowlist, and reach non-public addresses | refused | `CODEGUARD_ALLOW_CONFIG_AI_ENDPOINTS=1` | `--allow-config-ai-endpoints` |

For a repository you control end-to-end, enable the capabilities you need:

```bash
codeguard scan --allow-config-commands # run configured commands
CODEGUARD_ALLOW_CONFIG_AI_ENDPOINTS=1 codeguard scan --ai # custom/self-hosted LLM
```

### What the defaults protect against

- **Command injection / RCE in CI (A03 / A08).** Without the opt-in, codeguard
refuses to execute any command supplied by the repository config.
- **Credential exfiltration & SSRF (A10 / A02).** AI provider base URLs from
config are restricted to a small allowlist of known public hosts
(`api.openai.com`, `api.anthropic.com`). The provider HTTP client also refuses
to connect to loopback, private, and link-local addresses (including the cloud
metadata endpoint), defending against DNS-rebinding and redirect-based SSRF.
Provider responses are size-bounded to prevent memory exhaustion.
- **Path traversal / arbitrary file write (A01).** Config-controlled artifact
paths (`baseline.path`, `cache.path`, `ai.cache.path`) are resolved relative
to the config directory and rejected if they escape that directory tree.

Environment variables are the trust anchor because, in a standard
`pull_request` workflow, the environment is controlled by the workflow author
(base branch), not by the PR.

## OWASP Top 10 (2021) coverage

Every built-in security rule is tagged with its OWASP Top 10 (2021) category.
The mapping is surfaced in:

- `codeguard rules` — appends the category as a trailing column for security rules.
- `codeguard explain <rule-id>` — an `owasp:` line (text) / `owasp_category` field (`-format agent`).
- SARIF output — each rule carries `properties.tags` (`OWASP:Axx:2021`) and an `owasp` property, which GitHub code scanning surfaces.

Use the coverage report to see which categories have rules and which are gaps:

```bash
codeguard owasp # text report
codeguard owasp -format json # machine-readable
```

Example:

```
OWASP Top 10 (2021) coverage: 8/10 categories have rules

[ok ] A01:2021-Broken Access Control (2 rules)
[ok ] A02:2021-Cryptographic Failures (11 rules)
[ok ] A03:2021-Injection (24 rules)
[gap ] A04:2021-Insecure Design (0 rules)
[ok ] A05:2021-Security Misconfiguration (4 rules)
[ok ] A06:2021-Vulnerable and Outdated Components (1 rules)
[ok ] A07:2021-Identification and Authentication Failures (1 rules)
[ok ] A08:2021-Software and Data Integrity Failures (1 rules)
[gap ] A09:2021-Security Logging and Monitoring Failures (0 rules)
[ok ] A10:2021-Server-Side Request Forgery (SSRF) (2 rules)
```

`A04` (Insecure Design) and `A09` (Security Logging and Monitoring) are left as
explicit gaps: both are design- and operations-level risks that static
heuristics cannot reliably detect, and a false "covered" there would be
misleading.

### Newly added detection rules

These heuristic rules close the previously-empty categories. The misconfiguration
and crypto rules are text-based and default to `warn`; the SSRF rules use the
taint engine and default to `fail`.

| Rule | OWASP | Detects |
| --- | --- | --- |
| `security.cors-wildcard` | A05 | `Access-Control-Allow-Origin: *` |
| `security.debug-enabled` | A05 | framework debug flag enabled (`debug=True`) |
| `security.bind-all-interfaces` | A05 | binding to `0.0.0.0` |
| `security.dockerfile-root` | A05 | Dockerfile `USER root` |
| `security.weak-hash` | A02 | MD5 / SHA-1 used for security |
| `security.weak-cipher` | A02 | DES / RC4 / ECB mode |
| `security.insecure-deserialization` | A08 | `pickle`, unsafe `yaml.load`, Java `readObject`, `Marshal.load`, `unserialize` |
| `security.ssrf.go` / `security.ssrf.python` | A10 | untrusted input flowing into an outbound HTTP request URL |

## Release integrity (supply chain)

codeguard's own releases are hardened against tampering:

- **Signed artifacts** — the `SHA256SUMS` checksum file is signed with keyless
cosign (Sigstore/Fulcio); container images are signed with `cosign sign`.
- **SBOM** — a CycloneDX SBOM is generated per archive via `syft`.
- **SLSA provenance** — a signed in-toto build provenance attestation is produced
over the artifact hashes and attached to the release.
- **Dependabot** — weekly updates for Go modules, GitHub Actions, and Docker base
images keep dependencies current (A06).

To verify a downloaded release:

```bash
cosign verify-blob \
--certificate SHA256SUMS.pem \
--signature SHA256SUMS.sig \
--certificate-identity-regexp 'https://github.com/devr-tools/codeguard/.*' \
--certificate-oidc-issuer https://token.actions.githubusercontent.com \
SHA256SUMS
```
2 changes: 2 additions & 0 deletions internal/cli/commands.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ func runScan(args []string, stdin io.Reader, stdout io.Writer, stderr io.Writer)
if err := fs.Parse(args); err != nil {
return 1
}
flags.applyTrustPolicy()

if err := promptScanInputs(*interactive, stdin, stdout, &inputs); err != nil {
_, _ = fmt.Fprintf(stderr, "interactive scan: %v\n", err)
Expand Down Expand Up @@ -161,6 +162,7 @@ func runBaseline(args []string, stdout io.Writer, stderr io.Writer) int {
if err := fs.Parse(args); err != nil {
return 1
}
flags.applyTrustPolicy()

cfg, err := loadConfigWithProfile(*flags.configPath, *flags.profile)
if err != nil {
Expand Down
1 change: 1 addition & 0 deletions internal/cli/fix.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ func runFix(args []string, stdout io.Writer, stderr io.Writer) int {
if err := fs.Parse(args); err != nil {
return 1
}
flags.applyTrustPolicy()
if !*enableAI {
_, _ = fmt.Fprintln(stderr, "fix requires -ai so unverified AI patch generation is never implicit")
return 1
Expand Down
3 changes: 2 additions & 1 deletion internal/cli/helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,12 +31,13 @@ Usage:
codeguard init [-output codeguard.yaml] [-interactive] [-profile startup|strict|enterprise|ai-safe]
codeguard validate [-config codeguard.yaml] [-profile startup|strict|enterprise|ai-safe]
codeguard validate-patch [-config codeguard.yaml] [-format text|json|sarif|github] [-profile startup|strict|enterprise|ai-safe] [-ai] < patch.diff
codeguard scan [-config codeguard.yaml] [-mode full|diff] [-base-ref main] [-format text|json|sarif|github] [-interactive] [-profile startup|strict|enterprise|ai-safe] [-ai]
codeguard scan [-config codeguard.yaml] [-mode full|diff] [-base-ref main] [-format text|json|sarif|github] [-interactive] [-profile startup|strict|enterprise|ai-safe] [-ai] [-allow-config-commands] [-allow-config-ai-endpoints]
codeguard fix [-config codeguard.yaml] [-mode full|diff] [-base-ref main] [-profile startup|strict|enterprise|ai-safe] [-rule rule.id] [-path rel/path] [-line N] -ai
codeguard baseline [-config codeguard.yaml] [-output codeguard-baseline.json] [-mode full|diff] [-base-ref main] [-profile startup|strict|enterprise|ai-safe]
codeguard report -slop-history [-config codeguard.yaml] [-limit N] [-profile startup|strict|enterprise|ai-safe]
codeguard rules [-config codeguard.yaml]
codeguard explain [-config codeguard.yaml] [-format text|agent] <rule-id>
codeguard owasp [-config codeguard.yaml] [-format text|json] [-profile startup|strict|enterprise|ai-safe]
codeguard serve --mcp [-config codeguard.yaml] [-profile startup|strict|enterprise|ai-safe]
codeguard doctor [-config codeguard.yaml] [-profile startup|strict|enterprise|ai-safe]
codeguard profiles
Expand Down
Loading
Loading