test_security.sh 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417
  1. #!/usr/bin/env bash
  2. #
  3. # Local security scanning - mirrors GitHub Actions pipeline
  4. # Runs all scans in parallel and shows a consolidated summary.
  5. #
  6. # Usage:
  7. # ./test_security.sh # Run fast scans (bandit, pip-audit, npm-audit)
  8. # ./test_security.sh --full # Run full pipeline (all scans below)
  9. # ./test_security.sh bandit # Run a specific scan
  10. # ./test_security.sh codeql trivy # Run multiple specific scans
  11. #
  12. # Available scans:
  13. # bandit Python static security analysis (SAST)
  14. # codeql CodeQL analysis (Actions + JavaScript + Python)
  15. # codeql-actions CodeQL GitHub Actions only
  16. # codeql-python CodeQL Python only
  17. # codeql-js CodeQL JavaScript/TypeScript only
  18. # trivy Trivy container image + Dockerfile/IaC scan
  19. # trivy-image Trivy container image scan only
  20. # trivy-config Trivy Dockerfile/IaC scan only
  21. # pip-audit Python dependency vulnerability audit
  22. # npm-audit Frontend dependency vulnerability audit
  23. # gitleaks Secrets scan across full git history (slow — only in --full or by name)
  24. #
  25. # Prerequisites:
  26. # pip install bandit[sarif] pip-audit # Python tools
  27. # gh extension install github/gh-codeql # CodeQL CLI
  28. # curl -sfL https://raw.githubusercontent.com/aquasecurity/trivy/main/contrib/install.sh | sh # Trivy
  29. # go install github.com/zricethezav/gitleaks/v8@latest # gitleaks (ensure ~/go/bin on PATH)
  30. #
  31. set -uo pipefail
  32. # Navigate to project root
  33. PROJECT_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
  34. cd "$PROJECT_ROOT"
  35. # Colors
  36. RED='\033[0;31m'
  37. GREEN='\033[0;32m'
  38. YELLOW='\033[1;33m'
  39. CYAN='\033[0;36m'
  40. BOLD='\033[1m'
  41. DIM='\033[2m'
  42. NC='\033[0m'
  43. # ── Temp directory for scan output ───────────────────────────────────────
  44. WORK_DIR=$(mktemp -d)
  45. trap 'rm -rf "$WORK_DIR"' EXIT
  46. # Parallel job tracking
  47. declare -A PIDS=() # scan_name -> PID
  48. declare -A RESULTS=() # scan_name -> PASS|FAIL|SKIP
  49. declare -A DURATIONS=() # scan_name -> seconds
  50. # Scan display order
  51. SCAN_ORDER=()
  52. # ── SARIF parser (used for CodeQL result display) ────────────────────────
  53. parse_sarif() {
  54. local sarif_file="$1"
  55. python3 << PYEOF
  56. import json
  57. from collections import defaultdict
  58. with open("$sarif_file") as f:
  59. data = json.load(f)
  60. rule_desc = {}
  61. for run in data.get("runs", []):
  62. for rule in run.get("tool", {}).get("driver", {}).get("rules", []):
  63. rid = rule.get("id", "")
  64. desc = rule.get("shortDescription", {}).get("text", "")
  65. rule_desc[rid] = desc
  66. by_rule = defaultdict(list)
  67. for run in data.get("runs", []):
  68. for result in run.get("results", []):
  69. rule_id = result.get("ruleId", "unknown")
  70. msg = result.get("message", {}).get("text", "")
  71. locs = result.get("locations", [])
  72. loc = ""
  73. if locs:
  74. pl = locs[0].get("physicalLocation", {})
  75. uri = pl.get("artifactLocation", {}).get("uri", "")
  76. line = pl.get("region", {}).get("startLine", "")
  77. loc = f"{uri}:{line}" if line else uri
  78. by_rule[rule_id].append((loc, msg))
  79. total = sum(len(v) for v in by_rule.values())
  80. if total == 0:
  81. print("No findings.")
  82. else:
  83. print(f"{total} findings:")
  84. print()
  85. for rule_id, findings in sorted(by_rule.items(), key=lambda x: -len(x[1])):
  86. desc = rule_desc.get(rule_id, "")
  87. print(f" {rule_id} ({len(findings)}) -- {desc}")
  88. for loc, msg in findings:
  89. short_msg = msg[:100] + "..." if len(msg) > 100 else msg
  90. print(f" {loc:60s} {short_msg}")
  91. print()
  92. PYEOF
  93. }
  94. # ── Scan functions (write to stdout, return exit code) ───────────────────
  95. check_command() {
  96. command -v "$1" &>/dev/null
  97. }
  98. has_codeql() {
  99. check_command gh && gh codeql version &>/dev/null
  100. }
  101. scan_bandit() {
  102. if ! check_command bandit; then
  103. echo "SKIP: 'bandit' not found. Install: pip install bandit[sarif]"
  104. return 2
  105. fi
  106. bandit -r backend/ --severity-level medium -x backend/tests 2>&1
  107. }
  108. scan_codeql_python() {
  109. local sarif="$PROJECT_ROOT/codeql-python-results.sarif"
  110. if ! has_codeql; then
  111. echo "SKIP: CodeQL CLI not installed. Install: gh extension install github/gh-codeql"
  112. return 2
  113. fi
  114. echo "Creating database..."
  115. gh codeql database create --overwrite --language=python --threads=0 /tmp/bambuddy-codeql-python &>/dev/null
  116. echo "Analyzing..."
  117. gh codeql database analyze /tmp/bambuddy-codeql-python \
  118. "$PROJECT_ROOT/.codeql/python-bambuddy.qls" \
  119. --threads=0 --format=sarifv2.1.0 --output="$sarif" &>/dev/null
  120. echo ""
  121. parse_sarif "$sarif"
  122. }
  123. scan_codeql_js() {
  124. local sarif="$PROJECT_ROOT/codeql-javascript-results.sarif"
  125. if ! has_codeql; then
  126. echo "SKIP: CodeQL CLI not installed."
  127. return 2
  128. fi
  129. echo "Creating database..."
  130. gh codeql database create --overwrite --language=javascript --source-root=frontend --threads=0 /tmp/bambuddy-codeql-javascript &>/dev/null
  131. echo "Analyzing..."
  132. gh codeql database analyze /tmp/bambuddy-codeql-javascript \
  133. "$PROJECT_ROOT/.codeql/javascript-bambuddy.qls" \
  134. --threads=0 --format=sarifv2.1.0 --output="$sarif" &>/dev/null
  135. echo ""
  136. parse_sarif "$sarif"
  137. }
  138. scan_codeql_actions() {
  139. local sarif="$PROJECT_ROOT/codeql-actions-results.sarif"
  140. if ! has_codeql; then
  141. echo "SKIP: CodeQL CLI not installed."
  142. return 2
  143. fi
  144. echo "Creating database..."
  145. gh codeql database create --overwrite --language=actions --threads=0 /tmp/bambuddy-codeql-actions &>/dev/null
  146. echo "Analyzing..."
  147. gh codeql database analyze /tmp/bambuddy-codeql-actions \
  148. codeql/actions-queries \
  149. --threads=0 --format=sarifv2.1.0 --output="$sarif" &>/dev/null
  150. echo ""
  151. parse_sarif "$sarif"
  152. }
  153. scan_trivy_image() {
  154. if ! check_command trivy; then
  155. echo "SKIP: 'trivy' not found. Install: curl -sfL https://raw.githubusercontent.com/aquasecurity/trivy/main/contrib/install.sh | sh"
  156. return 2
  157. fi
  158. if ! check_command docker; then
  159. echo "SKIP: 'docker' not found."
  160. return 2
  161. fi
  162. echo "Building Docker image..."
  163. docker build -t bambuddy:security-scan . 2>&1
  164. echo ""
  165. trivy image --severity CRITICAL,HIGH,MEDIUM bambuddy:security-scan 2>&1
  166. }
  167. scan_trivy_config() {
  168. if ! check_command trivy; then
  169. echo "SKIP: 'trivy' not found. Install: curl -sfL https://raw.githubusercontent.com/aquasecurity/trivy/main/contrib/install.sh | sh"
  170. return 2
  171. fi
  172. trivy config --severity CRITICAL,HIGH,MEDIUM . 2>&1
  173. }
  174. scan_pip_audit() {
  175. if ! check_command pip-audit; then
  176. echo "SKIP: 'pip-audit' not found. Install: pip install pip-audit"
  177. return 2
  178. fi
  179. pip-audit --desc on 2>&1
  180. }
  181. scan_npm_audit() {
  182. if ! check_command npm; then
  183. echo "SKIP: 'npm' not found. Install Node.js"
  184. return 2
  185. fi
  186. (cd frontend && npm audit --audit-level=high) 2>&1
  187. }
  188. scan_gitleaks() {
  189. local bin
  190. if check_command gitleaks; then
  191. bin="gitleaks"
  192. elif [ -x "$HOME/go/bin/gitleaks" ]; then
  193. bin="$HOME/go/bin/gitleaks"
  194. else
  195. echo "SKIP: 'gitleaks' not found. Install: go install github.com/zricethezav/gitleaks/v8@latest"
  196. return 2
  197. fi
  198. "$bin" detect --source . --redact --no-banner --verbose 2>&1
  199. }
  200. # ── Job launcher (streams output live with prefix, captures to log) ──────
  201. launch_scan() {
  202. local name="$1"
  203. local func="$2"
  204. local prefix
  205. prefix=$(printf "${DIM}[%-14s]${NC} " "$name")
  206. SCAN_ORDER+=("$name")
  207. (
  208. set -o pipefail
  209. local start_time
  210. start_time=$(date +%s)
  211. "$func" 2>&1 | tee "$WORK_DIR/${name}.log" | sed "s|^|${prefix}|"
  212. local exit_code=${PIPESTATUS[0]}
  213. echo $(( $(date +%s) - start_time )) > "$WORK_DIR/${name}.duration"
  214. exit "$exit_code"
  215. ) &
  216. PIDS["$name"]=$!
  217. }
  218. # ── Wait for all scans ───────────────────────────────────────────────────
  219. wait_for_scans() {
  220. local total=${#PIDS[@]}
  221. local completed=0
  222. while [ "$completed" -lt "$total" ]; do
  223. for name in "${SCAN_ORDER[@]}"; do
  224. local pid=${PIDS[$name]:-}
  225. [ -z "$pid" ] && continue
  226. if ! kill -0 "$pid" 2>/dev/null; then
  227. wait "$pid" 2>/dev/null
  228. local exit_code=$?
  229. if [ "$exit_code" -eq 2 ]; then
  230. RESULTS["$name"]="SKIP"
  231. elif [ "$exit_code" -eq 0 ]; then
  232. RESULTS["$name"]="PASS"
  233. else
  234. RESULTS["$name"]="FAIL"
  235. fi
  236. if [ -f "$WORK_DIR/${name}.duration" ]; then
  237. DURATIONS["$name"]=$(cat "$WORK_DIR/${name}.duration")
  238. else
  239. DURATIONS["$name"]="?"
  240. fi
  241. local status_color
  242. case "${RESULTS[$name]}" in
  243. PASS) status_color="$GREEN" ;;
  244. FAIL) status_color="$RED" ;;
  245. SKIP) status_color="$YELLOW" ;;
  246. esac
  247. echo -e "${status_color}${BOLD}[${RESULTS[$name]}]${NC} ${name} ${DIM}(${DURATIONS[$name]}s)${NC}"
  248. unset "PIDS[$name]"
  249. completed=$((completed + 1))
  250. fi
  251. done
  252. sleep 0.5
  253. done
  254. }
  255. # ── Summary ──────────────────────────────────────────────────────────────
  256. print_summary() {
  257. local pass=0 fail=0 skip=0
  258. for name in "${SCAN_ORDER[@]}"; do
  259. case "${RESULTS[$name]}" in
  260. PASS) pass=$((pass + 1)) ;;
  261. FAIL) fail=$((fail + 1)) ;;
  262. SKIP) skip=$((skip + 1)) ;;
  263. esac
  264. done
  265. # ── Results table ────────────────────────────────────────────────────
  266. echo ""
  267. echo -e "${CYAN}${BOLD}══════════════════════════════════════════════════════════════${NC}"
  268. echo -e "${CYAN}${BOLD} Security Scan Results${NC}"
  269. echo -e "${CYAN}${BOLD}══════════════════════════════════════════════════════════════${NC}"
  270. echo ""
  271. printf " ${BOLD}%-6s %-24s %s${NC}\n" "Status" "Scan" "Duration"
  272. printf " %-6s %-24s %s\n" "──────" "────────────────────────" "────────"
  273. for name in "${SCAN_ORDER[@]}"; do
  274. local status="${RESULTS[$name]}"
  275. local duration="${DURATIONS[$name]:-?}s"
  276. local status_color
  277. case "$status" in
  278. PASS) status_color="$GREEN" ;;
  279. FAIL) status_color="$RED" ;;
  280. SKIP) status_color="$YELLOW" ;;
  281. esac
  282. printf " ${status_color}%-6s${NC} %-24s ${DIM}%s${NC}\n" "$status" "$name" "$duration"
  283. done
  284. echo ""
  285. echo -e " ${GREEN}$pass passed${NC} ${RED}$fail failed${NC} ${YELLOW}$skip skipped${NC}"
  286. # ── Full output per scan ─────────────────────────────────────────────
  287. for name in "${SCAN_ORDER[@]}"; do
  288. local log="$WORK_DIR/${name}.log"
  289. [ ! -f "$log" ] && continue
  290. local status="${RESULTS[$name]}"
  291. local status_color
  292. case "$status" in
  293. PASS) status_color="$GREEN" ;;
  294. FAIL) status_color="$RED" ;;
  295. SKIP) status_color="$YELLOW" ;;
  296. esac
  297. echo ""
  298. echo -e "${CYAN}──────────────────────────────────────────────────────────────${NC}"
  299. echo -e "${BOLD} $name${NC} ${status_color}[$status]${NC}"
  300. echo -e "${CYAN}──────────────────────────────────────────────────────────────${NC}"
  301. sed 's/^/ /' "$log"
  302. done
  303. echo ""
  304. echo -e "${CYAN}${BOLD}══════════════════════════════════════════════════════════════${NC}"
  305. echo ""
  306. if [ "$fail" -gt 0 ]; then
  307. exit 1
  308. fi
  309. }
  310. # ── Main ─────────────────────────────────────────────────────────────────
  311. if [ "${1:-}" = "--help" ] || [ "${1:-}" = "-h" ]; then
  312. head -29 "$0" | tail -27
  313. exit 0
  314. fi
  315. echo -e "${BOLD}Bambuddy Security Scanner${NC}"
  316. echo -e "${DIM}$(date '+%Y-%m-%d %H:%M:%S') • $(nproc) CPU cores available${NC}"
  317. echo ""
  318. SCANS_TO_RUN=()
  319. if [ $# -eq 0 ]; then
  320. SCANS_TO_RUN=(bandit pip-audit npm-audit)
  321. elif [ "$1" = "--full" ]; then
  322. SCANS_TO_RUN=(bandit pip-audit npm-audit codeql-actions codeql-python codeql-js trivy-image trivy-config gitleaks)
  323. else
  324. for scan in "$@"; do
  325. case "$scan" in
  326. codeql) SCANS_TO_RUN+=(codeql-actions codeql-python codeql-js) ;;
  327. trivy) SCANS_TO_RUN+=(trivy-image trivy-config) ;;
  328. bandit|codeql-actions|codeql-python|codeql-js|trivy-image|trivy-config|pip-audit|npm-audit|gitleaks)
  329. SCANS_TO_RUN+=("$scan") ;;
  330. *)
  331. echo -e "${RED}Unknown scan: $scan${NC}"
  332. echo "Run with --help for available scans"
  333. exit 1
  334. ;;
  335. esac
  336. done
  337. fi
  338. # Launch all scans in parallel
  339. for scan in "${SCANS_TO_RUN[@]}"; do
  340. case "$scan" in
  341. bandit) launch_scan "bandit" scan_bandit ;;
  342. codeql-actions) launch_scan "codeql-actions" scan_codeql_actions ;;
  343. codeql-python) launch_scan "codeql-python" scan_codeql_python ;;
  344. codeql-js) launch_scan "codeql-js" scan_codeql_js ;;
  345. trivy-image) launch_scan "trivy-image" scan_trivy_image ;;
  346. trivy-config) launch_scan "trivy-config" scan_trivy_config ;;
  347. pip-audit) launch_scan "pip-audit" scan_pip_audit ;;
  348. npm-audit) launch_scan "npm-audit" scan_npm_audit ;;
  349. gitleaks) launch_scan "gitleaks" scan_gitleaks ;;
  350. esac
  351. done
  352. echo -e "${BOLD}Running ${#SCANS_TO_RUN[@]} scan(s) in parallel...${NC}"
  353. echo ""
  354. wait_for_scans
  355. print_summary