#!/usr/bin/env bash # CWE-407 scanner: O(n) linear membership checks in graph traversal code. # # Pattern: Any call to a linear-scan membership check (.contains, "in list", # std::find, etc.) inside a function whose name suggests graph traversal # (tarjan, scc, dfs, topo, cycle, closure, reachab*). # # Usage: # scan-cwe407.sh [file-glob] # # Output: TSV — file:line | function-context | snippet | verdict # # Supports: .java .py .kt .scala .cpp .h .c .go .rs .rb .js .ts .cs .swift set -euo pipefail ROOT="${1:-.}" GLOB="${2:-}" # ── Membership check patterns per language ───────────────────────────────────── JAVA_MEM='\.contains\s*\(|List\.contains|ArrayList\.contains|LinkedList\.contains|Deque\.contains|ArrayDeque\.contains' PY_MEM='[^a-z]in (stack|path|visited|seen|queue|nodes)|path\.append|\.contains\(' KT_MEM='\.contains\s*\(|in\s+(?:stack|path|visited|list|nodes)\b' SCALA_MEM='\.contains\s*\(|\.exists\s*\{|List\.contains|Seq\.contains|ArrayBuffer\.contains' CPP_MEM='std::find[(]|[.]count[(]|linear_search|find.*begin.*end' GO_MEM='slices\.Contains|contains\s*\(.*\[\]|for.*range.*==\s*\w' RS_MEM='\.contains\s*\(&|iter\(\)\.any\s*\(|\.position\s*\(' # ── Graph/traversal function name patterns ───────────────────────────────────── GRAPH_FN='tarjan|scc|strongly.connected|dfs|bfs|topolog|topo.sort|cycle|closure|reachab|postorder|preorder|dominator|liveness|worklist|scfg|cfg' # ── Scan a single file ───────────────────────────────────────────────────────── scan_file() { local f="$1" local ext="${f##*.}" local mem_pat="" case "$ext" in java) mem_pat="$JAVA_MEM" ;; py) mem_pat="$PY_MEM" ;; kt|kts) mem_pat="$KT_MEM" ;; scala) mem_pat="$SCALA_MEM" ;; cpp|cc|cxx|h|hpp|c) mem_pat="$CPP_MEM" ;; go) mem_pat="$GO_MEM" ;; rs) mem_pat="$RS_MEM" ;; *) return ;; esac # Two-pass: find graph-related functions first, then check for membership # within ±20 lines of any match. local graph_lines graph_lines=$(grep -inE "$GRAPH_FN" "$f" 2>/dev/null | cut -d: -f1 || true) [ -z "$graph_lines" ] && return while IFS= read -r lineno; do local start=$(( lineno - 20 )) local end=$(( lineno + 20 )) [ $start -lt 1 ] && start=1 # Check if any membership pattern appears in the window local hit hit=$(sed -n "${start},${end}p" "$f" 2>/dev/null \ | grep -iE "$mem_pat" 2>/dev/null || true) if [ -n "$hit" ]; then echo -e "CANDIDATE\t${f}:${lineno}\t$(echo "$hit" | head -1 | sed 's/^\s*//')" fi done <<< "$graph_lines" } export -f scan_file export JAVA_MEM PY_MEM KT_MEM SCALA_MEM CPP_MEM GO_MEM RS_MEM GRAPH_FN # ── Find files ───────────────────────────────────────────────────────────────── echo "# CWE-407 scan: $ROOT" echo "# $(date -u)" echo "# file:line | snippet" echo "#" if [ -n "$GLOB" ]; then find "$ROOT" -name "$GLOB" -type f else find "$ROOT" -type f \( \ -name "*.java" -o -name "*.py" -o -name "*.kt" -o -name "*.scala" \ -o -name "*.cpp" -o -name "*.cc" -o -name "*.c" -o -name "*.h" \ -o -name "*.go" -o -name "*.rs" \ \) -not -path "*/.git/*" -not -path "*/build/*" -not -path "*/target/*" fi | sort | while IFS= read -r f; do scan_file "$f" done echo "#" echo "# Scan complete."