diff --git a/.agents/scripts/add-skill-helper.sh b/.agents/scripts/add-skill-helper.sh index 3bda1fe2e0..9a2645b2e4 100755 --- a/.agents/scripts/add-skill-helper.sh +++ b/.agents/scripts/add-skill-helper.sh @@ -585,7 +585,7 @@ scan_skill_security() { } # Run VirusTotal scan on skill files and referenced domains -# Returns: 0 = safe or VT not configured, 1 = threats detected +# Returns: 0 (always, as VT scans are advisory; Cisco scanner is the gate) scan_skill_virustotal() { local scan_path="$1" local skill_name="$2" @@ -612,7 +612,7 @@ scan_skill_virustotal() { if ! "$vt_helper" scan-skill "$scan_path" --quiet; then log_warning "VirusTotal flagged potential threats in '$skill_name'" - log_info "Run: virustotal-helper.sh scan-skill '$scan_path' for details" + log_info "Run: $vt_helper scan-skill '$scan_path' for details" # VT findings are advisory, not blocking (Cisco scanner is the gate) return 0 fi diff --git a/.agents/scripts/security-helper.sh b/.agents/scripts/security-helper.sh index cf59d1a0bf..ece544b240 100755 --- a/.agents/scripts/security-helper.sh +++ b/.agents/scripts/security-helper.sh @@ -594,10 +594,12 @@ cmd_skill_scan() { continue fi - local scan_dir - scan_dir="$(dirname "$full_path")" echo -e "${CYAN}VT Scanning${NC}: $name" - "$vt_helper" scan-skill "$scan_dir" --quiet 2>/dev/null || { + local scan_target="$full_path" + if [[ -d "${full_path%.*}" ]]; then + scan_target="${full_path%.*}" + fi + "$vt_helper" scan-skill "$scan_target" --quiet 2>/dev/null || { vt_issues=$((vt_issues + 1)) echo -e " ${YELLOW}VT flagged issues${NC} for $name" } diff --git a/.agents/scripts/virustotal-helper.sh b/.agents/scripts/virustotal-helper.sh index 3f327486bc..9508e230dc 100755 --- a/.agents/scripts/virustotal-helper.sh +++ b/.agents/scripts/virustotal-helper.sh @@ -38,25 +38,25 @@ readonly NC='\033[0m' log_info() { local msg="$1" - echo -e "${BLUE}[virustotal]${NC} ${msg}" + echo -e "${BLUE}[virustotal]${NC} ${msg}" >&2 return 0 } log_success() { local msg="$1" - echo -e "${GREEN}[OK]${NC} ${msg}" + echo -e "${GREEN}[OK]${NC} ${msg}" >&2 return 0 } log_warning() { local msg="$1" - echo -e "${YELLOW}[WARN]${NC} ${msg}" + echo -e "${YELLOW}[WARN]${NC} ${msg}" >&2 return 0 } log_error() { local msg="$1" - echo -e "${RED}[ERROR]${NC} ${msg}" + echo -e "${RED}[ERROR]${NC} ${msg}" >&2 return 0 } @@ -173,12 +173,16 @@ vt_request() { } # Check for API errors + # Exit codes: 0=success, 1=general error, 2=not found (allows callers to distinguish) local error_code="" error_code=$(echo "$response" | jq -r '.error.code // empty' 2>/dev/null || echo "") if [[ -n "$error_code" ]]; then local error_msg="" error_msg=$(echo "$response" | jq -r '.error.message // "Unknown error"' 2>/dev/null || echo "Unknown error") log_error "VT API error: ${error_code} - ${error_msg}" + if [[ "$error_code" == "NotFoundError" ]]; then + return 2 + fi return 1 fi @@ -253,8 +257,11 @@ cmd_scan_file() { fi local response="" - response=$(vt_request "GET" "/files/${sha256}") || { - # File not in VT database -- this is normal for text/markdown files + local vt_exit=0 + response=$(vt_request "GET" "/files/${sha256}") || vt_exit=$? + + if [[ $vt_exit -eq 2 ]]; then + # NotFoundError: file not in VT database -- normal for text/markdown files if [[ "$quiet" == "true" ]]; then echo "UNKNOWN" else @@ -263,7 +270,15 @@ cmd_scan_file() { fi echo "UNKNOWN|File not in VT database" return 0 - } + elif [[ $vt_exit -ne 0 ]]; then + # Real API error (quota exceeded, network failure, etc.) + if [[ "$quiet" == "true" ]]; then + echo "UNKNOWN" + else + log_error "API request failed for file: ${file}" + fi + return 1 + fi if [[ "$output_json" == "true" ]]; then echo "$response" @@ -502,15 +517,21 @@ cmd_scan_skill() { sha256=$(file_sha256 "$file") || continue local response="" - response=$(vt_request "GET" "/files/${sha256}" 2>/dev/null) || { - # Not in VT database -- normal for text files + local vt_exit=0 + response=$(vt_request "GET" "/files/${sha256}" 2>/dev/null) || vt_exit=$? + request_count=$((request_count + 1)) + + if [[ $vt_exit -ne 0 ]]; then if [[ "$quiet" != "true" ]]; then - echo -e " ${BLUE}SKIP${NC} ${basename_file} (not in VT database)" + if [[ $vt_exit -eq 2 ]]; then + echo -e " ${BLUE}SKIP${NC} ${basename_file} (not in VT database)" + else + echo -e " ${YELLOW}SKIP${NC} ${basename_file} (API error)" + fi fi unknown_count=$((unknown_count + 1)) continue - } - request_count=$((request_count + 1)) + fi local verdict="" verdict=$(parse_verdict "$response") @@ -551,10 +572,9 @@ cmd_scan_skill() { done # Phase 2: Extract and scan URLs from skill content + local -A domains_seen=() for file in "${files[@]}"; do - # Extract URLs (http/https) from file content while IFS= read -r url; do - # Skip common safe domains case "$url" in *github.com*|*githubusercontent.com*|*npmjs.com*|*pypi.org*|*docs.virustotal.com*) continue @@ -562,17 +582,10 @@ cmd_scan_skill() { esac urls_found+=("$url") - # Extract domain local domain="" domain=$(echo "$url" | sed -E 's|https?://([^/]+).*|\1|') - local already_found=false - for d in "${domains_found[@]+"${domains_found[@]}"}"; do - if [[ "$d" == "$domain" ]]; then - already_found=true - break - fi - done - if [[ "$already_found" == "false" ]]; then + if [[ -z "${domains_seen[$domain]+x}" ]]; then + domains_seen[$domain]=1 domains_found+=("$domain") fi done < <(grep -oE 'https?://[^ "'"'"'<>]+' "$file" 2>/dev/null | sort -u || true) @@ -594,13 +607,16 @@ cmd_scan_skill() { rate_limit_wait local response="" - response=$(vt_request "GET" "/domains/${domain}" 2>/dev/null) || { + local vt_exit=0 + response=$(vt_request "GET" "/domains/${domain}" 2>/dev/null) || vt_exit=$? + request_count=$((request_count + 1)) + + if [[ $vt_exit -ne 0 ]]; then if [[ "$quiet" != "true" ]]; then echo -e " ${BLUE}SKIP${NC} ${domain} (lookup failed)" fi continue - } - request_count=$((request_count + 1)) + fi local verdict="" verdict=$(parse_verdict "$response") @@ -748,6 +764,14 @@ main() { local command="${1:-help}" shift || true + # Verify required dependencies (jq is needed for all scan commands) + if [[ "$command" != "help" && "$command" != "--help" && "$command" != "-h" ]]; then + if ! command -v jq &>/dev/null; then + log_error "jq is required but not installed (brew install jq)" + return 1 + fi + fi + # Parse global flags local output_json=false local quiet=false diff --git a/TODO.md b/TODO.md index a9ca0f20bf..548bac0dce 100644 --- a/TODO.md +++ b/TODO.md @@ -66,8 +66,7 @@ Tasks with no open blockers - ready to work on. Use `/ready` to refresh this lis - [ ] t147 Retroactive triage: 50 unresolved review threads across 11 merged PRs #quality #review ~4h (ai:3h test:30m read:30m) logged:2026-02-07 ref:GH#438 - [x] t147.1 Triage PR #435 (4 threads, 2 high/critical) - fix $SUPERVISOR_DB bug ~30m blocked-by:none completed:2026-02-07 - [x] t147.2 Triage PR #392 (6 threads, 4 high/critical) - stderr suppression ~45m blocked-by:none completed:2026-02-07 - - [ ] t147.3 Triage PR #410 (9 threads, 3 high/critical) - VirusTotal error handling ~45m blocked-by:none - - Notes: BLOCKED by supervisor: Max retries exceeded: backend_infrastructure_error + - [ ] t147.3 Triage PR #410 (9 threads, 3 high/critical) - VirusTotal error handling ~45m blocked-by:none started:2026-02-07 - [x] t147.4 Triage PR #391 (1 thread, 1 critical) - schema-validator set -e ~15m blocked-by:none completed:2026-02-07 - [ ] t147.5 Triage PR #406 (3 threads, 1 high) - sed -i portability, attribution ~30m blocked-by:none - [ ] t147.6 Triage PR #403 (12 threads, 1 high) - voice AI unimplemented commands ~30m blocked-by:none @@ -139,7 +138,7 @@ Tasks with no open blockers - ready to work on. Use `/ready` to refresh this lis - [x] t135.7.2 Replace with array-based command construction ~2h blocked-by:t135.7.1 completed:2026-02-07 - [x] t135.7.3 Test affected command paths ~30m blocked-by:t135.7.2 completed:2026-02-07 - [ ] t135.8 P2-B: Increase shared-constants.sh adoption from 17% (29/170) to 80%+ ~4h blocked-by:none - - Notes: BLOCKED by supervisor: Re-prompt dispatch failed: backend_infrastructure_error - [ ] t135.8.1 Audit shared-constants.sh vs what scripts duplicate ~30m + - Notes: BLOCKED by supervisor: Re-prompt dispatch failed: backend_infrastructure_error - [ ] t135.8.1 Audit shared-constants.sh vs what scripts duplicate ~30m BLOCKED: Max retries exceeded: backend_infrastructure_error - [ ] t135.8.2 Create migration script to replace inline print_* with source shared-constants.sh ~1.5h blocked-by:t135.8.1 - [ ] t135.8.3 Run migration in batches, testing each for regressions ~2h blocked-by:t135.8.2 - [x] t135.9 P2-C: Add trap cleanup for temp files in setup.sh and mktemp scripts ~1h blocked-by:none started:2026-02-07 completed:2026-02-07