#!/usr/bin/env bash # scan-skill.sh — security scan before a skill enters the team library. # # Usage: ./scripts/scan-skill.sh [plugin-dir ...] # With no arguments scans every skills-*/*-plugin directory. # Exit 0 = clean (warnings allowed), 1 = high severity finding. set -uo pipefail ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" HIGH=0 WARN=0 # High severity: destructive, remote-code-execution, secret material. HIGH_PATTERNS=( 'rm[[:space:]]+-[a-zA-Z]*r[a-zA-Z]*[[:space:]]+/($|[[:space:]])' 'curl[^|]*\|[[:space:]]*(ba|z)?sh' 'wget[^|]*\|[[:space:]]*(ba|z)?sh' 'chmod[[:space:]]+777' 'BEGIN (RSA|OPENSSH|EC|DSA) PRIVATE KEY' 'sk-[A-Za-z0-9]{16,}' 'AKIA[0-9A-Z]{16}' 'ghp_[A-Za-z0-9]{20,}' ) # Warnings: machine-specific paths, plain http endpoints. WARN_PATTERNS=( '/Users/[A-Za-z0-9._-]+/' 'http://[A-Za-z0-9.:-]+' 'password[[:space:]]*=[[:space:]]*[^$]' ) targets=("$@") if [[ ${#targets[@]} -eq 0 ]]; then while IFS= read -r dir; do targets+=("$(dirname "$(dirname "$dir")")") done < <(find "$ROOT" -path "*/skills-*/*-plugin/.claude-plugin/plugin.json" | sort) fi ALLOWLIST="$ROOT/scripts/scan-allowlist.txt" is_allowlisted() { # is_allowlisted local path="$1" line="$2" entry sub needle [[ -f "$ALLOWLIST" ]] || return 1 while IFS= read -r entry; do [[ -z "$entry" || "$entry" == \#* ]] && continue sub="${entry%%::*}" needle="${entry#*::}" if [[ "$path" == *"$sub"* && "$line" == *"$needle"* ]]; then return 0 fi done < "$ALLOWLIST" return 1 } for dir in "${targets[@]}"; do rel="${dir#"$ROOT"/}" for pattern in "${HIGH_PATTERNS[@]}"; do while IFS= read -r hit; do abs="${hit%%:*}" detail="${hit#*:}" if is_allowlisted "$abs" "$hit"; then echo "[allow] $rel: $detail" continue fi echo "[HIGH] $rel: $detail" HIGH=$((HIGH + 1)) done < <(grep -rnE --include='*.md' --include='*.sh' --include='*.py' --include='*.js' --include='*.ts' --include='*.json' -- "$pattern" "$dir" 2>/dev/null | cut -c1-200) done for pattern in "${WARN_PATTERNS[@]}"; do while IFS= read -r hit; do # 本机回环地址不算风险(本地开发/测试示例) case "$hit" in *"http://localhost"*|*"http://127.0.0.1"*) continue ;; esac if is_allowlisted "${hit%%:*}" "$hit"; then echo "[allow] $rel: ${hit#*:}" continue fi echo "[warn] $rel: ${hit%%:*}:${hit#*:}" WARN=$((WARN + 1)) done < <(grep -rnE --include='*.md' --include='*.sh' --include='*.py' --include='*.js' --include='*.ts' -- "$pattern" "$dir" 2>/dev/null | cut -c1-200) done done echo "" echo "扫描完成:${#targets[@]} 个插件,$HIGH 个高危,$WARN 个警告" if [[ "$HIGH" -gt 0 ]]; then echo "存在高危项,禁止入库,除非逐条确认并记录原因。" exit 1 fi exit 0