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
129 changes: 108 additions & 21 deletions docs/checks.md

Large diffs are not rendered by default.

14 changes: 11 additions & 3 deletions docs/features.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,33 +10,41 @@ This page lists the current `codeguard` feature surface and the main config entr
- language-native quality heuristics for Go, Python, TypeScript, JavaScript, Rust, Java, C++, C#, and Ruby
- AI-quality heuristics such as swallowed errors, narrative comments, hallucinated imports, dead code, over-mocked tests, idiom drift, semantic review, provenance policy, and change-risk rollups
- changed-line coverage gating in diff mode
- opt-in `clang-format` and sanitized `clang++ -fsyntax-only` validation backed by safe `compile_commands.json` metadata
- `design`
- layering and boundary rules
- import cycle and god-module detection
- high-impact-change analysis and dependency graph artifacts
- C++ target-local include and named-module graphs, generic filename checks, and qualified method-count limits
- `security`
- hardcoded secrets and private keys
- Go, Python, TypeScript, and JavaScript taint-style flow checks
- insecure API heuristics
- C++ insecure TLS, shell execution, and unsafe C string API checks
- C++ same-file taint-flow and SSRF analysis for common process and networking APIs
- optional `govulncheck`
- `prompts`
- prompt-asset governance
- agent config and MCP config checks
- dangerous instruction and standing-permission detection
- `ci`
- workflow/release policy
- test-quality heuristics
- test-quality heuristics, including GoogleTest, Catch2/doctest, and Boost.Test
- `supply_chain`
- manifest normalization
- manifest normalization, including `vcpkg.json`, `conanfile.txt`, statically analyzed `conanfile.py`, and CMake dependency declarations
- lockfile presence and drift validation
- unpinned dependency detection
- dependency and manifest license policy
- Cargo manifest hygiene for missing package licenses and non-hermetic dependency sources
- `performance`
- N+1 query patterns, allocation-heavy loops, blocking I/O in request paths, and unbounded concurrency
- Go package rebuild-cascade analysis for rebuild hot spots and amplifiers
- Go package and C++ include/module rebuild-cascade analysis for rebuild hot spots and amplifiers
- Rust and C++ loop-smell coverage for regex construction, non-preallocated string growth, and polling sleeps
- C++ loop-driven unbounded thread/task launch detection
- build regression, benchmark regression, artifact-size budgets, and clang `-ftime-trace` budgets
- `contracts`
- exported Go and public C++ API compatibility against a diff base
- OpenAPI, protobuf, and destructive migration checks

## Agent-native features

Expand Down
12 changes: 7 additions & 5 deletions docs/security.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,15 +66,15 @@ Example:
OWASP Top 10 (2021) coverage: 9/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)
[ok ] A02:2021-Cryptographic Failures (12 rules)
[ok ] A03:2021-Injection (27 rules)
[gap ] A04:2021-Insecure Design (0 rules)
[ok ] A05:2021-Security Misconfiguration (4 rules)
[ok ] A05:2021-Security Misconfiguration (5 rules)
[ok ] A06:2021-Vulnerable and Outdated Components (1 rules)
[ok ] A07:2021-Identification and Authentication Failures (1 rules)
[ok ] A07:2021-Identification and Authentication Failures (3 rules)
[ok ] A08:2021-Software and Data Integrity Failures (1 rules)
[ok ] A09:2021-Security Logging and Monitoring Failures (2 rules)
[ok ] A10:2021-Server-Side Request Forgery (SSRF) (2 rules)
[ok ] A10:2021-Server-Side Request Forgery (SSRF) (3 rules)
```

`A04` (Insecure Design) is left as an explicit gap: it is a design-level risk
Expand Down Expand Up @@ -103,6 +103,8 @@ taint engine and default to `fail`.
| `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 |
| `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 |
| `security.ssrf.go` / `security.ssrf.python` | A10 | untrusted input flowing into an outbound HTTP request URL |
| `security.taint.cpp` | A03 | environment, argv/stdin, or recognized request input flowing into process execution through bounded same-file summaries |
| `security.ssrf.cpp` | A10 | untrusted input flowing into libcurl, cpr, Boost resolver, cpprestsdk, or Poco outbound destinations |

## Release integrity (supply chain)

Expand Down
6 changes: 6 additions & 0 deletions examples/codeguard.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,12 @@
"max_function_lines": 80,
"max_parameters": 5,
"max_cyclomatic_complexity": 10,
"cpp_tooling": {
"clang_format_mode": "off",
"clang_format_command": "clang-format",
"compiler_mode": "off",
"compiler_command": "clang++"
},
"language_commands": {
"typescript": [
{
Expand Down
33 changes: 33 additions & 0 deletions internal/codeguard/checks/ci/ci_target_files.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ package ci
import (
"path/filepath"
"strings"

"github.com/devr-tools/codeguard/internal/codeguard/checks/support"
)

func isTargetTestFile(language, rel string) bool {
Expand All @@ -21,11 +23,42 @@ func isTargetTestFile(language, rel string) bool {
return isCSharpTestFile(rel)
case "ruby", "rb":
return isRubyTestFile(rel)
case "c++", "cpp", "cxx", "cc":
return isCPPTestFile(rel)
default:
return false
}
}

func isCPPTestFile(rel string) bool {
if !support.IsCPPPath(rel, true) {
return false
}
slashPath := strings.ToLower(filepath.ToSlash(rel))
base := filepath.Base(slashPath)
stem := strings.TrimSuffix(base, filepath.Ext(base))
originalBase := filepath.Base(rel)
originalStem := strings.TrimSuffix(originalBase, filepath.Ext(originalBase))
if stem == "test" || stem == "tests" || stem == "unittest" || stem == "unittests" ||
strings.HasPrefix(stem, "test_") || strings.HasPrefix(stem, "tests_") {
return true
}
for _, suffix := range []string{"_test", "_tests", "_unittest", "_unittests"} {
if strings.HasSuffix(stem, suffix) {
return true
}
}
if strings.HasSuffix(originalStem, "Test") || strings.HasSuffix(originalStem, "Tests") {
return true
}
for _, directory := range []string{"test", "tests", "unittest", "unittests"} {
if strings.HasPrefix(slashPath, directory+"/") || strings.Contains(slashPath, "/"+directory+"/") {
return true
}
}
return false
}

func normalizedLanguage(language string) string {
return strings.ToLower(strings.TrimSpace(language))
}
Expand Down
30 changes: 30 additions & 0 deletions internal/codeguard/checks/ci/ci_test_quality_blocks.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,12 @@ var (
goTestDeclPattern = regexp.MustCompile(`^func\s+(Test\w+)\s*\(`)
jsTestDeclPattern = regexp.MustCompile(`^\s*(?:it|test)(?:\.\w+)?\s*\(\s*(?:'([^']*)'|"([^"]*)")?`)
pythonTestDeclPattern = regexp.MustCompile(`^(\s*)def\s+(test_\w*)\s*\(`)
cppGoogleTestPattern = regexp.MustCompile(`^\s*(?:TEST|TEST_F|TEST_P|TYPED_TEST|TYPED_TEST_P)\s*\(\s*([^,]+)\s*,\s*([^)]+)\)`)
cppGoogleTestStart = regexp.MustCompile(`^\s*((?:TEST|TEST_F|TEST_P|TYPED_TEST|TYPED_TEST_P))\s*\(`)
cppCatchTestPattern = regexp.MustCompile(`^\s*(?:TEST_CASE(?:_METHOD|_FIXTURE|_CLASS|_TEMPLATE(?:_DEFINE)?)?|SCENARIO(?:_METHOD)?|TEMPLATE_(?:PRODUCT_)?TEST_CASE(?:_METHOD)?)\s*\((.*)`)
cppBoostTestPattern = regexp.MustCompile(`^\s*BOOST_(?:AUTO|FIXTURE|DATA)_TEST_CASE(?:_TEMPLATE)?\s*\(\s*([^,\s)]+)`)
cppBoostTestStart = regexp.MustCompile(`^\s*(BOOST_(?:AUTO|FIXTURE|DATA)_TEST_CASE(?:_TEMPLATE)?)\s*\(`)
quotedTestNamePattern = regexp.MustCompile(`["']([^"']+)["']`)
braceElsePattern = regexp.MustCompile(`(?:^|\W)else(?:\W|$)`)
pythonElsePattern = regexp.MustCompile(`^\s*(?:else\s*:|elif\b)`)
envVarGuardPattern = regexp.MustCompile(`if\s+os\.Getenv\(\s*"[^"]*"\s*\)\s*[!=]=`)
Expand Down Expand Up @@ -94,11 +100,35 @@ func extractTestBlocks(language string, text string) []testBlock {
}, '(', ')')
case "python", "py":
return pythonTestBlocks(lines)
case "c++", "cpp", "cxx", "cc":
return delimitedTestBlocks(lines, cppTestDeclaration, '{', '}')
default:
return nil
}
}

func cppTestDeclaration(line string) (string, bool) {
if match := cppGoogleTestPattern.FindStringSubmatch(line); match != nil {
return strings.TrimSpace(match[1]) + "." + strings.TrimSpace(match[2]), true
}
if match := cppGoogleTestStart.FindStringSubmatch(line); match != nil {
return match[1] + " (multiline declaration)", true
}
if match := cppCatchTestPattern.FindStringSubmatch(line); match != nil {
if quoted := quotedTestNamePattern.FindStringSubmatch(match[1]); quoted != nil {
return quoted[1], true
}
return "(unnamed)", true
}
if match := cppBoostTestPattern.FindStringSubmatch(line); match != nil {
return match[1], true
}
if match := cppBoostTestStart.FindStringSubmatch(line); match != nil {
return match[1] + " (multiline declaration)", true
}
return "", false
}

// delimitedTestBlocks collects blocks for brace or parenthesis delimited
// languages by balancing the open/close runes from the declaration line on.
func delimitedTestBlocks(lines []string, matchDecl func(string) (string, bool), open rune, closing rune) []testBlock {
Expand Down
19 changes: 19 additions & 0 deletions internal/codeguard/checks/ci/ci_test_quality_patterns.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,23 @@ var (
`|\bexpect\s*\(\s*(?:true|1|` + literalPattern + `\s*===?\s*` + literalPattern + `)\s*\)\s*\.\s*(?:toBeTruthy|toBeDefined|toBeFalsy)\s*\(\s*\)` +
`|\bassert\s*\(\s*(?:true|1)\s*\)`)

cppAssertionPattern = regexp.MustCompile(
`\b(?:EXPECT|ASSERT)_[A-Z0-9_]+\s*\(` +
`|\b(?:CHECK|REQUIRE)(?:_[A-Z0-9_]+)?\s*\(` +
`|\bBOOST_(?:CHECK|REQUIRE|WARN)(?:_[A-Z0-9_]+)?\s*\(` +
`|\b(?:ADD_FAILURE|FAIL|FAIL_CHECK|SUCCEED)\s*\(` +
`|\bBOOST_(?:ERROR|FAIL)\s*\(` +
`|\bassert\s*\(`)
cppIdiomaticPattern = regexp.MustCompile(`\b(?:ADD_FAILURE|FAIL|FAIL_CHECK)\s*\(|\bBOOST_(?:ERROR|FAIL)\s*\(`)
cppConstantPattern = regexp.MustCompile(
`\b(?:EXPECT|ASSERT)_TRUE\s*\(\s*(?:true|1)\s*\)` +
`|\b(?:EXPECT|ASSERT)_FALSE\s*\(\s*(?:false|0)\s*\)` +
`|\b(?:CHECK|REQUIRE)(?:_MESSAGE)?\s*\(\s*(?:true|1)\s*[,)]` +
`|\b(?:CHECK|REQUIRE)_FALSE\s*\(\s*(?:false|0)\s*\)` +
`|\bBOOST_(?:CHECK|REQUIRE|WARN)(?:_MESSAGE)?\s*\(\s*(?:true|1)\s*[,)]` +
`|\bassert\s*\(\s*(?:true|1)\s*\)` +
`|\bSUCCEED\s*\(`)

braceConditionalOpener = regexp.MustCompile(`^\s*\}?\s*(?:else\s+)?if\b`)
pythonConditionalOpener = regexp.MustCompile(`^\s*if\b.*:`)

Expand All @@ -64,6 +81,8 @@ func testQualityPatternsFor(language string) (testQualityPatterns, bool) {
return testQualityPatterns{assertion: pythonAssertionPattern, idiomatic: pythonIdiomaticPattern, constant: pythonConstantPattern}, true
case "typescript", "javascript", "ts", "tsx", "js", "jsx":
return testQualityPatterns{assertion: jsAssertionPattern, constant: jsConstantPattern, braceBased: true}, true
case "c++", "cpp", "cxx", "cc":
return testQualityPatterns{assertion: cppAssertionPattern, idiomatic: cppIdiomaticPattern, constant: cppConstantPattern, braceBased: true}, true
default:
return testQualityPatterns{}, false
}
Expand Down
1 change: 1 addition & 0 deletions internal/codeguard/checks/contracts/contracts.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ func findingsForTarget(_ context.Context, env support.Context, target core.Targe
changed := changedFilesForTarget(env, target)
findings := make([]core.Finding, 0) //nolint:prealloc // count not known up front; each rule appends a variable number
findings = append(findings, goBreakingFindings(env, target, changed)...)
findings = append(findings, cppBreakingFindings(env, target, changed)...)
findings = append(findings, openAPIBreakingFindings(env, target, changed)...)
findings = append(findings, protoBreakingFindings(env, target, changed)...)
findings = append(findings, migrationFindings(env, target, changed)...)
Expand Down
Loading
Loading