diff --git a/.agents/scripts/stats-functions.sh b/.agents/scripts/stats-functions.sh index 6dd34367ce..aeb554da53 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,8 @@ _cleanup_stale_pinned_issues() { } } } - }" 2>/dev/null || echo "") + } + " 2>>"$LOGFILE" || echo "") [[ -z "$pinned_json" ]] && return 0 @@ -1441,7 +1466,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,23 +1520,19 @@ _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 - ) { + 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") diff --git a/.agents/scripts/tests/test-pr-3885-recovery.sh b/.agents/scripts/tests/test-pr-3885-recovery.sh index 9eecd9b98f..0999b8940c 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 bb8417b032..eeb35b93f5 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)"