skill-gate / gate (pull_request) Waiting to run
- scan-skill.sh: loopback 不算风险、命中走允许清单 - 11 个文件的 /Users/<机器名> 绝对路径去环境化(脚本 $HOME、文档 ~) - 允许清单登记运维环境地址与示例占位符(非凭据),附理由 - scan: 57 warns -> 0 warns / 0 high - 新增 .gitea/workflows/skill-gate.yml(validate --strict + scan + catalog --check + 测试)
91 lines
2.8 KiB
Bash
Executable File
91 lines
2.8 KiB
Bash
Executable File
#!/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 <abs_path> <matched_line>
|
|
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
|