run.sh 4.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293
  1. #!/usr/bin/env bash
  2. # Self-test for security-ops scanners; fully offline.
  3. #
  4. # Usage: bash tests/run.sh
  5. # Exit: 0 all pass, 1 one or more failures
  6. set -uo pipefail
  7. HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
  8. SKILL="$(dirname "$HERE")"
  9. SCAN="$SKILL/scripts/security-scan.sh"
  10. AUDIT="$SKILL/scripts/dependency-audit.sh"
  11. BAD="$HERE/fixtures/bad"
  12. CLEAN="$HERE/fixtures/clean"
  13. SB="$(mktemp -d)"; trap 'rm -rf "$SB"' EXIT
  14. PASS=0; FAIL=0
  15. ok() { PASS=$((PASS + 1)); printf ' PASS %s\n' "$1"; }
  16. no() { FAIL=$((FAIL + 1)); printf ' FAIL %s\n' "$1"; }
  17. expect_exit() { [[ "$2" == "$3" ]] && ok "$1 (exit $3)" || no "$1 (want $3 got $2)"; }
  18. expect_has() { case "$3" in *"$2"*) ok "$1";; *) no "$1 (missing '$2')";; esac; }
  19. expect_lacks() { case "$3" in *"$2"*) no "$1 (unexpected '$2')";; *) ok "$1";; esac; }
  20. printf '%s\n' '=== security-ops self-test ==='
  21. printf '%s\n' '-- contract --'
  22. for script in "$SCAN" "$AUDIT"; do
  23. name="$(basename "$script")"
  24. bash -n "$script" 2>/dev/null && ok "bash -n $name" || no "bash -n $name"
  25. bash "$script" --help >"$SB/help" 2>/dev/null
  26. expect_exit "$name --help exits 0" "$?" 0
  27. expect_has "$name --help has EXAMPLES" 'EXAMPLES' "$(cat "$SB/help")"
  28. bash "$script" --bogus >/dev/null 2>&1
  29. expect_exit "$name unknown flag" "$?" 2
  30. done
  31. printf '%s\n' '-- true positives and stream separation --'
  32. bash "$SCAN" "$BAD" >"$SB/bad.out" 2>"$SB/bad.err"
  33. expect_exit 'bad fixture signals findings' "$?" 10
  34. bad_out="$(cat "$SB/bad.out")"
  35. bad_err="$(cat "$SB/bad.err")"
  36. expect_has 'hardcoded secret is flagged' 'hardcoded_secret.py' "$bad_out"
  37. expect_has 'eval use is flagged' 'eval_case.py' "$bad_out"
  38. expect_has 'unsafe deserialization is flagged' 'unsafe_deserialization.py' "$bad_out"
  39. expect_lacks 'stdout excludes scan banner' 'Security Scan' "$bad_out"
  40. expect_lacks 'stdout excludes progress' 'Checking:' "$bad_out"
  41. expect_has 'stderr carries scan banner' 'Security Scan' "$bad_err"
  42. finding_count="$(printf '%s\n' "$bad_out" | grep -cE 'hardcoded_secret\.py|eval_case\.py|unsafe_deserialization\.py' || true)"
  43. [[ "$finding_count" == 3 ]] && ok 'exactly three claimed patterns are flagged' || no "expected 3 flagged patterns, got $finding_count"
  44. printf '%s\n' '-- true negative --'
  45. bash "$SCAN" "$CLEAN" >"$SB/clean.out" 2>"$SB/clean.err"
  46. expect_exit 'clean fixture exits clean' "$?" 0
  47. [[ ! -s "$SB/clean.out" ]] && ok 'clean fixture emits no findings' || no 'clean fixture emitted stdout'
  48. printf '%s\n' '-- mutation --'
  49. cp -R "$BAD" "$SB/mutated"
  50. eval_trigger='ev''al(user_expression)'
  51. sed "s/$eval_trigger/safe_evaluate(user_expression)/" "$BAD/eval_case.py" >"$SB/mutated/eval_case.py"
  52. bash "$SCAN" "$SB/mutated" >"$SB/mutated.out" 2>"$SB/mutated.err"
  53. expect_exit 'remaining bad patterns still signal findings' "$?" 10
  54. mutated_out="$(cat "$SB/mutated.out")"
  55. expect_lacks 'removed eval trigger is no longer reported' 'eval_case.py' "$mutated_out"
  56. expect_has 'mutation retains independent secret finding' 'hardcoded_secret.py' "$mutated_out"
  57. expect_has 'mutation retains independent pickle finding' 'unsafe_deserialization.py' "$mutated_out"
  58. printf '%s\n' '-- fail-loud when scan engine (rg) is absent --'
  59. # A security scanner that reports "clean" because rg is missing is a false
  60. # all-clear — it must refuse (exit 5), never exit 0. The scanner hits its
  61. # `command -v rg` guard before it needs any other tool, so a shim of the
  62. # standard bin dirs minus rg is enough to trip it. Skip-guarded: if the shim
  63. # can't be built (e.g. Windows symlink quirks) we don't fail the suite —
  64. # CI (Linux) builds it cleanly and runs the real assertion.
  65. RGSHIM="$SB/rgless"; mkdir -p "$RGSHIM"
  66. for _d in /usr/bin /bin /usr/local/bin; do
  67. [ -d "$_d" ] || continue
  68. for _f in "$_d"/*; do
  69. _b="$(basename "$_f")"
  70. [ "$_b" = "rg" ] && continue
  71. [ -e "$RGSHIM/$_b" ] || ln -sf "$_f" "$RGSHIM/$_b" 2>/dev/null
  72. done
  73. done
  74. if [ -x "$RGSHIM/bash" ] && ! PATH="$RGSHIM" command -v rg >/dev/null 2>&1; then
  75. PATH="$RGSHIM" bash "$SCAN" "$BAD" >"$SB/norg.out" 2>/dev/null
  76. expect_exit 'refuses (exit 5) when rg is absent — no false clean' "$?" 5
  77. else
  78. printf ' SKIP rg-absent shim unavailable on this host (CI runs it for real)\n'
  79. fi
  80. printf '\n=== %d passed, %d failed ===\n' "$PASS" "$FAIL"
  81. [[ "$FAIL" -eq 0 ]] || exit 1
  82. exit 0