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
1 change: 1 addition & 0 deletions docs/checks.md
Original file line number Diff line number Diff line change
Expand Up @@ -1186,6 +1186,7 @@ These rules live outside the repository-wide `Change Safety` section in report o
| Defensive programming | `defensive.invalid-state-representable` | warn | Booleans or raw status strings can represent impossible state combinations. |
| Defensive programming | `defensive.null-assumption` | warn | Nullable boundary values are dereferenced without a nil/null guard. |
| Defensive programming | `defensive.integer-overflow` | warn | Arithmetic on count, size, or length input lacks an overflow bound check. |
| Defensive programming | `defensive.sequence-collision-risk` | warn | External ID allocation derives the next value from current count without guarded unique-collision retry. |
| Defensive programming | `defensive.bounds-assumption` | warn | Indexed access assumes collection bounds without a nearby length check. |
| Defensive programming | `defensive.unsafe-default` | warn | A config/env fallback can fail open or disable a safety control. |
| Defensive programming | `defensive.non-exhaustive-branch` | warn | Enum-like state/kind/type branching lacks default or exhaustive handling. |
Expand Down
101 changes: 97 additions & 4 deletions internal/codeguard/checks/quality/quality_defensive.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ const (
defensiveInvalidStateRepresentableRuleID = "defensive.invalid-state-representable"
defensiveNullAssumptionRuleID = "defensive.null-assumption"
defensiveIntegerOverflowRuleID = "defensive.integer-overflow"
defensiveSequenceCollisionRiskRuleID = "defensive.sequence-collision-risk"
defensiveBoundsAssumptionRuleID = "defensive.bounds-assumption"
defensiveUnsafeDefaultRuleID = "defensive.unsafe-default"
defensiveNonExhaustiveBranchRuleID = "defensive.non-exhaustive-branch"
Expand All @@ -35,6 +36,10 @@ var (
structStartPattern = regexp.MustCompile(`(?i)\b(type\s+\w+\s+struct|interface\s+\w+|class\s+\w+|struct\s+\w+)`)
boolFieldPattern = regexp.MustCompile(`(?i)\b(bool|boolean)\b`)
stringStateFieldPattern = regexp.MustCompile(`(?i)\b(status|state|kind)\b.*\b(string|str|std::string|String)\b|\b(string|str|std::string|String)\b.*\b(status|state|kind)\b`)
resourceCountGuard = regexp.MustCompile(`(?i)\b(?:count|size|length|len|bytes)\s*(?:<=|<|>|>=)\s*(?:max|limit|quota|cap|[0-9])`)
resourceNamedCountLimit = regexp.MustCompile(`(?i)\b(?:max|limit|quota|cap)[A-Za-z0-9_]*(?:count|size|length|len|bytes)\b`)
sequenceAllocationLine = regexp.MustCompile(`(?i)\b(?:external[_]?id|next[_]?id|sequence|slug|number)\b.*(?:count|max)\s*\+\s*1|(?:count|max)\s*\+\s*1.*\b(?:external[_]?id|next[_]?id|sequence|slug|number)\b`)
jsonReaderSchemaCall = regexp.MustCompile(`(?i)\b(?:read|parse|decode)Json[A-Za-z0-9_]*\s*\([^)\n,]+,\s*[A-Za-z_$][\w$]*(?:Schema|Validator|Codec|Parser)\b`)
)

func defensiveBoundaryFindings(env support.Context, file string, fn precisionFunction) []core.Finding {
Expand All @@ -53,6 +58,10 @@ func defensiveBoundaryFindings(env support.Context, file string, fn precisionFun
findings = append(findings, precisionWarnFinding(env, defensiveNullAssumptionRuleID, file, line,
"nullable boundary value is dereferenced without a nil/null guard", core.ConfidenceMedium))
}
if line, ok := sequenceCollisionRiskLine(fn, loweredBody); ok {
findings = append(findings, precisionWarnFinding(env, defensiveSequenceCollisionRiskRuleID, file, line,
"external ID allocation derives the next value from current count without guarded unique-collision retry", core.ConfidenceMedium))
}
if line, ok := integerOverflowLine(file, fn, loweredBody); ok {
findings = append(findings, precisionWarnFinding(env, defensiveIntegerOverflowRuleID, file, line,
"arithmetic on count, size, or length input lacks an overflow bound check", core.ConfidenceMedium))
Expand Down Expand Up @@ -130,6 +139,9 @@ func unvalidatedBoundaryInputLine(fn precisionFunction, loweredBody string) (int
if !boundaryFunctionName(fn.Name) && !hasBoundaryParam(fn.Params) {
return 0, false
}
if isValidationOrExtractionHelperName(fn.Name) {
return 0, false
}
if validatedBoundaryInputPattern(fn, loweredBody) {
return 0, false
}
Expand All @@ -149,7 +161,13 @@ func formDataHasContentLengthPreflight(loweredBody string) bool {
}

func validatedBoundaryInputPattern(fn precisionFunction, loweredBody string) bool {
if containsAny(loweredBody, []string{"validate", "schema", "sanitize", "bind", "decodevalid", "safeparse", "zod.", "yup.", "pydantic", "jsonschema"}) {
if containsAny(loweredBody, []string{"validate", "schema", "sanitize", "bind", "decodevalid", "safeparse", "z.safeparse", "zod.", "yup.", "pydantic", "jsonschema"}) {
return true
}
if jsonReaderSchemaCall.MatchString(functionRawBody(fn)) {
return true
}
if strings.Contains(loweredBody, "nextresponse.") && containsAny(loweredBody, []string{"return nextresponse", ".json(", "redirect("}) && containsAny(loweredBody, []string{"if (!", "if (!", "if(", "if "}) {
return true
}
if regexp.MustCompile(`(?i)\b(parse|assert|guard|ensure|decode)[A-Z_][A-Za-z0-9_]*(?:Input|Payload|Body|Params|Query|Record|Request|Event|Config)?\s*\(`).MatchString(functionRawBody(fn)) {
Expand All @@ -158,6 +176,16 @@ func validatedBoundaryInputPattern(fn precisionFunction, loweredBody string) boo
return false
}

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") {
return true
}
return containsAny(lowered, []string{"bearertokenfrom", "tokenfrom", "headerfrom", "requestbodyfrom"})
}

func hasBoundaryParam(params []support.ParsedParam) bool {
for _, param := range params {
name := strings.ToLower(param.Name)
Expand Down Expand Up @@ -205,7 +233,7 @@ func integerOverflowLine(file string, fn precisionFunction, loweredBody string)
if isUIRenderArithmeticContext(file, fn, loweredBody) {
return 0, false
}
if guardedSequenceCollisionRetry(loweredBody) {
if sequenceAllocationArithmetic(loweredBody) || metricStatArithmeticContext(fn, loweredBody) || dateCountFormattingContext(fn, loweredBody) {
return 0, false
}
if containsAny(loweredBody, []string{"maxint", "math.max", "checked", "saturating", "overflow", "limits<", "safeint"}) {
Expand All @@ -220,7 +248,35 @@ func integerOverflowLine(file string, fn precisionFunction, loweredBody string)
return 0, false
}

func sequenceCollisionRiskLine(fn precisionFunction, loweredBody string) (int, bool) {
if !sequenceAllocationArithmetic(loweredBody) || guardedSequenceCollisionRetry(loweredBody) {
return 0, false
}
return firstSequenceAllocationLine(fn), true
}

func sequenceAllocationArithmetic(loweredBody string) bool {
if !containsAny(loweredBody, []string{"count + 1", "count+1", "max + 1", "max+1"}) {
return false
}
return containsAny(loweredBody, []string{"externalid", "external_id", "nextid", "next_id", "sequence", "slug", "number"})
}

func firstSequenceAllocationLine(fn precisionFunction) int {
for _, statement := range fn.Statements {
raw := firstNonEmptyString(statement.Raw, statement.Text)
if sequenceAllocationLine.MatchString(raw) {
return statement.Line
}
}
return fn.StartLine
}

func guardedSequenceCollisionRetry(loweredBody string) bool {
if containsAny(loweredBody, []string{"withexternalidretry", "with_external_id_retry"}) &&
containsAny(loweredBody, []string{"p2002", "unique", "collision", "externalid", "external_id"}) {
return true
}
if !containsAny(loweredBody, []string{"p2002", "unique", "collision", "prisma"}) {
return false
}
Expand All @@ -230,6 +286,25 @@ func guardedSequenceCollisionRetry(loweredBody string) bool {
return containsAny(loweredBody, []string{"count + 1", "count+1", "externalid", "external_id", "nextid", "next_id"})
}

func metricStatArithmeticContext(fn precisionFunction, loweredBody string) bool {
loweredName := strings.ToLower(fn.Name)
if containsAny(loweredName, []string{"metric", "metrics", "stat", "stats", "counter", "histogram", "telemetry"}) {
return true
}
return containsAny(loweredBody, []string{"metric.", "metrics.", "counter.", "histogram", "stat.", "stats.", "telemetry", "prometheus", "datadog"})
}

func dateCountFormattingContext(fn precisionFunction, loweredBody string) bool {
loweredName := strings.ToLower(fn.Name)
if !containsAny(loweredName, []string{"format", "display", "label", "render", "summary", "calendar", "date", "time"}) {
return false
}
return containsAny(loweredBody, []string{
"date", "time", "calendar", "duration", "intl.", "datetimeformat", "formatdistance",
"formatrelative", "plural", "label", "title", "subtitle", "`${", " + \"", " + '",
})
}

func isUIRenderArithmeticContext(file string, fn precisionFunction, loweredBody string) bool {
if isUIHelperOrMappingContext(file, fn) {
return true
Expand Down Expand Up @@ -333,7 +408,7 @@ func missingSchemaValidationLine(fn precisionFunction, loweredBody string) (int,
if !jsonDecodePattern.MatchString(functionRawBody(fn)) {
return 0, false
}
if validatedBoundaryInputPattern(fn, loweredBody) || containsAny(loweredBody, []string{"jsonschema", "isvalid", "required"}) {
if validatedBoundaryInputPattern(fn, loweredBody) || jsonReaderSchemaCall.MatchString(functionRawBody(fn)) || containsAny(loweredBody, []string{"jsonschema", "isvalid", "required"}) {
return 0, false
}
return firstPatternLine(fn, jsonDecodePattern), true
Expand All @@ -343,7 +418,10 @@ func missingResourceLimitLine(fn precisionFunction, loweredBody string) (int, bo
if !resourceReadPattern.MatchString(functionRawBody(fn)) {
return 0, false
}
if containsAny(loweredBody, []string{"limitreader", "maxbytes", "max_bytes", "content-length", "contentlength", "limit(", "take(", "buffer_size", "quota"}) {
if uploadValidationHelperPattern(loweredBody) {
return 0, false
}
if resourceLimitProofPattern(loweredBody) {
return 0, false
}
if boundedReadByteLengthCheck(loweredBody) {
Expand All @@ -352,6 +430,21 @@ func missingResourceLimitLine(fn precisionFunction, loweredBody string) (int, bo
return firstPatternLine(fn, resourceReadPattern), true
}

func resourceLimitProofPattern(loweredBody string) bool {
if containsAny(loweredBody, []string{
"limitreader", "maxbytes", "max_bytes", "content-length", "contentlength",
"limit(", "take(", "slice(", ".slice(", "buffer_size", "quota",
}) {
return true
}
return resourceCountGuard.MatchString(loweredBody) || resourceNamedCountLimit.MatchString(loweredBody)
}

func uploadValidationHelperPattern(loweredBody string) bool {
return containsAny(loweredBody, []string{"validateinternaluploadfile", "validateuploadfile", "validatefileupload", "validateupload"}) ||
containsAny(loweredBody, []string{"internal_upload_max_bytes", "upload_max_bytes", "max_upload_bytes", "max_file_bytes"})
}

func boundedReadByteLengthCheck(loweredBody string) bool {
if !containsAny(loweredBody, []string{"arraybuffer", ".text", "readall", ".read"}) {
return false
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import (

var conventionalMutationBoundaryPattern = regexp.MustCompile(`^(accept|apply|approve|archive|clear|close|commit|deliver|download|drop|ensure|exists|fetch|import|list|notify|open|process|read|reconcile|record|run|seed|submit|sync|toggle|upload)`)

var localAccumulatorExprPattern = regexp.MustCompile(`(?i)^(?:new\s+)?(?:array|formdata|map|object|set|urlsearchparams|weakmap|weakset)\b|^\[|^\{|^make\s*\(|^array\.from\b|\.map\s*\(|\.filter\s*\(|\.reduce\s*\(|^(?:bytes|strings)\.buffer\b|^strings\.builder\b`)
var localAccumulatorExprPattern = regexp.MustCompile(`(?i)^(?:new\s+)?(?:array|formdata|map|object|set|urlsearchparams|weakmap|weakset)\b|^\[|^\{|^make\s*\(|^array\.from\b|\.map\s*\(|\.filter\s*\(|\.reduce\s*\(|\.split\s*\(|cheerio\.load\s*\(|^(?:bytes|strings)\.buffer\b|^strings\.builder\b`)

func localMutationTargets(fn precisionFunction) map[string]struct{} {
params := paramNames(fn)
Expand Down Expand Up @@ -133,7 +133,7 @@ func isAccumulatorLikeLocalName(name string) bool {
"bucket", "buckets", "buffer", "builder", "calendar", "cells", "copy", "doc",
"document", "filter", "filters", "form", "items", "lines", "params", "parts",
"payload", "primarycells", "query", "result", "rows", "scopes", "sections",
"serializer", "text", "urlparams", "values", "csv", "export", "map",
"serializer", "text", "urlparams", "values", "csv", "export", "map", "$",
} {
if strings.Contains(lowered, token) {
return true
Expand All @@ -146,7 +146,7 @@ func isAccumulatorBuilderFunctionName(name string) bool {
lowered := strings.ToLower(strings.Trim(name, "_$"))
for _, token := range []string{
"bucket", "build", "collect", "derive", "format", "group", "map", "parse",
"primary", "render", "serialize", "transform",
"primary", "render", "serialize", "transform", "clean", "filter",
} {
if strings.Contains(lowered, token) {
return true
Expand Down Expand Up @@ -178,6 +178,9 @@ func paramNames(fn precisionFunction) map[string]struct{} {
}

func isLocalMutationCall(callee string, localTargets map[string]struct{}) bool {
if isDerivedCollectionMutationCall(callee) {
return true
}
if isBareLocalMutationCall(callee) {
return true
}
Expand All @@ -188,6 +191,11 @@ func isLocalMutationCall(callee string, localTargets map[string]struct{}) bool {
return isLocalMutationTarget(target, localTargets)
}

func isDerivedCollectionMutationCall(callee string) bool {
lowered := strings.ToLower(strings.ReplaceAll(strings.TrimSpace(callee), " ", ""))
return strings.Contains(lowered, ".split.") && (strings.HasSuffix(lowered, ".pop") || strings.HasSuffix(lowered, ".sort") || strings.HasSuffix(lowered, ".reverse"))
}

func isBareLocalMutationCall(callee string) bool {
switch strings.TrimSpace(callee) {
case "append", "Set", "Array", "Object", "Map", "WeakMap", "WeakSet", "push_back":
Expand All @@ -202,6 +210,9 @@ func mutationCallTarget(callee string) string {
if callee == "" {
return ""
}
if strings.HasPrefix(callee, "$.") {
return "$"
}
for _, sep := range []string{".", "->", "::"} {
if idx := strings.Index(callee, sep); idx > 0 {
return strings.TrimSpace(callee[:idx])
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package quality

import "strings"

func isDomainSideEffectBoundaryName(name string) bool {
lowered := strings.ToLower(strings.TrimSpace(name))
if lowered == "" {
return false
}
if strings.HasPrefix(lowered, "maybe") && containsAny(lowered, []string{"alert", "notify", "record", "track", "emit"}) {
return true
}
if strings.HasPrefix(lowered, "evaluate") && containsAny(lowered, []string{"abuse", "policy", "rule", "risk", "fraud", "quota", "limit"}) {
return true
}
if strings.HasPrefix(lowered, "load") && containsAny(lowered, []string{"config", "defaults", "settings", "policy"}) {
return true
}
return false
}

func isAdapterOrOrchestrationFunction(file string, fn precisionFunction) bool {
loweredName := strings.ToLower(strings.Trim(fn.Name, "_$"))
if containsAny(loweredName, []string{"adapter", "bugreport", "bug_report", "slack", "webhook", "sync", "abuseconfig", "abuse_config"}) {
return true
}
if strings.HasPrefix(loweredName, "save") || strings.HasPrefix(loweredName, "insert") || strings.HasPrefix(loweredName, "post") ||
strings.HasPrefix(loweredName, "send") || strings.HasPrefix(loweredName, "publish") || strings.HasPrefix(loweredName, "record") {
if containsAny(loweredName, []string{"config", "report", "slack", "webhook", "audit", "event", "job"}) {
return true
}
}
normalized := strings.ToLower(strings.ReplaceAll(file, "\\", "/"))
return containsAny(normalized, []string{"/adapters/", "/adapter/", "/connectors/", "/connector/", "/integrations/", "/webhooks/", "/slack/", "/jobs/"})
}

func isAdapterOrchestrationName(name string) bool {
loweredName := strings.ToLower(strings.Trim(name, "_$"))
return containsAny(loweredName, []string{"abuseconfig", "abuse_config", "bugreport", "bug_report", "slack", "webhook", "adapter"})
}

func configuredPluralDomainAbbreviation(name string) bool {
switch name {
case "docs", "krs":
return true
default:
return false
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,13 @@ func isAllowedBooleanUIName(file string, fn precisionFunction, name string) bool
if !isReactComponentOrHookBoundary(file, fn) {
return false
}
switch strings.ToLower(strings.Trim(name, "_$")) {
normalized := strings.ToLower(strings.Trim(name, "_$"))
for _, suffix := range []string{"active", "visible", "enabled", "disabled", "open", "closed", "expanded", "collapsed", "selected", "checked", "pending", "loading"} {
if strings.HasSuffix(normalized, suffix) {
return true
}
}
switch normalized {
case "open", "loading", "active", "pending", "checked", "selected", "expanded", "collapsed":
return true
default:
Expand Down Expand Up @@ -191,7 +197,7 @@ func isResourceIdentifierName(name string) bool {
func conventionalCardinalityName(name string) bool {
base := strings.ToLower(strings.Trim(name, "_$"))
switch base {
case "answers", "args", "claims", "columns", "contracts", "entries", "files", "ids", "items", "k", "keys", "matters", "messages", "next", "out", "params", "props", "records", "risks", "rows", "searchparams", "sections", "source", "status", "thresholds", "users", "versions", "v", "i", "j", "x", "y":
case "all", "answers", "args", "claims", "columns", "contracts", "docs", "entries", "files", "filtered", "ids", "items", "k", "keys", "krs", "matters", "messages", "next", "out", "params", "props", "records", "risks", "rows", "searchparams", "sections", "source", "status", "thresholds", "users", "versions", "v", "i", "j", "x", "y":
return true
default:
return len(name) <= 2 ||
Expand Down
Loading
Loading