-
-
Notifications
You must be signed in to change notification settings - Fork 2k
214 lines (198 loc) · 10.4 KB
/
Copy pathcve.yml
File metadata and controls
214 lines (198 loc) · 10.4 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
name: CVE & SBOM
# Weekly OWASP dependency-check across the karate reactor, consolidated into one CVE + CycloneDX
# SBOM report by etc/generate-cve-report.py. Runs standalone (workflow_dispatch) or on the weekly
# Saturday schedule — the dedicated, schedulable security scan, separate from maven-release.yml.
#
# Deliberately NOT part of the release: the release must not wait hours on an NVD scan, so it no
# longer blocks on critical CVEs. This workflow is the CVE gate, and it fails on CVSS >= 9.0 via
# etc/generate-cve-report.py.
#
# Mirrors karate-ext/.github/workflows/cve.yml — keep the two in step when changing either.
#
# Secrets:
# NVD_API_KEY — NVD API key (faster, rate-limit-friendly dep-check updates)
# SMTP_SERVER / SMTP_USERNAME / SMTP_PASSWORD / CVE_ALERT_EMAIL — weekly email (schedule only)
on:
workflow_dispatch:
inputs:
version:
description: Version to scan (e.g. 2.1.1) — blank scans the pom version as-is
required: false
default: ''
schedule:
# Weekly CVE & SBOM scan on Saturdays (30 min after OpenSSF Scorecard)
- cron: '0 2 * * 6'
permissions: read-all
jobs:
cve-scan:
runs-on: ubuntu-latest
# must exceed the NVD update step's own timeout plus the build around it, or a
# cold bootstrap dies at the job level before it can cache the DB. 360 is the
# GitHub-hosted maximum; only a cold run goes anywhere near it.
timeout-minutes: 355
env:
DEPCHECK_VERSION: 12.2.2
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
- name: Set up JDK 21
uses: actions/setup-java@1bcf9fb12cf4aa7d266a90ae39939e61372fe520 # v5.4.0
with:
distribution: temurin
java-version: 21
cache: maven
# Release reports are cut before the version commit lands (or off a pom still
# carrying .RC1), so the dispatch takes the version to stamp. It must rewrite the
# pom rather than only feed --version to the report: dep-check reads the built
# jars off disk, so the SBOM lists karate's own modules at whatever the pom says
# (io.karatelabs:karate-core:X.Y.Z, and asset entries like
# "karate-core-X.Y.Z.jar: alpine.min.js"). Stamping the header alone would ship a
# report whose title and component list disagree.
#
# Nothing is pushed - the rewrite is ephemeral, local to the runner's checkout.
# Empty on a schedule run, which then scans the pom as-is.
- name: Set version
if: github.event.inputs.version != ''
run: mvn versions:set versions:commit -B -ntp -DnewVersion=${{ github.event.inputs.version }}
# Always read back from the pom, so the report header, the SBOM components and the
# artifact name cannot drift from each other or from what was actually scanned.
- name: Determine version
id: version
run: |
VERSION=$(mvn help:evaluate -Dexpression=project.version -q -DforceStdout -B -ntp)
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
- name: Build and install
run: mvn -B -ntp clean install -DskipTests
- name: Restore NVD database cache
uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: ~/.dependency-check/data
key: nvd-db-${{ github.run_id }}
restore-keys: |
nvd-db-
# Update the NVD DB as its own step, before the scan, separating the slow
# cacheable work from the fast scan. A cold bootstrap pulls ~350k records and
# takes hours; the scan then runs offline against the cached DB in minutes.
#
# Keeping the update inside the scan step couples the two: a scan that fails
# (the report exits 1 on critical CVEs) would discard a good multi-hour NVD
# download, and an update that overruns the scan's timeout can never cache
# what it did fetch — which deadlocks the workflow permanently cold.
#
# nvdApiResultsPerPage=1000 works around dep-check#8371 (NVD blocks requests
# where resultsPerPage>1000 with HTTP 404; plugin defaults to 2000 — "closed
# as not planned" upstream). It halves the records per request and so doubles
# the request count: revisit if NVD ever accepts 2000 again.
#
# nvdApiDelay=3000 is already *more* aggressive than the plugin default (3500
# with an API key, 8000 without); NVD itself documents 50 requests per rolling
# 30s with a key and recommends a 6s sleep. Do not lower it chasing runtime -
# the delay is a small fraction of a cold bootstrap, which is dominated by NVD
# throttling and retries, and going faster invites the retry storms upstream
# reports. The real lever is the cache below (and see nvdDatafeedUrl).
- name: Update NVD database
timeout-minutes: 300
env:
NVD_API_KEY: ${{ secrets.NVD_API_KEY }}
run: |
mvn -B -ntp org.owasp:dependency-check-maven:${DEPCHECK_VERSION}:update-only \
-DnvdApiKey=${NVD_API_KEY} \
-DnvdApiDelay=3000 \
-DnvdApiResultsPerPage=1000 \
-DdataDirectory=${HOME}/.dependency-check/data \
-DnvdValidForHours=144
# Cheap insurance, and the only thing that logs the DB size - which is what the
# floor below is baselined on. The "partial DB silently reports zero CVEs"
# story is dep-check lore and looks weaker than its reputation: with
# autoUpdate=false and no database dep-check throws NoDataException rather
# than passing, and it records the update timestamp only after a *completed*
# update (dependency-check#524), so an interrupted update should re-run rather
# than be trusted. Unverified for a DB that exists but is underpopulated, so
# keep the guard - it costs seconds - but do not read it as a known live bug.
- name: Verify NVD database is populated
id: nvdcheck
run: |
DATA="${HOME}/.dependency-check/data"
DB=$(find "${DATA}" -name '*.mv.db' -print -quit)
if [ -z "${DB}" ]; then
echo "::error::no H2 database found under ${DATA} - dep-check would report zero CVEs"
ls -laR "${DATA}" || true
exit 1
fi
BYTES=$(stat -c%s "${DB}")
echo "NVD database : ${DB}"
echo "NVD size : ${BYTES} bytes ($((BYTES / 1048576)) MB)"
du -sh "${DATA}" || true
# Floor for a complete DB. A schema-only ODC database is a few MB; a
# populated one holds ~364k records and measures ~231MB as the raw .mv.db
# this stats. Do not sanity-check that against the ~112MB cache entry -
# that number is the compressed archive, and reasoning from it is how the
# floor first landed at 200MB (a 14% gap that a 86%-built DB sailed
# through). 220MB sits just under a known-good DB, which only grows as
# CVEs accumulate, so it cannot false-positive while still catching a
# partial build. Re-baseline if DEPCHECK_VERSION changes - a schema
# change moves the size.
MIN=$((220 * 1048576))
if [ "${BYTES}" -lt "${MIN}" ]; then
echo "::error::NVD database is only $((BYTES / 1048576)) MB (floor: $((MIN / 1048576)) MB) - looks partial, refusing to cache it or scan against it"
exit 1
fi
# Gated on the verify step, which is itself implicitly gated on success() and
# so only runs when the update succeeded - meaning this one condition means
# "the DB both built and looks real". always() is load-bearing: a bare `if:` is
# implicitly wrapped in success(), which would re-couple the save to whatever
# runs later, and the report step exits 1 on critical CVEs.
- name: Save NVD database cache
if: always() && steps.nvdcheck.outcome == 'success'
uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: ~/.dependency-check/data
key: nvd-db-${{ github.run_id }}
# -DautoUpdate=false: the DB is already current, and the scan must never
# re-enter the update path. Pin the plugin version explicitly (rather than
# inheriting from the depcheck profile in pom.xml) so CI is decoupled from
# pom drift.
- name: CVE check
timeout-minutes: 30
run: |
mvn -B -ntp org.owasp:dependency-check-maven:${DEPCHECK_VERSION}:check \
-DautoUpdate=false \
-DossIndexAnalyzerEnabled=false \
-DskipProvidedScope=true \
-DskipTestScope=true \
-DsuppressionFiles=${GITHUB_WORKSPACE}/etc/cve-suppressions.xml \
-DdataDirectory=${HOME}/.dependency-check/data \
-Dformat=JSON
# Quoted: this step is always(), so a failed/skipped Determine version would
# otherwise expand to a bare --version and argparse would die on the missing
# argument, burying the real failure. An empty string is a valid version here.
- name: Generate CVE & SBOM report
if: always()
run: python3 etc/generate-cve-report.py --version "${{ steps.version.outputs.version }}"
- name: Upload CVE & SBOM report
if: always()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: cve-sbom-report-${{ steps.version.outputs.version }}
retention-days: 30
path: |
target/cve-sbom-report.html
target/sbom.json
karate-core/target/dependency-check-report.json
karate-junit6/target/dependency-check-report.json
karate-gatling/target/dependency-check-report.json
- name: Email CVE & SBOM report
if: always() && github.event_name == 'schedule'
uses: dawidd6/action-send-mail@94de994a9f6fffee200243214e17002e2920bb59 # v18
with:
server_address: ${{ secrets.SMTP_SERVER }}
server_port: 465
secure: true
username: ${{ secrets.SMTP_USERNAME }}
password: ${{ secrets.SMTP_PASSWORD }}
subject: "Karate Weekly CVE & SBOM Report - ${{ steps.version.outputs.version }}"
to: ${{ secrets.CVE_ALERT_EMAIL }}
from: ${{ secrets.SMTP_USERNAME }}
body: "Weekly CVE & SBOM scan completed for Karate ${{ steps.version.outputs.version }}. See attached report."
attachments: target/cve-sbom-report.html,target/sbom.json