From 7d4f44c642af1cebfee5c4baa187dad2052ffe74 Mon Sep 17 00:00:00 2001 From: AI DevOps Date: Wed, 11 Mar 2026 13:08:56 -0600 Subject: [PATCH 1/3] fix: test harness early exit and security posture stderr suppression - assert_line_exists(): return 0 on failure (consistent with assert_contains) so the test harness runs all checks before reporting, instead of exiting on the first failure under set -e. The set +e wrapper in run_checks() mitigated this, but the function contract was still wrong. Closes #4150 - aidevops.sh: remove 2>/dev/null from security-posture-helper.sh invocation so users see detailed security findings during 'aidevops init' instead of only the pass/fail summary. Closes #3149 --- .agents/scripts/tests/test-pr-3885-recovery.sh | 2 +- aidevops.sh | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.agents/scripts/tests/test-pr-3885-recovery.sh b/.agents/scripts/tests/test-pr-3885-recovery.sh index 9eecd9b98..0999b8940 100755 --- a/.agents/scripts/tests/test-pr-3885-recovery.sh +++ b/.agents/scripts/tests/test-pr-3885-recovery.sh @@ -53,7 +53,7 @@ assert_line_exists() { fi fail "$message" - return 1 + return 0 } run_checks() { diff --git a/aidevops.sh b/aidevops.sh index bb8417b03..eeb35b93f 100755 --- a/aidevops.sh +++ b/aidevops.sh @@ -2012,7 +2012,7 @@ SOPSEOF local security_posture_script="$AGENTS_DIR/scripts/security-posture-helper.sh" if [[ -f "$security_posture_script" ]]; then print_info "Running security posture assessment..." - if bash "$security_posture_script" store "$project_root" 2>/dev/null; then + if bash "$security_posture_script" store "$project_root"; then print_success "Security posture assessed and stored in .aidevops.json" else print_warning "Security posture assessment found issues (review with: aidevops security audit)" From 18664cacdd553940d0d19375fc490eb20ad9d14d Mon Sep 17 00:00:00 2001 From: AI DevOps Date: Wed, 11 Mar 2026 13:17:49 -0600 Subject: [PATCH 2/3] fix: prevent GraphQL injection and add slug validation in stats-functions.sh - Add _validate_repo_slug() to reject path traversal and injection characters in repo slugs before they reach API paths - Parameterize GraphQL queries in _cleanup_stale_pinned_issues() using -F owner/name variables instead of string interpolation (prevents injection via double quotes in repo slug) - Parameterize GraphQL search queries for quality-debt counts using -F searchQuery variable instead of inline interpolation - Add slug validation gate in _get_runner_role() before API path construction - Redirect GraphQL stderr to LOGFILE instead of /dev/null for debuggability - Update auto-generated message references from pulse-wrapper.sh to stats-wrapper.sh to reflect the t1431 extraction Closes #4155 --- .agents/scripts/stats-functions.sh | 66 +++++++++++++++++++----------- 1 file changed, 43 insertions(+), 23 deletions(-) diff --git a/.agents/scripts/stats-functions.sh b/.agents/scripts/stats-functions.sh index 6dd34367c..5566b2ced 100755 --- a/.agents/scripts/stats-functions.sh +++ b/.agents/scripts/stats-functions.sh @@ -47,6 +47,24 @@ if type _validate_int &>/dev/null; then SESSION_COUNT_WARN=$(_validate_int SESSION_COUNT_WARN "$SESSION_COUNT_WARN" 5 1) fi +####################################### +# Validate a repo slug matches the expected owner/repo format. +# Rejects path traversal, quotes, and other injection vectors. +# Arguments: +# $1 - repo slug to validate +# Returns: 0 if valid, 1 if invalid +####################################### +_validate_repo_slug() { + local slug="$1" + # Must be non-empty, match owner/repo with only alphanumeric, hyphens, + # underscores, and dots (GitHub's allowed characters) + if [[ "$slug" =~ ^[a-zA-Z0-9._-]+/[a-zA-Z0-9._-]+$ ]]; then + return 0 + fi + echo "[stats] Invalid repo slug rejected: ${slug}" >>"$LOGFILE" + return 1 +} + ####################################### # Count interactive AI sessions (duplicate of pulse-wrapper's check_session_count) # @@ -93,6 +111,12 @@ _get_runner_role() { local runner_user="$1" local repo_slug="$2" + # Validate slug before using in API path (defense-in-depth) + if ! _validate_repo_slug "$repo_slug"; then + echo "contributor" + return 0 + fi + # Check cache (env var keyed by slug — avoids repeated API calls) local cache_key="__RUNNER_ROLE_${repo_slug//[^a-zA-Z0-9]/_}" local cached_role="${!cache_key:-}" @@ -553,7 +577,7 @@ ${cross_repo_person_stats_md:-_Cross-repo person stats unavailable._} | Processes | ${sys_procs} | --- -_Auto-updated by ${runner_role} pulse. Do not edit manually._" +_Auto-updated by ${runner_role} stats process. Do not edit manually._" # Update the issue body gh issue edit "$health_issue_number" --repo "$repo_slug" --body "$body" >/dev/null 2>&1 || { @@ -602,11 +626,11 @@ _cleanup_stale_pinned_issues() { local owner="${repo_slug%%/*}" local name="${repo_slug##*/}" - # Query all pinned issues via GraphQL + # Query all pinned issues via GraphQL (parameterized to prevent injection) local pinned_json - pinned_json=$(gh api graphql -f query=" - query { - repository(owner: \"${owner}\", name: \"${name}\") { + pinned_json=$(gh api graphql -F owner="$owner" -F name="$name" -f query=' + query($owner: String!, $name: String!) { + repository(owner: $owner, name: $name) { pinnedIssues(first: 10) { nodes { issue { @@ -618,7 +642,7 @@ _cleanup_stale_pinned_issues() { } } } - }" 2>/dev/null || echo "") + }' 2>>"$LOGFILE" || echo "") [[ -z "$pinned_json" ]] && return 0 @@ -1441,7 +1465,7 @@ ${coderabbit_section} ${review_scan_section} --- -_Auto-generated by pulse-wrapper.sh daily quality sweep. The supervisor will review findings and create actionable issues._" +_Auto-generated by stats-wrapper.sh daily quality sweep. The supervisor will review findings and create actionable issues._" # --- 7. Update issue body with stats dashboard (t1411) --- # Mirrors the supervisor health issue pattern: the issue body is a live @@ -1495,26 +1519,22 @@ _update_quality_issue_body() { # (CodeRabbit review feedback — gh issue list defaults to 30 results). local debt_open=0 local debt_closed=0 - debt_open=$(gh api graphql -f query=" - query { - search( - query: \"repo:${repo_slug} is:issue is:open label:quality-debt\", - type: ISSUE, - first: 1 - ) { + debt_open=$(gh api graphql \ + -F searchQuery="repo:${repo_slug} is:issue is:open label:quality-debt" \ + -f query=' + query($searchQuery: String!) { + search(query: $searchQuery, type: ISSUE, first: 1) { issueCount } - }" --jq '.data.search.issueCount' 2>>"$LOGFILE" || echo "0") - debt_closed=$(gh api graphql -f query=" - query { - search( - query: \"repo:${repo_slug} is:issue is:closed label:quality-debt\", - type: ISSUE, - first: 1 - ) { + }' --jq '.data.search.issueCount' 2>>"$LOGFILE" || echo "0") + debt_closed=$(gh api graphql \ + -F searchQuery="repo:${repo_slug} is:issue is:closed label:quality-debt" \ + -f query=' + query($searchQuery: String!) { + search(query: $searchQuery, type: ISSUE, first: 1) { issueCount } - }" --jq '.data.search.issueCount' 2>>"$LOGFILE" || echo "0") + }' --jq '.data.search.issueCount' 2>>"$LOGFILE" || echo "0") # Validate integers [[ "$debt_open" =~ ^[0-9]+$ ]] || debt_open=0 [[ "$debt_closed" =~ ^[0-9]+$ ]] || debt_closed=0 From b38bcd13b83b8f55318564c6a8641b2192c64567 Mon Sep 17 00:00:00 2001 From: marcusquinn <6428977+marcusquinn@users.noreply.github.com> Date: Wed, 11 Mar 2026 20:16:08 +0000 Subject: [PATCH 3/3] fix: resolve SC2016 in stats GraphQL queries --- .agents/scripts/stats-functions.sh | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/.agents/scripts/stats-functions.sh b/.agents/scripts/stats-functions.sh index 5566b2ced..aeb554da5 100755 --- a/.agents/scripts/stats-functions.sh +++ b/.agents/scripts/stats-functions.sh @@ -628,9 +628,9 @@ _cleanup_stale_pinned_issues() { # Query all pinned issues via GraphQL (parameterized to prevent injection) local pinned_json - pinned_json=$(gh api graphql -F owner="$owner" -F name="$name" -f query=' - query($owner: String!, $name: String!) { - repository(owner: $owner, name: $name) { + pinned_json=$(gh api graphql -F owner="$owner" -F name="$name" -f query=" + query(\$owner: String!, \$name: String!) { + repository(owner: \$owner, name: \$name) { pinnedIssues(first: 10) { nodes { issue { @@ -642,7 +642,8 @@ _cleanup_stale_pinned_issues() { } } } - }' 2>>"$LOGFILE" || echo "") + } + " 2>>"$LOGFILE" || echo "") [[ -z "$pinned_json" ]] && return 0 @@ -1521,20 +1522,20 @@ _update_quality_issue_body() { local debt_closed=0 debt_open=$(gh api graphql \ -F searchQuery="repo:${repo_slug} is:issue is:open label:quality-debt" \ - -f query=' - query($searchQuery: String!) { - search(query: $searchQuery, type: ISSUE, first: 1) { + -f query=" + query(\$searchQuery: String!) { + search(query: \$searchQuery, type: ISSUE, first: 1) { issueCount } - }' --jq '.data.search.issueCount' 2>>"$LOGFILE" || echo "0") + }" --jq '.data.search.issueCount' 2>>"$LOGFILE" || echo "0") debt_closed=$(gh api graphql \ -F searchQuery="repo:${repo_slug} is:issue is:closed label:quality-debt" \ - -f query=' - query($searchQuery: String!) { - search(query: $searchQuery, type: ISSUE, first: 1) { + -f query=" + query(\$searchQuery: String!) { + search(query: \$searchQuery, type: ISSUE, first: 1) { issueCount } - }' --jq '.data.search.issueCount' 2>>"$LOGFILE" || echo "0") + }" --jq '.data.search.issueCount' 2>>"$LOGFILE" || echo "0") # Validate integers [[ "$debt_open" =~ ^[0-9]+$ ]] || debt_open=0 [[ "$debt_closed" =~ ^[0-9]+$ ]] || debt_closed=0