Skip to content

Commit ba47d6a

Browse files
authored
Improvments (#27)
2 parents 43e2e10 + e1050a9 commit ba47d6a

272 files changed

Lines changed: 10264 additions & 295 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.claude/knowledge/testing-patterns.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,8 @@ Testing strategies, test infrastructure quirks, how to run/debug specific test s
77
- 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. This is enforced by `ci.test-file-location` (the repo's own scan fails on `_test.go` files outside `tests/**`), so do not add tests next to the code even for unexported access — import the internal package from `tests/` and test via its exported API instead.
88
- **Never commit a contiguous real-format secret as a test fixture.** GitHub push protection rejects the push (it flags GitLab/Stripe/SendGrid/Twilio/etc. tokens even in `_test.go` files), and it is poor practice in a secret-detection repo. Assemble fixtures at runtime from a prefix + body via the `cred(prefix, body)` helper in `tests/checks/test_helpers_test.go` (e.g. `cred("AKIA", "1234567890ABCDEF")`), splitting the recognizable provider prefix from the rest so no full token literal appears in source. The reconstructed value still exercises the scanner identically. `goConst(value)` wraps a value as a minimal Go source fixture.
99
- The MCP server smoke tests (`tests/mcp/`) drive the **real binary in a subprocess**, not the in-process handler, to keep tests external. Pattern: a `Test...HelperProcess` func gated by an env var (`GO_WANT_MCP_HELPER_PROCESS` for stdio, `GO_WANT_MCP_HTTP_HELPER_PROCESS` for HTTP) re-execs `os.Args[0]` and calls `cli.Run(...)`. stdio replays NDJSON transcripts over stdin; HTTP reserves a free `127.0.0.1:0` port, launches `serve --mcp --http`, polls `/healthz` for readiness, then issues real HTTP requests. Add new MCP behavior as a transcript + assertion (stdio) or a subtest in `http_test.go` (HTTP).
10+
- **Detector precision corpus** (`tests/corpus/`): fixtures live under `testdata/<language-group>/<rule>/{vulnerable,clean}/` with ground truth in `expectations.yaml`. `known_gaps` entries assert a documented FP/FN *still exists* — when you improve a detector the corpus test fails on purpose with a "promote it to must_fire / remove the known_gaps entry" message; update the manifest in the same change. Fixture credentials must be synthetic but pattern-shaped (see the secret-fixture rule above).
11+
- **Every catalog rule must ship a `FixTemplate`** with a valid `Kind` (`deterministic`|`guided`) — `TestSDKRuleMetadataFixTemplatesPopulated` iterates the full catalog and fails on any rule without one. When adding a rule, add its template to the family map in `internal/codeguard/rules/catalog_fix_templates_*.go` (or inline in the catalog entry; inline wins over the registry).
12+
- `gofmt -l .` at repo root is polluted by `.claude/worktrees/` (live agent worktrees) and `.gomodcache/`; scope it to `gofmt -l cmd internal pkg tests changelog.go` or use `make fmt-check`.
1013
- **Bidirectional (server→client) MCP tests** live in `tests/mcp/sampling_test.go`: the test acts as the MCP client, advertises `sampling`/`roots` at `initialize`, and answers the server's server-initiated requests. stdio uses interactive `StdinPipe`/`StdoutPipe` (not the replay harness). HTTP opens the `GET /mcp` SSE stream (waits for the `: ready` comment to avoid the attach race), reads the request off the stream, and POSTs the response with the matching `Mcp-Session-Id`. propose_fix verification is expected to fail on the throwaway diff — assert the round trip fired, not a verified patch. The HTTP helper passes `-config` via `CODEGUARD_TEST_HTTP_CONFIG`.
14+
- **TS tests can be hijacked by the Node semantic engine**: on hosts with a discoverable `typescript.js` (e.g. VS Code installed), TypeScript targets route through the Node runner instead of the per-file Go path. Tests that must exercise the per-file path (tree-sitter differential tests, corpus TS groups) set `CODEGUARD_TYPESCRIPT_LIB_PATH` to an existing-but-invalid lib to force the fallback.

.codeguard/codeguard.yaml

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,10 @@ exclude:
55
- .codeguard/cache.slop-history.json
66
- .gomodcache/**
77
- tests/**/.codeguard/cache.json
8+
# Detection-precision fixtures: intentionally vulnerable-looking sample
9+
# files asserted by tests/corpus/corpus_test.go; keep them out of the
10+
# repository self-scan.
11+
- tests/corpus/testdata/**
812
waivers:
913
- rule: quality.max-file-lines
1014
path: internal/codeguard/rules/catalog_quality.go
@@ -18,6 +22,15 @@ waivers:
1822
- rule: quality.unbounded-goroutines-in-loop
1923
path: internal/codeguard/runner/checks/checks.go
2024
reason: section workers are bounded by a NumCPU-sized semaphore before each goroutine is launched
25+
- rule: ci.test-file-location
26+
path: internal/codeguard/checks/support/treesitter/*_test.go
27+
reason: the tree-sitter design spike is an isolated Go module whose differential tests must live beside the prototype they validate (docs/treesitter-spike.md)
28+
- rule: supply_chain.lockfile-drift
29+
path: internal/codeguard/checks/support/treesitter/go.mod
30+
reason: the spike module resolves the root module through a local replace directive, which never records a go.sum entry
31+
- rule: quality.ai.hallucinated-import
32+
path: internal/codeguard/checks/support/treesitter/*.go
33+
reason: the spike directory is a nested Go module with its own go.mod; its imports resolve there, not against the root module
2134
targets:
2235
- name: repository
2336
path: ..

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,3 +36,4 @@ go.work.sum
3636
.idea/
3737
.vscode/
3838
**/.codeguard/cache.slop-history.json
39+
**/.codeguard/cache.rule-stats-history.json

.goreleaser.yaml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,11 @@ builds:
1414
goarch:
1515
- amd64
1616
- arm64
17+
# Embed only the grammars codeguard parses (TS/TSX/JS) instead of the
18+
# full ~206-grammar registry: ~+4 MB binary delta instead of ~+22 MB.
19+
# See docs/treesitter-spike.md §5.3.
20+
flags:
21+
- -tags=grammar_subset,grammar_subset_typescript,grammar_subset_tsx,grammar_subset_javascript
1722
ldflags:
1823
- -s -w -X github.com/devr-tools/codeguard/internal/version.Number=v{{ .Version }}
1924

Makefile

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,12 @@ GOFILES := $(shell find cmd internal pkg tests -type f -name '*.go' 2>/dev/null)
2424
MANIFEST_VERSION := $(shell grep -oE '[0-9]+\.[0-9]+\.[0-9]+' .release-please-manifest.json 2>/dev/null | head -1)
2525
MENU_VERSION ?= $(if $(MANIFEST_VERSION),$(MANIFEST_VERSION),$(VERSION))
2626
MENU_LDFLAGS := -X github.com/devr-tools/codeguard/internal/version.Number=v$(MENU_VERSION)
27+
# GRAMMAR_TAGS restricts the gotreesitter grammar registry to the languages
28+
# codeguard parses (docs/treesitter-spike.md §5.3): the subset build embeds
29+
# only the TypeScript/TSX/JavaScript grammar blobs (~+4 MB) instead of all
30+
# ~206 (~+22 MB). Builds without these tags (plain `go build`, `go install`)
31+
# still work; they just embed every grammar.
32+
GRAMMAR_TAGS := grammar_subset,grammar_subset_typescript,grammar_subset_tsx,grammar_subset_javascript
2733

2834
export GOCACHE
2935
export GOMODCACHE
@@ -85,7 +91,7 @@ ci: check
8591

8692
build:
8793
@mkdir -p dist
88-
$(GO) build -trimpath -o $(CODEGUARD_BIN) ./cmd/codeguard
94+
$(GO) build -trimpath -tags $(GRAMMAR_TAGS) -o $(CODEGUARD_BIN) ./cmd/codeguard
8995

9096
release: release-snapshot
9197

docs/checks.md

Lines changed: 85 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,16 @@ This file documents the current check categories in `codeguard` and the config k
1212
"security": true,
1313
"prompts": true,
1414
"ci": true,
15-
"supply_chain": false
15+
"supply_chain": false,
16+
"context": true
1617
}
1718
}
1819
```
1920

2021
Each top-level boolean enables or disables an entire check family.
2122

23+
`context` covers agent-context legibility: when the key is omitted the family defaults to enabled in full scans and disabled in diff scans; see [Agent Context](#agent-context).
24+
2225
`supply_chain` is opt-in and currently covers normalized manifest parsing plus initial policy checks for missing lockfiles, content-based lockfile drift validation, unpinned dependencies, and dependency license policy resolved from local manifest and installed metadata where available.
2326

2427
For ecosystems where local metadata is not present, `supply_chain_rules.license_commands` can provide an opt-in per-ecosystem command that prints JSON license mappings for unresolved dependencies.
@@ -146,6 +149,16 @@ codeguard baseline -config codeguard.yaml -output codeguard-baseline.json
146149

147150
Current behavior:
148151
- baseline fingerprints are filtered before section status is computed
152+
- each entry stores two fingerprints: the legacy line-based one
153+
(`fingerprint`) and a context fingerprint (`context_fingerprint`) hashed
154+
from the rule, path, and the whitespace-normalized source lines around the
155+
finding (2 lines either side), so unrelated edits that only shift line
156+
numbers do not break suppression
157+
- a finding is suppressed when either fingerprint matches; two identical
158+
findings in the same file (same rule and surrounding source) share a context
159+
fingerprint, so baselining one also baselines its identical twins
160+
- baseline files written before context fingerprints existed keep working:
161+
their legacy fingerprints still match unchanged findings
149162
- suppressed counts remain visible in the report summary
150163

151164
## Policy profiles
@@ -221,6 +234,21 @@ TypeScript semantic runtime:
221234
- discovery order is `CODEGUARD_TYPESCRIPT_LIB_PATH`, then `node_modules/typescript/lib/typescript.js` from the target path upward, then the bundled VS Code TypeScript runtime
222235
- if no runtime is available, codeguard falls back to the lightweight parser-based checks for TypeScript and JavaScript
223236

237+
Tree-sitter parsing (opt-in):
238+
- `parsers.treesitter: "auto"` (default `"off"`) routes the lightweight
239+
TypeScript/TSX/JavaScript checks through embedded tree-sitter grammars
240+
instead of regexes for `quality.typescript.explicit-any`,
241+
`quality.typescript.non-null-assertion`,
242+
`quality.typescript.double-assertion`, and
243+
`security.typescript.unsafe-html-sink` (plus their `*.javascript.*`
244+
mirrors where the syntax exists in JavaScript)
245+
- tree-based findings keep the same rule IDs, levels, and messages and set
246+
`confidence: high`; they see through template-literal interpolations,
247+
regex literals, JSX text, formatter-split expressions, and compound
248+
assignments where the regex path cannot
249+
- oversized files (> 256 KiB), parse failures, and error-heavy trees fall
250+
back to the regex path per file
251+
224252
## Quality
225253

226254
Purpose:
@@ -685,6 +713,62 @@ Rules:
685713
- `ci.always-true-test-assertion` warns when every assertion in a test only compares constants (`expect(true).toBe(true)`, `assert 1 == 1`, `require.True(t, true)`), so the test can never fail
686714
- `ci.conditional-assertion` warns when every assertion in a test sits inside a conditional without an else branch, so the assertions may silently never run; idiomatic Go failure checks (`if got != want { t.Errorf(...) }`) are not flagged
687715

716+
## Agent Context
717+
718+
Purpose:
719+
- Agent instruction file presence (CLAUDE.md, AGENTS.md, .cursorrules, .github/copilot-instructions.md)
720+
- Drift between agent docs / README commands and the actual repository
721+
- Agent context budget for individual source files
722+
- Basename ambiguity that defeats filename-based navigation
723+
- A `repo_legibility` artifact scoring how legible the repository is to AI agents
724+
725+
Config keys:
726+
727+
```json
728+
{
729+
"checks": {
730+
"context": true,
731+
"context_rules": {
732+
"detect_missing_agent_docs": true,
733+
"detect_agent_docs_drift": true,
734+
"detect_readme_drift": true,
735+
"detect_oversized_files": true,
736+
"detect_ambiguous_symbols": true,
737+
"max_file_lines": 1500,
738+
"ambiguous_symbol_threshold": 4
739+
}
740+
}
741+
}
742+
```
743+
744+
When `checks.context` is omitted the family runs in full scans and is skipped in diff scans: its signature findings are repo-level (missing agent docs, duplicated basenames) and would repeat on every PR regardless of the change under review. Set `"context": true` to force it on in diff scans, or `false` to disable it entirely.
745+
746+
Current behavior:
747+
- `context.agent-docs-missing` warns once at repo level when none of the recognized agent instruction files exist at the target root
748+
- `context.agent-docs-drift` warns when an agent instruction file references a file or directory path, a `make` target, or an npm/pnpm/yarn `run` script that provably does not exist
749+
- `context.readme-drift` applies the same resolution to fenced `bash`/`sh`/`shell` blocks in the root README.md: `./`-prefixed paths, make targets, and run scripts that resolve nowhere
750+
- `context.oversized-context-unit` warns when a source file exceeds `context_rules.max_file_lines` (default 1500); the message is framed as agent context cost, distinct from `quality.max-file-lines` maintainability thresholds; generated and vendored files are skipped
751+
- `context.ambiguous-symbol` warns once per source-file basename shared by at least `context_rules.ambiguous_symbol_threshold` files (default 4), listing up to five locations
752+
753+
Drift resolution is deliberately conservative — precision over recall. It only flags references it can positively prove broken, and skips:
754+
- URLs, module/domain paths (`github.com/...`), absolute paths, and `..` traversals
755+
- placeholders and expansions (`<name>`, `$VAR`), globs, and template syntax
756+
- all fenced blocks except shell command fences (code samples and captured output are never treated as paths)
757+
- shell blocks after a `cd`/`pushd` or a heredoc, and `make -C`/`-f` invocations that select another makefile
758+
- make targets when no root Makefile exists or the Makefile uses `include` or pattern rules
759+
- npm scripts when there is no root package.json or it declares workspaces
760+
761+
`repo_legibility` artifact:
762+
763+
Every context run publishes one `repo_legibility` artifact per target with a 0-100 score (higher is more legible) and an explainable component breakdown:
764+
- `agent_docs` (25): any agent instruction file present
765+
- `readme` (10): root README.md present
766+
- `doc_accuracy` (20): minus 4 points per unresolvable doc/README reference
767+
- `context_economy` (25): scaled down by the share of source files over the context budget (10% oversized zeroes it)
768+
- `navigability` (20): scaled down by the share of source files caught in ambiguous basename groups (20% affected zeroes it)
769+
770+
The artifact is emitted even when individual rules are toggled off, so the score always reports reality.
771+
688772
## Output
689773

690774
Config keys:

docs/features.md

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,8 +57,42 @@ This page lists the current `codeguard` feature surface and the main config entr
5757
- `quality.ai.change-risk`
5858
- aggregates AI-quality and review-risk signals into a target-level artifact plus a `Code Quality` finding when thresholds are crossed
5959

60+
## Parsers
61+
62+
- `parsers.treesitter: "off" | "auto"` (default `"off"`) selects the parsing
63+
substrate for TypeScript/TSX/JavaScript rules (`docs/treesitter-spike.md`).
64+
- `"off"`: the regex-based scanners run exactly as before.
65+
- `"auto"`: script files parse through embedded tree-sitter grammars; the
66+
migrated rules (`quality.typescript.explicit-any`,
67+
`quality.typescript.non-null-assertion`,
68+
`quality.typescript.double-assertion`,
69+
`security.typescript.unsafe-html-sink` and their `*.javascript.*`
70+
mirrors) evaluate grammar queries instead of regexes and report
71+
`confidence: high`. Files that exceed the 256 KiB parse cap, fail to
72+
parse, or produce error-heavy trees fall back to the regex path per file,
73+
so enabling the flag can never lose coverage.
74+
6075
## JSON/YAML config examples
6176

77+
### Tree-sitter parsing for TypeScript/JavaScript
78+
79+
YAML:
80+
81+
```yaml
82+
parsers:
83+
treesitter: auto
84+
```
85+
86+
JSON:
87+
88+
```json
89+
{
90+
"parsers": {
91+
"treesitter": "auto"
92+
}
93+
}
94+
```
95+
6296
### Enable AI change risk
6397

6498
YAML:

docs/security.md

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ codeguard owasp -format json # machine-readable
6363
Example:
6464

6565
```
66-
OWASP Top 10 (2021) coverage: 8/10 categories have rules
66+
OWASP Top 10 (2021) coverage: 9/10 categories have rules
6767
6868
[ok ] A01:2021-Broken Access Control (2 rules)
6969
[ok ] A02:2021-Cryptographic Failures (11 rules)
@@ -73,14 +73,15 @@ OWASP Top 10 (2021) coverage: 8/10 categories have rules
7373
[ok ] A06:2021-Vulnerable and Outdated Components (1 rules)
7474
[ok ] A07:2021-Identification and Authentication Failures (1 rules)
7575
[ok ] A08:2021-Software and Data Integrity Failures (1 rules)
76-
[gap ] A09:2021-Security Logging and Monitoring Failures (0 rules)
76+
[ok ] A09:2021-Security Logging and Monitoring Failures (2 rules)
7777
[ok ] A10:2021-Server-Side Request Forgery (SSRF) (2 rules)
7878
```
7979

80-
`A04` (Insecure Design) and `A09` (Security Logging and Monitoring) are left as
81-
explicit gaps: both are design- and operations-level risks that static
82-
heuristics cannot reliably detect, and a false "covered" there would be
83-
misleading.
80+
`A04` (Insecure Design) is left as an explicit gap: it is a design-level risk
81+
that static heuristics cannot reliably detect, and a false "covered" there
82+
would be misleading. `A09` is covered by two heuristics that target the
83+
code-visible slice of the category: secrets flowing into log output and raw
84+
errors leaking to HTTP clients instead of being logged server-side.
8485

8586
### Newly added detection rules
8687

@@ -99,6 +100,8 @@ taint engine and default to `fail`.
99100
| `security.weak-hash` | A02 | MD5 / SHA-1 used for security |
100101
| `security.weak-cipher` | A02 | DES / RC4 / ECB mode |
101102
| `security.insecure-deserialization` | A08 | `pickle`, unsafe `yaml.load`, Java `readObject`, `Marshal.load`, `unserialize` |
103+
| `security.log-secret-exposure` | A09 | secret-named identifiers (password, token, api_key, …) inside the argument list of a Go/Python/TS/JS logging call, secret-named structured-log keys, and secret-labeled string literals concatenated or format-directed into log output |
104+
| `security.unsanitized-error-response` | A09 | raw error values written directly into HTTP responses: Go `http.Error(w, err.Error(), …)` / `fmt.Fprintf(w, …, err)`, TS/JS `res.send(err)` / `res.json(err)` / `res.status(…).send(err.stack \|\| err.message)`, Python `return str(e)` / `HttpResponse(str(e))` inside `except` blocks |
102105
| `security.ssrf.go` / `security.ssrf.python` | A10 | untrusted input flowing into an outbound HTTP request URL |
103106

104107
## Release integrity (supply chain)

0 commit comments

Comments
 (0)