11#! /usr/bin/env bash
2- # OpenAPI Security Check (OWASP ZAP )
2+ # OpenAPI Security Lint (Spectral )
33#
4- # This script runs an OWASP ZAP API security scan against an OpenAPI
5- # specification located in the repository .
4+ # This script runs fast static security lint checks against an OpenAPI
5+ # specification using Spectral in Docker .
66#
77# It is designed to be:
8- # - Safe for CI (no side effects)
8+ # - Fast for CI and local runs
99# - Optional (exits successfully if no OpenAPI spec is present)
10- #
11- # The scan helps identify common API security issues early in the pipeline.
10+ # - Static (no running API server required)
1211
1312set -euo pipefail
1413
1514# Logging helpers
16- # All output goes to stderr for consistent CI logs
1715log () { printf -- " ** %s\n" " $* " >&2 ; }
1816error () { printf -- " ** ERROR: %s\n" " $* " >&2 ; }
1917fatal () {
2018 error " $@ "
2119 exit 1
2220}
2321
24- # Resolve script directory
25- # This allows the script to be run from any subdirectory.
2622SCRIPT_SOURCE=" ${BASH_SOURCE[0]-$0 } "
2723SCRIPT_DIR=" $( cd -- " $( dirname -- " ${SCRIPT_SOURCE} " ) " && pwd) "
2824
2925OPENAPI_PATH=" openapi"
26+ RULESET_FILE=" "
27+ RULESET_CONTAINER_PATH=" /tmp/spectral-security-ruleset.yaml"
3028
3129resolve_repo_root () {
32- # Prefer git root for local execution and subdirectory calls.
3330 if root=" $( git rev-parse --show-toplevel 2> /dev/null) " ; then
3431 printf ' %s\n' " ${root} "
3532 return 0
3633 fi
3734
38- # Fallback for checked-out scripts under a conventional ./scripts layout.
3935 if [ -d " ${SCRIPT_DIR} /../.git" ]; then
4036 (
4137 cd -- " ${SCRIPT_DIR} /.."
@@ -44,7 +40,6 @@ resolve_repo_root() {
4440 return 0
4541 fi
4642
47- # Last resort for piped execution (for example: curl | bash).
4843 pwd
4944}
5045
@@ -58,6 +53,13 @@ Options:
5853EOF
5954}
6055
56+ cleanup () {
57+ if [ -n " ${RULESET_FILE} " ]; then
58+ rm -f " ${RULESET_FILE} "
59+ fi
60+ }
61+ trap cleanup EXIT
62+
6163while getopts " :f:h" flag; do
6264 case " ${flag} " in
6365 f) OPENAPI_PATH=" ${OPTARG} " ;;
@@ -71,10 +73,8 @@ while getopts ":f:h" flag; do
7173done
7274
7375if [[ " ${OPENAPI_PATH} " = /* ]]; then
74- # Absolute paths are used as-is.
7576 OPENAPI_ABS_PATH=" ${OPENAPI_PATH} "
7677else
77- # Relative paths prefer repository root, then current working directory.
7878 REPO_ROOT=" $( resolve_repo_root) "
7979 REPO_OPENAPI_PATH=" ${REPO_ROOT} /${OPENAPI_PATH} "
8080 CWD_OPENAPI_PATH=" $( pwd) /${OPENAPI_PATH} "
8888 fi
8989fi
9090
91- # Allow extension fallback between .yml and .yaml .
91+ # Allow extension fallback among .yml, .yaml, and .json .
9292if [ ! -e " ${OPENAPI_ABS_PATH} " ]; then
93- # If the requested extension does not exist, try the sibling extension.
9493 if [[ " ${OPENAPI_ABS_PATH} " == * .yml ]] && [ -f " ${OPENAPI_ABS_PATH% .yml} .yaml" ]; then
9594 OPENAPI_ABS_PATH=" ${OPENAPI_ABS_PATH% .yml} .yaml"
95+ elif [[ " ${OPENAPI_ABS_PATH} " == * .yml ]] && [ -f " ${OPENAPI_ABS_PATH% .yml} .json" ]; then
96+ OPENAPI_ABS_PATH=" ${OPENAPI_ABS_PATH% .yml} .json"
9697 elif [[ " ${OPENAPI_ABS_PATH} " == * .yaml ]] && [ -f " ${OPENAPI_ABS_PATH% .yaml} .yml" ]; then
9798 OPENAPI_ABS_PATH=" ${OPENAPI_ABS_PATH% .yaml} .yml"
99+ elif [[ " ${OPENAPI_ABS_PATH} " == * .yaml ]] && [ -f " ${OPENAPI_ABS_PATH% .yaml} .json" ]; then
100+ OPENAPI_ABS_PATH=" ${OPENAPI_ABS_PATH% .yaml} .json"
101+ elif [[ " ${OPENAPI_ABS_PATH} " == * .json ]] && [ -f " ${OPENAPI_ABS_PATH% .json} .yaml" ]; then
102+ OPENAPI_ABS_PATH=" ${OPENAPI_ABS_PATH% .json} .yaml"
103+ elif [[ " ${OPENAPI_ABS_PATH} " == * .json ]] && [ -f " ${OPENAPI_ABS_PATH% .json} .yml" ]; then
104+ OPENAPI_ABS_PATH=" ${OPENAPI_ABS_PATH% .json} .yml"
98105 fi
99106fi
100107
@@ -105,23 +112,45 @@ elif [ -d "${OPENAPI_ABS_PATH}" ]; then
105112 OPENAPI_SPEC_FILE=" ${OPENAPI_ABS_PATH} /openapi.yaml"
106113 elif [ -f " ${OPENAPI_ABS_PATH} /openapi.yml" ]; then
107114 OPENAPI_SPEC_FILE=" ${OPENAPI_ABS_PATH} /openapi.yml"
115+ elif [ -f " ${OPENAPI_ABS_PATH} /openapi.json" ]; then
116+ OPENAPI_SPEC_FILE=" ${OPENAPI_ABS_PATH} /openapi.json"
108117 else
109- log " ❗ OpenAPI spec not found in directory ${OPENAPI_ABS_PATH} — skipping security scan ."
118+ log " ❗ OpenAPI spec not found in directory ${OPENAPI_ABS_PATH} — skipping security lint ."
110119 exit 0
111120 fi
112121else
113- log " ❗ OpenAPI path not found — skipping security scan ."
122+ log " ❗ OpenAPI path not found — skipping security lint ."
114123 exit 0
115124fi
116125
117- # Run OWASP ZAP API scan in a Docker container
118- #
119- # - Mounts the OpenAPI file into the container
120- # - Uses the OpenAPI specification as the scan target
121- # - Fails the script if security issues are detected
122- #
123- # The container is removed after execution to keep the environment clean
124- docker run --rm --name " check-openapi-security" \
125- -v " ${OPENAPI_SPEC_FILE} :/app/openapi.yaml" \
126- -t zaproxy/zap-stable:latest zap-api-scan.py \
127- -t /app/openapi.yaml -f openapi
126+ RULESET_FILE=" $( mktemp " ${TMPDIR:-/ tmp} /spectral-security-ruleset.XXXXXX.yaml" ) "
127+ cat > " ${RULESET_FILE} " << 'EOF '
128+ extends:
129+ - spectral:oas
130+ rules:
131+ security-requirement-defined:
132+ description: "Define security requirements at root and/or operation level."
133+ severity: error
134+ given:
135+ - "$"
136+ then:
137+ field: security
138+ function: truthy
139+ no-http-server-urls:
140+ description: "Server URLs should use HTTPS."
141+ severity: error
142+ given: "$.servers[*].url"
143+ then:
144+ function: pattern
145+ functionOptions:
146+ match: "^https://"
147+ EOF
148+
149+ OPENAPI_SPEC_BASENAME=" $( basename " ${OPENAPI_SPEC_FILE} " ) "
150+ docker run --rm --name " check-openapi-security-lint" \
151+ -v " ${OPENAPI_SPEC_FILE} :/app/${OPENAPI_SPEC_BASENAME} " \
152+ -v " ${RULESET_FILE} :${RULESET_CONTAINER_PATH} " \
153+ stoplight/spectral:latest lint " /app/${OPENAPI_SPEC_BASENAME} " \
154+ --fail-severity error \
155+ --display-only-failures \
156+ --ruleset " ${RULESET_CONTAINER_PATH} "
0 commit comments