a1a17e81a1
Includes updated JS challenge scripts with Claude-User whitelist, same-site referer bypass, Blackbox-Exporter allowed bot, and all new exporters, cheat sheets, and automation scripts.
651 lines
23 KiB
Bash
651 lines
23 KiB
Bash
#!/usr/bin/env bash
|
|
|
|
#########################################################################################
|
|
#### opa-policy-tester.sh — Run OPA/Rego policy bundles against infrastructure ####
|
|
#### configs, report violations with severity levels and CI-friendly exit codes ####
|
|
#### Requires: bash 4+, opa binary (auto-install available) ####
|
|
#### ####
|
|
#### Author: Phil Connor ####
|
|
#### Contact: contact@mylinux.work ####
|
|
#### License: MIT ####
|
|
#### Version 1.01 ####
|
|
#### ####
|
|
#### Usage: ####
|
|
#### ./opa-policy-tester.sh --eval --policy ./policies --input config.json ####
|
|
#### ####
|
|
#### See --help for all options. ####
|
|
#########################################################################################
|
|
|
|
set -euo pipefail
|
|
|
|
# ── Defaults ──────────────────────────────────────────────────────────
|
|
OPA_PATH="${OPA_PATH:-opa}"
|
|
POLICY_DIR="${OPA_POLICY_DIR:-}"
|
|
INPUT_PATH="${OPA_INPUT:-}"
|
|
DATA_FILE=""
|
|
QUERY_DENY="${OPA_QUERY:-data.main.deny}"
|
|
QUERY_WARN="data.main.warn"
|
|
FAIL_ON="${OPA_FAIL_ON:-deny}"
|
|
OUTPUT_FORMAT="${OPA_FORMAT:-text}"
|
|
OUTPUT_FILE=""
|
|
VERBOSE="${VERBOSE:-false}"
|
|
COLOR="${COLOR:-auto}"
|
|
|
|
# ── State ─────────────────────────────────────────────────────────────
|
|
SCRIPT_NAME="$(basename "$0")"
|
|
readonly SCRIPT_NAME
|
|
RUN_MODE=""
|
|
START_TIME=""
|
|
TOTAL_INPUTS=0
|
|
TOTAL_POLICIES=0
|
|
TOTAL_PASS=0
|
|
TOTAL_WARN=0
|
|
TOTAL_FAIL=0
|
|
TMPDIR_WORK=""
|
|
RESULTS_JSON="[]"
|
|
|
|
# ── Colors ────────────────────────────────────────────────────────────
|
|
RED="" GREEN="" YELLOW="" BLUE="" CYAN="" BOLD="" DIM="" RESET=""
|
|
|
|
setup_colors() {
|
|
if [[ "$COLOR" == "never" ]]; then
|
|
RED="" GREEN="" YELLOW="" BLUE="" CYAN="" BOLD="" DIM="" RESET=""
|
|
return
|
|
fi
|
|
if [[ "$COLOR" == "always" ]] || [[ -t 1 ]]; then
|
|
RED='\033[0;31m'
|
|
GREEN='\033[0;32m'
|
|
YELLOW='\033[0;33m'
|
|
BLUE='\033[0;34m'
|
|
CYAN='\033[0;36m'
|
|
BOLD='\033[1m'
|
|
DIM='\033[2m'
|
|
RESET='\033[0m'
|
|
else
|
|
RED="" GREEN="" YELLOW="" BLUE="" CYAN="" BOLD="" DIM="" RESET=""
|
|
fi
|
|
}
|
|
|
|
# ── Logging ───────────────────────────────────────────────────────────
|
|
log() { echo -e "${BLUE}[INFO]${RESET} $*"; }
|
|
warn() { echo -e "${YELLOW}[WARN]${RESET} $*" >&2; }
|
|
err() { echo -e "${RED}[ERROR]${RESET} $*" >&2; }
|
|
verbose() { if [[ "$VERBOSE" == "true" ]]; then echo -e "${DIM}[DEBUG]${RESET} $*"; fi; }
|
|
die() { err "$*"; exit 1; }
|
|
|
|
# ── Helpers ───────────────────────────────────────────────────────────
|
|
section_header() {
|
|
echo ""
|
|
echo -e " ${BOLD}${CYAN}── $1 ──${RESET}"
|
|
echo ""
|
|
}
|
|
|
|
field() {
|
|
printf " ${BOLD}%-22s${RESET} %s\n" "$1" "$2"
|
|
}
|
|
|
|
field_color() {
|
|
printf " ${BOLD}%-22s${RESET} %b\n" "$1" "$2"
|
|
}
|
|
|
|
elapsed() {
|
|
local end_time
|
|
end_time=$(date +%s)
|
|
echo "$(( end_time - START_TIME ))s"
|
|
}
|
|
|
|
cleanup() {
|
|
if [[ -n "$TMPDIR_WORK" && -d "$TMPDIR_WORK" ]]; then
|
|
rm -rf "$TMPDIR_WORK"
|
|
fi
|
|
}
|
|
trap cleanup EXIT
|
|
|
|
make_tmpdir() {
|
|
if [[ -z "$TMPDIR_WORK" ]]; then
|
|
TMPDIR_WORK=$(mktemp -d "${TMPDIR:-/tmp}/opa-tester.XXXXXX")
|
|
fi
|
|
}
|
|
|
|
# ── Dependency checks ────────────────────────────────────────────────
|
|
require_opa() {
|
|
if ! command -v "$OPA_PATH" &>/dev/null; then
|
|
err "OPA binary not found: ${OPA_PATH}"
|
|
err "Install with: ${SCRIPT_NAME} --install"
|
|
err "Or set OPA_PATH to the opa binary location"
|
|
exit 1
|
|
fi
|
|
verbose "OPA found: $(command -v "$OPA_PATH") ($("$OPA_PATH" version 2>/dev/null | head -1 || echo 'unknown'))"
|
|
}
|
|
|
|
require_jq() {
|
|
if ! command -v jq &>/dev/null; then
|
|
die "jq is required but not installed"
|
|
fi
|
|
}
|
|
|
|
yaml_to_json() {
|
|
local yaml_file="$1" json_file="$2"
|
|
if command -v yq &>/dev/null; then
|
|
yq -o=json '.' "$yaml_file" > "$json_file"
|
|
elif command -v python3 &>/dev/null; then
|
|
python3 -c "
|
|
import sys, json, yaml
|
|
with open('$yaml_file') as f:
|
|
data = yaml.safe_load(f)
|
|
json.dump(data, sys.stdout)
|
|
" > "$json_file"
|
|
else
|
|
die "Cannot convert YAML to JSON — install yq or python3 with PyYAML"
|
|
fi
|
|
}
|
|
|
|
# ── Install OPA ──────────────────────────────────────────────────────
|
|
install_opa() {
|
|
local os arch url dest
|
|
os="$(uname -s | tr '[:upper:]' '[:lower:]')"
|
|
arch="$(uname -m)"
|
|
case "$arch" in
|
|
x86_64) arch="amd64" ;;
|
|
aarch64|arm64) arch="arm64" ;;
|
|
*) die "Unsupported architecture: $arch" ;;
|
|
esac
|
|
|
|
dest="/usr/local/bin/opa"
|
|
if [[ ! -w "$(dirname "$dest")" ]]; then
|
|
dest="${HOME}/.local/bin/opa"
|
|
mkdir -p "$(dirname "$dest")"
|
|
fi
|
|
|
|
url="https://github.com/open-policy-agent/opa/releases/latest/download/opa_${os}_${arch}"
|
|
log "Downloading OPA from ${url}"
|
|
if command -v curl &>/dev/null; then
|
|
curl -fsSL -o "$dest" "$url"
|
|
elif command -v wget &>/dev/null; then
|
|
wget -qO "$dest" "$url"
|
|
else
|
|
die "Neither curl nor wget available for download"
|
|
fi
|
|
chmod +x "$dest"
|
|
log "OPA installed to ${dest}"
|
|
"$dest" version
|
|
}
|
|
|
|
# ── Collect input files ──────────────────────────────────────────────
|
|
collect_inputs() {
|
|
local path="$1"
|
|
local -n arr=$2
|
|
if [[ -f "$path" ]]; then
|
|
arr+=("$path")
|
|
elif [[ -d "$path" ]]; then
|
|
while IFS= read -r -d '' f; do
|
|
arr+=("$f")
|
|
done < <(find "$path" -type f \( -name '*.json' -o -name '*.yaml' -o -name '*.yml' \) -print0 | sort -z)
|
|
[[ ${#arr[@]} -eq 0 ]] && die "No JSON/YAML files found in ${path}"
|
|
else
|
|
die "Input path does not exist: ${path}"
|
|
fi
|
|
}
|
|
|
|
prepare_input() {
|
|
local file="$1"
|
|
case "$file" in
|
|
*.yaml|*.yml)
|
|
make_tmpdir
|
|
local tmp_json
|
|
tmp_json="${TMPDIR_WORK}/$(basename "${file}").json"
|
|
verbose "Converting YAML to JSON: ${file}"
|
|
yaml_to_json "$file" "$tmp_json"
|
|
echo "$tmp_json"
|
|
;;
|
|
*.json)
|
|
echo "$file"
|
|
;;
|
|
*)
|
|
die "Unsupported file format: ${file}"
|
|
;;
|
|
esac
|
|
}
|
|
|
|
# ── Count .rego files ────────────────────────────────────────────────
|
|
count_policies() {
|
|
local path="$1"
|
|
if [[ -f "$path" ]]; then
|
|
echo 1
|
|
elif [[ -d "$path" ]]; then
|
|
find "$path" -name '*.rego' -not -name '*_test.rego' | wc -l | tr -d ' '
|
|
else
|
|
echo 0
|
|
fi
|
|
}
|
|
|
|
# ── Evaluate policies ───────────────────────────────────────────────
|
|
eval_single() {
|
|
local input_file="$1" json_input policy_args raw_deny raw_warn
|
|
json_input=$(prepare_input "$input_file")
|
|
|
|
policy_args=()
|
|
if [[ -d "$POLICY_DIR" ]]; then
|
|
policy_args+=(--bundle "$POLICY_DIR")
|
|
else
|
|
policy_args+=(--data "$POLICY_DIR")
|
|
fi
|
|
[[ -n "$DATA_FILE" ]] && policy_args+=(--data "$DATA_FILE")
|
|
|
|
verbose "Evaluating: ${input_file}"
|
|
verbose " policy: ${POLICY_DIR}"
|
|
verbose " query deny: ${QUERY_DENY}"
|
|
verbose " query warn: ${QUERY_WARN}"
|
|
|
|
raw_deny=$("$OPA_PATH" eval "${policy_args[@]}" --input "$json_input" \
|
|
--format json "$QUERY_DENY" 2>/dev/null || echo '{"result":[]}')
|
|
raw_warn=$("$OPA_PATH" eval "${policy_args[@]}" --input "$json_input" \
|
|
--format json "$QUERY_WARN" 2>/dev/null || echo '{"result":[]}')
|
|
|
|
local deny_msgs warn_msgs
|
|
deny_msgs=$(echo "$raw_deny" | jq -r '
|
|
[.result[]?.expressions[]?.value // [] | if type == "array" then .[] else . end] |
|
|
if length > 0 then .[] else empty end' 2>/dev/null || true)
|
|
warn_msgs=$(echo "$raw_warn" | jq -r '
|
|
[.result[]?.expressions[]?.value // [] | if type == "array" then .[] else . end] |
|
|
if length > 0 then .[] else empty end' 2>/dev/null || true)
|
|
|
|
local file_denies=0 file_warns=0
|
|
local input_base
|
|
input_base=$(basename "$input_file")
|
|
|
|
if [[ -n "$deny_msgs" ]]; then
|
|
while IFS= read -r msg; do
|
|
[[ -z "$msg" ]] && continue
|
|
((file_denies++)) || true
|
|
((TOTAL_FAIL++)) || true
|
|
RESULTS_JSON=$(echo "$RESULTS_JSON" | jq --arg f "$input_base" \
|
|
--arg m "$msg" --arg s "deny" \
|
|
'. + [{"file": $f, "severity": $s, "message": $m}]')
|
|
done <<< "$deny_msgs"
|
|
fi
|
|
|
|
if [[ -n "$warn_msgs" ]]; then
|
|
while IFS= read -r msg; do
|
|
[[ -z "$msg" ]] && continue
|
|
((file_warns++)) || true
|
|
((TOTAL_WARN++)) || true
|
|
RESULTS_JSON=$(echo "$RESULTS_JSON" | jq --arg f "$input_base" \
|
|
--arg m "$msg" --arg s "warn" \
|
|
'. + [{"file": $f, "severity": $s, "message": $m}]')
|
|
done <<< "$warn_msgs"
|
|
fi
|
|
|
|
if [[ $file_denies -eq 0 && $file_warns -eq 0 ]]; then
|
|
((TOTAL_PASS++)) || true
|
|
fi
|
|
}
|
|
|
|
run_eval() {
|
|
[[ -z "$POLICY_DIR" ]] && die "No policy path specified (--policy)"
|
|
[[ -z "$INPUT_PATH" ]] && die "No input path specified (--input)"
|
|
[[ ! -e "$POLICY_DIR" ]] && die "Policy path does not exist: ${POLICY_DIR}"
|
|
require_opa
|
|
require_jq
|
|
|
|
START_TIME=$(date +%s)
|
|
local inputs=()
|
|
collect_inputs "$INPUT_PATH" inputs
|
|
TOTAL_INPUTS=${#inputs[@]}
|
|
TOTAL_POLICIES=$(count_policies "$POLICY_DIR")
|
|
|
|
section_header "OPA Policy Evaluation"
|
|
field "Policy path:" "$POLICY_DIR"
|
|
field "Input path:" "$INPUT_PATH"
|
|
field "Input files:" "$TOTAL_INPUTS"
|
|
field "Policies:" "$TOTAL_POLICIES"
|
|
field "Fail threshold:" "$FAIL_ON"
|
|
echo ""
|
|
|
|
for input_file in "${inputs[@]}"; do
|
|
eval_single "$input_file"
|
|
done
|
|
|
|
render_results
|
|
write_output
|
|
compute_exit
|
|
}
|
|
|
|
# ── Render results ───────────────────────────────────────────────────
|
|
render_results() {
|
|
local violation_count
|
|
violation_count=$(echo "$RESULTS_JSON" | jq 'length')
|
|
|
|
if [[ "$violation_count" -gt 0 ]]; then
|
|
section_header "Violations"
|
|
printf " ${BOLD}%-28s %-8s %s${RESET}\n" "FILE" "LEVEL" "MESSAGE"
|
|
printf " %-28s %-8s %s\n" "----------------------------" "--------" "$(printf '%0.s-' {1..40})"
|
|
|
|
echo "$RESULTS_JSON" | jq -r '.[] | [.file, .severity, .message] | @tsv' | \
|
|
while IFS=$'\t' read -r vf vs vm; do
|
|
local color="$RESET"
|
|
case "$vs" in
|
|
deny) color="$RED" ;;
|
|
warn) color="$YELLOW" ;;
|
|
esac
|
|
printf " %-28s ${color}%-8s${RESET} %s\n" "$vf" "${vs^^}" "$vm"
|
|
done
|
|
fi
|
|
|
|
section_header "Summary"
|
|
field "Total inputs:" "$TOTAL_INPUTS"
|
|
field "Policies evaluated:" "$TOTAL_POLICIES"
|
|
field_color "Passed:" "${GREEN}${TOTAL_PASS}${RESET}"
|
|
field_color "Warnings:" "${YELLOW}${TOTAL_WARN}${RESET}"
|
|
field_color "Failures:" "${RED}${TOTAL_FAIL}${RESET}"
|
|
field "Duration:" "$(elapsed)"
|
|
}
|
|
|
|
compute_exit() {
|
|
case "$FAIL_ON" in
|
|
warn)
|
|
if [[ $TOTAL_FAIL -gt 0 || $TOTAL_WARN -gt 0 ]]; then
|
|
exit 2
|
|
fi
|
|
;;
|
|
deny)
|
|
if [[ $TOTAL_FAIL -gt 0 ]]; then
|
|
exit 2
|
|
fi
|
|
;;
|
|
esac
|
|
}
|
|
|
|
# ── Output formats ───────────────────────────────────────────────────
|
|
write_output() {
|
|
[[ -z "$OUTPUT_FILE" ]] && return
|
|
case "$OUTPUT_FORMAT" in
|
|
json) write_json ;;
|
|
junit) write_junit ;;
|
|
tap) write_tap ;;
|
|
text) write_text ;;
|
|
*) die "Unknown output format: ${OUTPUT_FORMAT}" ;;
|
|
esac
|
|
log "Results written to ${OUTPUT_FILE}"
|
|
}
|
|
|
|
write_json() {
|
|
jq -n --argjson v "$RESULTS_JSON" \
|
|
--argjson inputs "$TOTAL_INPUTS" \
|
|
--argjson policies "$TOTAL_POLICIES" \
|
|
--argjson pass "$TOTAL_PASS" \
|
|
--argjson warns "$TOTAL_WARN" \
|
|
--argjson fails "$TOTAL_FAIL" \
|
|
'{summary: {inputs: $inputs, policies: $policies, pass: $pass, warnings: $warns, failures: $fails}, violations: $v}' \
|
|
> "$OUTPUT_FILE"
|
|
}
|
|
|
|
write_junit() {
|
|
local total tests failures
|
|
total=$(echo "$RESULTS_JSON" | jq 'length')
|
|
tests=$((TOTAL_PASS + total))
|
|
failures=$TOTAL_FAIL
|
|
|
|
{
|
|
echo '<?xml version="1.0" encoding="UTF-8"?>'
|
|
echo "<testsuites tests=\"${tests}\" failures=\"${failures}\">"
|
|
echo " <testsuite name=\"opa-policy-tester\" tests=\"${tests}\" failures=\"${failures}\">"
|
|
|
|
local i=0
|
|
while [[ $i -lt $TOTAL_PASS ]]; do
|
|
echo " <testcase name=\"input-pass-$((i+1))\" classname=\"opa\"/>"
|
|
((i++)) || true
|
|
done
|
|
|
|
echo "$RESULTS_JSON" | jq -r '.[] | @base64' | while read -r entry; do
|
|
local vf vs vm
|
|
vf=$(echo "$entry" | base64 -d | jq -r '.file')
|
|
vs=$(echo "$entry" | base64 -d | jq -r '.severity')
|
|
vm=$(echo "$entry" | base64 -d | jq -r '.message')
|
|
echo " <testcase name=\"${vf}:${vs}\" classname=\"opa\">"
|
|
echo " <failure message=\"${vm}\" type=\"${vs}\"/>"
|
|
echo " </testcase>"
|
|
done
|
|
|
|
echo " </testsuite>"
|
|
echo "</testsuites>"
|
|
} > "$OUTPUT_FILE"
|
|
}
|
|
|
|
write_tap() {
|
|
local total
|
|
total=$(echo "$RESULTS_JSON" | jq 'length')
|
|
local plan=$((TOTAL_PASS + total))
|
|
{
|
|
echo "TAP version 13"
|
|
echo "1..${plan}"
|
|
local idx=1
|
|
local i=0
|
|
while [[ $i -lt $TOTAL_PASS ]]; do
|
|
echo "ok ${idx} - input passed all policies"
|
|
((idx++)) || true
|
|
((i++)) || true
|
|
done
|
|
|
|
echo "$RESULTS_JSON" | jq -r '.[] | [.file, .severity, .message] | @tsv' | \
|
|
while IFS=$'\t' read -r vf vs vm; do
|
|
echo "not ok ${idx} - ${vf}: [${vs}] ${vm}"
|
|
((idx++)) || true
|
|
done
|
|
} > "$OUTPUT_FILE"
|
|
}
|
|
|
|
write_text() {
|
|
{
|
|
echo "OPA Policy Test Results"
|
|
echo "======================"
|
|
echo ""
|
|
echo "$RESULTS_JSON" | jq -r '.[] | "[\(.severity | ascii_upcase)] \(.file): \(.message)"'
|
|
echo ""
|
|
echo "Inputs: ${TOTAL_INPUTS} Policies: ${TOTAL_POLICIES} Pass: ${TOTAL_PASS} Warn: ${TOTAL_WARN} Fail: ${TOTAL_FAIL}"
|
|
} > "$OUTPUT_FILE"
|
|
}
|
|
|
|
# ── Run unit tests ───────────────────────────────────────────────────
|
|
run_test() {
|
|
[[ -z "$POLICY_DIR" ]] && die "No policy path specified (--policy)"
|
|
[[ ! -e "$POLICY_DIR" ]] && die "Policy path does not exist: ${POLICY_DIR}"
|
|
require_opa
|
|
|
|
START_TIME=$(date +%s)
|
|
section_header "OPA Unit Tests"
|
|
field "Policy path:" "$POLICY_DIR"
|
|
echo ""
|
|
|
|
local test_output exit_code=0
|
|
test_output=$("$OPA_PATH" test --verbose "$POLICY_DIR" 2>&1) || exit_code=$?
|
|
|
|
local test_pass=0 test_fail=0
|
|
while IFS= read -r line; do
|
|
if [[ "$line" =~ ^PASS ]]; then
|
|
((test_pass++)) || true
|
|
echo -e " ${GREEN}✓${RESET} ${line#PASS }"
|
|
elif [[ "$line" =~ ^FAIL ]]; then
|
|
((test_fail++)) || true
|
|
echo -e " ${RED}✗${RESET} ${line#FAIL }"
|
|
elif [[ -n "$line" ]]; then
|
|
verbose "$line"
|
|
fi
|
|
done <<< "$test_output"
|
|
|
|
section_header "Test Summary"
|
|
field_color "Passed:" "${GREEN}${test_pass}${RESET}"
|
|
field_color "Failed:" "${RED}${test_fail}${RESET}"
|
|
field "Duration:" "$(elapsed)"
|
|
|
|
[[ $test_fail -gt 0 ]] && exit 2
|
|
[[ $exit_code -ne 0 ]] && exit 1
|
|
}
|
|
|
|
# ── Format Rego files ────────────────────────────────────────────────
|
|
run_fmt() {
|
|
[[ -z "$POLICY_DIR" ]] && die "No policy path specified (--policy)"
|
|
[[ ! -e "$POLICY_DIR" ]] && die "Policy path does not exist: ${POLICY_DIR}"
|
|
require_opa
|
|
|
|
section_header "OPA Format"
|
|
field "Policy path:" "$POLICY_DIR"
|
|
echo ""
|
|
|
|
local files=() formatted=0
|
|
if [[ -f "$POLICY_DIR" ]]; then
|
|
files=("$POLICY_DIR")
|
|
else
|
|
while IFS= read -r -d '' f; do
|
|
files+=("$f")
|
|
done < <(find "$POLICY_DIR" -name '*.rego' -print0 | sort -z)
|
|
fi
|
|
|
|
[[ ${#files[@]} -eq 0 ]] && die "No .rego files found in ${POLICY_DIR}"
|
|
|
|
for f in "${files[@]}"; do
|
|
local before after
|
|
before=$(cat "$f")
|
|
"$OPA_PATH" fmt -w "$f"
|
|
after=$(cat "$f")
|
|
if [[ "$before" != "$after" ]]; then
|
|
((formatted++)) || true
|
|
echo -e " ${YELLOW}reformatted${RESET} $(basename "$f")"
|
|
else
|
|
verbose "unchanged: $(basename "$f")"
|
|
fi
|
|
done
|
|
|
|
echo ""
|
|
field "Total files:" "${#files[@]}"
|
|
field "Reformatted:" "$formatted"
|
|
}
|
|
|
|
# ── Syntax check ─────────────────────────────────────────────────────
|
|
run_check() {
|
|
[[ -z "$POLICY_DIR" ]] && die "No policy path specified (--policy)"
|
|
[[ ! -e "$POLICY_DIR" ]] && die "Policy path does not exist: ${POLICY_DIR}"
|
|
require_opa
|
|
|
|
section_header "OPA Syntax Check"
|
|
field "Policy path:" "$POLICY_DIR"
|
|
echo ""
|
|
|
|
local check_output exit_code=0
|
|
check_output=$("$OPA_PATH" check "$POLICY_DIR" 2>&1) || exit_code=$?
|
|
|
|
if [[ $exit_code -eq 0 ]]; then
|
|
echo -e " ${GREEN}✓${RESET} All policies pass syntax check"
|
|
else
|
|
echo -e " ${RED}✗${RESET} Syntax errors found:"
|
|
echo ""
|
|
while IFS= read -r line; do
|
|
echo " $line"
|
|
done <<< "$check_output"
|
|
exit 1
|
|
fi
|
|
}
|
|
|
|
# ── Usage ─────────────────────────────────────────────────────────────
|
|
usage() {
|
|
cat <<EOF
|
|
${BOLD}${SCRIPT_NAME}${RESET} — OPA Policy Tester
|
|
|
|
Run OPA/Rego policy bundles against infrastructure configs and report
|
|
violations with severity levels and CI-friendly exit codes.
|
|
|
|
${BOLD}MODES${RESET}
|
|
--eval Evaluate policies against input configs
|
|
--test Run OPA unit tests on policy directory
|
|
--fmt Format .rego files (opa fmt)
|
|
--check Syntax-check .rego files (opa check)
|
|
--install Install OPA binary if not present
|
|
|
|
${BOLD}OPTIONS${RESET}
|
|
--policy DIR|FILE Path to policy directory or .rego file
|
|
--input FILE|DIR Input data file (JSON/YAML) or directory
|
|
--data FILE Additional data file for evaluation
|
|
--query QUERY Custom OPA deny query (default: data.main.deny)
|
|
--fail-on LEVEL Fail threshold: warn or deny (default: deny)
|
|
--format FORMAT Output format: text, json, junit, tap (default: text)
|
|
--output-file FILE Write results to file
|
|
--verbose Show debug output
|
|
--no-color Disable colored output
|
|
--help Show this help message
|
|
|
|
${BOLD}ENVIRONMENT VARIABLES${RESET}
|
|
OPA_PATH Path to opa binary (default: opa)
|
|
OPA_POLICY_DIR Default policy directory
|
|
OPA_INPUT Default input path
|
|
OPA_QUERY Default deny query
|
|
OPA_FORMAT Default output format
|
|
OPA_FAIL_ON Default fail threshold
|
|
VERBOSE Enable verbose output (true/false)
|
|
COLOR Color mode: auto, always, never
|
|
|
|
${BOLD}EXAMPLES${RESET}
|
|
# Evaluate Kubernetes manifests
|
|
${SCRIPT_NAME} --eval --policy ./policies --input ./k8s-manifests/
|
|
|
|
# Check a single Terraform plan
|
|
${SCRIPT_NAME} --eval --policy ./policies --input plan.json --format json
|
|
|
|
# Run unit tests
|
|
${SCRIPT_NAME} --test --policy ./policies
|
|
|
|
# Syntax check and format
|
|
${SCRIPT_NAME} --check --policy ./policies
|
|
${SCRIPT_NAME} --fmt --policy ./policies
|
|
|
|
# CI pipeline with strict threshold
|
|
${SCRIPT_NAME} --eval --policy ./policies --input . --fail-on warn --format junit --output-file results.xml
|
|
|
|
${BOLD}EXIT CODES${RESET}
|
|
0 All checks passed
|
|
1 Runtime error
|
|
2 Violations exceed threshold
|
|
EOF
|
|
}
|
|
|
|
# ── Parse arguments ──────────────────────────────────────────────────
|
|
parse_args() {
|
|
while [[ $# -gt 0 ]]; do
|
|
case "$1" in
|
|
--eval) RUN_MODE="eval"; shift ;;
|
|
--test) RUN_MODE="test"; shift ;;
|
|
--fmt) RUN_MODE="fmt"; shift ;;
|
|
--check) RUN_MODE="check"; shift ;;
|
|
--install) RUN_MODE="install"; shift ;;
|
|
--policy) POLICY_DIR="${2:?--policy requires a path}"; shift 2 ;;
|
|
--input) INPUT_PATH="${2:?--input requires a path}"; shift 2 ;;
|
|
--data) DATA_FILE="${2:?--data requires a path}"; shift 2 ;;
|
|
--query) QUERY_DENY="${2:?--query requires a value}"; shift 2 ;;
|
|
--fail-on) FAIL_ON="${2:?--fail-on requires a value}"; shift 2 ;;
|
|
--format) OUTPUT_FORMAT="${2:?--format requires a value}"; shift 2 ;;
|
|
--output-file) OUTPUT_FILE="${2:?--output-file requires a path}"; shift 2 ;;
|
|
--verbose) VERBOSE="true"; shift ;;
|
|
--no-color) COLOR="never"; shift ;;
|
|
--help|-h) setup_colors; usage; exit 0 ;;
|
|
*) die "Unknown option: $1 (see --help)" ;;
|
|
esac
|
|
done
|
|
}
|
|
|
|
# ── Main ─────────────────────────────────────────────────────────────
|
|
main() {
|
|
parse_args "$@"
|
|
setup_colors
|
|
|
|
case "$RUN_MODE" in
|
|
eval) run_eval ;;
|
|
test) run_test ;;
|
|
fmt) run_fmt ;;
|
|
check) run_check ;;
|
|
install) install_opa ;;
|
|
"") err "No mode specified"; echo ""; usage; exit 1 ;;
|
|
*) die "Unknown mode: ${RUN_MODE}" ;;
|
|
esac
|
|
}
|
|
|
|
main "$@"
|