-
Notifications
You must be signed in to change notification settings - Fork 0
133 lines (117 loc) · 5.2 KB
/
Copy pathsecurity-audit.yml
File metadata and controls
133 lines (117 loc) · 5.2 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
name: Security Audit
# Dependabot already opens PRs for advisories it can fix on its own. This job
# covers the gap: advisories it cannot fix unaided (transitive pins needing an
# `overrides` entry) and fixable ones whose PRs are sitting unmerged.
#
# Deliberately does NOT gate pull requests. A newly published advisory would
# turn every PR red, including the Dependabot PR carrying the fix, which is how
# vulnerabilities pile up in the first place. It reports instead of blocking.
on:
schedule:
# Mondays 06:17 UTC, after Dependabot's weekly run so same-day fixes land first.
- cron: '17 6 * * 1'
workflow_dispatch:
permissions:
contents: read
issues: write
concurrency:
group: security-audit-${{ github.ref }}
cancel-in-progress: true
jobs:
audit:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: actions/setup-node@v7
with:
node-version: 22
# Some repos intentionally ship without a committed lockfile; `npm ci`
# requires one, so fall back to `npm install` in that case.
- name: Install dependencies
working-directory: npm
run: |
if [ -f package-lock.json ]; then
npm ci
else
npm install --package-lock-only
fi
- name: Run npm audit
id: audit
working-directory: npm
run: |
# npm audit exits non-zero when it finds anything; capture rather than fail.
npm audit --audit-level=high --json > audit.json || true
node --input-type=module <<'EOF' >> "$GITHUB_OUTPUT"
import { readFileSync } from 'node:fs';
let report;
try {
report = JSON.parse(readFileSync('audit.json', 'utf8'));
} catch {
// A malformed report means the audit itself failed. Surface that
// rather than silently reporting "all clear".
console.log('status=error');
console.log('count=0');
process.exit(0);
}
const severities = ['high', 'critical'];
const found = Object.values(report.vulnerabilities ?? {}).filter((v) =>
severities.includes(v.severity),
);
const lines = found
.map((v) => {
const advisories = (v.via ?? [])
.filter((entry) => typeof entry === 'object')
.map((entry) => `[${entry.title}](${entry.url})`);
const fix = v.fixAvailable
? typeof v.fixAvailable === 'object'
? `\`${v.fixAvailable.name}@${v.fixAvailable.version}\`${v.fixAvailable.isSemVerMajor ? ' (semver-major)' : ''}`
: 'yes'
: 'none available';
return [
`- **${v.name}** (${v.severity}, ${v.isDirect ? 'direct' : 'transitive'})`,
` - fix: ${fix}`,
...advisories.map((a) => ` - ${a}`),
].join('\n');
})
.join('\n');
console.log(`status=${found.length ? 'vulnerable' : 'clean'}`);
console.log(`count=${found.length}`);
console.log(`body<<AUDIT_EOF\n${lines}\nAUDIT_EOF`);
EOF
- name: Open or update tracking issue
if: steps.audit.outputs.status == 'vulnerable'
env:
GH_TOKEN: ${{ github.token }}
COUNT: ${{ steps.audit.outputs.count }}
DETAILS: ${{ steps.audit.outputs.body }}
RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
run: |
TITLE="Security: $COUNT high/critical advisory(ies) in dependencies"
BODY=$(printf '%s\n\n%s\n\n---\nFound by [Security Audit](%s). Updated automatically; closes itself when `npm audit --audit-level=high` is clean.\n\nIf a fix needs a transitive pin Dependabot cannot express, add an `overrides` entry to `package.json`.\n' \
"$DETAILS" "" "$RUN_URL")
# Create the label first: `gh issue list --label` errors when the
# label does not exist yet, which is the state on the first run.
gh label create security-audit --color B60205 \
--description "Raised by the scheduled dependency audit" 2>/dev/null || true
EXISTING=$(gh issue list --label security-audit --state open \
--json number --jq '.[0].number // empty' 2>/dev/null || true)
if [ -n "$EXISTING" ]; then
gh issue edit "$EXISTING" --title "$TITLE" --body "$BODY"
echo "Updated issue #$EXISTING"
else
gh issue create --title "$TITLE" --body "$BODY" --label security-audit
fi
- name: Close tracking issue when clean
if: steps.audit.outputs.status == 'clean'
env:
GH_TOKEN: ${{ github.token }}
run: |
for n in $(gh issue list --label security-audit --state open --json number --jq '.[].number' 2>/dev/null || true); do
gh issue close "$n" --comment "\`npm audit --audit-level=high\` is now clean."
echo "Closed issue #$n"
done
- name: Fail if the audit could not be parsed
if: steps.audit.outputs.status == 'error'
run: |
echo "::error::npm audit did not produce a parseable report"
exit 1