Repository navigation
fix: stop codeguard silently passing on broken scans (high-priority review fixes) - #189
Merged
Merged
Conversation
…WarnFinding
The quality and performance sections built warn findings through a
variadic warnFinding(env, args ...any) helper that type-asserted each
positional argument at runtime. A swapped argument compiled cleanly and
panicked mid-scan, which section-panic recovery then downgraded to a
warning.
Migrate all 41 call sites (29 quality, 12 performance) to the typed
support.NewWarnFinding(env, support.WarnFindingInput{...}) builder and
delete both variadic helpers. Quality call sites go through a thin typed
newQualityWarnFinding wrapper that keeps the aiRuleConfidence lookup in
one place, so findings output is unchanged (self-scan JSON is identical
before and after apart from version/timestamp).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The comment step ran `codeguard scan -format github-comment`, but config
validation rejected github-comment even though the report writer renders it,
so the scan printed nothing. The step ran under `set +e`, logged "comment
report was empty; skipping", and the workflow stayed green. It also ran from
github.action_path, so a working scan would have covered codeguard's own
source instead of the caller's repository.
- Accept github-comment in output validation; list it (and cyclonedx-json)
in the error message, the -format flag help, and the docs.
- Run the comment scan from the default working directory
($GITHUB_WORKSPACE), and build the comment helper with
`go -C "$GITHUB_ACTION_PATH"`.
- Pass inputs to the step through env: instead of interpolating
${{ inputs.* }} into the shell script.
- When the report is empty, emit ::error and a stderr message and exit 1.
The exit status cannot tell a broken scan from one with findings (both
exit 1), so the report being empty is the test.
- Add regression tests: config validation, an end-to-end
`scan -format github-comment` run, and structural checks on action.yml.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A recovered section panic was reported as a hand-built finding with level "warning" (not the canonical "warn"), no fingerprint, severity or catalog entry, and a warn section status. A crashing Security section therefore exited 0 and passed CI. SafeRun now reports the crash as an operational fail-level core.Diagnostic (scan.section.panic) on a failing section, for every section: a crashed section checked nothing. A diagnostic, not a finding, because findings can be baselined, waived or confidence-filtered, which would silence the crash on every later run. The user-facing message stays clean; the panic value and a short stack excerpt go in metadata and evidence. The recovered section is also streamed to OnSectionComplete, which it previously skipped. The catalog emission coverage test now also walks runner/checks and holds checks.* ids to the catalog. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Go repositories that gate on gosec via golangci-lint already record each
reviewed subprocess call site as `//nolint:gosec // reason` or
`#nosec G204 -- reason`. Accept those as inline suppressions of
security.shell-execution so the review does not have to be written twice.
The translation is deliberately narrow:
- Go source only, and a justification containing letters is required
- //nolint must list gosec explicitly; #nosec must name G204
- G204 maps to security.shell-execution only; a linter-wide nolint:gosec
never suppresses taint, secret, private-key or crypto findings
- trailing directives cover their own line, comment-only directives the
next line (never both)
- suppressed findings carry {"kind":"inline","match":"gosec-justification"}
Also document the reason convention for codeguard:ignore and the MCP
config files the prompts section governs.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Every insecure-deserialization alternative is a call or type name, never a string value, yet it was matched against the raw line. A loader named in a string literal or comment fired: the detector's own gate table, fix-template examples, and Go test fixtures. Match the masked line instead; languages without a masker keep raw matching, and the safe-loader filter still reads the raw line. The string-content rules in the same file (bind-all-interfaces, weak-cipher, ...) cannot be fixed this way because their signal lives in string literals, so the detector's own gate literals and finding text carry line-bound codeguard:ignore directives instead. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
isMCPConfigFile treated every file under a directory named mcp/ as an
MCP config, so MCP server source and its tests became prompt assets: a
Go test carrying a `${OPENAI_API_KEY}` diff fixture failed
prompts.secret-interpolation. Restrict the directory heuristic to config
formats (.json, .jsonc, .yaml, .yml, .toml). The heuristic now also
matches a top-level mcp/ directory.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Enable checks.security and checks.prompts in .codeguard/codeguard.yaml and resolve every fail-level finding: - tests/mcp: re-exec the test binary via os.Executable instead of os.Args[0] (clears three security.taint.go fails) - security_fixture_classification.go: spell out the filler-run example instead of committing an AKIA-prefixed literal in a production comment - private-key fixtures and the fix-template example: line-bound codeguard:ignore with reasons; one waiver for the line-number-sensitive scanner fixture that cannot carry an inline directive - triage mock counter: codeguard:ignore security.taint.go with a reason (operator-controlled env path, test instrumentation) - patch.go: codeguard:ignore for the constant-argv git apply call Waivers are scoped per rule and path class: shell-execution and the string-content OWASP rules in tests (mirrors the golangci gosec exclusion for tests/), the same OWASP rules in in-package _test.go files, pattern quotes in docs/, and the deliberately public Devin MCP HTTP example. Secret, private-key and taint rules are never waived by path. local_precision stays off: enabling it adds ~2060 warnings across 53 rules, almost all in production code. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A target path that did not exist walked as empty, produced no findings, and reported every file-driven section as a clean pass with exit 0; `codeguard validate` also accepted it. Walk errors and per-file read errors were dropped (`files, _ :=`, `return nil`, `continue`). - Add config.ValidateTargetPaths (and the SDK ValidateTargetPaths): each target path must be an existing, readable directory. config.Validate and SDK ValidateConfig stay pure. RunWithOptions and `codeguard validate` call the new check, so both now error with a non-zero exit. - Record target walk errors in the shared corpus as an operational fail diagnostic, scan.target-walk. It is not drained like the other corpus diagnostics: every section that finalizes after the failure reports it, so no section that consumed the partial listing passes. - Report files that were listed but could not be read as an operational warn diagnostic, scan.file-read, capped at 20 itemised files plus one summary diagnostic. - Document target path requirements and the scan diagnostics in docs/checks.md. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The per-file findings cache keyed an entry on section, target, path, the file's own content hash and the section config hash. Several evaluators read more than that file, so editing a sibling or a manifest replayed stale findings for unchanged files: - Go quality: with local_precision on (the default) the mutation resolver sees package types and variables from a package-wide index built from sibling files; quality.dependency-direction resolves import paths from go.mod/go.work. - Go design: design.cmd-through-internal-cli and design.service-import-internal match against the SDK facade, resolved from go.mod and the pkg/ layout. Evaluators can now declare those inputs with checks/support.FileScanDependencies (an in-memory Key, same-directory PackageFiles, target-wide SharedFiles) via ScanTargetFilesWithDependencies. The runner computes one digest per directory once per scan pass, from the shared corpus listing and reads, and stores it as DepsHash on the cache entry. Evaluators that declare nothing store an empty DepsHash, so their entries and hit rate are unchanged. Nothing is computed when the cache is off. Also give the local-abstraction design pass its own cache section id. It scanned the same Go/Python/TS/C++ files as the per-language design pass under "design", so whichever pass ran second replayed the first pass's cached findings, even within one cold cached scan, and the local-abstraction findings disappeared. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> # Conflicts: # docs/checks.md
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
literalEnd returned len(raw)-1 for an unterminated literal, so an argument span that was just an opening quote (a multi-line template or string continued on the next line) sliced raw[1:0] and panicked. The section-level recovery then discarded every Security finding for the repository; with the section-panic change on this branch it fails the scan instead. Return len(raw) for an unterminated literal so raw[open+1:end] is always valid and also keeps the literal's final character, which was previously dropped. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The codeguard job ran the pinned Marketplace action (v1.10.0), so the self-scan checked each PR with an older binary than the code under review. Enabling the prompts section here tripped a false positive that this same branch fixes (Go source under tests/mcp/ treated as MCP config), failing the job. Build and run the PR's own binary via make codeguard-ci instead, and drop the duplicate direct go test step (make test already runs it). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Resolve ci.yml: keep the in-repo make codeguard-ci self-scan over the Dependabot-bumped Marketplace action pin. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This was referenced Oct 30, 2026
alxxjohn
added a commit
that referenced
this pull request
Oct 30, 2026
…aths (#191) ## Summary Three profiled hot paths in the quality section, each prototyped first and then verified to be output-identical. Follow-up to #189. **Self-scan, cache off (Apple M3), measured against the #189 tree:** | | Before | After | |---|---|---| | Wall time | 1.21s | **0.65s** (−46%) | | Instructions retired | 42.9B | **29.5B** (−31%) | | Findings JSON | 87 findings | byte-identical | | # | Change | Measured | Commit | |---|---|---|---| | P1 | **Single-pass TS/JS module-scope detection.** `sourceMutableGlobalFindings` called `scriptSourceLineAtModuleScope` once per line, and each call rescanned the file from the top (O(lines²)). The flags are now computed once per file. | Daintree's 51k-line `tsWorker.js` goes from 232s to 13.2s; a 20k-line JS file from 14.5s to 2.0s | `310df11` | | P2 | **Byte lexer replaces the clone-tokenizer regex.** A 256-entry byte-class table produces the same spans; the regex only ever matches ASCII, so byte scanning is exact on any input, invalid UTF-8 included. Also drops singleton window hashes before sorting, which is provably output-neutral. | 456ms to 22ms per 1.1M tokens (21×); 1.11M allocs to 2 | `1e24245` | | P3 | **Literal pre-gates on the style-profile regexes.** `goPkgErrorsPattern` and others had no literal prefix, so RE2 attempted a match at every byte. Each gate is justified in a comment; `func` names are matched only at line starts. | `goRepoStyleProfile` goes from 212ms to 21ms | `4852024` | ## Equivalence evidence - **P1:** - A 26-file pin test (nested braces, strings, regex literals, `${}` templates, CRLF, JSX) passes on both the old and new implementations. - A temporary 200k random-input fuzz comparison found no mismatch. - **P2:** - The original regex is kept in tests as a reference. - Spans are identical on the whole repo (1.1M tokens), on adversarial inputs (all 256 bytes, broken UTF-8, operator overlaps) and on a 60s fuzz run. - An out-of-tree check covered 39M tokens across 8 languages. - At `clone_token_threshold=40` (1,408 duplicate-code findings) the scans are identical, and so is the Daintree quality scan. - **P3:** - An oracle test compiles the original expressions from the production patterns and compares results across every repo `.go` file plus about 50 edge cases. - It was mutation-checked: three deliberate breaks were each caught. ## Test plan - [x] `go build ./...`, `go vet ./...`, `gofmt -l cmd internal pkg tests changelog.go` - [x] `go test ./...`: 30 packages pass - [x] `make codeguard-ci`: exit 0, no new findings - [x] Self-scan JSON byte-identical to the #189 tree - [ ] CI `golangci-lint` ## Notes - New `...ForTest` seams were added to `internal/codeguard/checks/quality/export_test_helpers.go`, following the existing `SplitIdentifierWordsForTest` convention. - P2's token slice is sized at `len(source)/5+1` instead of an exact count, so it may hold up to ~25% spare capacity (~5 MB on codeguard). A counting pre-pass would make it exact at some lexing cost. - `quality_precision.go` is now 1,082 lines (it was already over the 1,000-line warn threshold). 🤖 Generated with [Claude Code](https://claude.com/claude-code)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
High-priority correctness fixes from a codebase review. The common thread is codeguard silently passing when it should fail. Each fix was developed on its own branch with regression tests, then merged here.
validateOutputrejectedgithub-comment, and the comment step ran ingithub.action_path(codeguard's own source) instead of the user's workspace.set +eswallowed both. The step now scans$GITHUB_WORKSPACE, fails with::erroron an empty report, and takes inputs viaenv:rather than${{ }}inrun:.b2c9020files, _ := sc.corpusFiles(target)dropped walk errors.validateandscannow reject missing or non-directory targets. Walk errors produce ascan.target-walk(fail) diagnostic and read errors ascan.file-read(warn) one. New SDKValidateTargetPaths;ValidateConfigstays pure.00a6a48"warning"finding and exited 0. It is now ascan.section.panicdiagnostic (not baselinable or waivable) plus a failing section status, so a crashed section fails the run. Crashed sections now also reachOnSectionComplete.35349a5FileScanDependencies, folded into a per-directory digest that is computed only when caching is on. Also fixes the local-abstraction design pass sharing the"design"cache id with the per-language passes, which dropped its findings under caching.5bed0adwarnFinding(env, args ...any)used runtime type assertions across 41 call sites. Migrated to the typedsupport.NewWarnFinding(WarnFindingInput{...}). Self-scan JSON is byte-identical before and after.3365396securityorprompts. Both are now enabled; triage details are below.f7f8423ae3810187f3fcc018089craw[1:0]inscanArgLiterals), which discarded every security finding. Found while profiling a large TS repo. Without this fix, H3 would turn that into failed scans. Also fixes unterminated literals losing their last character.a6b1615H6 triage (126 findings)
os.Args[0](nowos.Executable()), and an AKIA-shaped literal sat in a production comment.insecure-deserializationnow matches masked source.mcp/dirs as MCP config.//nolint:gosecdirectives.codeguard:ignore <rule> -- reason.//nolint:gosec // reasonor#nosec G204 -- reasonnow suppressessecurity.shell-execution, and only that rule.docs/checks.md.Test plan
go build ./...,go vet ./...,gofmt -l cmd internal pkg tests changelog.gogo test ./...: 30 packages passgo test -raceoninternal/codeguard/runner/...,tests/support,tests/codeguard,tests/checksmake codeguard-ci: exit 0, now with security and prompts enabledaction.ymlstructure is tested)golangci-lint(the local install is broken, so it was not run)Follow-ups (not in this PR)
exec.Command(os.Args[1]).Run().security-secretsdiagnostics are lost on a cache hit.go.workusedirs outside the target are not in the cache digest.scan.target-walkdiagnostic attaches to every section that finalizes after the failure, including ones that never read files.🤖 Generated with Claude Code