test_security.sh 14 KB

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