diff --git a/.agents/scripts/add-skill-helper.sh b/.agents/scripts/add-skill-helper.sh index dc6e6c357..a5e64c600 100755 --- a/.agents/scripts/add-skill-helper.sh +++ b/.agents/scripts/add-skill-helper.sh @@ -584,6 +584,43 @@ scan_skill_security() { return 0 } +# Run VirusTotal scan on skill files and referenced domains +# Returns: 0 = safe or VT not configured, 1 = threats detected +scan_skill_virustotal() { + local scan_path="$1" + local skill_name="$2" + local skip_security="${3:-false}" + + if [[ "$skip_security" == true ]]; then + return 0 + fi + + # Check if virustotal-helper.sh is available + local vt_helper="" + vt_helper="$(dirname "$0")/virustotal-helper.sh" + if [[ ! -x "$vt_helper" ]]; then + return 0 + fi + + # Check if VT API key is configured (don't fail if not) + if ! "$vt_helper" status 2>/dev/null | grep -q "API key configured"; then + log_info "VirusTotal: API key not configured (skipping VT scan)" + return 0 + fi + + log_info "Running VirusTotal scan on '$skill_name'..." + + 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" + # VT findings are advisory, not blocking (Cisco scanner is the gate) + return 0 + fi + + log_success "VirusTotal: No threats detected" + return 0 +} + # Log a single skill scan result to SKILL-SCAN-RESULTS.md # Args: skill_name action critical_count high_count medium_count max_severity log_skill_scan_result() { @@ -910,6 +947,9 @@ cmd_add() { return 1 fi + # VirusTotal scan (advisory, non-blocking -- runs after Cisco scanner gate) + scan_skill_virustotal "$skill_source_dir" "$skill_name" "$skip_security" + # Get commit hash for tracking local commit_hash="" if [[ -d "$TEMP_DIR/repo/.git" ]]; then @@ -1081,6 +1121,9 @@ EOF return 1 fi + # VirusTotal scan (advisory, non-blocking) + scan_skill_virustotal "$fetch_dir" "$skill_name" "$skip_security" + # Register in skill-sources.json local upstream_url="https://clawdhub.com/${owner_handle}/${slug}" register_skill "$skill_name" "$upstream_url" ".agents/${target_path}.md" "clawdhub" "$version" "added" "ClawdHub v${version} by @${owner_handle}" diff --git a/.agents/scripts/security-helper.sh b/.agents/scripts/security-helper.sh index b8b46fa18..3f4db68df 100755 --- a/.agents/scripts/security-helper.sh +++ b/.agents/scripts/security-helper.sh @@ -42,6 +42,7 @@ Commands: history [commits|range] Scan git history for vulnerabilities scan-deps [path] Scan dependencies for known vulnerabilities (OSV) skill-scan [name|all] Scan imported skills for threats (Cisco Skill Scanner) + vt-scan Scan file/URL/domain/skill via VirusTotal API ferret [path] Scan AI CLI configurations (Ferret) report [format] Generate comprehensive security report Formats: text (default), json, sarif @@ -55,6 +56,7 @@ Examples: $(basename "$0") scan-deps # Scan dependencies $(basename "$0") skill-scan # Scan all imported skills $(basename "$0") skill-scan cloudflare # Scan specific skill + $(basename "$0") vt-scan skill .agents/ # VirusTotal scan on skills $(basename "$0") ferret # Scan AI CLI configs $(basename "$0") report --format=sarif # Generate SARIF report EOF @@ -124,6 +126,20 @@ cmd_status() { print_status "Secretlint" "false" fi + # VirusTotal + local vt_helper="${SCRIPT_DIR}/virustotal-helper.sh" + if [[ -x "$vt_helper" ]]; then + local vt_output="" + vt_output=$("$vt_helper" status 2>/dev/null || true) + if echo "$vt_output" | grep -q "API key configured"; then + print_status "VirusTotal" "true" "API key configured" + else + print_status "VirusTotal" "false" "(helper installed, API key missing)" + fi + else + print_status "VirusTotal" "false" + fi + # Snyk (optional) if check_command snyk; then local snyk_version @@ -556,6 +572,43 @@ cmd_skill_scan() { return 1 fi + # Advisory: Run VirusTotal scan if available + local vt_helper="${SCRIPT_DIR}/virustotal-helper.sh" + if [[ -x "$vt_helper" ]] && "$vt_helper" status 2>/dev/null | grep -q "API key configured"; then + echo "" + echo -e "${CYAN}Running advisory VirusTotal scan...${NC}" + echo -e "${YELLOW}(VT scans are advisory only - Cisco scanner is the security gate)${NC}" + echo "" + + local vt_issues=0 + while IFS= read -r skill_json; do + local name local_path + name=$(echo "$skill_json" | jq -r '.name') + local_path=$(echo "$skill_json" | jq -r '.local_path') + local full_path="${agents_dir}/${local_path#.agents/}" + + if [[ ! -f "$full_path" ]]; then + 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 || { + vt_issues=$((vt_issues + 1)) + echo -e " ${YELLOW}VT flagged issues${NC} for $name" + } + done < <(jq -c '.skills[]' "$skill_sources" 2>/dev/null) + + if [[ $vt_issues -gt 0 ]]; then + echo "" + echo -e "${YELLOW}VirusTotal flagged ${vt_issues} skill(s) - review recommended${NC}" + else + echo "" + echo -e "${GREEN}VirusTotal: No threats detected${NC}" + fi + fi + echo "" echo -e "${GREEN}All imported skills passed security scan.${NC}" return 0 @@ -582,6 +635,68 @@ cmd_skill_scan() { fi } +cmd_vt_scan() { + local target="${1:-}" + shift || true + + print_header + + local vt_helper="${SCRIPT_DIR}/virustotal-helper.sh" + if [[ ! -x "$vt_helper" ]]; then + echo -e "${RED}VirusTotal helper not found: ${vt_helper}${NC}" + echo "Expected at: .agents/scripts/virustotal-helper.sh" + return 1 + fi + + if [[ -z "$target" ]]; then + echo -e "${RED}Target required.${NC}" + echo "" + echo "Usage: $(basename "$0") vt-scan [target]" + echo "" + echo "Types:" + echo " file Scan a file by SHA256 hash lookup" + echo " url Scan a URL for threats" + echo " domain Check domain reputation" + echo " skill Scan all files in a skill directory" + echo " status Check VT API key and quota" + return 1 + fi + + # Delegate to virustotal-helper.sh + case "$target" in + file|scan-file) + "$vt_helper" scan-file "$@" + ;; + url|scan-url) + "$vt_helper" scan-url "$@" + ;; + domain|scan-domain) + "$vt_helper" scan-domain "$@" + ;; + skill|scan-skill) + "$vt_helper" scan-skill "$@" + ;; + status) + "$vt_helper" status + ;; + *) + # Treat as a path -- auto-detect file vs directory + if [[ -d "$target" ]]; then + "$vt_helper" scan-skill "$target" "$@" + elif [[ -f "$target" ]]; then + "$vt_helper" scan-file "$target" "$@" + elif [[ "$target" =~ ^https?:// ]]; then + "$vt_helper" scan-url "$target" "$@" + else + # Assume domain + "$vt_helper" scan-domain "$target" "$@" + fi + ;; + esac + + return $? +} + cmd_ferret() { local path="${1:-.}" shift || true @@ -826,6 +941,9 @@ main() { skill-scan|skills) cmd_skill_scan "$@" ;; + vt-scan|virustotal) + cmd_vt_scan "$@" + ;; ferret|ai-config) cmd_ferret "$@" ;; diff --git a/.agents/scripts/virustotal-helper.sh b/.agents/scripts/virustotal-helper.sh new file mode 100755 index 000000000..3f327486b --- /dev/null +++ b/.agents/scripts/virustotal-helper.sh @@ -0,0 +1,813 @@ +#!/usr/bin/env bash +# virustotal-helper.sh - VirusTotal API v3 integration for skill security scanning +# Scans skill files, URLs, and domains against VirusTotal's 70+ AV engines +# +# Usage: +# virustotal-helper.sh scan-file Scan a file by SHA256 hash lookup +# virustotal-helper.sh scan-url Scan a URL for threats +# virustotal-helper.sh scan-domain Check domain reputation +# virustotal-helper.sh scan-skill Scan all files in a skill directory +# virustotal-helper.sh status Check API key and quota +# virustotal-helper.sh help Show this help +# +# Environment: +# VIRUSTOTAL_API_KEY - API key (loaded from credentials.sh or gopass) +# +# Rate limits (free tier): 4 requests/minute, 500 requests/day, 15.5K requests/month +set -euo pipefail + +# shellcheck source=/dev/null +[[ -f "${HOME}/.config/aidevops/credentials.sh" ]] && source "${HOME}/.config/aidevops/credentials.sh" + +# Constants +readonly VT_API_BASE="https://www.virustotal.com/api/v3" +readonly VERSION="1.0.0" +readonly RATE_LIMIT_DELAY=16 # 4 req/min = 1 every 15s, add 1s buffer + +# Colors +readonly RED='\033[0;31m' +readonly GREEN='\033[0;32m' +readonly YELLOW='\033[1;33m' +readonly BLUE='\033[0;34m' +readonly CYAN='\033[0;36m' +readonly NC='\033[0m' + +# ============================================================================= +# Helper Functions +# ============================================================================= + +log_info() { + local msg="$1" + echo -e "${BLUE}[virustotal]${NC} ${msg}" + return 0 +} + +log_success() { + local msg="$1" + echo -e "${GREEN}[OK]${NC} ${msg}" + return 0 +} + +log_warning() { + local msg="$1" + echo -e "${YELLOW}[WARN]${NC} ${msg}" + return 0 +} + +log_error() { + local msg="$1" + echo -e "${RED}[ERROR]${NC} ${msg}" + return 0 +} + +print_usage() { + cat << 'EOF' +VirusTotal Helper - Skill Security Scanning via VT API v3 + +USAGE: + virustotal-helper.sh [options] + +COMMANDS: + scan-file Scan a file by SHA256 hash lookup + scan-url Scan a URL for threats + scan-domain Check domain reputation + scan-skill Scan all files in a skill directory + status Check API key configuration and quota + help Show this help message + +OPTIONS: + --json Output raw JSON response + --quiet Only output verdict (SAFE/MALICIOUS/SUSPICIOUS/UNKNOWN) + +ENVIRONMENT: + VIRUSTOTAL_API_KEY API key (from credentials.sh or gopass) + +RATE LIMITS (free tier): + 4 requests/minute, 500 requests/day, 15.5K requests/month + +EXAMPLES: + virustotal-helper.sh scan-file .agents/tools/browser/playwright-skill.md + virustotal-helper.sh scan-url https://example.com/skill.md + virustotal-helper.sh scan-domain github.com + virustotal-helper.sh scan-skill .agents/tools/browser/playwright-skill/ + virustotal-helper.sh status +EOF + return 0 +} + +# Resolve VT API key from gopass or credentials.sh +resolve_api_key() { + # Already set in environment (from credentials.sh source or export) + if [[ -n "${VIRUSTOTAL_API_KEY:-}" ]]; then + echo "${VIRUSTOTAL_API_KEY}" + return 0 + fi + + # Try gopass (encrypted storage) + if command -v gopass &>/dev/null; then + local key="" + # Try user-specific key first, then generic + key=$(gopass show -o "aidevops/VIRUSTOTAL_MARCUSQUINN" 2>/dev/null || true) + if [[ -z "$key" ]]; then + key=$(gopass show -o "aidevops/VIRUSTOTAL_API_KEY" 2>/dev/null || true) + fi + if [[ -n "$key" ]]; then + echo "$key" + return 0 + fi + fi + + # Try aidevops secret helper + local script_dir="" + script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" || return 1 + if [[ -x "${script_dir}/secret-helper.sh" ]]; then + local key="" + key=$("${script_dir}/secret-helper.sh" get VIRUSTOTAL_MARCUSQUINN 2>/dev/null || true) + if [[ -z "$key" ]]; then + key=$("${script_dir}/secret-helper.sh" get VIRUSTOTAL_API_KEY 2>/dev/null || true) + fi + if [[ -n "$key" ]]; then + echo "$key" + return 0 + fi + fi + + return 1 +} + +# Make authenticated VT API request +# Args: method endpoint [data] +vt_request() { + local method="$1" + local endpoint="$2" + local data="${3:-}" + + local api_key="" + api_key=$(resolve_api_key) || { + log_error "VirusTotal API key not found" + log_info "Set with: aidevops secret set VIRUSTOTAL_MARCUSQUINN" + log_info "Or export VIRUSTOTAL_API_KEY in credentials.sh" + return 1 + } + + local curl_args=( + -s + --connect-timeout 10 + --max-time 30 + -H "x-apikey: ${api_key}" + -H "Accept: application/json" + ) + + if [[ "$method" == "POST" ]]; then + if [[ -n "$data" ]]; then + curl_args+=(-X POST -d "$data") + else + curl_args+=(-X POST) + fi + fi + + local response="" + response=$(curl "${curl_args[@]}" "${VT_API_BASE}${endpoint}" 2>/dev/null) || { + log_error "API request failed: ${endpoint}" + return 1 + } + + # Check for API errors + 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}" + return 1 + fi + + echo "$response" + return 0 +} + +# Compute SHA256 hash of a file +file_sha256() { + local file="$1" + if command -v shasum &>/dev/null; then + shasum -a 256 "$file" | awk '{print $1}' + elif command -v sha256sum &>/dev/null; then + sha256sum "$file" | awk '{print $1}' + else + log_error "No SHA256 tool found (need shasum or sha256sum)" + return 1 + fi + return 0 +} + +# Parse VT analysis stats into a verdict +# Args: json_response +parse_verdict() { + local response="$1" + + local malicious harmless suspicious undetected timeout + malicious=$(echo "$response" | jq -r '.data.attributes.last_analysis_stats.malicious // 0' 2>/dev/null || echo "0") + harmless=$(echo "$response" | jq -r '.data.attributes.last_analysis_stats.harmless // 0' 2>/dev/null || echo "0") + suspicious=$(echo "$response" | jq -r '.data.attributes.last_analysis_stats.suspicious // 0' 2>/dev/null || echo "0") + undetected=$(echo "$response" | jq -r '.data.attributes.last_analysis_stats.undetected // 0' 2>/dev/null || echo "0") + timeout=$(echo "$response" | jq -r '.data.attributes.last_analysis_stats.timeout // 0' 2>/dev/null || echo "0") + + local total=$((malicious + harmless + suspicious + undetected + timeout)) + + if [[ "$malicious" -gt 0 ]]; then + echo "MALICIOUS|${malicious}/${total} engines detected threats" + elif [[ "$suspicious" -gt 0 ]]; then + echo "SUSPICIOUS|${suspicious}/${total} engines flagged as suspicious" + else + echo "SAFE|${harmless}/${total} engines found no threats" + fi + return 0 +} + +# Rate limit: sleep between requests to stay within free tier +rate_limit_wait() { + sleep "$RATE_LIMIT_DELAY" + return 0 +} + +# ============================================================================= +# Commands +# ============================================================================= + +cmd_scan_file() { + local file="$1" + local output_json="${2:-false}" + local quiet="${3:-false}" + + if [[ ! -f "$file" ]]; then + log_error "File not found: ${file}" + return 1 + fi + + local sha256="" + sha256=$(file_sha256 "$file") || return 1 + + if [[ "$quiet" != "true" ]]; then + log_info "Scanning file: ${file}" + log_info "SHA256: ${sha256}" + fi + + local response="" + response=$(vt_request "GET" "/files/${sha256}") || { + # File not in VT database -- this is normal for text/markdown files + if [[ "$quiet" == "true" ]]; then + echo "UNKNOWN" + else + log_info "File not found in VirusTotal database (never submitted)" + log_info "This is normal for text/markdown skill files" + fi + echo "UNKNOWN|File not in VT database" + return 0 + } + + if [[ "$output_json" == "true" ]]; then + echo "$response" + return 0 + fi + + local verdict="" + verdict=$(parse_verdict "$response") + local status="${verdict%%|*}" + local detail="${verdict#*|}" + + if [[ "$quiet" == "true" ]]; then + echo "$status" + return 0 + fi + + case "$status" in + MALICIOUS) + echo -e "${RED}MALICIOUS${NC}: ${detail}" + # Show which engines detected it + echo "$response" | jq -r '.data.attributes.last_analysis_results | to_entries[] | select(.value.category == "malicious") | " [\(.key)] \(.value.result)"' 2>/dev/null || true + ;; + SUSPICIOUS) + echo -e "${YELLOW}SUSPICIOUS${NC}: ${detail}" + echo "$response" | jq -r '.data.attributes.last_analysis_results | to_entries[] | select(.value.category == "suspicious") | " [\(.key)] \(.value.result)"' 2>/dev/null || true + ;; + SAFE) + echo -e "${GREEN}SAFE${NC}: ${detail}" + ;; + *) + echo -e "${BLUE}UNKNOWN${NC}: ${detail}" + ;; + esac + + return 0 +} + +cmd_scan_url() { + local url="$1" + local output_json="${2:-false}" + local quiet="${3:-false}" + + if [[ -z "$url" ]]; then + log_error "URL required" + return 1 + fi + + if [[ "$quiet" != "true" ]]; then + log_info "Scanning URL: ${url}" + fi + + # VT URL ID is base64url-encoded URL (without padding) + local url_id="" + url_id=$(printf '%s' "$url" | base64 | tr '+/' '-_' | tr -d '=') + + # Try to get existing report first + local response="" + response=$(vt_request "GET" "/urls/${url_id}" 2>/dev/null) || { + # No existing report, submit for scanning + if [[ "$quiet" != "true" ]]; then + log_info "No existing report, submitting URL for scanning..." + fi + local submit_response="" + submit_response=$(vt_request "POST" "/urls" "url=${url}") || return 1 + + # Get analysis ID and poll for results + local analysis_id="" + analysis_id=$(echo "$submit_response" | jq -r '.data.id // empty' 2>/dev/null || echo "") + if [[ -z "$analysis_id" ]]; then + log_error "Failed to submit URL for scanning" + return 1 + fi + + if [[ "$quiet" != "true" ]]; then + log_info "Analysis submitted, waiting for results..." + fi + + # Wait and retry (VT typically processes URLs in a few seconds) + rate_limit_wait + response=$(vt_request "GET" "/urls/${url_id}") || { + echo "UNKNOWN|URL submitted but results not yet available" + return 0 + } + } + + if [[ "$output_json" == "true" ]]; then + echo "$response" + return 0 + fi + + local verdict="" + verdict=$(parse_verdict "$response") + local status="${verdict%%|*}" + local detail="${verdict#*|}" + + if [[ "$quiet" == "true" ]]; then + echo "$status" + return 0 + fi + + case "$status" in + MALICIOUS) + echo -e "${RED}MALICIOUS${NC}: ${detail}" + echo "$response" | jq -r '.data.attributes.last_analysis_results | to_entries[] | select(.value.category == "malicious") | " [\(.key)] \(.value.result)"' 2>/dev/null || true + ;; + SUSPICIOUS) + echo -e "${YELLOW}SUSPICIOUS${NC}: ${detail}" + ;; + SAFE) + echo -e "${GREEN}SAFE${NC}: ${detail}" + ;; + *) + echo -e "${BLUE}UNKNOWN${NC}: ${detail}" + ;; + esac + + return 0 +} + +cmd_scan_domain() { + local domain="$1" + local output_json="${2:-false}" + local quiet="${3:-false}" + + if [[ -z "$domain" ]]; then + log_error "Domain required" + return 1 + fi + + # Strip protocol and path if present + domain="${domain#https://}" + domain="${domain#http://}" + domain="${domain%%/*}" + + if [[ "$quiet" != "true" ]]; then + log_info "Checking domain reputation: ${domain}" + fi + + local response="" + response=$(vt_request "GET" "/domains/${domain}") || return 1 + + if [[ "$output_json" == "true" ]]; then + echo "$response" + return 0 + fi + + local verdict="" + verdict=$(parse_verdict "$response") + local status="${verdict%%|*}" + local detail="${verdict#*|}" + + # Also extract reputation score + local reputation="" + reputation=$(echo "$response" | jq -r '.data.attributes.reputation // "N/A"' 2>/dev/null || echo "N/A") + + if [[ "$quiet" == "true" ]]; then + echo "$status" + return 0 + fi + + case "$status" in + MALICIOUS) + echo -e "${RED}MALICIOUS${NC}: ${detail} (reputation: ${reputation})" + ;; + SUSPICIOUS) + echo -e "${YELLOW}SUSPICIOUS${NC}: ${detail} (reputation: ${reputation})" + ;; + SAFE) + echo -e "${GREEN}SAFE${NC}: ${detail} (reputation: ${reputation})" + ;; + *) + echo -e "${BLUE}UNKNOWN${NC}: ${detail} (reputation: ${reputation})" + ;; + esac + + return 0 +} + +# Scan all files in a skill directory +# Hashes each file, checks URLs/domains found in content +cmd_scan_skill() { + local skill_path="$1" + local output_json="${2:-false}" + local quiet="${3:-false}" + + if [[ ! -e "$skill_path" ]]; then + log_error "Path not found: ${skill_path}" + return 1 + fi + + # Determine files to scan + local files=() + if [[ -f "$skill_path" ]]; then + files=("$skill_path") + elif [[ -d "$skill_path" ]]; then + while IFS= read -r -d '' f; do + files+=("$f") + done < <(find "$skill_path" -type f \( -name "*.md" -o -name "*.sh" -o -name "*.py" -o -name "*.js" -o -name "*.ts" -o -name "*.yaml" -o -name "*.yml" -o -name "*.json" \) -print0 2>/dev/null) + fi + + if [[ ${#files[@]} -eq 0 ]]; then + log_warning "No scannable files found in: ${skill_path}" + return 0 + fi + + local skill_name="" + skill_name=$(basename "$skill_path" | sed 's/-skill$//' | sed 's/\.md$//') + + if [[ "$quiet" != "true" ]]; then + log_info "Scanning skill '${skill_name}': ${#files[@]} file(s)" + echo "" + fi + + local total_files=${#files[@]} + local malicious_count=0 + local suspicious_count=0 + local safe_count=0 + local unknown_count=0 + local urls_found=() + local domains_found=() + local request_count=0 + local max_requests=8 # Limit per skill scan to avoid rate limiting + + # Phase 1: Hash-based file scanning + for file in "${files[@]}"; do + if [[ $request_count -ge $max_requests ]]; then + log_warning "Rate limit reached (${max_requests} requests), skipping remaining file hashes" + unknown_count=$((unknown_count + 1)) + continue + fi + + local basename_file="" + basename_file=$(basename "$file") + + local sha256="" + sha256=$(file_sha256 "$file") || continue + + local response="" + response=$(vt_request "GET" "/files/${sha256}" 2>/dev/null) || { + # Not in VT database -- normal for text files + if [[ "$quiet" != "true" ]]; then + echo -e " ${BLUE}SKIP${NC} ${basename_file} (not in VT database)" + fi + unknown_count=$((unknown_count + 1)) + continue + } + request_count=$((request_count + 1)) + + local verdict="" + verdict=$(parse_verdict "$response") + local status="${verdict%%|*}" + local detail="${verdict#*|}" + + case "$status" in + MALICIOUS) + malicious_count=$((malicious_count + 1)) + if [[ "$quiet" != "true" ]]; then + echo -e " ${RED}MALICIOUS${NC} ${basename_file}: ${detail}" + fi + ;; + SUSPICIOUS) + suspicious_count=$((suspicious_count + 1)) + if [[ "$quiet" != "true" ]]; then + echo -e " ${YELLOW}SUSPICIOUS${NC} ${basename_file}: ${detail}" + fi + ;; + SAFE) + safe_count=$((safe_count + 1)) + if [[ "$quiet" != "true" ]]; then + echo -e " ${GREEN}SAFE${NC} ${basename_file}: ${detail}" + fi + ;; + *) + unknown_count=$((unknown_count + 1)) + if [[ "$quiet" != "true" ]]; then + echo -e " ${BLUE}UNKNOWN${NC} ${basename_file}: ${detail}" + fi + ;; + esac + + # Rate limit between requests + if [[ $request_count -lt $max_requests ]]; then + rate_limit_wait + fi + done + + # Phase 2: Extract and scan URLs from skill content + 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 + ;; + 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 + domains_found+=("$domain") + fi + done < <(grep -oE 'https?://[^ "'"'"'<>]+' "$file" 2>/dev/null | sort -u || true) + done + + # Scan unique domains (up to rate limit) + if [[ ${#domains_found[@]} -gt 0 && $request_count -lt $max_requests ]]; then + if [[ "$quiet" != "true" ]]; then + echo "" + log_info "Checking ${#domains_found[@]} domain(s) referenced in skill..." + fi + + for domain in "${domains_found[@]}"; do + if [[ $request_count -ge $max_requests ]]; then + log_warning "Rate limit reached, skipping remaining domains" + break + fi + + rate_limit_wait + + local response="" + response=$(vt_request "GET" "/domains/${domain}" 2>/dev/null) || { + if [[ "$quiet" != "true" ]]; then + echo -e " ${BLUE}SKIP${NC} ${domain} (lookup failed)" + fi + continue + } + request_count=$((request_count + 1)) + + local verdict="" + verdict=$(parse_verdict "$response") + local status="${verdict%%|*}" + local detail="${verdict#*|}" + + case "$status" in + MALICIOUS) + malicious_count=$((malicious_count + 1)) + if [[ "$quiet" != "true" ]]; then + echo -e " ${RED}MALICIOUS${NC} ${domain}: ${detail}" + fi + ;; + SUSPICIOUS) + suspicious_count=$((suspicious_count + 1)) + if [[ "$quiet" != "true" ]]; then + echo -e " ${YELLOW}SUSPICIOUS${NC} ${domain}: ${detail}" + fi + ;; + SAFE) + if [[ "$quiet" != "true" ]]; then + echo -e " ${GREEN}SAFE${NC} ${domain}: ${detail}" + fi + ;; + *) + if [[ "$quiet" != "true" ]]; then + echo -e " ${BLUE}UNKNOWN${NC} ${domain}: ${detail}" + fi + ;; + esac + done + fi + + # Summary + if [[ "$quiet" != "true" ]]; then + echo "" + echo "═══════════════════════════════════════" + echo -e "Skill: ${skill_name}" + echo -e "Files scanned: ${total_files}" + echo -e "Domains checked: ${#domains_found[@]}" + echo -e "VT API requests: ${request_count}" + if [[ $malicious_count -gt 0 ]]; then + echo -e "Result: ${RED}MALICIOUS (${malicious_count} threat(s))${NC}" + elif [[ $suspicious_count -gt 0 ]]; then + echo -e "Result: ${YELLOW}SUSPICIOUS (${suspicious_count} flag(s))${NC}" + else + echo -e "Result: ${GREEN}SAFE${NC}" + fi + echo "═══════════════════════════════════════" + fi + + # Output JSON summary if requested + if [[ "$output_json" == "true" ]]; then + cat << ENDJSON +{ + "skill": "${skill_name}", + "files_scanned": ${total_files}, + "domains_checked": ${#domains_found[@]}, + "malicious": ${malicious_count}, + "suspicious": ${suspicious_count}, + "safe": ${safe_count}, + "unknown": ${unknown_count}, + "verdict": "$(if [[ $malicious_count -gt 0 ]]; then echo "MALICIOUS"; elif [[ $suspicious_count -gt 0 ]]; then echo "SUSPICIOUS"; else echo "SAFE"; fi)" +} +ENDJSON + fi + + # Return non-zero if threats found + if [[ $malicious_count -gt 0 ]]; then + return 1 + fi + return 0 +} + +cmd_status() { + echo -e "${CYAN}" + echo "╔═══════════════════════════════════════════════════════════╗" + echo "║ VirusTotal Helper v${VERSION} ║" + echo "║ Skill security scanning via VT API v3 ║" + echo "╚═══════════════════════════════════════════════════════════╝" + echo -e "${NC}" + + # Check API key + local api_key="" + api_key=$(resolve_api_key 2>/dev/null) || true + + if [[ -n "$api_key" ]]; then + # Mask key for display + local masked="${api_key:0:4}...${api_key: -4}" + echo -e " ${GREEN}✓${NC} API key configured (${masked})" + + # Check quota by making a lightweight request + local response="" + response=$(vt_request "GET" "/users/me" 2>/dev/null) || { + echo -e " ${YELLOW}○${NC} Could not verify API key (request failed)" + return 0 + } + + local username="" + username=$(echo "$response" | jq -r '.data.id // "unknown"' 2>/dev/null || echo "unknown") + local api_type="" + api_type=$(echo "$response" | jq -r '.data.attributes.privileges // {} | keys[0] // "public"' 2>/dev/null || echo "public") + + echo -e " ${GREEN}✓${NC} Account: ${username}" + echo -e " ${GREEN}✓${NC} API type: ${api_type}" + else + echo -e " ${RED}✗${NC} API key not configured" + echo "" + echo " Set with:" + echo " aidevops secret set VIRUSTOTAL_MARCUSQUINN" + echo " Or:" + echo " export VIRUSTOTAL_API_KEY= in ~/.config/aidevops/credentials.sh" + fi + + echo "" + + # Check dependencies + echo -e "${BLUE}Dependencies:${NC}" + if command -v curl &>/dev/null; then + echo -e " ${GREEN}✓${NC} curl" + else + echo -e " ${RED}✗${NC} curl (required)" + fi + + if command -v jq &>/dev/null; then + echo -e " ${GREEN}✓${NC} jq" + else + echo -e " ${RED}✗${NC} jq (required for JSON parsing)" + fi + + if command -v shasum &>/dev/null || command -v sha256sum &>/dev/null; then + echo -e " ${GREEN}✓${NC} SHA256 (shasum/sha256sum)" + else + echo -e " ${RED}✗${NC} SHA256 tool (need shasum or sha256sum)" + fi + + return 0 +} + +# ============================================================================= +# Main +# ============================================================================= + +main() { + local command="${1:-help}" + shift || true + + # Parse global flags + local output_json=false + local quiet=false + local args=() + + local opt + for opt in "$@"; do + case "$opt" in + --json) output_json=true ;; + --quiet|-q) quiet=true ;; + *) args+=("$opt") ;; + esac + done + + case "$command" in + scan-file|file) + if [[ ${#args[@]} -lt 1 ]]; then + log_error "File path required" + echo "Usage: virustotal-helper.sh scan-file " + return 1 + fi + cmd_scan_file "${args[0]}" "$output_json" "$quiet" + ;; + scan-url|url) + if [[ ${#args[@]} -lt 1 ]]; then + log_error "URL required" + echo "Usage: virustotal-helper.sh scan-url " + return 1 + fi + cmd_scan_url "${args[0]}" "$output_json" "$quiet" + ;; + scan-domain|domain) + if [[ ${#args[@]} -lt 1 ]]; then + log_error "Domain required" + echo "Usage: virustotal-helper.sh scan-domain " + return 1 + fi + cmd_scan_domain "${args[0]}" "$output_json" "$quiet" + ;; + scan-skill|skill) + if [[ ${#args[@]} -lt 1 ]]; then + log_error "Skill path required" + echo "Usage: virustotal-helper.sh scan-skill " + return 1 + fi + cmd_scan_skill "${args[0]}" "$output_json" "$quiet" + ;; + status) + cmd_status + ;; + help|--help|-h) + print_usage + ;; + *) + log_error "Unknown command: ${command}" + echo "" + print_usage + return 1 + ;; + esac +} + +main "$@" diff --git a/.agents/tools/code-review/security-analysis.md b/.agents/tools/code-review/security-analysis.md index 6691101f1..8e3424148 100644 --- a/.agents/tools/code-review/security-analysis.md +++ b/.agents/tools/code-review/security-analysis.md @@ -22,12 +22,12 @@ mcp: ## Quick Reference - **Helper**: `.agents/scripts/security-helper.sh` -- **Commands**: `analyze [scope]` | `scan-deps` | `history [commits]` | `ferret` | `report` +- **Commands**: `analyze [scope]` | `scan-deps` | `history [commits]` | `skill-scan` | `vt-scan` | `ferret` | `report` - **Scopes**: `diff` (default), `staged`, `branch`, `full` - **Output**: `.security-analysis/` directory with reports - **Severity**: critical > high > medium > low > info - **Benchmarks**: 90% precision, 93% recall (OpenSSF CVE Benchmark) -- **Integrations**: OSV-Scanner (deps), Secretlint (secrets), Ferret (AI configs), Snyk (optional) +- **Integrations**: OSV-Scanner (deps), Secretlint (secrets), Ferret (AI configs), VirusTotal (file/URL/domain), Snyk (optional) - **MCP**: `gemini-cli-security` tools: find_line_numbers, get_audit_scope, run_poc **Vulnerability Categories**: @@ -57,6 +57,8 @@ This tool provides comprehensive security scanning capabilities: | **Full Codebase** | Scan entire codebase | `security-helper.sh analyze full` | | **Git History** | Scan historical commits for vulnerabilities | `security-helper.sh history 100` | | **Dependency Scan** | Find vulnerable dependencies via OSV | `security-helper.sh scan-deps` | +| **Skill Scan** | Scan imported skills (Cisco + VT advisory) | `security-helper.sh skill-scan` | +| **VirusTotal Scan** | Scan file/URL/domain/skill via VT API | `security-helper.sh vt-scan` | | **AI Config Scan** | Scan AI CLI configs for threats (Ferret) | `security-helper.sh ferret` | ## Quick Start @@ -188,6 +190,59 @@ Detects hardcoded credentials: - **Improper Output Handling**: Unvalidated LLM output used unsafely - **Insecure Tool Use**: Overly permissive LLM tool access +## VirusTotal Scanning + +VirusTotal integration provides advisory threat intelligence by checking file hashes against 70+ AV engines and scanning domains/URLs referenced in skill content. + +**Helper**: `.agents/scripts/virustotal-helper.sh` + +**Role**: Advisory layer -- does not block imports. The Cisco Skill Scanner remains the security gate. + +### Usage + +```bash +# Check VT API status and quota +./.agents/scripts/security-helper.sh vt-scan status + +# Scan a skill directory +./.agents/scripts/security-helper.sh vt-scan skill .agents/tools/browser/ + +# Scan a specific file +./.agents/scripts/security-helper.sh vt-scan file .agents/tools/browser/playwright-skill.md + +# Scan a URL +./.agents/scripts/security-helper.sh vt-scan url https://example.com/payload + +# Scan a domain +./.agents/scripts/security-helper.sh vt-scan domain example.com + +# Auto-detect target type +./.agents/scripts/security-helper.sh vt-scan /path/to/anything +``` + +### How it works + +1. **File hash lookup**: Computes SHA256 of each file and queries VT's database (most text/markdown files won't be in VT -- this is normal) +2. **Domain/URL extraction**: Parses URLs from skill content and checks domain reputation +3. **Rate limiting**: 16s between requests (free tier: 4 req/min), max 8 requests per skill scan +4. **Verdicts**: SAFE, MALICIOUS, SUSPICIOUS, or UNKNOWN (not in database) + +### API key setup + +```bash +# Recommended: gopass encrypted storage +aidevops secret set VIRUSTOTAL_MARCUSQUINN + +# Alternative: credentials.sh (600 permissions) +echo 'export VIRUSTOTAL_API_KEY="your_key"' >> ~/.config/aidevops/credentials.sh +``` + +### Integration points + +- **`security-helper.sh skill-scan all`**: Runs VT as advisory after Cisco scanner +- **`add-skill-helper.sh`**: Runs VT advisory scan after import (GitHub + ClawdHub) +- **`security-helper.sh vt-scan`**: Standalone VT scanning for any target + ## AI CLI Configuration Scanning (Ferret) Ferret is a specialized security scanner for AI assistant configurations (Claude Code, Cursor, Windsurf, Continue, Aider, Cline). It detects prompt injection, jailbreaks, credential leaks, and backdoors with 65+ rules across 9 threat categories. @@ -470,18 +525,20 @@ const query = buildQuery(validatedInput); ## Comparison with Other Tools -| Feature | Security Analysis | Ferret | Snyk Code | SonarCloud | CodeQL | -|---------|------------------|--------|-----------|------------|--------| -| AI-Powered | Yes | No | Partial | No | No | -| Taint Analysis | Yes | No | Yes | Yes | Yes | -| Git History Scan | Yes | No | No | No | No | -| Full Codebase | Yes | Yes | Yes | Yes | Yes | -| Dependency Scan | Via OSV | No | Yes | Yes | No | -| LLM Safety | Yes | Yes | No | No | No | -| AI CLI Configs | Via Ferret | Yes | No | No | No | -| Prompt Injection | Yes | Yes | No | No | No | -| Local/Offline | Yes | Yes | No | No | Yes | -| MCP Integration | Yes | No | Yes | No | No | +| Feature | Security Analysis | Ferret | VirusTotal | Snyk Code | SonarCloud | CodeQL | +|---------|------------------|--------|------------|-----------|------------|--------| +| AI-Powered | Yes | No | No | Partial | No | No | +| Taint Analysis | Yes | No | No | Yes | Yes | Yes | +| Git History Scan | Yes | No | No | No | No | No | +| Full Codebase | Yes | Yes | No | Yes | Yes | Yes | +| Dependency Scan | Via OSV | No | No | Yes | Yes | No | +| File Hash Scan | No | No | Yes (70+ AV) | No | No | No | +| Domain/URL Scan | No | No | Yes | No | No | No | +| LLM Safety | Yes | Yes | No | No | No | No | +| AI CLI Configs | Via Ferret | Yes | No | No | No | No | +| Prompt Injection | Yes | Yes | No | No | No | No | +| Local/Offline | Yes | Yes | No | No | No | Yes | +| MCP Integration | Yes | No | No | Yes | No | No | ## Troubleshooting @@ -549,6 +606,8 @@ Run the helper script directly: ./.agents/scripts/security-helper.sh analyze full # Full codebase scan ./.agents/scripts/security-helper.sh history 50 # Scan last 50 commits ./.agents/scripts/security-helper.sh scan-deps # Dependency scan +./.agents/scripts/security-helper.sh skill-scan # Cisco + VT skill scan +./.agents/scripts/security-helper.sh vt-scan status # VirusTotal API status ./.agents/scripts/security-helper.sh ferret # AI CLI config scan ./.agents/scripts/security-helper.sh report # Generate report ``` @@ -558,10 +617,12 @@ Run the helper script directly: - **OSV-Scanner**: [https://github.com/google/osv-scanner](https://github.com/google/osv-scanner) - **OSV Database**: [https://osv.dev/](https://osv.dev/) - **Ferret Scan**: [https://github.com/fubak/ferret-scan](https://github.com/fubak/ferret-scan) +- **VirusTotal**: [https://www.virustotal.com/](https://www.virustotal.com/) +- **VirusTotal API v3**: [https://docs.virustotal.com/reference/overview](https://docs.virustotal.com/reference/overview) - **CWE Database**: [https://cwe.mitre.org/](https://cwe.mitre.org/) - **OWASP Top 10**: [https://owasp.org/Top10/](https://owasp.org/Top10/) - **Gemini CLI Security**: [https://github.com/gemini-cli-extensions/security](https://github.com/gemini-cli-extensions/security) --- -**Security Analysis provides comprehensive AI-powered vulnerability detection with support for code changes, full codebase scans, git history analysis, and AI CLI configuration security via Ferret.** +**Security Analysis provides comprehensive AI-powered vulnerability detection with support for code changes, full codebase scans, git history analysis, VirusTotal threat intelligence, and AI CLI configuration security via Ferret.** diff --git a/.agents/tools/code-review/skill-scanner.md b/.agents/tools/code-review/skill-scanner.md index 974cf1013..0336516d7 100644 --- a/.agents/tools/code-review/skill-scanner.md +++ b/.agents/tools/code-review/skill-scanner.md @@ -49,7 +49,7 @@ tools: | Behavioral (AST dataflow) | Free | ~150ms | None | | LLM-as-judge | API cost | ~2s | `SKILL_SCANNER_LLM_API_KEY` | | Meta-analyzer (FP filter) | API cost | ~1s | `SKILL_SCANNER_LLM_API_KEY` | -| VirusTotal | Free tier | ~1s | `VIRUSTOTAL_API_KEY` | +| VirusTotal | Free tier | ~16s/req | `VIRUSTOTAL_MARCUSQUINN` or `VIRUSTOTAL_API_KEY` (gopass) | | Cisco AI Defense | Enterprise | ~1s | `AI_DEFENSE_API_KEY` | ## aidevops Integration Points @@ -120,6 +120,53 @@ skill-scanner scan /path/to/skill --disable-rule YARA_script_injection skill-scanner scan /path/to/skill --yara-mode permissive ``` +## VirusTotal Integration + +VirusTotal provides an advisory second layer of security scanning alongside the +Cisco Skill Scanner. It checks file hashes against 70+ AV engines and scans +domains/URLs referenced in skill content. + +**Key design**: VT scans are **advisory only** -- the Cisco Skill Scanner remains +the security gate for imports. VT adds value by detecting known malware hashes +and flagging malicious domains that static analysis cannot catch. + +### How it works + +1. **File hash lookup**: SHA256 of each skill file is checked against VT's database +2. **Domain reputation**: URLs extracted from skill content are checked for threats +3. **Rate limiting**: 16s between requests (free tier: 4 req/min, 500 req/day) +4. **Max 8 requests per skill scan** to avoid exhausting daily quota + +### Usage + +```bash +# Standalone VT scan +virustotal-helper.sh scan-skill /path/to/skill/ +virustotal-helper.sh scan-file /path/to/file.md +virustotal-helper.sh scan-domain example.com +virustotal-helper.sh scan-url https://example.com/payload +virustotal-helper.sh status + +# Via security-helper +security-helper.sh vt-scan skill /path/to/skill/ +security-helper.sh vt-scan file /path/to/file.md +security-helper.sh vt-scan status + +# Automatic: VT runs as advisory after Cisco scanner in: +# - security-helper.sh skill-scan all +# - add-skill-helper.sh (GitHub and ClawdHub imports) +``` + +### API key setup + +```bash +# Recommended: gopass encrypted storage +aidevops secret set VIRUSTOTAL_MARCUSQUINN + +# Alternative: credentials.sh +echo 'export VIRUSTOTAL_API_KEY="your_key"' >> ~/.config/aidevops/credentials.sh +``` + ## Environment Variables ```bash @@ -127,7 +174,7 @@ skill-scanner scan /path/to/skill --yara-mode permissive export SKILL_SCANNER_LLM_API_KEY="your_api_key" export SKILL_SCANNER_LLM_MODEL="claude-3-5-sonnet-20241022" -# VirusTotal (optional) +# VirusTotal (optional - prefer gopass: aidevops secret set VIRUSTOTAL_MARCUSQUINN) export VIRUSTOTAL_API_KEY="your_key" # Cisco AI Defense (optional) diff --git a/README.md b/README.md index bf445793f..58c514aa0 100644 --- a/README.md +++ b/README.md @@ -479,6 +479,8 @@ Skills are registered in `~/.aidevops/agents/configs/skill-sources.json` with up Imported skills are automatically security-scanned using [Cisco Skill Scanner](https://github.com/cisco-ai-defense/skill-scanner) when installed. Scanning runs on both initial import and updates -- pulling a new version of a skill triggers the same security checks as the first import. CRITICAL/HIGH findings block the operation; MEDIUM/LOW findings warn but allow. Telemetry is disabled - no data is sent to third parties. +When a [VirusTotal](https://www.virustotal.com/) API key is configured (`aidevops secret set VIRUSTOTAL_MARCUSQUINN`), an advisory second layer scans file hashes against 70+ AV engines and checks domains/URLs referenced in skill content. VT scans are non-blocking -- the Cisco scanner remains the security gate. + | Scenario | Security scan runs? | CRITICAL/HIGH blocks? | |----------|--------------------|-----------------------| | `aidevops skill add ` | Yes | Yes | @@ -859,6 +861,7 @@ The setup script offers to install these tools automatically. - **[Socket](https://socket.dev/)**: Dependency security and supply chain protection - **[Sentry](https://sentry.io/)**: Error monitoring and performance tracking - **[Cisco Skill Scanner](https://github.com/cisco-ai-defense/skill-scanner)**: Security scanner for AI agent skills (prompt injection, exfiltration, malicious code) +- **[VirusTotal](https://www.virustotal.com/)**: Advisory threat intelligence via VT API v3 -- file hash scanning (70+ AV engines), domain/URL reputation checks for imported skills - **[Secretlint](https://github.com/secretlint/secretlint)**: Detect exposed secrets in code - **[OSV Scanner](https://google.github.io/osv-scanner/)**: Google's vulnerability database scanner - **[Qlty](https://qlty.sh/)**: Universal code quality platform (70+ linters, auto-fixes)