Skip to content

Commit 42291dc

Browse files
feat: initial commit of SBOM dashboard
1 parent cd17dcb commit 42291dc

79 files changed

Lines changed: 13067 additions & 1 deletion

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.env.example

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
NODE_ENV=development
2+
PORT=3000
3+
4+
DATABASE_HOST=localhost
5+
DATABASE_PORT=5432
6+
DATABASE_USER=sbom
7+
DATABASE_PASSWORD=sbom
8+
DATABASE_NAME=sbom_service
9+
10+
S3_BUCKET_NAME=vodg-sbom-local
11+
S3_REGION=eu-west-2
12+
S3_ENDPOINT=http://localhost:4566
13+
14+
OSV_API_URL=https://api.osv.dev
15+
16+
OIDC_ISSUER_URL=http://localhost:8090/default
17+
OIDC_CLIENT_ID=sbom-service
18+
OIDC_CLIENT_SECRET=sbom-secret
19+
OIDC_REDIRECT_URI=http://localhost:3000/auth/callback
20+
21+
SESSION_SECRET=local-dev-session-secret-minimum-32-characters-long

.github/workflows/ci.yml

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
name: CI
2+
3+
on:
4+
pull_request:
5+
branches: [main]
6+
merge_group:
7+
branches: [main]
8+
push:
9+
branches: [main]
10+
11+
permissions:
12+
contents: read
13+
pull-requests: read
14+
15+
jobs:
16+
gitleaks:
17+
runs-on: ubuntu-latest
18+
steps:
19+
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
20+
with:
21+
fetch-depth: 0
22+
persist-credentials: false
23+
- uses: gitleaks/gitleaks-action@ff98106e4c7b2bc287b24eaf42907196329070c7 # v2
24+
env:
25+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
26+
GITLEAKS_LICENSE: ${{ secrets.GITLEAKS_LICENSE }}
27+
28+
zizmor:
29+
runs-on: ubuntu-latest
30+
steps:
31+
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
32+
with:
33+
persist-credentials: false
34+
- uses: zizmorcore/zizmor-action@b1d7e1fb5de872772f31590499237e7cce841e8e # v0.5.3
35+
with:
36+
advanced-security: false
37+
38+
commitlint:
39+
runs-on: ubuntu-latest
40+
steps:
41+
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
42+
with:
43+
fetch-depth: 0
44+
persist-credentials: false
45+
- uses: wagoid/commitlint-github-action@b948419dd99f3fd78a6548d48f94e3df7f6bf3ed # v6
46+
47+
lint:
48+
runs-on: ubuntu-latest
49+
steps:
50+
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
51+
with:
52+
persist-credentials: false
53+
- uses: pnpm/action-setup@b906affcce14559ad1aafd4ab0e942779e9f58b1 # v4
54+
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
55+
with:
56+
node-version-file: .nvmrc
57+
cache: pnpm
58+
- run: pnpm install --frozen-lockfile
59+
- run: pnpm run lint
60+
61+
test:
62+
runs-on: ubuntu-latest
63+
steps:
64+
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
65+
with:
66+
persist-credentials: false
67+
- uses: pnpm/action-setup@b906affcce14559ad1aafd4ab0e942779e9f58b1 # v4
68+
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
69+
with:
70+
node-version-file: .nvmrc
71+
cache: pnpm
72+
- run: pnpm install --frozen-lockfile
73+
- run: pnpm run test
74+
75+
build:
76+
needs: [lint, test]
77+
runs-on: ubuntu-latest
78+
permissions:
79+
contents: read
80+
packages: write
81+
id-token: write
82+
attestations: write
83+
steps:
84+
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
85+
with:
86+
persist-credentials: false
87+
- uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3
88+
- uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3
89+
if: github.event_name == 'push'
90+
with:
91+
registry: ghcr.io
92+
username: ${{ github.actor }}
93+
password: ${{ secrets.GITHUB_TOKEN }}
94+
- uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6
95+
with:
96+
push: ${{ github.event_name == 'push' }}
97+
provenance: ${{ github.event_name == 'push' }}
98+
sbom: ${{ github.event_name == 'push' }}
99+
tags: |
100+
ghcr.io/${{ github.repository }}:${{ github.sha }}
101+
ghcr.io/${{ github.repository }}:latest
102+
cache-from: type=gha
103+
cache-to: type=gha,mode=max
104+
105+
check:
106+
if: always()
107+
needs: [gitleaks, zizmor, commitlint, lint, test, build]
108+
runs-on: ubuntu-latest
109+
steps:
110+
- name: Verify all jobs passed
111+
run: |
112+
results=( \
113+
"${{ needs.gitleaks.result }}" \
114+
"${{ needs.zizmor.result }}" \
115+
"${{ needs.commitlint.result }}" \
116+
"${{ needs.lint.result }}" \
117+
"${{ needs.test.result }}" \
118+
"${{ needs.build.result }}" \
119+
)
120+
for r in "${results[@]}"; do
121+
if [[ "$r" != "success" && "$r" != "skipped" ]]; then
122+
echo "Job failed or was cancelled: $r"
123+
exit 1
124+
fi
125+
done
126+
echo "All jobs passed"

.gitignore

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
node_modules/
2+
dist/
3+
.env
4+
.env.local
5+
*.log
6+
playwright-report/
7+
test-results/
8+
.DS_Store

.gitleaks.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
[allowlist]
2+
description = "Known safe patterns"
3+
paths = ["docker-compose.yml", ".env.example"]

.husky/pre-commit

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
npx lint-staged
2+
gitleaks git --pre-commit --staged --verbose
3+
zizmor .github/

.npmrc

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
ignore-scripts=true
2+
save-exact=true

.nvmrc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
20

CLAUDE.md

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
# SBOM Service
2+
3+
## Overview
4+
5+
TypeScript/Node.js service that ingests SBOMs (CycloneDX + SPDX), scans for vulnerabilities via a local OSV.dev mirror, tracks dependency freshness and release cadence, and serves a GOV.UK-styled dashboard. Part of the View of Digital Government (VODG) project.
6+
7+
## Architecture
8+
9+
- Express server + pg-boss worker (single process)
10+
- Ingest: `POST /api/modules/sbom/services/:service_id` (X-API-Key auth, SHA-256 hashed)
11+
- Normalisation: parses CycloneDX/SPDX, extracts components into PostgreSQL
12+
- Scanner: queries local OSV mirror for vulnerability matches
13+
- OSV Sync: downloads ecosystem ZIPs from GCS every 6h
14+
- Freshness: evaluates dependency age against endoflife.date, npm registry, and Maven Central. Watchlist-driven (`src/freshness/watchlist.yaml`). Daily sync at 3 AM via pg-boss cron. States: green/amber/red based on majors behind + EOL status.
15+
- Cadence: monitors SBOM submission frequency
16+
- Dashboard: GOV.UK Frontend v6, Nunjucks templates, OIDC SSO auth
17+
18+
## Running locally
19+
20+
```bash
21+
docker compose up postgres localstack oidc-mock -d
22+
AWS_ACCESS_KEY_ID=test AWS_SECRET_ACCESS_KEY=test aws --endpoint-url=http://localhost:4566 s3 mb s3://vodg-sbom-local --region eu-west-2
23+
cp .env.example .env
24+
pnpm install
25+
pnpm run db:migrate
26+
pnpm dev:watch
27+
```
28+
29+
## Package manager
30+
31+
pnpm (v10.33.0). Node 20+ required (see `.nvmrc`).
32+
33+
## Key files
34+
35+
- `src/main.ts` — entrypoint (Express + pg-boss)
36+
- `src/config.ts` — environment configuration loader
37+
- `src/ingest/router.ts` — SBOM ingest endpoint
38+
- `src/normalise/` — CycloneDX + SPDX parsers
39+
- `src/scanner/` — vulnerability matching against local OSV mirror
40+
- `src/sync/` — OSV.dev data sync (downloads ecosystem ZIPs from GCS)
41+
- `src/freshness/` — dependency freshness analysis
42+
- `src/cadence/` — release cadence tracking
43+
- `src/server/routes/` — Express routes (admin, dashboard, services)
44+
- `src/db/migrations/` — PostgreSQL schema migrations
45+
46+
## Scripts
47+
48+
| Script | Description |
49+
|--------|-------------|
50+
| `pnpm run build` | Compile TypeScript to /dist |
51+
| `pnpm run dev` | Start development server |
52+
| `pnpm run dev:watch` | Start with file watching |
53+
| `pnpm run lint` | ESLint + Prettier |
54+
| `pnpm run test` | Jest unit tests |
55+
| `pnpm run test:e2e` | Playwright E2E tests |
56+
| `pnpm run db:migrate` | Run database migrations |
57+
| `pnpm run generate-key` | Generate API key for a service |
58+
59+
## Docker
60+
61+
Multi-stage Dockerfile: build stage (pnpm install + tsc), release stage (prod deps only). Base: `node:20-slim`.
62+
63+
## TypeScript conventions
64+
65+
- `strict: true` — no implicit any
66+
- ESM-style imports compiled to CommonJS
67+
- Collocated tests (`*.test.ts` alongside source)
68+
- Jest with `ts-jest` and `--experimental-vm-modules`
69+
70+
## OSV Sync
71+
72+
Ecosystems: npm, PyPI, Go, Maven, NuGet, crates.io, Packagist
73+
Schedule: every 6 hours (pg-boss cron)
74+
Data source: `https://osv-vulnerabilities.storage.googleapis.com/{ecosystem}/all.zip` (public, no auth)

Dockerfile

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
FROM node:20-slim AS build
2+
3+
RUN corepack enable && corepack prepare pnpm@10.33.0 --activate
4+
5+
WORKDIR /app
6+
COPY package.json pnpm-lock.yaml .npmrc ./
7+
RUN pnpm install --frozen-lockfile
8+
9+
COPY tsconfig.json ./
10+
COPY src/ src/
11+
RUN pnpm build
12+
13+
FROM node:20-slim AS release
14+
15+
RUN corepack enable && corepack prepare pnpm@10.33.0 --activate
16+
17+
WORKDIR /app
18+
COPY package.json pnpm-lock.yaml .npmrc ./
19+
RUN pnpm install --frozen-lockfile --prod
20+
21+
COPY --from=build /app/dist dist/
22+
COPY src/db/migrations/ dist/db/migrations/
23+
COPY src/server/views/ dist/server/views/
24+
COPY src/freshness/watchlist.yaml dist/freshness/watchlist.yaml
25+
COPY --from=build /app/node_modules/govuk-frontend/dist/ public/govuk-frontend/
26+
27+
USER node
28+
EXPOSE 3000
29+
30+
CMD ["node", "dist/main.js"]

LICENSE

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
Open Government Licence v3.0
2+
3+
Copyright (c) Crown Copyright
4+
5+
You may use and re-use the information featured in this repository (not including
6+
logos) free of charge in any format or medium, under the terms of the Open
7+
Government Licence v3.0.
8+
9+
To view this licence, visit:
10+
https://www.nationalarchives.gov.uk/doc/open-government-licence/version/3/
11+
12+
Or write to:
13+
Information Policy Team
14+
The National Archives
15+
Kew, London TW9 4DU
16+
17+
Or email: psi@nationalarchives.gov.uk

0 commit comments

Comments
 (0)