Skip to content

Commit 57b6b17

Browse files
committed
changes
- add tests cache on/off switch - openapi scripts support json files - update docc scripts - openapi-security uses stoplight/spectral:latest - more bats tests
1 parent 4761414 commit 57b6b17

12 files changed

Lines changed: 709 additions & 59 deletions

.github/workflows/extra_soundness.yml

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,10 @@ on:
1515
type: boolean
1616
description: "Boolean to enable run tests with .build cache."
1717
default: false
18+
tests_cache_enabled:
19+
type: boolean
20+
description: "Boolean to enable .build cache usage inside the run tests job."
21+
default: true
1822
run_tests_swift_versions:
1923
type: string
2024
description: "List of Swift versions to test with."
@@ -126,11 +130,13 @@ jobs:
126130
ssh-keyscan github.com >> ~/.ssh/known_hosts
127131
128132
- name: Install zstd
133+
if: ${{ inputs.tests_cache_enabled }}
129134
run: |
130135
sudo apt-get update -y
131136
sudo apt-get install -y zstd
132137
133138
- name: Restore .build
139+
if: ${{ inputs.tests_cache_enabled }}
134140
id: "restore-build"
135141
uses: actions/cache/restore@v4
136142
with:
@@ -142,11 +148,16 @@ jobs:
142148
run: swift build --build-tests
143149

144150
- name: Cache .build
145-
if: steps.restore-build.outputs.cache-hit != 'true'
151+
if: ${{ inputs.tests_cache_enabled && steps.restore-build.outputs.cache-hit != 'true' }}
146152
uses: actions/cache/save@v4
147153
with:
148154
path: .build
149155
key: "swiftpm-tests-build-${{ runner.os }}-${{ matrix.swift }}-${{ github.event.pull_request.base.sha || github.event.after }}"
150156

151157
- name: Run unit tests
152-
run: swift test --skip-build --parallel
158+
run: |
159+
if [ "${{ inputs.tests_cache_enabled }}" = "true" ]; then
160+
swift test --skip-build --parallel
161+
else
162+
swift test --parallel
163+
fi

README.md

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ This workflow provides configurable, robust checks and testing:
2626
* **Optional Local Swift Dependency Checks**: Checks for accidental `.package(path:)` usage.
2727
* **Optional Swift Headers Check**: Validates Swift source file headers using a strict 5-line format and respects `.swiftheaderignore`.
2828
* **Optional DocC Warnings Check**: Runs DocC analysis with `--warnings-as-errors` and fails on warnings.
29-
* **Optional Swift Test Execution**: Runs tests using **`.build` caching** for efficiency.
29+
* **Optional Swift Test Execution**: Runs tests with optional **`.build` caching** for efficiency.
3030
* **Optional Swift Package Validation**: Validates Swift package structure, settings, and conventions to ensure consistency.
3131
* **Multi-Version Support**: Tests across multiple Swift versions, configurable via input (defaulting to `["6.0", "6.1"]`).
3232
* **SSH Support**: Includes steps to set up **SSH credentials** (via the `SSH_PRIVATE_KEY` secret) for projects relying on private Git dependencies.
@@ -38,7 +38,8 @@ This workflow provides configurable, robust checks and testing:
3838
| `local_swift_dependencies_check_enabled` | Enables local Swift dependency check | `false` |
3939
| `headers_check_enabled` | Enables Swift headers validation | `false` |
4040
| `docc_warnings_check_enabled` | Enables DocC warnings check | `false` |
41-
| `run_tests_with_cache_enabled` | Enables Swift tests with `.build` cache | `false` |
41+
| `run_tests_with_cache_enabled` | Enables the Swift test job | `false` |
42+
| `tests_cache_enabled` | Enables `.build` cache restore/save in the Swift test job | `true` |
4243
| `run_tests_swift_versions` | Swift versions to test | `["6.1","6.2"]` |
4344
| `swift_package_validation_enabled` | Runs Swift package validation in the repository. | `false` |
4445

@@ -53,6 +54,7 @@ jobs:
5354
headers_check_enabled: true
5455
docc_warnings_check_enabled: true
5556
run_tests_with_cache_enabled: true
57+
tests_cache_enabled: true
5658
run_tests_swift_versions: '["6.1","6.2"]'
5759
swift_package_validation_enabled: true
5860
```
@@ -257,14 +259,16 @@ curl -s $(baseUrl)/check-local-swift-dependencies.sh | bash
257259

258260
#### Purpose
259261

260-
Runs a security scan of an OpenAPI specification using OWASP ZAP.
262+
Runs fast static OpenAPI security lint checks using Spectral.
261263

262264
#### Behavior
263265

264266
* Executes inside Docker
265267
* Accepts an OpenAPI file or directory (default: `openapi`)
266268
* Relative `-f` paths are resolved from the git repository root first, then current working directory
267-
* For file paths, supports `.yml`/`.yaml` extension fallback
269+
* For file paths, supports `.yml`/`.yaml`/`.json` extension fallback
270+
* For directory paths, resolves `openapi.yaml`, then `openapi.yml`, then `openapi.json`
271+
* Performs static linting only (no running API server required)
268272
* Skips execution if no OpenAPI specification can be resolved
269273

270274
#### Parameters
@@ -298,12 +302,17 @@ Validates an OpenAPI specification for schema correctness.
298302
* Runs the OpenAPI validator in Docker
299303
* Accepts an OpenAPI file or directory (default: `openapi`)
300304
* Relative `-f` paths are resolved from the git repository root first, then current working directory
301-
* For file paths, supports `.yml`/`.yaml` extension fallback
305+
* For file paths, supports `.yml`/`.yaml`/`.json` extension fallback
306+
* For directory paths, resolves `openapi.yaml`, then `openapi.yml`, then `openapi.json`
307+
* Supports debug tracing via `-d` (prints resolved paths and command trace)
308+
* Supports `--detailed` to run additional Spectral diagnostics when validation fails
302309
* Skips execution if no OpenAPI specification can be resolved
303310

304311
#### Parameters
305312

306313
* `-f <path>` – OpenAPI file or directory path
314+
* `-d` – enable debug tracing
315+
* `--detailed` – run additional detailed diagnostics on validation failure
307316

308317
#### Ignore files
309318

@@ -498,11 +507,15 @@ Installs the Swift OpenAPI Generator CLI tool.
498507
#### Behavior
499508

500509
* Builds from source
501-
* Supports version pinning
510+
* Installs the latest available tag by default
511+
* Supports version pinning via `-v`
512+
* Validates required tools (`git`, `curl`, `tar`, `swift`, `install`)
513+
* Uses fail-fast download behavior for release archives
502514

503515
#### Parameters
504516

505517
* `-v <version>` – install a specific version
518+
* `-h` – show usage help
506519

507520
#### Ignore files
508521

@@ -593,7 +606,7 @@ Serves OpenAPI documentation locally using Docker.
593606
* Accepts an OpenAPI file or directory (default: `openapi`)
594607
* Relative `-f` paths are resolved from the git repository root first, then current working directory
595608
* If a file is provided, mounts its parent directory
596-
* For file paths, supports `.yml`/`.yaml` extension fallback
609+
* For file paths, supports `.yml`/`.yaml`/`.json` extension fallback
597610
* Runs Nginx in the foreground
598611
* Exposes documentation over HTTP
599612

examples/tests.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,5 +43,6 @@ jobs:
4343
local_swift_dependencies_check_enabled : true
4444
docc_warnings_check_enabled : true
4545
run_tests_with_cache_enabled : true
46+
tests_cache_enabled : true
4647
headers_check_enabled : true
4748
run_tests_swift_versions: '["6.1","6.2"]'

scripts/check-docc-warnings.sh

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ PACKAGE_FILE="Package.swift"
4040
INJECT_MARKER='// [docc-plugin-placeholder]'
4141
# Dependency line injected immediately after the marker
4242
DOCC_DEP=' .package(url: "https://github.com/apple/swift-docc-plugin", from: "1.4.0"),'
43+
DOCC_PLUGIN_INJECTED=false
4344

4445
# Git safety (local runs only)
4546
#
@@ -72,6 +73,31 @@ reset_git_after_analysis() {
7273
fi
7374
}
7475

76+
restore_injected_package_manifest() {
77+
if [ "${DOCC_PLUGIN_INJECTED}" != "true" ]; then
78+
return 0
79+
fi
80+
81+
rm -f "$PACKAGE_FILE.tmp"
82+
83+
if [ -n "${GITHUB_ACTIONS:-}" ]; then
84+
return 0
85+
fi
86+
87+
if git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
88+
log "Restoring ${PACKAGE_FILE} after failed DocC analysis"
89+
git checkout -- "$PACKAGE_FILE" || true
90+
fi
91+
}
92+
93+
cleanup_on_exit() {
94+
local rc=$?
95+
if [ "$rc" -ne 0 ]; then
96+
restore_injected_package_manifest
97+
fi
98+
}
99+
trap cleanup_on_exit EXIT
100+
75101
# Ensures that swift-docc-plugin is available to SwiftPM.
76102
#
77103
# This function injects the dependency using a fixed marker
@@ -125,6 +151,7 @@ ensure_docc_plugin() {
125151
' "$PACKAGE_FILE" >"$PACKAGE_FILE.tmp"
126152

127153
mv "$PACKAGE_FILE.tmp" "$PACKAGE_FILE"
154+
DOCC_PLUGIN_INJECTED=true
128155

129156
# Validate manifest after mutation
130157
swift package dump-package >/dev/null ||

scripts/check-openapi-security.sh

Lines changed: 58 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,41 +1,37 @@
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

1312
set -euo pipefail
1413

1514
# Logging helpers
16-
# All output goes to stderr for consistent CI logs
1715
log() { printf -- "** %s\n" "$*" >&2; }
1816
error() { printf -- "** ERROR: %s\n" "$*" >&2; }
1917
fatal() {
2018
error "$@"
2119
exit 1
2220
}
2321

24-
# Resolve script directory
25-
# This allows the script to be run from any subdirectory.
2622
SCRIPT_SOURCE="${BASH_SOURCE[0]-$0}"
2723
SCRIPT_DIR="$(cd -- "$(dirname -- "${SCRIPT_SOURCE}")" && pwd)"
2824

2925
OPENAPI_PATH="openapi"
26+
RULESET_FILE=""
27+
RULESET_CONTAINER_PATH="/tmp/spectral-security-ruleset.yaml"
3028

3129
resolve_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:
5853
EOF
5954
}
6055

56+
cleanup() {
57+
if [ -n "${RULESET_FILE}" ]; then
58+
rm -f "${RULESET_FILE}"
59+
fi
60+
}
61+
trap cleanup EXIT
62+
6163
while getopts ":f:h" flag; do
6264
case "${flag}" in
6365
f) OPENAPI_PATH="${OPTARG}" ;;
@@ -71,10 +73,8 @@ while getopts ":f:h" flag; do
7173
done
7274

7375
if [[ "${OPENAPI_PATH}" = /* ]]; then
74-
# Absolute paths are used as-is.
7576
OPENAPI_ABS_PATH="${OPENAPI_PATH}"
7677
else
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}"
@@ -88,13 +88,20 @@ else
8888
fi
8989
fi
9090

91-
# Allow extension fallback between .yml and .yaml.
91+
# Allow extension fallback among .yml, .yaml, and .json.
9292
if [ ! -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
99106
fi
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
112121
else
113-
log "❗ OpenAPI path not found — skipping security scan."
122+
log "❗ OpenAPI path not found — skipping security lint."
114123
exit 0
115124
fi
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

Comments
 (0)