- Implemented InjectionTests.cs to cover various injection vulnerabilities including SQL, NoSQL, Command, LDAP, and XPath injections. - Created SsrfTests.cs to test for Server-Side Request Forgery (SSRF) vulnerabilities, including internal URL access, cloud metadata access, and URL allowlist bypass attempts. - Introduced MaliciousPayloads.cs to store a collection of malicious payloads for testing various security vulnerabilities. - Added SecurityAssertions.cs for common security-specific assertion helpers. - Established SecurityTestBase.cs as a base class for security tests, providing common infrastructure and mocking utilities. - Configured the test project StellaOps.Security.Tests.csproj with necessary dependencies for testing.
327 lines
8.8 KiB
Bash
327 lines
8.8 KiB
Bash
#!/usr/bin/env bash
|
|
# =============================================================================
|
|
# enforce-performance-slos.sh
|
|
# Enforces scan time and compute budget SLOs in CI
|
|
#
|
|
# Usage: ./enforce-performance-slos.sh [options]
|
|
# --results-path PATH Path to benchmark results directory
|
|
# --slos-file FILE Path to SLO definitions (default: scripts/ci/performance-slos.yaml)
|
|
# --output FILE Output JSON file (default: stdout)
|
|
# --dry-run Show what would be enforced
|
|
# --strict Exit non-zero if any SLO is violated
|
|
# --verbose Enable verbose output
|
|
#
|
|
# Output: JSON with SLO evaluation results and violations
|
|
# =============================================================================
|
|
|
|
set -euo pipefail
|
|
|
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
REPO_ROOT="$(cd "${SCRIPT_DIR}/../.." && pwd)"
|
|
|
|
# Default paths
|
|
RESULTS_PATH="${REPO_ROOT}/bench/results"
|
|
SLOS_FILE="${SCRIPT_DIR}/performance-slos.yaml"
|
|
OUTPUT_FILE=""
|
|
DRY_RUN=false
|
|
STRICT=false
|
|
VERBOSE=false
|
|
|
|
# Parse arguments
|
|
while [[ $# -gt 0 ]]; do
|
|
case "$1" in
|
|
--results-path)
|
|
RESULTS_PATH="$2"
|
|
shift 2
|
|
;;
|
|
--slos-file)
|
|
SLOS_FILE="$2"
|
|
shift 2
|
|
;;
|
|
--output)
|
|
OUTPUT_FILE="$2"
|
|
shift 2
|
|
;;
|
|
--dry-run)
|
|
DRY_RUN=true
|
|
shift
|
|
;;
|
|
--strict)
|
|
STRICT=true
|
|
shift
|
|
;;
|
|
--verbose)
|
|
VERBOSE=true
|
|
shift
|
|
;;
|
|
-h|--help)
|
|
head -20 "$0" | tail -15
|
|
exit 0
|
|
;;
|
|
*)
|
|
echo "Unknown option: $1" >&2
|
|
exit 1
|
|
;;
|
|
esac
|
|
done
|
|
|
|
log() {
|
|
if [[ "${VERBOSE}" == "true" ]]; then
|
|
echo "[$(date -u '+%Y-%m-%dT%H:%M:%SZ')] $*" >&2
|
|
fi
|
|
}
|
|
|
|
error() {
|
|
echo "[ERROR] $*" >&2
|
|
}
|
|
|
|
warn() {
|
|
echo "[WARN] $*" >&2
|
|
}
|
|
|
|
if [[ "${DRY_RUN}" == "true" ]]; then
|
|
log "[DRY RUN] Would enforce performance SLOs..."
|
|
|
|
cat <<EOF
|
|
{
|
|
"timestamp": "$(date -u '+%Y-%m-%dT%H:%M:%SZ')",
|
|
"dry_run": true,
|
|
"results_path": "${RESULTS_PATH}",
|
|
"slos_file": "${SLOS_FILE}",
|
|
"slo_evaluations": {
|
|
"scan_time_p95": {
|
|
"slo_name": "Scan Time P95",
|
|
"threshold_ms": 30000,
|
|
"actual_ms": 25000,
|
|
"passed": true,
|
|
"margin_pct": 16.7
|
|
},
|
|
"memory_peak_mb": {
|
|
"slo_name": "Peak Memory Usage",
|
|
"threshold_mb": 2048,
|
|
"actual_mb": 1650,
|
|
"passed": true,
|
|
"margin_pct": 19.4
|
|
},
|
|
"cpu_time_seconds": {
|
|
"slo_name": "CPU Time",
|
|
"threshold_seconds": 60,
|
|
"actual_seconds": 45,
|
|
"passed": true,
|
|
"margin_pct": 25.0
|
|
}
|
|
},
|
|
"summary": {
|
|
"total_slos": 3,
|
|
"passed": 3,
|
|
"failed": 0,
|
|
"all_passed": true
|
|
}
|
|
}
|
|
EOF
|
|
exit 0
|
|
fi
|
|
|
|
# Validate paths
|
|
if [[ ! -d "${RESULTS_PATH}" ]]; then
|
|
error "Results directory not found: ${RESULTS_PATH}"
|
|
exit 1
|
|
fi
|
|
|
|
if [[ ! -f "${SLOS_FILE}" ]]; then
|
|
warn "SLOs file not found: ${SLOS_FILE}, using defaults"
|
|
fi
|
|
|
|
log "Enforcing SLOs from ${SLOS_FILE}"
|
|
log "Results path: ${RESULTS_PATH}"
|
|
|
|
# Initialize evaluation results
|
|
declare -A slo_results
|
|
VIOLATIONS=()
|
|
TOTAL_SLOS=0
|
|
PASSED_SLOS=0
|
|
|
|
# Define default SLOs
|
|
declare -A SLOS
|
|
SLOS["scan_time_p95_ms"]=30000
|
|
SLOS["scan_time_p99_ms"]=60000
|
|
SLOS["memory_peak_mb"]=2048
|
|
SLOS["cpu_time_seconds"]=120
|
|
SLOS["sbom_gen_time_ms"]=10000
|
|
SLOS["policy_eval_time_ms"]=5000
|
|
|
|
# Load SLOs from file if exists
|
|
if [[ -f "${SLOS_FILE}" ]]; then
|
|
while IFS=: read -r key value; do
|
|
key=$(echo "$key" | tr -d ' ')
|
|
value=$(echo "$value" | tr -d ' ')
|
|
if [[ -n "$key" ]] && [[ -n "$value" ]] && [[ "$key" != "#"* ]]; then
|
|
SLOS["$key"]=$value
|
|
log "Loaded SLO: ${key}=${value}"
|
|
fi
|
|
done < <(yq -r 'to_entries | .[] | "\(.key):\(.value.threshold // .value)"' "${SLOS_FILE}" 2>/dev/null || true)
|
|
fi
|
|
|
|
# Collect metrics from results
|
|
SCAN_TIMES=()
|
|
MEMORY_VALUES=()
|
|
CPU_TIMES=()
|
|
SBOM_TIMES=()
|
|
POLICY_TIMES=()
|
|
|
|
for result_file in "${RESULTS_PATH}"/*.json "${RESULTS_PATH}"/**/*.json; do
|
|
[[ -f "${result_file}" ]] || continue
|
|
|
|
log "Processing: ${result_file}"
|
|
|
|
# Extract metrics
|
|
SCAN_TIME=$(jq -r '.duration_ms // .scan_time_ms // empty' "${result_file}" 2>/dev/null || true)
|
|
MEMORY=$(jq -r '.peak_memory_mb // .memory_mb // empty' "${result_file}" 2>/dev/null || true)
|
|
CPU_TIME=$(jq -r '.cpu_time_seconds // .cpu_seconds // empty' "${result_file}" 2>/dev/null || true)
|
|
SBOM_TIME=$(jq -r '.sbom_generation_ms // empty' "${result_file}" 2>/dev/null || true)
|
|
POLICY_TIME=$(jq -r '.policy_evaluation_ms // empty' "${result_file}" 2>/dev/null || true)
|
|
|
|
[[ -n "${SCAN_TIME}" ]] && SCAN_TIMES+=("${SCAN_TIME}")
|
|
[[ -n "${MEMORY}" ]] && MEMORY_VALUES+=("${MEMORY}")
|
|
[[ -n "${CPU_TIME}" ]] && CPU_TIMES+=("${CPU_TIME}")
|
|
[[ -n "${SBOM_TIME}" ]] && SBOM_TIMES+=("${SBOM_TIME}")
|
|
[[ -n "${POLICY_TIME}" ]] && POLICY_TIMES+=("${POLICY_TIME}")
|
|
done
|
|
|
|
# Helper: calculate percentile from array
|
|
calc_percentile() {
|
|
local -n values=$1
|
|
local pct=$2
|
|
|
|
if [[ ${#values[@]} -eq 0 ]]; then
|
|
echo "0"
|
|
return
|
|
fi
|
|
|
|
IFS=$'\n' sorted=($(sort -n <<<"${values[*]}")); unset IFS
|
|
local n=${#sorted[@]}
|
|
local idx=$(echo "scale=0; ($n - 1) * $pct / 100" | bc)
|
|
echo "${sorted[$idx]}"
|
|
}
|
|
|
|
# Helper: calculate max from array
|
|
calc_max() {
|
|
local -n values=$1
|
|
|
|
if [[ ${#values[@]} -eq 0 ]]; then
|
|
echo "0"
|
|
return
|
|
fi
|
|
|
|
local max=0
|
|
for v in "${values[@]}"; do
|
|
if (( $(echo "$v > $max" | bc -l) )); then
|
|
max=$v
|
|
fi
|
|
done
|
|
echo "$max"
|
|
}
|
|
|
|
# Evaluate each SLO
|
|
evaluate_slo() {
|
|
local name=$1
|
|
local threshold=$2
|
|
local actual=$3
|
|
local unit=$4
|
|
|
|
((TOTAL_SLOS++))
|
|
|
|
local passed=true
|
|
local margin_pct=0
|
|
|
|
if (( $(echo "$actual > $threshold" | bc -l) )); then
|
|
passed=false
|
|
margin_pct=$(echo "scale=2; ($actual - $threshold) * 100 / $threshold" | bc)
|
|
VIOLATIONS+=("${name}: ${actual}${unit} exceeds threshold ${threshold}${unit} (+${margin_pct}%)")
|
|
warn "SLO VIOLATION: ${name} = ${actual}${unit} (threshold: ${threshold}${unit})"
|
|
else
|
|
((PASSED_SLOS++))
|
|
margin_pct=$(echo "scale=2; ($threshold - $actual) * 100 / $threshold" | bc)
|
|
log "SLO PASSED: ${name} = ${actual}${unit} (threshold: ${threshold}${unit}, margin: ${margin_pct}%)"
|
|
fi
|
|
|
|
echo "{\"slo_name\": \"${name}\", \"threshold\": ${threshold}, \"actual\": ${actual}, \"unit\": \"${unit}\", \"passed\": ${passed}, \"margin_pct\": ${margin_pct}}"
|
|
}
|
|
|
|
# Calculate actuals
|
|
SCAN_P95=$(calc_percentile SCAN_TIMES 95)
|
|
SCAN_P99=$(calc_percentile SCAN_TIMES 99)
|
|
MEMORY_MAX=$(calc_max MEMORY_VALUES)
|
|
CPU_MAX=$(calc_max CPU_TIMES)
|
|
SBOM_P95=$(calc_percentile SBOM_TIMES 95)
|
|
POLICY_P95=$(calc_percentile POLICY_TIMES 95)
|
|
|
|
# Run evaluations
|
|
SLO_SCAN_P95=$(evaluate_slo "Scan Time P95" "${SLOS[scan_time_p95_ms]}" "${SCAN_P95}" "ms")
|
|
SLO_SCAN_P99=$(evaluate_slo "Scan Time P99" "${SLOS[scan_time_p99_ms]}" "${SCAN_P99}" "ms")
|
|
SLO_MEMORY=$(evaluate_slo "Peak Memory" "${SLOS[memory_peak_mb]}" "${MEMORY_MAX}" "MB")
|
|
SLO_CPU=$(evaluate_slo "CPU Time" "${SLOS[cpu_time_seconds]}" "${CPU_MAX}" "s")
|
|
SLO_SBOM=$(evaluate_slo "SBOM Generation P95" "${SLOS[sbom_gen_time_ms]}" "${SBOM_P95}" "ms")
|
|
SLO_POLICY=$(evaluate_slo "Policy Evaluation P95" "${SLOS[policy_eval_time_ms]}" "${POLICY_P95}" "ms")
|
|
|
|
# Generate output
|
|
ALL_PASSED=true
|
|
if [[ ${#VIOLATIONS[@]} -gt 0 ]]; then
|
|
ALL_PASSED=false
|
|
fi
|
|
|
|
# Build violations JSON array
|
|
VIOLATIONS_JSON="[]"
|
|
if [[ ${#VIOLATIONS[@]} -gt 0 ]]; then
|
|
VIOLATIONS_JSON="["
|
|
for i in "${!VIOLATIONS[@]}"; do
|
|
[[ $i -gt 0 ]] && VIOLATIONS_JSON+=","
|
|
VIOLATIONS_JSON+="\"${VIOLATIONS[$i]}\""
|
|
done
|
|
VIOLATIONS_JSON+="]"
|
|
fi
|
|
|
|
OUTPUT=$(cat <<EOF
|
|
{
|
|
"timestamp": "$(date -u '+%Y-%m-%dT%H:%M:%SZ')",
|
|
"dry_run": false,
|
|
"results_path": "${RESULTS_PATH}",
|
|
"slos_file": "${SLOS_FILE}",
|
|
"slo_evaluations": {
|
|
"scan_time_p95": ${SLO_SCAN_P95},
|
|
"scan_time_p99": ${SLO_SCAN_P99},
|
|
"memory_peak_mb": ${SLO_MEMORY},
|
|
"cpu_time_seconds": ${SLO_CPU},
|
|
"sbom_gen_time_ms": ${SLO_SBOM},
|
|
"policy_eval_time_ms": ${SLO_POLICY}
|
|
},
|
|
"summary": {
|
|
"total_slos": ${TOTAL_SLOS},
|
|
"passed": ${PASSED_SLOS},
|
|
"failed": $((TOTAL_SLOS - PASSED_SLOS)),
|
|
"all_passed": ${ALL_PASSED},
|
|
"violations": ${VIOLATIONS_JSON}
|
|
}
|
|
}
|
|
EOF
|
|
)
|
|
|
|
# Output results
|
|
if [[ -n "${OUTPUT_FILE}" ]]; then
|
|
echo "${OUTPUT}" > "${OUTPUT_FILE}"
|
|
log "Results written to ${OUTPUT_FILE}"
|
|
else
|
|
echo "${OUTPUT}"
|
|
fi
|
|
|
|
# Strict mode: fail on violations
|
|
if [[ "${STRICT}" == "true" ]] && [[ "${ALL_PASSED}" == "false" ]]; then
|
|
error "Performance SLO violations detected"
|
|
for v in "${VIOLATIONS[@]}"; do
|
|
error " - ${v}"
|
|
done
|
|
exit 1
|
|
fi
|
|
|
|
exit 0
|