Skip to content

Commit fca3763

Browse files
Jonathan D.A. Jewellclaude
andcommitted
feat: add 20 fix scripts covering all dispatch categories + registry
New fix scripts (20): - fix-command-injection.sh: backtick→$(), eval annotation, var quoting - fix-hardcoded-secrets.sh: detect+replace hardcoded passwords/keys/tokens - fix-secret-to-env.sh: replace secrets with env var lookups - fix-dynamic-code-exec.sh: eval/Function()/Code.eval_string warnings - fix-eval-to-safe.sh: curl|bash and eval pattern annotations - fix-atom-exhaustion.sh: String.to_atom → String.to_existing_atom - fix-unsafe-deserialize.sh: yaml.load→yaml.safe_load, pickle warnings - fix-sorry-lean.sh: annotate Lean sorry with PROOF_TODO comments - fix-unsafe-type-coercion.sh: unsafeCoerce/Obj.magic/Admitted warnings - fix-unwrap-to-match.sh: .unwrap()→.expect("TODO: handle error") - fix-unsafe-ffi.sh: add // SAFETY: TODO to undocumented unsafe blocks - fix-sql-parameterize.sh: SQL injection warning annotations - fix-innerhtml.sh: innerHTML→textContent, XSS warnings - fix-dependabot.sh: create dependabot.yml with detected ecosystems - fix-sast-workflow.sh: create CodeQL workflow with SHA-pinned actions - fix-license-file.sh: create LICENSE with PMPL-1.0-or-later - fix-deno-permissions.sh: --allow-all → specific permissions - fix-unchecked-error.sh: annotate discarded error returns - fix-resource-leak.sh: annotate unclosed file handles/connections Also added: - fix-script-registry.json: maps recipe IDs + categories to scripts - dispatch-runner.sh: auto-resolve fix scripts from registry when manifest entry has null fix_script Coverage: 28 of 31 categories now have fix scripts (was 13). Remaining nulls: UnboundedLoop, RaceCondition, BlockingIO (need manual review — no safe mechanical fix exists). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent c7e175a commit fca3763

21 files changed

Lines changed: 2303 additions & 11 deletions

scripts/dispatch-runner.sh

Lines changed: 32 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -49,8 +49,8 @@ validate_path_within() {
4949
# --- Configuration ---
5050
# Hypatia's local data store is the primary source for dispatch manifests.
5151
# Falls back to central verisimdb-data if HYPATIA_DATA is not set.
52-
HYPATIA_DATA="${HYPATIA_DATA:-/var/mnt/eclipse/repos/hypatia/data/verisimdb}"
53-
VERISIMDB_DATA="${VERISIMDB_DATA:-/var/mnt/eclipse/repos/verisimdb-data}"
52+
HYPATIA_DATA="${HYPATIA_DATA:-/var/mnt/eclipse/repos/nextgen-databases/verisimdb/verisimdb-data}"
53+
VERISIMDB_DATA="${VERISIMDB_DATA:-/var/mnt/eclipse/repos/nextgen-databases/verisimdb/verisimdb-data}"
5454
REPOS_BASE="${REPOS_BASE:-/var/mnt/eclipse/repos}"
5555
FLEET_SCRIPTS="${FLEET_SCRIPTS:-/var/mnt/eclipse/repos/gitbot-fleet/scripts}"
5656
RRA_BIN="${RRA_BIN:-/var/mnt/eclipse/repos/gitbot-fleet/robot-repo-automaton/target/release/robot-repo-automaton}"
@@ -154,7 +154,7 @@ record_outcome() {
154154
execute_entry() {
155155
local entry="$1"
156156

157-
local tier strategy repo pattern_id recipe_id confidence auto_fixable fix_script
157+
local tier strategy repo pattern_id recipe_id confidence auto_fixable fix_script program_path
158158
tier=$(echo "$entry" | jq -r '.tier')
159159
strategy=$(echo "$entry" | jq -r '.strategy')
160160
repo=$(echo "$entry" | jq -r '.repo')
@@ -163,16 +163,22 @@ execute_entry() {
163163
confidence=$(echo "$entry" | jq -r '.confidence // 0')
164164
auto_fixable=$(echo "$entry" | jq -r '.auto_fixable // false')
165165
fix_script=$(echo "$entry" | jq -r '.fix_script // "none"')
166-
167-
# Validate repo name (prevent directory traversal)
168-
if ! validate_safe_name "$repo" "repo"; then
169-
echo " SKIP: $pattern_id (unsafe repo name)"
170-
((SKIPPED++)) || true
171-
return
166+
program_path=$(echo "$entry" | jq -r '.program_path // ""')
167+
168+
# Resolve repo path: prefer program_path from manifest, fall back to REPOS_BASE/repo
169+
local repo_path=""
170+
if [[ -n "$program_path" && -d "$program_path" ]]; then
171+
repo_path="$program_path"
172+
else
173+
# Validate repo name (prevent directory traversal)
174+
if ! validate_safe_name "$repo" "repo"; then
175+
echo " SKIP: $pattern_id (unsafe repo name)"
176+
((SKIPPED++)) || true
177+
return
178+
fi
179+
repo_path="$REPOS_BASE/$repo"
172180
fi
173181

174-
local repo_path="$REPOS_BASE/$repo"
175-
176182
# Double-check path stays within REPOS_BASE
177183
if ! validate_path_within "$repo_path" "$REPOS_BASE"; then
178184
echo " SKIP: $pattern_id (repo path escapes base)"
@@ -196,6 +202,21 @@ execute_entry() {
196202
return
197203
fi
198204

205+
# Resolve fix_script from registry if not set in manifest
206+
if [[ "$fix_script" == "none" || "$fix_script" == "null" ]]; then
207+
local registry="$FLEET_SCRIPTS/fix-script-registry.json"
208+
if [[ -f "$registry" ]]; then
209+
# Try recipe_id first, then category
210+
fix_script=$(jq -r --arg rid "$recipe_id" '.registry.by_recipe[$rid] // empty' "$registry" 2>/dev/null || true)
211+
if [[ -z "$fix_script" || "$fix_script" == "null" ]]; then
212+
local category
213+
category=$(echo "$entry" | jq -r '.category // ""')
214+
fix_script=$(jq -r --arg cat "$category" '.registry.by_category[$cat] // empty' "$registry" 2>/dev/null || true)
215+
fi
216+
[[ -z "$fix_script" ]] && fix_script="none"
217+
fi
218+
fi
219+
199220
# Try fix script first, then robot-repo-automaton
200221
# Validate fix_script: must not contain path separators or traversal
201222
if [[ "$fix_script" != "none" && "$fix_script" != "null" ]] && \

scripts/fix-atom-exhaustion.sh

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
#!/usr/bin/env bash
2+
# SPDX-License-Identifier: PMPL-1.0-or-later
3+
#
4+
# fix-atom-exhaustion.sh — Replace String.to_atom with String.to_existing_atom
5+
#
6+
# String.to_atom/1 creates new atoms at runtime, which can exhaust the BEAM
7+
# atom table (limited, non-garbage-collected). String.to_existing_atom/1 only
8+
# converts strings to atoms that already exist in the table, preventing
9+
# exhaustion attacks from untrusted input.
10+
#
11+
# This is a safe, mechanical replacement — the function signatures are
12+
# identical; only the safety semantics differ.
13+
#
14+
# Usage: fix-atom-exhaustion.sh <repo-path> <finding-json>
15+
16+
set -euo pipefail
17+
18+
REPO_PATH="${1:?Usage: $0 <repo-path> <finding-json>}"
19+
FINDING_JSON="${2:?Missing finding JSON file}"
20+
21+
DESCRIPTION=$(jq -r '.description // ""' "$FINDING_JSON")
22+
PATTERN_ID=$(jq -r '.pattern_id // ""' "$FINDING_JSON")
23+
24+
echo "=== Atom Exhaustion Fix ==="
25+
echo " Repo: $REPO_PATH"
26+
echo " Pattern: $PATTERN_ID"
27+
echo ""
28+
29+
# Directories to skip
30+
SKIP_DIRS=( "_build" "deps" ".git" )
31+
32+
FIND_EXCLUDES=()
33+
for dir in "${SKIP_DIRS[@]}"; do
34+
FIND_EXCLUDES+=( -not -path "*/${dir}/*" )
35+
done
36+
37+
# Find all Elixir source files
38+
EX_FILES=()
39+
while IFS= read -r -d '' f; do
40+
EX_FILES+=("$f")
41+
done < <(find "$REPO_PATH" -type f \( -name "*.ex" -o -name "*.exs" \) "${FIND_EXCLUDES[@]}" -print0 2>/dev/null)
42+
43+
if [[ ${#EX_FILES[@]} -eq 0 ]]; then
44+
echo " No Elixir files found in $REPO_PATH"
45+
exit 0
46+
fi
47+
48+
FIXED_COUNT=0
49+
50+
for file in "${EX_FILES[@]}"; do
51+
# Skip binary files
52+
if file "$file" | grep -q "binary"; then
53+
continue
54+
fi
55+
56+
# Check if file contains String.to_atom (outside of comments)
57+
if ! grep -qP '^\s*[^#]*\bString\.to_atom\(' "$file" 2>/dev/null; then
58+
continue
59+
fi
60+
61+
# Replace String.to_atom( with String.to_existing_atom(
62+
# Only on lines that are not comments (lines where # appears before the match)
63+
tmpfile=$(mktemp)
64+
awk '
65+
/^\s*#/ { print; next }
66+
/\bString\.to_atom\(/ {
67+
gsub(/\bString\.to_atom\(/, "String.to_existing_atom(")
68+
print
69+
next
70+
}
71+
{ print }
72+
' "$file" > "$tmpfile"
73+
74+
if ! diff -q "$file" "$tmpfile" >/dev/null 2>&1; then
75+
cp "$tmpfile" "$file"
76+
rel_path="${file#"$REPO_PATH"/}"
77+
echo " Fixed String.to_atom → String.to_existing_atom in $rel_path"
78+
((FIXED_COUNT++)) || true
79+
fi
80+
rm -f "$tmpfile"
81+
done
82+
83+
echo ""
84+
if [[ "$FIXED_COUNT" -gt 0 ]]; then
85+
echo "Fixed $FIXED_COUNT file(s)"
86+
else
87+
echo "No String.to_atom patterns found (already safe or only in comments)"
88+
fi

scripts/fix-command-injection.sh

Lines changed: 194 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,194 @@
1+
#!/usr/bin/env bash
2+
# SPDX-License-Identifier: PMPL-1.0-or-later
3+
#
4+
# fix-command-injection.sh — Fix unsafe command execution patterns
5+
#
6+
# Targets CommandInjection findings across shell, Elixir, and Racket files.
7+
#
8+
# Shell fixes:
9+
# - eval "$var" → replaced with warning comment (not auto-fixable safely)
10+
# - backtick `cmd` → $(cmd) substitution
11+
# - Unquoted variables in command arguments → quoted
12+
#
13+
# Elixir fixes:
14+
# - System.cmd with string interpolation → annotated for review
15+
#
16+
# Racket fixes:
17+
# - system/process calls → annotated for review
18+
#
19+
# Usage: fix-command-injection.sh <repo-path> <finding-json>
20+
21+
set -euo pipefail
22+
23+
REPO_PATH="${1:?Usage: $0 <repo-path> <finding-json>}"
24+
FINDING_JSON="${2:?Missing finding JSON file}"
25+
26+
DESCRIPTION=$(jq -r '.description // ""' "$FINDING_JSON")
27+
PATTERN_ID=$(jq -r '.pattern_id // ""' "$FINDING_JSON")
28+
29+
echo "=== Command Injection Fix ==="
30+
echo " Repo: $REPO_PATH"
31+
echo " Pattern: $PATTERN_ID"
32+
echo ""
33+
34+
# Directories to skip
35+
SKIP_DIRS=(".git" "target" "node_modules" "_build" ".lake")
36+
37+
# Build the -not -path clauses for find
38+
FIND_EXCLUDES=()
39+
for d in "${SKIP_DIRS[@]}"; do
40+
FIND_EXCLUDES+=(-not -path "*/${d}/*")
41+
done
42+
43+
FIXED_COUNT=0
44+
45+
# ---------------------------------------------------------------------------
46+
# Phase 1: Shell scripts (.sh, .bash)
47+
# ---------------------------------------------------------------------------
48+
SHELL_FILES=()
49+
while IFS= read -r -d '' f; do
50+
SHELL_FILES+=("$f")
51+
done < <(find "$REPO_PATH" -type f \( -name "*.sh" -o -name "*.bash" \) \
52+
"${FIND_EXCLUDES[@]}" -print0 2>/dev/null)
53+
54+
for file in "${SHELL_FILES[@]}"; do
55+
# Skip binary files
56+
if file "$file" | grep -q "binary"; then
57+
continue
58+
fi
59+
60+
CHANGES=0
61+
rel_path="${file#"$REPO_PATH"/}"
62+
63+
# --- Fix 1: Replace backtick substitution with $() ---
64+
# Match lines containing `...` (backtick command substitution)
65+
# Convert `cmd args` → $(cmd args)
66+
# Careful: skip lines that are comments or already use $()
67+
if grep -qP '(?<!\\)`[^`]+`' "$file" 2>/dev/null; then
68+
# Replace backtick substitution with $() form
69+
# Skip comment lines, convert `...` to $(...)
70+
cp "$file" "$file.bak"
71+
sed -i -E '/^\s*#/!s/`([^`]+)`/$(\1)/g' "$file"
72+
if ! diff -q "$file" "$file.bak" >/dev/null 2>&1; then
73+
((CHANGES++)) || true
74+
echo " [shell] Replaced backtick substitution in $rel_path"
75+
fi
76+
rm -f "$file.bak"
77+
fi
78+
79+
# --- Fix 2: Annotate eval usage with warning ---
80+
# Insert a warning comment above eval lines that use variable expansion
81+
if grep -qP '^\s*eval\s+["\$]' "$file" 2>/dev/null; then
82+
# Check if warning comment already exists (idempotent)
83+
if ! grep -q "SECURITY: eval with variable expansion is a command injection risk" "$file" 2>/dev/null; then
84+
sed -i.bak '/^\s*eval\s\+["\$]/i\# SECURITY: eval with variable expansion is a command injection risk — refactor to avoid eval' "$file"
85+
if [[ -f "$file.bak" ]] && ! diff -q "$file" "$file.bak" >/dev/null 2>&1; then
86+
((CHANGES++)) || true
87+
echo " [shell] Annotated eval usage in $rel_path"
88+
fi
89+
rm -f "$file.bak"
90+
fi
91+
fi
92+
93+
# --- Fix 3: Quote unquoted variables in command positions ---
94+
# Fix patterns like: cmd $VAR → cmd "$VAR"
95+
# Only target common command prefixes to avoid false positives
96+
for cmd in install cp mv rm mkdir rmdir chmod chown cat grep sed awk curl wget; do
97+
if grep -qP "\\b${cmd}\\s+\\\$[A-Za-z_][A-Za-z0-9_]*(?![\"'])" "$file" 2>/dev/null; then
98+
sed -i.bak "s/\\(\\b${cmd}\\s\\+\\)\\$\\([A-Za-z_][A-Za-z0-9_]*\\)/\\1\"\\$\\2\"/g" "$file"
99+
if [[ -f "$file.bak" ]] && ! diff -q "$file" "$file.bak" >/dev/null 2>&1; then
100+
((CHANGES++)) || true
101+
fi
102+
rm -f "$file.bak"
103+
fi
104+
done
105+
106+
# --- Fix 4: Quote unquoted $() in command arguments ---
107+
# Fix: cmd $(subcmd) → cmd "$(subcmd)"
108+
# Only where $() is not already inside quotes
109+
if grep -qP '(?<!")\$\([^)]+\)(?!")' "$file" 2>/dev/null; then
110+
# Quote bare $() in command arguments
111+
cp "$file" "$file.bak"
112+
sed -i -E '/^\s*#/!s/ \$\(([^)]+)\)(?!")/ "$(\1)"/g' "$file"
113+
if ! diff -q "$file" "$file.bak" >/dev/null 2>&1; then
114+
((CHANGES++)) || true
115+
echo " [shell] Quoted bare command substitutions in $rel_path"
116+
fi
117+
rm -f "$file.bak"
118+
fi
119+
120+
if [[ "$CHANGES" -gt 0 ]]; then
121+
((FIXED_COUNT++)) || true
122+
fi
123+
done
124+
125+
# ---------------------------------------------------------------------------
126+
# Phase 2: Elixir files (.ex, .exs)
127+
# ---------------------------------------------------------------------------
128+
ELIXIR_FILES=()
129+
while IFS= read -r -d '' f; do
130+
ELIXIR_FILES+=("$f")
131+
done < <(find "$REPO_PATH" -type f \( -name "*.ex" -o -name "*.exs" \) \
132+
"${FIND_EXCLUDES[@]}" -print0 2>/dev/null)
133+
134+
for file in "${ELIXIR_FILES[@]}"; do
135+
rel_path="${file#"$REPO_PATH"/}"
136+
137+
# Find System.cmd with string interpolation in the command argument
138+
# Pattern: System.cmd("...#{...}...", ...)
139+
if grep -qP 'System\.cmd\(\s*"[^"]*#\{' "$file" 2>/dev/null; then
140+
# Check if annotation already present (idempotent)
141+
if ! grep -q "SECURITY: validate input before System.cmd" "$file" 2>/dev/null; then
142+
sed -i.bak '/System\.cmd(\s*"[^"]*#\{/i\ # SECURITY: validate input before System.cmd — string interpolation in command is a command injection risk' "$file"
143+
if [[ -f "$file.bak" ]] && ! diff -q "$file" "$file.bak" >/dev/null 2>&1; then
144+
((FIXED_COUNT++)) || true
145+
echo " [elixir] Annotated System.cmd interpolation in $rel_path"
146+
fi
147+
rm -f "$file.bak"
148+
fi
149+
fi
150+
151+
# Also catch :os.cmd with interpolation
152+
if grep -qP ':os\.cmd\(' "$file" 2>/dev/null; then
153+
if ! grep -q "SECURITY: :os.cmd executes shell commands" "$file" 2>/dev/null; then
154+
sed -i.bak '/:os\.cmd(/i\ # SECURITY: :os.cmd executes shell commands — validate all inputs to prevent command injection' "$file"
155+
if [[ -f "$file.bak" ]] && ! diff -q "$file" "$file.bak" >/dev/null 2>&1; then
156+
((FIXED_COUNT++)) || true
157+
echo " [elixir] Annotated :os.cmd usage in $rel_path"
158+
fi
159+
rm -f "$file.bak"
160+
fi
161+
fi
162+
done
163+
164+
# ---------------------------------------------------------------------------
165+
# Phase 3: Racket files (.rkt) — annotation only
166+
# ---------------------------------------------------------------------------
167+
RACKET_FILES=()
168+
while IFS= read -r -d '' f; do
169+
RACKET_FILES+=("$f")
170+
done < <(find "$REPO_PATH" -type f -name "*.rkt" \
171+
"${FIND_EXCLUDES[@]}" -print0 2>/dev/null)
172+
173+
for file in "${RACKET_FILES[@]}"; do
174+
rel_path="${file#"$REPO_PATH"/}"
175+
176+
# Annotate (system ...) and (process ...) calls
177+
if grep -qP '\(\s*(system|process)\s' "$file" 2>/dev/null; then
178+
if ! grep -q "SECURITY: system/process call needs manual review for command injection" "$file" 2>/dev/null; then
179+
sed -i.bak '/(\s*\(system\|process\)\s/i\; SECURITY: system/process call needs manual review for command injection' "$file"
180+
if [[ -f "$file.bak" ]] && ! diff -q "$file" "$file.bak" >/dev/null 2>&1; then
181+
((FIXED_COUNT++)) || true
182+
echo " [racket] Annotated system/process call in $rel_path"
183+
fi
184+
rm -f "$file.bak"
185+
fi
186+
fi
187+
done
188+
189+
echo ""
190+
if [[ "$FIXED_COUNT" -gt 0 ]]; then
191+
echo "Fixed/annotated $FIXED_COUNT file(s)"
192+
else
193+
echo "No fixable patterns found (may need manual review)"
194+
fi

0 commit comments

Comments
 (0)