Skip to content

Commit b147b72

Browse files
committed
Merge branch 'feature/cve-audit-protocol' into 'master'
chore: add CVE audit skill and scheduled GitLab CI jobs (#704) Closes #704 See merge request postgres-ai/database-lab!1137
2 parents 8edec32 + 866ebd7 commit b147b72

File tree

7 files changed

+327
-0
lines changed

7 files changed

+327
-0
lines changed

.claude/commands/cve-audit.md

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
---
2+
description: Run CVE audit across Go deps, UI deps, and Docker image. Produce a triage report with fix recommendations.
3+
argument-hint: [image-ref] (default: dblab_server:local, matches `make build-image`)
4+
---
5+
6+
# CVE Audit Protocol
7+
8+
You are running a vulnerability audit for DLE. The goal is a **triage report** that a
9+
developer can act on — not a raw scanner dump.
10+
11+
## Scope
12+
13+
Three layers, each with its own scanner:
14+
15+
| Layer | Tool | Command |
16+
|-------|------|---------|
17+
| Go source + deps | `govulncheck` | `cd engine && govulncheck ./...` |
18+
| UI deps | `pnpm audit` | `cd ui && pnpm audit` |
19+
| Built Docker image | `trivy` | `trivy image --severity CRITICAL,HIGH,MEDIUM $IMAGE` |
20+
21+
Target image: `$ARGUMENTS` if provided, else `dblab_server:local` (the tag produced
22+
by `cd engine && make build-image`). If the image is missing locally, tell the user
23+
to run `make build-image` first rather than silently falling back to a registry tag.
24+
25+
## Steps
26+
27+
1. **Preflight** — verify each tool is installed. If any are missing, report which
28+
and stop; do not silently skip. Tools:
29+
- `go` + `govulncheck` (install with `go install golang.org/x/vuln/cmd/govulncheck@latest`)
30+
- `pnpm`
31+
- `trivy`
32+
- `jq` (for JSON parsing below)
33+
34+
2. **Write outputs to `cve-reports/`** (create if missing). Use these filenames:
35+
- `cve-reports/govulncheck.txt` and `govulncheck.json`
36+
- `cve-reports/pnpm-audit.txt` and `pnpm-audit.json`
37+
- `cve-reports/trivy.txt` and `trivy.json`
38+
39+
3. **Run the three scanners in parallel** where possible (they don't depend on
40+
each other). Capture both human-readable text and JSON formats. Don't fail the
41+
audit if a scanner returns non-zero — findings produce non-zero exit codes.
42+
43+
```bash
44+
# Go
45+
cd engine && govulncheck -format json ./... > ../cve-reports/govulncheck.json; govulncheck ./... > ../cve-reports/govulncheck.txt; cd ..
46+
47+
# UI
48+
cd ui && pnpm audit --json > ../cve-reports/pnpm-audit.json; pnpm audit > ../cve-reports/pnpm-audit.txt; cd ..
49+
50+
# Image
51+
trivy image --severity CRITICAL,HIGH,MEDIUM --format json -o cve-reports/trivy.json "$IMAGE"
52+
trivy image --severity CRITICAL,HIGH -o cve-reports/trivy.txt "$IMAGE"
53+
```
54+
55+
4. **Build the triage report**`cve-reports/SUMMARY.md`. Structure:
56+
57+
### Header
58+
- Date, image ref, tool versions, Go version, Node/pnpm version.
59+
60+
### Severity counts (per layer)
61+
Table: CRITICAL / HIGH / MEDIUM / LOW for Go stdlib, Go deps, UI deps, image OS
62+
packages, image Go binaries.
63+
64+
### Top actionable items
65+
Filter by: `Severity in {CRITICAL, HIGH}` AND `Status == "fixed"` AND
66+
`FixedVersion != ""`. Group by remediation (e.g. "upgrade Go to 1.26.2",
67+
"`go get -u github.com/X`", "rebuild on `docker:29.3.1`"). One line per group with
68+
the CVEs it resolves.
69+
70+
### Unfixed but important
71+
`Severity in {CRITICAL, HIGH}` AND no fix available. Note these for tracking.
72+
Suggest `.trivyignore` entry only if we've confirmed non-exploitability.
73+
74+
### Notable CVSS vectors
75+
For anything scoring ≥9.0 or marked network-exploitable with no privileges
76+
(`AV:N/PR:N/UI:N`), call it out explicitly — these are the "drop everything"
77+
ones.
78+
79+
### Delta (optional)
80+
If a previous `cve-reports/SUMMARY.md` exists in git, diff the CVE IDs and
81+
highlight: new findings since last run, findings resolved since last run.
82+
83+
5. **Do not** propose automated `go get -u` across the board — some upgrades
84+
have breaking changes. List the commands and let the user run them.
85+
86+
6. **Output to the user** — a short paragraph summary (counts + top 3-5 actions),
87+
then a pointer to `cve-reports/SUMMARY.md` for the full report. Do not paste
88+
the full scanner output in chat.
89+
90+
## Notes on reading results
91+
92+
- Trivy's **Severity** label comes from the distro/vendor; consult **CVSS vector**
93+
in JSON (`.Results[].Vulnerabilities[].CVSS`) for true risk. Rules of thumb:
94+
- `AV:N` (network) + `PR:N` (no auth) + `UI:N` (no click) → fix urgently
95+
- `AV:L` (local only) → lower urgency for server containers
96+
- `UI:R` → often not reachable in server context
97+
- `govulncheck` separates **called** vs **imported-but-not-called** vulns. Prioritize
98+
called ones; imported-only can often wait for a bulk dep bump.
99+
- `pnpm audit` includes transitive deps — check if the vulnerable path is actually
100+
reachable before treating it as critical.

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,3 +17,6 @@ engine/coverage.out
1717
engine/meta
1818

1919
ui/packages/shared/dist/
20+
21+
cve-reports/
22+
.trivycache/

.gitlab-ci-security.yml

Lines changed: 203 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,203 @@
1+
# Scheduled CVE audit. Produces JSON + human-readable reports as artifacts.
2+
#
3+
# Trigger: GitLab CI Schedule with variable SECURITY_AUDIT=1.
4+
# Also runs on manual (web) trigger with the same variable.
5+
#
6+
# Configure a weekly schedule at: Settings > CI/CD > Schedules.
7+
# Recommended cadence: weekly (e.g. Monday 03:00 UTC).
8+
9+
.security_rules: &security_rules
10+
rules:
11+
- if: $CI_PIPELINE_SOURCE == "schedule" && $SECURITY_AUDIT == "1"
12+
- if: $CI_PIPELINE_SOURCE == "web" && $SECURITY_AUDIT == "1"
13+
interruptible: false
14+
15+
# Must list every stage used across all included files — a later `stages:`
16+
# declaration replaces earlier ones (GitLab merges by overwrite, not append).
17+
# Keep in sync with engine/.gitlab-ci.yml if stages change there.
18+
stages:
19+
- test
20+
- build-binary
21+
- build
22+
- integration-test
23+
- security
24+
25+
cve-govulncheck:
26+
<<: *security_rules
27+
stage: security
28+
image:
29+
name: golang:1.26
30+
pull_policy: if-not-present
31+
before_script:
32+
- go install golang.org/x/vuln/cmd/govulncheck@latest
33+
script:
34+
- mkdir -p cve-reports
35+
- cd engine
36+
- govulncheck -format json ./... > ../cve-reports/govulncheck.json || true
37+
- govulncheck ./... > ../cve-reports/govulncheck.txt || true
38+
- cd ..
39+
- echo "=== govulncheck summary ==="
40+
- tail -40 cve-reports/govulncheck.txt || true
41+
artifacts:
42+
when: always
43+
name: "cve-go-$CI_COMMIT_SHORT_SHA"
44+
paths:
45+
- cve-reports/govulncheck.json
46+
- cve-reports/govulncheck.txt
47+
expire_in: 90 days
48+
allow_failure: true
49+
50+
cve-pnpm-audit:
51+
<<: *security_rules
52+
stage: security
53+
image:
54+
name: node:20-alpine
55+
pull_policy: if-not-present
56+
before_script:
57+
- apk add --no-cache git jq
58+
- npm install -g pnpm@9
59+
script:
60+
- mkdir -p cve-reports
61+
- cd ui
62+
- pnpm install --frozen-lockfile --ignore-scripts || pnpm install --ignore-scripts
63+
- pnpm audit --json > ../cve-reports/pnpm-audit.json || true
64+
- pnpm audit > ../cve-reports/pnpm-audit.txt || true
65+
- cd ..
66+
- echo "=== pnpm audit summary ==="
67+
- jq '.metadata.vulnerabilities // .vulnerabilities // {}' cve-reports/pnpm-audit.json || true
68+
artifacts:
69+
when: always
70+
name: "cve-ui-$CI_COMMIT_SHORT_SHA"
71+
paths:
72+
- cve-reports/pnpm-audit.json
73+
- cve-reports/pnpm-audit.txt
74+
expire_in: 90 days
75+
allow_failure: true
76+
77+
cve-trivy-image:
78+
<<: *security_rules
79+
stage: security
80+
image:
81+
name: aquasec/trivy:latest
82+
entrypoint: [""]
83+
pull_policy: if-not-present
84+
variables:
85+
# Latest build from the master branch. Override via schedule variable for ad-hoc scans.
86+
CVE_SCAN_IMAGE: "registry.gitlab.com/postgres-ai/database-lab/dblab-server:master"
87+
TRIVY_CACHE_DIR: ".trivycache"
88+
TRIVY_NO_PROGRESS: "true"
89+
# Authenticate to GitLab Container Registry via CI job token.
90+
TRIVY_USERNAME: "$CI_REGISTRY_USER"
91+
TRIVY_PASSWORD: "$CI_REGISTRY_PASSWORD"
92+
cache:
93+
key: trivy-db
94+
paths:
95+
- .trivycache/
96+
script:
97+
- mkdir -p cve-reports
98+
- trivy --version
99+
- trivy image --download-db-only
100+
- trivy image --severity CRITICAL,HIGH,MEDIUM --format json -o cve-reports/trivy.json "$CVE_SCAN_IMAGE" || true
101+
- trivy image --severity CRITICAL,HIGH -o cve-reports/trivy.txt "$CVE_SCAN_IMAGE" || true
102+
- echo "=== trivy severity counts ==="
103+
- |
104+
trivy image --severity CRITICAL,HIGH,MEDIUM,LOW --format json "$CVE_SCAN_IMAGE" 2>/dev/null \
105+
| grep -oE '"Severity":"[A-Z]+"' | sort | uniq -c || true
106+
artifacts:
107+
when: always
108+
name: "cve-image-$CI_COMMIT_SHORT_SHA"
109+
paths:
110+
- cve-reports/trivy.json
111+
- cve-reports/trivy.txt
112+
expire_in: 90 days
113+
allow_failure: true
114+
115+
cve-summary:
116+
<<: *security_rules
117+
stage: security
118+
needs:
119+
- job: cve-govulncheck
120+
artifacts: true
121+
optional: true
122+
- job: cve-pnpm-audit
123+
artifacts: true
124+
optional: true
125+
- job: cve-trivy-image
126+
artifacts: true
127+
optional: true
128+
image:
129+
name: alpine:3.20
130+
pull_policy: if-not-present
131+
before_script:
132+
- apk add --no-cache jq bash
133+
script:
134+
- |
135+
bash -c '
136+
set -u
137+
mkdir -p cve-reports
138+
out=cve-reports/SUMMARY.md
139+
{
140+
echo "# CVE audit report"
141+
echo
142+
echo "- Date: $(date -u +%FT%TZ)"
143+
echo "- Pipeline: $CI_PIPELINE_URL"
144+
echo "- Commit: $CI_COMMIT_SHORT_SHA"
145+
echo
146+
147+
echo "## Severity counts"
148+
echo
149+
echo "### Docker image (trivy)"
150+
if [ -f cve-reports/trivy.json ]; then
151+
jq -r "[.Results[]? | .Vulnerabilities[]? | .Severity] | group_by(.) | map(\"- \(.[0]): \(length)\") | .[]" cve-reports/trivy.json || echo "- (no data)"
152+
else
153+
echo "- (trivy did not run)"
154+
fi
155+
echo
156+
157+
echo "### Go (govulncheck)"
158+
if [ -f cve-reports/govulncheck.json ]; then
159+
called=$(jq -rs "[.[] | .finding? | select(. != null) | select((.trace // []) | any(.function != null)) | .osv] | unique | length" cve-reports/govulncheck.json 2>/dev/null || echo "?")
160+
imported=$(jq -rs "[.[] | .finding? | select(. != null) | .osv] | unique | length" cve-reports/govulncheck.json 2>/dev/null || echo "?")
161+
echo "- called (code path reachable): $called unique advisories"
162+
echo "- imported (including unreachable): $imported unique advisories"
163+
else
164+
echo "- (govulncheck did not run)"
165+
fi
166+
echo
167+
168+
echo "### UI (pnpm audit)"
169+
if [ -f cve-reports/pnpm-audit.json ]; then
170+
jq -r "(.metadata.vulnerabilities // .vulnerabilities // {}) | to_entries[] | \"- \(.key): \(.value)\"" cve-reports/pnpm-audit.json 2>/dev/null || echo "- (parse failed; see pnpm-audit.txt)"
171+
else
172+
echo "- (pnpm audit did not run)"
173+
fi
174+
echo
175+
176+
echo "## Top fixable CRITICAL/HIGH in image"
177+
echo
178+
if [ -f cve-reports/trivy.json ]; then
179+
jq -r "[.Results[]? | .Vulnerabilities[]? | select((.Severity==\"CRITICAL\" or .Severity==\"HIGH\") and (.Status==\"fixed\" or .FixedVersion != null and .FixedVersion != \"\"))] | unique_by(.VulnerabilityID + .PkgName) | map(\"- \(.Severity) \(.VulnerabilityID) — \(.PkgName) \(.InstalledVersion) → \(.FixedVersion)\") | .[]" cve-reports/trivy.json | head -40
180+
fi
181+
echo
182+
183+
echo "## Unfixed CRITICAL/HIGH in image"
184+
echo
185+
if [ -f cve-reports/trivy.json ]; then
186+
jq -r "[.Results[]? | .Vulnerabilities[]? | select((.Severity==\"CRITICAL\" or .Severity==\"HIGH\") and ((.FixedVersion // \"\") == \"\"))] | unique_by(.VulnerabilityID + .PkgName) | map(\"- \(.Severity) \(.VulnerabilityID) — \(.PkgName) \(.InstalledVersion) (no upstream fix)\") | .[]" cve-reports/trivy.json | head -40
187+
fi
188+
echo
189+
190+
echo "## Artifacts"
191+
echo
192+
echo "- cve-reports/govulncheck.{json,txt}"
193+
echo "- cve-reports/pnpm-audit.{json,txt}"
194+
echo "- cve-reports/trivy.{json,txt}"
195+
} > "$out"
196+
cat "$out"
197+
'
198+
artifacts:
199+
when: always
200+
name: "cve-summary-$CI_COMMIT_SHORT_SHA"
201+
paths:
202+
- cve-reports/
203+
expire_in: 365 days

.gitlab-ci.yml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,21 @@ workflow:
1111

1212
include:
1313
- template: Security/SAST.gitlab-ci.yml
14+
rules:
15+
- if: $SECURITY_AUDIT != "1"
1416
- local: 'engine/.gitlab-ci.yml'
17+
rules:
18+
- if: $SECURITY_AUDIT != "1"
1519
- local: 'ui/.gitlab-ci.yml'
20+
rules:
21+
- if: $SECURITY_AUDIT != "1"
22+
- local: '.gitlab-ci-security.yml'
23+
rules:
24+
- if: $SECURITY_AUDIT == "1"
1625
- project: 'postgres-ai/infra'
1726
file: '/ci/templates/approval-check.yml'
27+
rules:
28+
- if: $SECURITY_AUDIT != "1"
1829

1930
empty-job:
2031
stage: test

ui/.gitlab-ci.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,11 @@ include:
77
- if: $CI_COMMIT_TAG =~ /^ui\/[0-9.]+$/
88
- if: $CI_COMMIT_TAG =~ /^v[a-zA-Z0-9_.-]*/
99
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
10+
changes:
11+
- ui/**/*
1012
- if: '$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH'
13+
changes:
14+
- ui/**/*
1115

1216
.ui_cache: &ui_cache
1317
image:

ui/packages/ce/.gitlab-ci.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,14 @@
22
.only_ui_feature: &only_ui_feature
33
rules:
44
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
5+
changes:
6+
- ui/**/*
57

68
.only_ui_master: &only_ui_master
79
rules:
810
- if: '$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH'
11+
changes:
12+
- ui/**/*
913

1014
.only_ui_tag_release: &only_ui_tag_release
1115
rules:

ui/packages/shared/.gitlab-ci.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
.only_ui_feature: &only_ui_feature
22
rules:
33
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
4+
changes:
5+
- ui/**/*
46

57
.only_ui_tag_release: &only_ui_tag_release
68
rules:

0 commit comments

Comments
 (0)