Skip to content

Commit 4151393

Browse files
Merge pull request #3 from kernel/ulzii/socket-centric-vuln-remediation
Harden vulnerability remediation workflow
2 parents 1995c9b + 86e59df commit 4151393

5 files changed

Lines changed: 1029 additions & 110 deletions

File tree

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
name: Vulnerability Remediation Self-Test
2+
3+
on:
4+
pull_request:
5+
paths:
6+
- '.github/workflows/vuln-remediation.yml'
7+
- '.github/workflows/vuln-remediation-self-test.yml'
8+
- '.github/workflows/vuln-remediation/**'
9+
- 'scripts/vuln_remediation.py'
10+
- 'scripts/test_vuln_remediation.py'
11+
push:
12+
branches:
13+
- main
14+
paths:
15+
- '.github/workflows/vuln-remediation.yml'
16+
- '.github/workflows/vuln-remediation-self-test.yml'
17+
- '.github/workflows/vuln-remediation/**'
18+
- 'scripts/vuln_remediation.py'
19+
- 'scripts/test_vuln_remediation.py'
20+
workflow_dispatch:
21+
inputs:
22+
run-live-remediation:
23+
description: Run the reusable remediation workflow against kernel/security-workflows
24+
type: boolean
25+
required: false
26+
default: false
27+
28+
jobs:
29+
tests:
30+
name: Remediation helper tests
31+
runs-on: ubuntu-latest
32+
steps:
33+
- name: Checkout repository
34+
uses: actions/checkout@v4
35+
36+
- name: Run unit and fixture tests
37+
run: python3 -m unittest scripts.test_vuln_remediation
38+
39+
self-remediation:
40+
name: Manual self remediation
41+
if: github.event_name == 'workflow_dispatch' && inputs['run-live-remediation']
42+
uses: ./.github/workflows/vuln-remediation.yml
43+
with:
44+
security-workflows-ref: ${{ github.ref_name }}
45+
secrets: inherit

‎.github/workflows/vuln-remediation.yml‎

Lines changed: 141 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,11 @@ on:
2323
type: string
2424
required: false
2525
default: '3.11'
26+
security-workflows-ref:
27+
description: Ref of kernel/security-workflows to fetch shared scripts and prompts from
28+
type: string
29+
required: false
30+
default: main
2631

2732
jobs:
2833
scan:
@@ -112,36 +117,69 @@ jobs:
112117
name: socket-report
113118
path: socket-report.json
114119

120+
- name: Normalize Socket report
121+
run: |
122+
curl -fsSL https://raw.githubusercontent.com/kernel/security-workflows/${{ inputs.security-workflows-ref }}/scripts/vuln_remediation.py -o /tmp/vuln_remediation.py
123+
python3 /tmp/vuln_remediation.py normalize \
124+
--input socket-report.json \
125+
--output remediation-input.json
126+
127+
- name: Upload normalized remediation input
128+
uses: actions/upload-artifact@v4
129+
with:
130+
name: remediation-input
131+
path: remediation-input.json
132+
115133
triage:
116134
needs: scan
117135
runs-on: ubuntu-latest
118136
steps:
119137
- name: Checkout repository
120138
uses: actions/checkout@v4
121139

122-
- name: Download scan report
140+
- name: Download normalized remediation input
123141
uses: actions/download-artifact@v4
124142
with:
125-
name: socket-report
143+
name: remediation-input
126144

127-
- name: Install Cursor CLI
128-
run: |
129-
curl https://cursor.com/install -fsS | bash
130-
echo "$HOME/.cursor/bin" >> $GITHUB_PATH
145+
- name: Install Socket CLI
146+
run: npm install -g @socketsecurity/cli
131147

132-
- name: Triage alerts
148+
- name: Ask Socket for a fix plan
133149
env:
134-
CURSOR_API_KEY: ${{ secrets.CURSOR_API_KEY }}
135150
SOCKET_SECURITY_API_KEY: ${{ secrets.SOCKET_API_TOKEN }}
136151
run: |
137-
export DATE="$(date -u +%Y-%m-%d)"
138-
curl -fsSL https://raw.githubusercontent.com/kernel/security-workflows/main/.github/workflows/vuln-remediation/triage-prompt.md | envsubst '${GITHUB_REPOSITORY} ${DATE}' | agent -p --model ${{ secrets.CURSOR_PREFERRED_MODEL }} --trust --force --output-format=text
152+
set +e
153+
socket fix . \
154+
--no-apply-fixes \
155+
--no-major-updates \
156+
--show-affected-direct-dependencies \
157+
--json \
158+
--output-file socket-fix-plan.json
159+
FIX_PLAN_EXIT=$?
160+
set -e
161+
if [ $FIX_PLAN_EXIT -ne 0 ] || ! jq empty socket-fix-plan.json 2>/dev/null; then
162+
echo "::warning::Socket fix planning failed; manager will defer unplanned or insufficiently described fixes."
163+
echo '{}' > socket-fix-plan.json
164+
fi
165+
166+
- name: Build remediation context
167+
run: |
168+
curl -fsSL https://raw.githubusercontent.com/kernel/security-workflows/${{ inputs.security-workflows-ref }}/scripts/vuln_remediation.py -o /tmp/vuln_remediation.py
169+
python3 /tmp/vuln_remediation.py build-context \
170+
--input remediation-input.json \
171+
--fix-plan socket-fix-plan.json \
172+
--output remediation-context.json
173+
cp remediation-context.json triage-result.json
139174
140175
- name: Upload triage results
141176
uses: actions/upload-artifact@v4
142177
with:
143178
name: triage-result
144-
path: triage-result.json
179+
path: |
180+
triage-result.json
181+
remediation-context.json
182+
socket-fix-plan.json
145183
146184
fix:
147185
needs: triage
@@ -168,11 +206,11 @@ jobs:
168206
- name: Check for fixable alerts
169207
id: check
170208
run: |
171-
if ! [ -f triage-result.json ]; then
209+
if ! [ -f remediation-context.json ]; then
172210
echo "skip=true" >> $GITHUB_OUTPUT
173211
exit 0
174212
fi
175-
FIX_COUNT=$(jq '[.alerts[] | select(.category == "fix")] | length' triage-result.json 2>/dev/null || echo "0")
213+
FIX_COUNT=$(jq '.fixes | length' remediation-context.json 2>/dev/null || echo "0")
176214
if [ "$FIX_COUNT" = "0" ]; then
177215
echo "skip=true" >> $GITHUB_OUTPUT
178216
else
@@ -234,18 +272,83 @@ jobs:
234272
env:
235273
CURSOR_API_KEY: ${{ secrets.CURSOR_API_KEY }}
236274
GOPRIVATE: github.com/kernel/*
237-
GH_TOKEN: ${{ steps.app-token.outputs.token }}
238275
SOCKET_SECURITY_API_KEY: ${{ secrets.SOCKET_API_TOKEN }}
239276
run: |
240-
export DATE="$(date -u +%Y-%m-%d)"
241-
curl -fsSL https://raw.githubusercontent.com/kernel/security-workflows/main/.github/workflows/vuln-remediation/fix-prompt.md | envsubst '${GITHUB_REPOSITORY} ${DATE}' | agent -p --model ${{ secrets.CURSOR_PREFERRED_MODEL }} --workspace . --trust --force --output-format=text
277+
FIX_IDS=$(jq -r '.fixes[].id' remediation-context.json | paste -sd, -)
278+
set +e
279+
socket fix . \
280+
--id "$FIX_IDS" \
281+
--no-major-updates \
282+
--json \
283+
--output-file socket-fix-apply.json
284+
SOCKET_FIX_EXIT=$?
285+
set -e
286+
if [ $SOCKET_FIX_EXIT -ne 0 ]; then
287+
echo "::warning::Socket fix failed; invoking bounded fallback agent."
288+
fi
289+
290+
if git diff --quiet; then
291+
export DATE="$(date -u +%Y-%m-%d)"
292+
curl -fsSL https://raw.githubusercontent.com/kernel/security-workflows/${{ inputs.security-workflows-ref }}/.github/workflows/vuln-remediation/fix-prompt.md | envsubst '${GITHUB_REPOSITORY} ${DATE}' | agent -p --model ${{ secrets.CURSOR_PREFERRED_MODEL }} --workspace . --trust --force --output-format=text
293+
fi
294+
295+
- name: Validate remediation diff
296+
id: diff
297+
if: steps.check.outputs.skip != 'true'
298+
run: |
299+
curl -fsSL https://raw.githubusercontent.com/kernel/security-workflows/${{ inputs.security-workflows-ref }}/scripts/vuln_remediation.py -o /tmp/vuln_remediation.py
300+
git diff --name-only > changed-files.txt
301+
if [ ! -s changed-files.txt ]; then
302+
echo '{"fixed": [], "reverted": [], "summary": "No dependency changes were produced."}' > fix-result.json
303+
echo "has_changes=false" >> $GITHUB_OUTPUT
304+
exit 0
305+
fi
306+
echo "has_changes=true" >> $GITHUB_OUTPUT
307+
python3 /tmp/vuln_remediation.py validate-diff \
308+
--repo-root . \
309+
--context remediation-context.json \
310+
--changed-files changed-files.txt \
311+
--output diff-validation.json
312+
313+
- name: Confirm fixes
314+
if: steps.check.outputs.skip != 'true' && steps.diff.outputs.has_changes == 'true'
315+
run: |
316+
set +e
317+
python3 /tmp/vuln_remediation.py confirm \
318+
--repo-root . \
319+
--context remediation-context.json \
320+
--output confirmation-result.json
321+
CONFIRM_EXIT=$?
322+
set -e
323+
python3 /tmp/vuln_remediation.py summarize-fix \
324+
--context remediation-context.json \
325+
--confirmation confirmation-result.json \
326+
--output fix-result.json
327+
exit $CONFIRM_EXIT
328+
329+
- name: Commit and push validated changes
330+
if: steps.check.outputs.skip != 'true' && steps.diff.outputs.has_changes == 'true'
331+
run: |
332+
if ! jq -e '.fixed | length > 0' fix-result.json >/dev/null; then
333+
echo "No confirmed fixes to commit."
334+
exit 0
335+
fi
336+
git add --pathspec-from-file=changed-files.txt
337+
git diff --cached --quiet && exit 0
338+
git commit -m "security: vulnerability remediation ($(date -u +%Y-%m-%d))"
339+
git push --force-with-lease origin security/vuln-remediation
242340
243341
- name: Upload fix results
244342
if: always()
245343
uses: actions/upload-artifact@v4
246344
with:
247345
name: fix-result
248-
path: fix-result.json
346+
path: |
347+
fix-result.json
348+
confirmation-result.json
349+
diff-validation.json
350+
changed-files.txt
351+
socket-fix-apply.json
249352
if-no-files-found: ignore
250353

251354
pr:
@@ -272,88 +375,44 @@ jobs:
272375
name: fix-result
273376
continue-on-error: true
274377

378+
- name: Render PR body
379+
run: |
380+
curl -fsSL https://raw.githubusercontent.com/kernel/security-workflows/${{ inputs.security-workflows-ref }}/scripts/vuln_remediation.py -o /tmp/vuln_remediation.py
381+
[ -f triage-result.json ] || echo '{"deferred":[]}' > triage-result.json
382+
[ -f fix-result.json ] || echo '{"fixed":[],"reverted":[]}' > fix-result.json
383+
[ -f confirmation-result.json ] || echo '{"ok":false,"confirmed":[],"failures":[]}' > confirmation-result.json
384+
python3 /tmp/vuln_remediation.py render-pr-body \
385+
--triage triage-result.json \
386+
--fix-result fix-result.json \
387+
--confirmation confirmation-result.json \
388+
--output pr-body.md
389+
275390
- name: Create or update PR
276391
env:
277392
GH_TOKEN: ${{ steps.app-token.outputs.token }}
278393
run: |
279-
DATE="$(date -u +%Y-%m-%d)"
280-
REPO="${{ github.repository }}"
281-
282-
FIXED_TABLE=""
283-
DISMISSED_TABLE=""
284-
DEFERRED_TABLE=""
285-
286-
if [ -f triage-result.json ]; then
287-
ALERT_COUNT=$(jq '.alerts | length' triage-result.json 2>/dev/null || echo "0")
288-
if [ "$ALERT_COUNT" = "0" ]; then
289-
echo "No alerts to report."
290-
exit 0
291-
fi
292-
293-
DISMISSED_TABLE=$(jq -r '
294-
.alerts[] | select(.category == "dismiss") |
295-
"| \(.type) | \(.package) | \(.severity) | \(.reason) |"
296-
' triage-result.json 2>/dev/null || echo "")
297-
298-
DEFERRED_TABLE=$(jq -r '
299-
.alerts[] | select(.category == "defer") |
300-
"| \(.cve // "N/A") | \(.package) | \(.severity) | \(.reason) |"
301-
' triage-result.json 2>/dev/null || echo "")
302-
fi
303-
304-
if [ -f fix-result.json ]; then
305-
FIXED_TABLE=$(jq -r '
306-
.fixed[] |
307-
"| \(.cve) | \(.package) | \(.ecosystem) | \(.old_version) | \(.new_version) | \(.manifest) |"
308-
' fix-result.json 2>/dev/null || echo "")
309-
310-
REVERTED=$(jq -r '
311-
.reverted[] |
312-
"| \(.cve) | \(.package) | \(.ecosystem) | \(.reason) |"
313-
' fix-result.json 2>/dev/null || echo "")
314-
if [ -n "$REVERTED" ]; then
315-
DEFERRED_TABLE="${DEFERRED_TABLE}
316-
${REVERTED}"
317-
fi
394+
if [ "${{ needs.fix.result }}" != "success" ]; then
395+
echo "Fix job did not complete successfully; not creating or updating a remediation PR."
396+
exit 0
318397
fi
319-
320-
if [ -z "$FIXED_TABLE" ] && [ -z "$DISMISSED_TABLE" ] && [ -z "$DEFERRED_TABLE" ]; then
321-
echo "No results to report."
398+
REPO="${{ github.repository }}"
399+
if ! jq -e '.fixed | length > 0' fix-result.json >/dev/null 2>&1; then
400+
echo "No confirmed fixes; not creating a PR."
322401
exit 0
323402
fi
324403
325-
BODY="## Vulnerability Remediation — ${DATE}
326-
327-
### Fixed
328-
| CVE | Package | Ecosystem | Old Version | New Version | Manifest |
329-
|-----|---------|-----------|-------------|-------------|----------|
330-
${FIXED_TABLE:-| (none) | | | | | |}
331-
332-
### Skipped (non-actionable)
333-
| Alert Type | Package | Severity | Reason |
334-
|------------|---------|----------|--------|
335-
${DISMISSED_TABLE:-| (none) | | | |}
336-
337-
### Deferred (needs human review)
338-
| CVE | Package | Severity | Reason |
339-
|-----|---------|----------|--------|
340-
${DEFERRED_TABLE:-| (none) | | | |}"
341-
342404
EXISTING_PR=$(gh pr list --repo "$REPO" --head security/vuln-remediation --state open --json number --jq '.[0].number' 2>/dev/null || echo "")
343405
344406
if [ -n "$EXISTING_PR" ] && [ "$EXISTING_PR" != "null" ]; then
345-
gh pr edit "$EXISTING_PR" --repo "$REPO" --body "$BODY" --add-assignee ulziibay-kernel --add-reviewer ulziibay-kernel
407+
gh pr edit "$EXISTING_PR" --repo "$REPO" --body-file pr-body.md --add-assignee ulziibay-kernel --add-reviewer ulziibay-kernel
346408
echo "Updated PR #${EXISTING_PR}"
347-
elif [ -n "$FIXED_TABLE" ]; then
409+
else
348410
gh pr create \
349411
--repo "$REPO" \
350412
--head security/vuln-remediation \
351413
--base main \
352414
--title "security: vulnerability remediation" \
353415
--assignee ulziibay-kernel \
354416
--reviewer ulziibay-kernel \
355-
--body "$BODY"
356-
else
357-
echo "No fixes applied and no existing PR. Logging results only."
358-
echo "$BODY"
417+
--body-file pr-body.md
359418
fi

0 commit comments

Comments
 (0)