-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathaction.yml
More file actions
274 lines (254 loc) · 13 KB
/
Copy pathaction.yml
File metadata and controls
274 lines (254 loc) · 13 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
name: Symfony Security Auditor
description: AI-powered multi-agent security audit for Symfony applications — provider-agnostic via symfony/ai.
author: Vincent Amstoutz
branding:
icon: shield
color: purple
inputs:
mode:
description: 'How to run the audit: "bundle" (require the Symfony bundle in the target project via Composer, then run `bin/console audit:run`) or "standalone" (download the checksum-verified standalone binary — no bundle dependency needed in the target project). Composer + a host PHP are still required in standalone mode to fetch the provider bridge via `init`.'
required: false
default: bundle
project-path:
description: Path to the Symfony project to audit.
required: false
default: "."
format:
description: "Output format: console, executive, json, sarif, html, markdown, junit, github, or github-comment."
required: false
default: sarif
output:
description: File path to write the report to. Leave empty to print to stdout.
required: false
default: report.sarif
baseline:
description: Path to a baseline file of accepted-finding fingerprints to suppress.
required: false
default: ""
generate-baseline:
description: If set to a path, run the audit, write the current findings to that baseline file, and exit 0.
required: false
default: ""
since:
description: Diff mode — audit only files changed against this git ref (e.g. origin/main).
required: false
default: ""
fail-on:
description: Minimum aggregate risk level (safe, low, medium, high, critical) that makes the audit exit 1. Leave empty to use the project's audit.fail_on configuration (default critical).
required: false
default: ""
min-score:
description: Minimum normalized score (0-100) below which the audit exits 1. Independent of fail-on — the audit fails when either gate trips. Leave empty to gate on the risk level alone.
required: false
default: ""
comment-pr:
description: Whether to post the report as a single pull-request comment, editing the previous one in place on every rerun instead of appending. Requires format github-comment plus a non-empty output, a pull_request event, and pull-requests write permission on the calling job's GITHUB_TOKEN (passed as the GITHUB_TOKEN env var).
required: false
default: "false"
update-badge:
description: Whether to write a shields.io endpoint-schema JSON file carrying the report's grade, for a live security badge in your README. Requires format json and a non-empty output. Only runs on push events. The file is written but never committed — that stays your workflow's decision.
required: false
default: "false"
badge-path:
description: Where to write the shields.io endpoint JSON when update-badge is true.
required: false
default: .github/security-auditor-badge.json
extra-args:
description: Additional raw arguments appended verbatim to the audit:run invocation (e.g. "--path=src --no-cache").
required: false
default: ""
php-version:
description: PHP version to install (ignored when setup-php is false).
required: false
default: "8.3"
setup-php:
description: Whether this action installs PHP via shivammathur/setup-php. Set to false if your workflow already set PHP up. Still required in standalone mode — a host PHP + Composer are needed to fetch the provider bridge via `init`.
required: false
default: "true"
install-dependencies:
description: Whether this action runs `composer install` before auditing. Only applies in bundle mode; ignored in standalone mode. Set to false if your workflow already installed dependencies.
required: false
default: "true"
working-directory:
description: Directory to run composer and the console command from.
required: false
default: "."
outputs:
exit-code:
description: The audit command's exit code (0 = passed, 1 = failed the fail-on or min-score gate, 2 = budget aborted).
value: ${{ steps.audit.outputs.exit-code }}
report-path:
description: The path the report was written to (mirrors the output input; empty when the report was printed to stdout).
value: ${{ steps.audit.outputs.report-path }}
findings-count:
description: Total number of findings in the report. Only populated when format is json (empty for other formats).
value: ${{ steps.audit.outputs.findings-count }}
highest-severity:
description: The report's aggregate risk level (safe, low, medium, high, critical). Only populated when format is json (empty for other formats).
value: ${{ steps.audit.outputs.highest-severity }}
grade:
description: The report's A-F security grade. Only populated when format is json (empty for other formats).
value: ${{ steps.audit.outputs.grade }}
badge-path:
description: The path the shields.io badge endpoint JSON was written to (empty when update-badge is false or no grade was available).
value: ${{ steps.badge.outputs.badge-path }}
comment-url:
description: The API URL of the pull-request comment that was posted or updated (empty when comment-pr is false or no comment was posted).
value: ${{ steps.comment.outputs.comment-url }}
runs:
using: composite
steps:
- name: Set up PHP
if: ${{ inputs.setup-php == 'true' }}
uses: shivammathur/setup-php@f3e473d116dcccaddc5834248c87452386958240 # 2.37.2
with:
php-version: ${{ inputs.php-version }}
coverage: none
- name: Install Composer dependencies
if: ${{ inputs.install-dependencies == 'true' && inputs.mode == 'bundle' }}
shell: bash
working-directory: ${{ inputs.working-directory }}
run: composer install --no-interaction --prefer-dist --no-progress
- name: Install the standalone binary
if: ${{ inputs.mode == 'standalone' }}
shell: bash
env:
SSA_INSTALL_DIR: ${{ github.action_path }}/.bin
SSA_ACTION_REF: ${{ github.action_ref }}
run: | # zizmor: ignore[github-env] SSA_INSTALL_DIR is derived from github.action_path, not attacker input
set -euo pipefail
# Run the install script from this action's own checkout, not a live
# fetch of main — a pinned `uses: …@<ref>` must not execute whatever
# install.sh currently is on main. When pinned to a release tag, install
# that exact version's binary; branch/sha pins fall back to the latest
# release (no binary is published per arbitrary ref).
if printf '%s' "$SSA_ACTION_REF" | grep -qE '^v?[0-9]+\.[0-9]+\.[0-9]+$'; then
export SSA_VERSION="${SSA_ACTION_REF#v}"
fi
sh "$GITHUB_ACTION_PATH/install.sh"
echo "$SSA_INSTALL_DIR" >> "$GITHUB_PATH"
- name: Configure the standalone binary
if: ${{ inputs.mode == 'standalone' }}
shell: bash
run: symfony-security-auditor init --no-interaction
- name: Run security audit
id: audit
shell: bash
working-directory: ${{ inputs.working-directory }}
env:
SSA_MODE: ${{ inputs.mode }}
SSA_PROJECT_PATH: ${{ inputs.project-path }}
SSA_FORMAT: ${{ inputs.format }}
SSA_OUTPUT: ${{ inputs.output }}
SSA_BASELINE: ${{ inputs.baseline }}
SSA_GENERATE_BASELINE: ${{ inputs.generate-baseline }}
SSA_SINCE: ${{ inputs.since }}
SSA_FAIL_ON: ${{ inputs.fail-on }}
SSA_MIN_SCORE: ${{ inputs.min-score }}
SSA_EXTRA_ARGS: ${{ inputs.extra-args }}
run: |
set -uo pipefail
args=("$SSA_PROJECT_PATH" "--format=$SSA_FORMAT")
if [ -n "$SSA_OUTPUT" ]; then args+=("--output=$SSA_OUTPUT"); fi
if [ -n "$SSA_BASELINE" ]; then args+=("--baseline=$SSA_BASELINE"); fi
if [ -n "$SSA_GENERATE_BASELINE" ]; then args+=("--generate-baseline=$SSA_GENERATE_BASELINE"); fi
if [ -n "$SSA_SINCE" ]; then args+=("--since=$SSA_SINCE"); fi
if [ -n "$SSA_FAIL_ON" ]; then args+=("--fail-on=$SSA_FAIL_ON"); fi
if [ -n "$SSA_MIN_SCORE" ]; then args+=("--min-score=$SSA_MIN_SCORE"); fi
# shellcheck disable=SC2206 -- intentional word-splitting of caller-supplied extra args
if [ -n "$SSA_EXTRA_ARGS" ]; then args+=($SSA_EXTRA_ARGS); fi
# Capture the audit exit code without letting the inherited errexit
# abort before the outputs below are written — the fail-on gate exit 1
# (and budget-abort exit 2) are the cases those outputs exist to report.
exit_code=0
if [ "$SSA_MODE" = "standalone" ]; then
symfony-security-auditor audit "${args[@]}" || exit_code=$?
else
php bin/console audit:run "${args[@]}" || exit_code=$?
fi
echo "exit-code=$exit_code" >> "$GITHUB_OUTPUT"
echo "report-path=$SSA_OUTPUT" >> "$GITHUB_OUTPUT"
if [ "$SSA_FORMAT" = "json" ] && [ -n "$SSA_OUTPUT" ] && [ -f "$SSA_OUTPUT" ]; then
echo "findings-count=$(jq -r '.total_vulnerabilities' "$SSA_OUTPUT")" >> "$GITHUB_OUTPUT"
echo "highest-severity=$(jq -r '.risk_level' "$SSA_OUTPUT")" >> "$GITHUB_OUTPUT"
echo "grade=$(jq -r '.grade' "$SSA_OUTPUT")" >> "$GITHUB_OUTPUT"
fi
exit "$exit_code"
# `always()` matters: the audit step ends with `exit "$exit_code"`, so a
# tripped fail-on gate fails it and would otherwise skip this step —
# leaving the badge showing the last passing grade exactly when it regressed.
- name: Emit the shields.io badge endpoint
id: badge
if: ${{ always() && inputs.update-badge == 'true' && github.event_name == 'push' }}
shell: bash
working-directory: ${{ inputs.working-directory }}
env:
SSA_BADGE_PATH: ${{ inputs.badge-path }}
SSA_GRADE: ${{ steps.audit.outputs.grade }}
run: |
set -euo pipefail
if [ -z "$SSA_GRADE" ] || [ "$SSA_GRADE" = "null" ]; then
echo "::warning::update-badge found no grade to render, so no badge was written. It needs the audit to have produced a JSON report: set format to json and output to a file path."
exit 0
fi
case "$SSA_GRADE" in
A) color=brightgreen ;;
B) color=green ;;
C) color=yellow ;;
D) color=orange ;;
F) color=red ;;
*)
echo "::warning::update-badge does not recognize the grade '$SSA_GRADE', so no badge was written."
exit 0
;;
esac
mkdir -p "$(dirname "$SSA_BADGE_PATH")"
jq -n --arg message "$SSA_GRADE" --arg color "$color" \
'{schemaVersion: 1, label: "security audit", message: $message, color: $color}' \
> "$SSA_BADGE_PATH"
echo "badge-path=$SSA_BADGE_PATH" >> "$GITHUB_OUTPUT"
echo "Wrote the shields.io badge endpoint for grade $SSA_GRADE to $SSA_BADGE_PATH."
# `always()` for the same reason as the badge step: the audit exits 1 on a
# tripped gate, and that is exactly the run whose summary reviewers need.
- name: Post or update the pull-request comment
id: comment
if: ${{ always() && inputs.comment-pr == 'true' && github.event_name == 'pull_request' }}
shell: bash
working-directory: ${{ inputs.working-directory }}
env:
SSA_OUTPUT: ${{ inputs.output }}
SSA_FORMAT: ${{ inputs.format }}
SSA_COMMENTS_URL: ${{ github.event.pull_request.comments_url }}
run: |
set -euo pipefail
if [ "$SSA_FORMAT" != "github-comment" ]; then
echo "::warning::comment-pr needs the github-comment format to build a comment body; format is '$SSA_FORMAT'. No comment was posted."
exit 0
fi
if [ -z "$SSA_OUTPUT" ] || [ ! -f "$SSA_OUTPUT" ]; then
echo "::warning::comment-pr found no report file at output '$SSA_OUTPUT'. No comment was posted."
exit 0
fi
if [ -z "${GH_TOKEN:-${GITHUB_TOKEN:-}}" ]; then
echo "::warning::comment-pr needs GITHUB_TOKEN in the calling job's env, and pull-requests write permission on it. No comment was posted."
exit 0
fi
# The renderer opens the body with this marker so a rerun edits its own
# comment instead of appending another one to the thread. The marker is
# public, so the author is checked too — otherwise anyone able to
# comment on the PR could post it first and hijack this job's write
# access to edit their own comment on every rerun.
marker='<!-- symfony-security-auditor:pr-comment -->'
matches="$RUNNER_TEMP/ssa-comment-matches.txt"
gh api --paginate "$SSA_COMMENTS_URL" \
--jq ".[] | select(.user.login == \"github-actions[bot]\" and (.body | contains(\"$marker\"))) | .url" > "$matches"
existing="$(head -n 1 "$matches")"
payload="$RUNNER_TEMP/ssa-comment-payload.json"
jq -Rs '{body: .}' < "$SSA_OUTPUT" > "$payload"
if [ -n "$existing" ]; then
url="$(gh api -X PATCH "$existing" --input "$payload" --jq '.url')"
else
url="$(gh api -X POST "$SSA_COMMENTS_URL" --input "$payload" --jq '.url')"
fi
echo "comment-url=$url" >> "$GITHUB_OUTPUT"
echo "Posted the audit summary to $url."