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
25 changes: 24 additions & 1 deletion internal/codeguard/checks/design/design_graph_rules.go
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ func godModuleFindings(env support.Context, graph *moduleGraph) []core.Finding {
continue
}
node := graph.modules[module]
if allowedCentralDataClientModule(node.file) {
if allowedCentralDataClientModule(node.file) || allowedCentralUtilityModule(node.file, fanIn[module], fanOut[module]) {
continue
}
findings = append(findings, env.NewFinding(support.FindingInput{
Expand All @@ -102,6 +102,29 @@ func godModuleFindings(env support.Context, graph *moduleGraph) []core.Finding {
return findings
}

func allowedCentralUtilityModule(file string, fanIn int, fanOut int) bool {
if fanIn <= 0 || fanOut > 4 {
return false
}
normalized := strings.ToLower(strings.ReplaceAll(file, "\\", "/"))
base := normalized
if slash := strings.LastIndex(base, "/"); slash >= 0 {
base = base[slash+1:]
}
return containsAnyLocal(base, []string{
"route-auth", "auth", "authorize", "validate-request", "validation", "validator",
})
}

func containsAnyLocal(source string, needles []string) bool {
for _, needle := range needles {
if strings.Contains(source, needle) {
return true
}
}
return false
}

func allowedCentralDataClientModule(file string) bool {
normalized := strings.ToLower(strings.ReplaceAll(file, "\\", "/"))
return normalized == "packages/db/src/client.ts" ||
Expand Down
16 changes: 16 additions & 0 deletions internal/codeguard/checks/quality/quality_ai_style_drift.go
Original file line number Diff line number Diff line change
Expand Up @@ -79,9 +79,25 @@ func scriptErrorStyleDriftFinding(env support.Context, file string, source strin
if isLikelyUIFile(file) || isSeedOrScriptSourcePath(file) || strings.Contains(strings.ToLower(file), "/integrations/") {
return nil
}
if scriptUsesSingleCustomErrorClass(source) {
return nil
}
return errorStyleDriftFinding(env, file, dominant, scriptErrorStyleCounts(source), "thrown error")
}

func scriptUsesSingleCustomErrorClass(source string) bool {
classes := map[string]struct{}{}
total := 0
for _, match := range scriptThrowNewPattern.FindAllStringSubmatch(source, -1) {
if match[1] == "Error" {
return false
}
classes[match[1]] = struct{}{}
total++
}
return total > 0 && len(classes) == 1
}

// --- shared style machinery ---

func dominantStyle(env support.Context, target core.TargetConfig, files []string, counter func(string) map[string]int) string {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package quality
import (
"path/filepath"
"regexp"
"strings"

"github.com/devr-tools/codeguard/internal/codeguard/checks/support"
"github.com/devr-tools/codeguard/internal/codeguard/core"
Expand All @@ -14,6 +15,19 @@ func isScriptTestFile(rel string) bool {
return scriptTestFilePattern.MatchString(filepath.ToSlash(rel))
}

func isScriptTestOrHelperFile(rel string) bool {
normalized := strings.ToLower(filepath.ToSlash(rel))
if isScriptTestFile(normalized) {
return true
}
base := filepath.Base(normalized)
return strings.Contains(base, "test-helper") ||
strings.Contains(base, "test_helpers") ||
strings.Contains(base, "testhelpers") ||
strings.Contains(base, "fixture") ||
strings.Contains(base, "mock")
}

func dominantScriptTestFramework(env support.Context, target core.TargetConfig, files []string, manifest packageManifest) string {
counts := frameworkSeedCounts(manifest)
for _, rel := range files {
Expand Down
61 changes: 55 additions & 6 deletions internal/codeguard/checks/quality/quality_defensive.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ func defensiveBoundaryFindings(env support.Context, file string, fn precisionFun
findings = append(findings, precisionWarnFinding(env, defensiveBoundsAssumptionRuleID, file, line,
"indexed access assumes collection bounds without a nearby length check", core.ConfidenceMedium))
}
if line, ok := unsafeDefaultLine(fn.Statements); ok {
if line, ok := unsafeDefaultLine(fn.Statements, loweredBody); ok {
findings = append(findings, precisionWarnFinding(env, defensiveUnsafeDefaultRuleID, file, line,
"configuration default can fail open or disable a safety control", core.ConfidenceHigh))
}
Expand Down Expand Up @@ -139,6 +139,9 @@ func sourceDefensiveInvariantFindings(env support.Context, file string, source s
break
}
}
if structuralPendingResultContainer(lines, idx) {
continue
}
if boolFields >= 2 || hasStringState {
return []core.Finding{precisionWarnFinding(env, defensiveInvalidStateRepresentableRuleID, file, idx+1,
"state shape uses booleans or raw status strings that can represent impossible combinations", core.ConfidenceMedium)}
Expand All @@ -164,21 +167,42 @@ func structuralDataTransferContainerLine(line string) bool {
}
name := strings.ToLower(strings.Trim(fields[idx+1], "_$"))
return strings.HasSuffix(name, "args") ||
strings.Contains(name, "context") ||
strings.HasSuffix(name, "base") ||
strings.HasSuffix(name, "data") ||
strings.HasSuffix(name, "response") ||
strings.HasSuffix(name, "responses") ||
strings.HasSuffix(name, "input") ||
strings.HasSuffix(name, "options") ||
strings.HasSuffix(name, "opts") ||
strings.HasSuffix(name, "row") ||
strings.Contains(name, "rowpart") ||
strings.HasSuffix(name, "part") ||
dataTransferPartPattern.MatchString(name) ||
strings.HasSuffix(name, "context") ||
strings.HasSuffix(name, "ctx") ||
strings.Contains(name, "dto") ||
strings.Contains(name, "schema") ||
strings.Contains(name, "payload") ||
strings.Contains(name, "record") ||
strings.Contains(name, "seed")
}
return false
}

func structuralPendingResultContainer(lines []string, start int) bool {
header := strings.ToLower(lines[start])
if !strings.Contains(header, "result") {
return false
}
blockEnd := start + 12
if blockEnd > len(lines) {
blockEnd = len(lines)
}
block := strings.ToLower(strings.Join(lines[start:blockEnd], "\n"))
return strings.Contains(block, "pending") &&
!containsAny(block, []string{"disabled", "deleted", "inactive", "archived"})
}

func unvalidatedBoundaryInputLine(file string, fn precisionFunction, loweredBody string) (int, bool) {
if isUIHelperOrMappingContext(file, fn) || isReactComponentOrHookBoundary(file, fn) || isLikelyUIFile(file) || isFrontendLibraryPath(file) {
return 0, false
Expand Down Expand Up @@ -211,7 +235,7 @@ func formDataHasContentLengthPreflight(loweredBody string) bool {
}

func validatedBoundaryInputPattern(fn precisionFunction, loweredBody string) bool {
if containsAny(loweredBody, []string{"validate", "schema", "sanitize", "bind", "decodevalid", "safeparse", "z.safeparse", "zod.", "yup.", "pydantic", "jsonschema"}) {
if containsAny(loweredBody, []string{"validate", "schema", "sanitize", "bind", "decodevalid", "safeparse", "z.safeparse", "zod.", "yup.", "pydantic", "jsonschema", "runadminaction", "runsupportaction", "validaterequest"}) {
return true
}
if jsonReaderSchemaCall.MatchString(functionRawBody(fn)) {
Expand All @@ -230,7 +254,9 @@ func isValidationOrExtractionHelperName(name string) bool {
lowered := strings.ToLower(strings.Trim(name, "_$"))
if strings.HasPrefix(lowered, "parse") || strings.HasPrefix(lowered, "assert") ||
strings.HasPrefix(lowered, "guard") || strings.HasPrefix(lowered, "ensure") ||
strings.HasPrefix(lowered, "decode") || strings.HasPrefix(lowered, "validate") {
strings.HasPrefix(lowered, "decode") || strings.HasPrefix(lowered, "validate") ||
strings.HasPrefix(lowered, "normalize") || strings.HasPrefix(lowered, "map") ||
strings.HasPrefix(lowered, "pick") || strings.HasPrefix(lowered, "resolve") {
return true
}
return containsAny(lowered, []string{"bearertokenfrom", "tokenfrom", "headerfrom", "requestbodyfrom"})
Expand Down Expand Up @@ -589,15 +615,25 @@ func nearbyBoundsGuard(statements []support.ParsedStatement, idx int, target str
return false
}

func unsafeDefaultLine(statements []support.ParsedStatement) (int, bool) {
func unsafeDefaultLine(statements []support.ParsedStatement, loweredBody string) (int, bool) {
for _, statement := range statements {
if unsafeDefaultPattern.MatchString(firstNonEmptyString(statement.Raw, statement.Text)) {
raw := strings.ToLower(firstNonEmptyString(statement.Raw, statement.Text))
if unsafeDefaultPattern.MatchString(raw) && unsafeDefaultIsSafetySensitive(raw, loweredBody) {
return statement.Line, true
}
}
return 0, false
}

func unsafeDefaultIsSafetySensitive(line string, loweredBody string) bool {
evidence := line + "\n" + loweredBody
return containsAny(evidence, []string{
"auth", "authorize", "permission", "policy", "admin", "security", "secure",
"csrf", "cors", "token", "secret", "allow", "deny", "disabled", "disable",
"skip", "insecure", "unsafe",
})
}

func nonExhaustiveBranchLine(fn precisionFunction, loweredBody string) (int, bool) {
if !switchLikePattern.MatchString(functionRawBody(fn)) || containsAny(loweredBody, []string{"default", "else", "unreachable", "assert_never", "exhaustive"}) {
return 0, false
Expand Down Expand Up @@ -632,12 +668,25 @@ func missingSchemaValidationLine(fn precisionFunction, loweredBody string) (int,
if !jsonDecodePattern.MatchString(functionRawBody(fn)) {
return 0, false
}
if genericJSONParserWrapper(fn) {
return 0, false
}
if validatedBoundaryInputPattern(fn, loweredBody) || jsonReaderSchemaCall.MatchString(functionRawBody(fn)) || containsAny(loweredBody, []string{"jsonschema", "isvalid", "required"}) {
return 0, false
}
return firstPatternLine(fn, jsonDecodePattern), true
}

func genericJSONParserWrapper(fn precisionFunction) bool {
loweredName := strings.ToLower(strings.Trim(fn.Name, "_$"))
if !containsAny(loweredName, []string{"jsonparse", "parsejson", "safejson", "safeparse"}) {
return false
}
loweredBody := strings.ToLower(fn.Body)
return strings.Contains(loweredBody, "json.parse") &&
containsAny(loweredBody, []string{"ok: true", "ok: false", "return null", "return undefined"})
}

func missingResourceLimitLine(fn precisionFunction, loweredBody string) (int, bool) {
if !resourceReadPattern.MatchString(functionRawBody(fn)) {
if line, ok := missingORMCollectionLimitLine(fn, loweredBody); ok {
Expand Down
26 changes: 22 additions & 4 deletions internal/codeguard/checks/quality/quality_environment.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,14 +22,15 @@ func environmentBranchingFindings(env support.Context, target core.TargetConfig)
return environmentBranchingEligiblePath(env.Config.Checks.DeliveryRules, rel)
}, func(file string, data []byte) []core.Finding {
text := string(data)
if !environmentBranchPattern.MatchString(text) {
line := environmentBranchLine(text)
if line == 0 {
return nil
}
return []core.Finding{env.NewFinding(support.FindingInput{
RuleID: "quality.environment-branching",
Level: "warn",
Path: file,
Line: environmentBranchLine(text),
Line: line,
Column: 1,
Message: "domain/source code branches on deployment environment; move environment policy to configuration or bootstrap boundaries",
Confidence: core.ConfidenceHigh,
Expand Down Expand Up @@ -85,9 +86,26 @@ func environmentBranchingEligiblePath(cfg core.DeliveryRulesConfig, rel string)

func environmentBranchLine(text string) int {
for idx, line := range strings.Split(text, "\n") {
if environmentBranchPattern.MatchString(line) {
if environmentBranchLineIsDeploymentDecision(line) {
return idx + 1
}
}
return 1
return 0
}

func environmentBranchLineIsDeploymentDecision(line string) bool {
if !environmentBranchPattern.MatchString(line) {
return false
}
lowered := strings.ToLower(line)
if containsAny(lowered, []string{
"process.env", "node_env", "rails.env", "os.getenv", "os.getenv", "getenv(", "std::getenv",
}) {
return true
}
if !regexp.MustCompile(`\b(if|switch|case|when)\b`).MatchString(lowered) {
return false
}
return containsAny(lowered, []string{" env", "env.", "env[", "environment", "deployment"}) &&
containsAny(lowered, []string{"prod", "production", "staging", "stage", "dev", "development", "test"})
}
15 changes: 15 additions & 0 deletions internal/codeguard/checks/quality/quality_errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,9 @@ func partialFailureHiddenLine(fn precisionFunction, loweredBody string) (int, bo
if partialFailureSurfacedInResult(loweredBody) {
return 0, false
}
if strings.Contains(loweredBody, "allsettled") && allSettledResultIsReturned(loweredBody) {
return 0, false
}
if strings.Contains(loweredBody, "allsettled") && !containsAny(loweredBody, []string{"rejected", "throw", "return err", "return error"}) {
return fn.StartLine, true
}
Expand All @@ -222,6 +225,12 @@ func partialFailureHiddenLine(fn precisionFunction, loweredBody string) (int, bo
return 0, false
}

func allSettledResultIsReturned(loweredBody string) bool {
return containsAny(loweredBody, []string{
"return {", "normalize", "mapsettled", "settledresults", "fulfilled", "rejected",
})
}

func partialFailureContinueAccounted(statements []support.ParsedStatement, idx int) bool {
if idx < 0 || idx >= len(statements) {
return false
Expand Down Expand Up @@ -273,6 +282,9 @@ func fallbackHidesCorruptionLine(fn precisionFunction, loweredBody string) (int,
}
for _, statement := range fn.Statements {
lowered := strings.ToLower(firstNonEmptyString(statement.Raw, statement.Text))
if containsAny(lowered, []string{"return fallback", "return default"}) {
continue
}
if containsAny(lowered, []string{"return {}", "return []", "return map[", "return default", "return fallback"}) {
return statement.Line, true
}
Expand All @@ -288,6 +300,9 @@ func retryableUndistinguishedLine(fn precisionFunction, loweredBody string) (int
!containsAny(loweredBody, []string{"err", "error", "catch", "except", "failure"}) {
return 0, false
}
if !containsAny(loweredBody, []string{"for ", "while ", "retrylater", "retry_again", "retry("}) {
return 0, false
}
if containsAny(loweredBody, []string{"retryable", "transient", "permanent", "temporary", "timeout", "status", "rate limit"}) {
return 0, false
}
Expand Down
31 changes: 28 additions & 3 deletions internal/codeguard/checks/quality/quality_precision.go
Original file line number Diff line number Diff line change
Expand Up @@ -386,6 +386,7 @@ func precisionFunctionFindings(env support.Context, file string, fn precisionFun
fmt.Sprintf("parameter %q is ambiguous without domain context", param.Name), core.ConfidenceHigh))
}
if isBooleanParameter(param) && !isAllowedBooleanArgumentFunction(fn.Name) && !isPredicateName(param.Name) && !isConventionalNonPredicateName(param.Name) &&
!isLocalBooleanParserFlag(fn, param.Name) &&
!isReactComponentOrHookBoundary(file, fn) && !isUIHelperOrMappingContext(file, fn) && !isSeedOrScriptSourcePath(file) {
findings = append(findings, precisionWarnFinding(env, qualityBooleanArgumentRuleID, file, fn.StartLine,
fmt.Sprintf("boolean parameter %q hides behavior behind a flag", param.Name), core.ConfidenceHigh))
Expand Down Expand Up @@ -436,6 +437,15 @@ func precisionFunctionFindings(env support.Context, file string, fn precisionFun
return findings
}

func isLocalBooleanParserFlag(fn precisionFunction, name string) bool {
loweredName := strings.ToLower(strings.Trim(name, "_$"))
if loweredName != "lower" && loweredName != "trim" && loweredName != "strict" {
return false
}
loweredFunction := strings.ToLower(strings.Trim(fn.Name, "_$"))
return containsAny(loweredFunction, []string{"parse", "split", "normalize", "format", "read"})
}

func isGenericIdentifier(name string) bool {
name = strings.Trim(name, "_$")
if name == "" {
Expand Down Expand Up @@ -590,7 +600,7 @@ func errorHandlingFindings(env support.Context, file string, fn precisionFunctio
line := strings.TrimSpace(statement.Text)
lowered := strings.ToLower(line)
if strings.Contains(lowered, "err") || strings.Contains(lowered, "except") || strings.Contains(lowered, "catch") {
if logsError(line) && nearbyIgnoredError(statements, idx) {
if logsError(line) && nearbyIgnoredError(statements, idx) && !nearbyExplicitDegradedFallback(statements, idx) {
findings = append(findings, precisionWarnFinding(env, errorLoggedAndIgnoredRuleID, file, statement.Line,
"error is logged and then ignored or converted to success", core.ConfidenceHigh))
}
Expand All @@ -603,6 +613,19 @@ func errorHandlingFindings(env support.Context, file string, fn precisionFunctio
return findings
}

func nearbyExplicitDegradedFallback(statements []support.ParsedStatement, idx int) bool {
for lookahead := idx; lookahead < len(statements) && lookahead <= idx+4; lookahead++ {
line := strings.ToLower(strings.TrimSpace(firstNonEmptyString(statements[lookahead].Raw, statements[lookahead].Text)))
if containsAny(line, []string{
"return []", "return {}", "return reports", "return fallback", "return default",
"return;",
}) {
return true
}
}
return false
}

func logsError(line string) bool {
lowered := strings.ToLower(line)
return strings.Contains(lowered, "log.") || strings.Contains(lowered, "logger.") ||
Expand Down Expand Up @@ -653,8 +676,10 @@ func defensiveStatementFindings(env support.Context, file string, statement supp
text := statement.Text
findings := make([]core.Finding, 0, 2)
if strings.Contains(text, " as unknown as ") || strings.Contains(text, " as any as ") || strings.Contains(text, "typing.cast(") {
findings = append(findings, precisionWarnFinding(env, defensiveUncheckedTypeAssertionRuleID, file, statement.Line,
"type assertion bypasses runtime validation", core.ConfidenceHigh))
if !isScriptTestOrHelperFile(file) {
findings = append(findings, precisionWarnFinding(env, defensiveUncheckedTypeAssertionRuleID, file, statement.Line,
"type assertion bypasses runtime validation", core.ConfidenceHigh))
}
}
if unsafeScriptNumericConversion(text) {
findings = append(findings, precisionWarnFinding(env, defensiveUnsafeNumericConversionRuleID, file, statement.Line,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,12 @@ func duplicatedKnowledgeLineIsStructural(line string) bool {
strings.Contains(lowered, "accept:") || strings.Contains(lowered, "headers") {
return true
}
if containsAny(lowered, []string{".from(", ".select(", ".order(", ".gte(", ".lte(", ".eq(", ".in(", ".rpc("}) {
return true
}
if containsAny(lowered, []string{"key:", "keys:", "field:", "fields:", "column:", "columns:"}) {
return true
}
return false
}

Expand Down
Loading
Loading