Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
58fd889
chore(deps): bump github.com/go-jose/go-jose/v4 from 4.1.3 to 4.1.4 (…
dependabot[bot] Apr 5, 2026
01846f1
chore(deps): bump defu from 6.1.4 to 6.1.6 in /web (#72)
dependabot[bot] Apr 5, 2026
0b9e46e
chore(deps): bump picomatch in /web (#66)
dependabot[bot] Apr 5, 2026
10ec597
chore(deps): bump github.com/danielgtaylor/huma/v2 from 2.37.2 to 2.3…
dependabot[bot] Apr 5, 2026
efcbd6b
chore(deps): bump google.golang.org/genai from 1.50.0 to 1.52.0 (#70)
dependabot[bot] Apr 5, 2026
b0cbca4
chore(deps): bump github.com/lib/pq from 1.11.2 to 1.12.0 (#60)
dependabot[bot] Apr 5, 2026
52fda94
chore(deps): bump actions/setup-go from 6.3.0 to 6.4.0 (#68)
dependabot[bot] Apr 5, 2026
96c199e
chore(deps): bump @tailwindcss/vite from 4.2.1 to 4.2.2 in /web (#65)
dependabot[bot] Apr 5, 2026
62a7ecc
chore(deps): bump yaml from 2.8.2 to 2.8.3 in /web (#67)
dependabot[bot] Apr 5, 2026
7995dcf
chore(deps): bump vue-router from 5.0.3 to 5.0.4 in /web (#63)
dependabot[bot] Apr 5, 2026
403257e
chore(deps-dev): bump eslint from 10.0.3 to 10.1.0 in /web (#64)
dependabot[bot] Apr 5, 2026
abc07e3
chore(deps-dev): bump @vitest/eslint-plugin in /web (#62)
dependabot[bot] Apr 5, 2026
c7611ca
chore(deps): bump github.com/jackc/pgx/v5 from 5.8.0 to 5.9.1 (#59)
dependabot[bot] Apr 5, 2026
1bab411
chore(deps-dev): bump vite from 7.3.1 to 8.0.1 in /web (#61)
dependabot[bot] Apr 5, 2026
b6eb60d
chore(deps): remove vite-plugin-vue-devtools (no Vite 8 support)
scarson Apr 5, 2026
866ab4a
feat(crypto): bind GCM ciphertext to entity context via AAD (#83)
scarson Apr 8, 2026
922732c
chore(deps): bump reka-ui from 2.9.2 to 2.9.3 in /web (#80)
dependabot[bot] Apr 8, 2026
3e95c0e
chore(deps): bump vue from 3.5.30 to 3.5.32 in /web (#79)
dependabot[bot] Apr 8, 2026
64b104c
chore(deps): bump github.com/lib/pq from 1.12.0 to 1.12.3 (#76)
dependabot[bot] Apr 8, 2026
cd8c461
chore(deps): bump google.golang.org/genai from 1.52.0 to 1.52.1 (#75)
dependabot[bot] Apr 8, 2026
5bc0df7
chore(deps-dev): bump oxlint and eslint-plugin-oxlint to ~1.58.0
scarson Apr 8, 2026
ea50fee
chore(deps-dev): bump @types/node from 24.12.0 to 25.5.2 in /web (#77)
dependabot[bot] Apr 8, 2026
6bae0c2
Merge remote-tracking branch 'origin/main' into dev
scarson Apr 8, 2026
6efb34b
chore(deps): upgrade TypeScript 5.9 to 6.0
scarson Apr 8, 2026
02c8474
revert(deps): revert TypeScript 6 upgrade, keep baseUrl removal
scarson Apr 8, 2026
1763dbb
chore(lint): disable require-mock-type-parameters rule
scarson Apr 8, 2026
26873dd
Revert "chore(lint): disable require-mock-type-parameters rule"
scarson Apr 8, 2026
e85a60a
fix(lint): add type parameters to all vi.fn() mock calls
scarson Apr 8, 2026
e53c030
fix(lint): use precise mock types where generic unknown breaks type-c…
scarson Apr 8, 2026
558e9d3
fix(lint): match proxy signature to typed mockGET in CveDetailView test
scarson Apr 8, 2026
2366a83
docs: fix accomodate → accommodate and architectutral → architectural…
scarson Apr 23, 2026
7347cb0
Merge pull request #97 from scarson/docs/fix-claude-md-typos
scarson Apr 23, 2026
08e0f2d
docs: refresh README with SCIM, MFA, and infrastructure updates
scarson Apr 24, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ Rule #1: If you want exception to ANY rule, YOU MUST STOP and get explicit permi
- If you're uncomfortable pushing back out loud, just say "Strange things are afoot at the Circle K". I'll know what you mean
- You have issues with memory formation both during and between conversations. Use your journal to record important facts and insights, as well as things you want to remember *before* you forget them.
- You search your journal when you trying to remember or figure stuff out.
- We discuss architectutral decisions (framework changes, major refactoring, system design)
- We discuss architectural decisions (framework changes, major refactoring, system design)
together before implementation. Routine fixes and clear implementations don't need
discussion.

Expand All @@ -53,7 +53,7 @@ When asked to do something, just do it - including obvious follow-up actions nee

## Designing software

- YAGNI. The best code is no code. Don't add features we don't need right now, unless they're foundational to later planned work and refactoring to accomodate would be difficult.
- YAGNI. The best code is no code. Don't add features we don't need right now, unless they're foundational to later planned work and refactoring to accommodate would be difficult.
- When it doesn't conflict with YAGNI, architect for extensibility and flexibility.

## Third-Party Dependencies
Expand Down
4 changes: 2 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ Rule #1: If you want exception to ANY rule, YOU MUST STOP and get explicit permi
- If you're uncomfortable pushing back out loud, just say "Strange things are afoot at the Circle K". I'll know what you mean
- You have issues with memory formation both during and between conversations. Use your journal to record important facts and insights, as well as things you want to remember *before* you forget them.
- You search your journal when you trying to remember or figure stuff out.
- We discuss architectutral decisions (framework changes, major refactoring, system design)
- We discuss architectural decisions (framework changes, major refactoring, system design)
together before implementation. Routine fixes and clear implementations don't need
discussion.

Expand All @@ -53,7 +53,7 @@ When asked to do something, just do it - including obvious follow-up actions nee

## Designing software

- YAGNI. The best code is no code. Don't add features we don't need right now, unless they're foundational to later planned work and refactoring to accomodate would be difficult.
- YAGNI. The best code is no code. Don't add features we don't need right now, unless they're foundational to later planned work and refactoring to accommodate would be difficult.
- When it doesn't conflict with YAGNI, architect for extensibility and flexibility.

## Third-Party Dependencies
Expand Down
44 changes: 32 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ CVErt Ops pulls from 10 vulnerability data sources and merges them into a unifie
- **CSAF** — Common Security Advisory Framework documents
- **Generic** — Configurable adapter for custom or internal feeds

Each source is ingested independently, and a merge pipeline recomputes the canonical CVE record from all available sources on every update. A material hash (SHA-256 over normalized fields) tracks meaningful changes and drives alert evaluation — cosmetic updates don't trigger false alerts.
Each source is ingested independently through a shared HTTP client with per-feed circuit breakers (sony/gobreaker v2), SSRF-hardened transport, and response-body size limits. A merge pipeline recomputes the canonical CVE record from all available sources on every update. A material hash (SHA-256 over normalized fields) tracks meaningful changes and drives alert evaluation — cosmetic updates don't trigger false alerts.

### Full-Text Search and Faceted Filtering

Expand Down Expand Up @@ -76,9 +76,17 @@ Every org gets full data isolation through dual-layer tenant separation:

Four RBAC roles control access: **Owner** > **Admin** > **Member** > **Viewer**. Per-route middleware enforces minimum role requirements. API key authentication is supported with org-scoping and role caps.

### Multi-Factor Authentication

MFA can be required per-org or per-user, with TOTP (authenticator apps) and email OTP as second factors. Enrollment is a multi-step flow gated by pending tokens that encode the remaining MFA requirements directly in their claims — the client can't skip a step by replaying an earlier token. TOTP verification uses `FOR UPDATE` locking with skew-aware step tracking to prevent code replay. Email OTP challenges have per-attempt rate limits and emit security events when exhausted. Admins can reset a user's MFA atomically, and password reset completion itself is MFA-gated.

### Enterprise SSO

Organizations can configure OIDC-based single sign-on with domain-based auto-discovery. Supports GitHub OAuth, Google OIDC, and generic OIDC providers. Users can link SSO identities to existing accounts. SCIM provisioning for automated user lifecycle management is planned.
Organizations can configure OIDC-based single sign-on with domain-based auto-discovery. Supports GitHub OAuth, Google OIDC, and generic OIDC providers. Users can link SSO identities to existing accounts.

### SCIM Provisioning

Full SCIM 2.0 support for automated user lifecycle management via your identity provider (Okta, Azure AD, Google Workspace, etc.). Bearer-token authenticated endpoints expose standard `/Users` and `/Groups` resources with create/read/update/patch/delete and filter parsing. Group-to-role mappings let you drive RBAC membership directly from IdP groups — changes take effect on the next SCIM sync with no admin action required. Org members can be flagged `scim_exempt` to keep local accounts (emergency access, service accounts) from being deactivated by an IdP sync. A dedicated per-org rate limiter isolates SCIM traffic from the main API budget.

### Site Administration

Expand All @@ -92,13 +100,13 @@ Site admins get a dedicated set of endpoints and UI views for:

### Testing

CVErt Ops has extensive test coverage — over 1,600 Go test functions across 147 test files, plus 32 frontend test suites. Aggregate statement coverage is 62%, but that number is diluted by generated code (sqlc output), test infrastructure, and CLI boilerplate — all at 0%. Business logic packages where coverage matters most range from 80% to 100%: alert DSL 94%, feed adapters 84-100%, auth 89%, merge 87%, retention 96%, worker 91%.
CVErt Ops has extensive test coverage — over 2,200 Go test functions across 200+ test files, plus 32 frontend test suites. Aggregate statement coverage is diluted by generated code (sqlc output), test infrastructure, and CLI boilerplate — all at 0% by design. Business logic packages where coverage matters most alert DSL, feed adapters, auth, merge, retention, worker — sit consistently in the 80–100% range.

**Integration tests hit real infrastructure.** Over 70 test files run against a real PostgreSQL instance (via testcontainers) with full RLS enforcement, real migrations, and seeded data. API tests stand up real HTTP servers and exercise the full middleware stack — auth, RBAC, CSRF, tier enforcement, rate limiting. No mocking away the hard parts.
**Integration tests hit real infrastructure.** Over 100 test files run against a real PostgreSQL instance (via testcontainers) with full RLS enforcement, real migrations, and seeded data. API tests stand up real HTTP servers and exercise the full middleware stack — auth, RBAC, MFA, SCIM, CSRF, tier enforcement, rate limiting. No mocking away the hard parts.

**Shared test infrastructure** in `internal/testutil/` provides reusable helpers: a managed test database with automatic migration, seed data utilities, a mock OIDC provider for SSO testing, and a local SMTP server for email delivery tests. This keeps individual test files focused on the behavior under test rather than setup boilerplate.
**Shared test infrastructure** in `internal/testutil/` provides reusable helpers: a managed test database with automatic migration, seed data utilities, a mock OIDC provider for SSO testing, and a local SMTP server for email delivery tests. `testutil.SeedCorpus` seeds a test database with 65 real CVEs across 8 feeds (NVD, MITRE, GHSA, OSV, KEV, MSRC, Red Hat, EPSS) by running captured upstream responses through the real merge pipeline — giving downstream tests (alert evaluation, search, reports) a realistic corpus without hand-crafted fixtures.

**Feed adapter tests** use recorded HTTP responses to verify parsing, streaming, error handling, and rate limit compliance without hitting upstream APIs. Alert DSL tests cover the compiler, evaluator, and all three evaluation paths (realtime, batch, EPSS). Notification delivery tests verify the transactional safety guarantees — claim, commit, deliver, record — with real database state.
**Feed adapter tests** use captured HTTP responses served via `httptest` to verify parsing, streaming, error handling, and rate limit compliance without hitting upstream APIs. Each adapter has a golden-file test that runs real captured responses end-to-end, catching upstream schema drift that unit tests with hand-crafted fixtures cannot detect. Alert DSL tests cover the compiler, evaluator, and all three evaluation paths (realtime, batch, EPSS). Notification delivery tests verify the transactional safety guarantees — claim, commit, deliver, record — with real database state.

The frontend uses Vitest with jsdom and Vue Test Utils for component and composable testing.

Expand All @@ -118,17 +126,23 @@ This project is developed with [Claude Code](https://claude.com/claude-code) usi

**Supply chain security** — GitHub CodeQL scans on every PR, Dependabot alerts and automated security update PRs for vulnerable dependencies, secret scanning with push protection, and weekly version update PRs for Go modules, npm packages, and GitHub Actions.

**Structured planning** — features are designed in `docs/plans/` before implementation, with research notes in `dev/research-findings/` capturing technical investigations and trade-off analyses for architectural decisions.
**Structured planning** — features are designed in `dev/plans/` before implementation, with research notes in `dev/research-findings/` capturing technical investigations and trade-off analyses for architectural decisions.

## Architecture

CVErt Ops is a single Go binary with three runtime modes:
CVErt Ops is a single Go binary (`cvert-ops`) with cobra subcommands covering every operational task. A second small binary (`healthcheck`) ships alongside it for container probes.

| Command | What it runs |
|---------|-------------|
| `cvert-ops serve` | HTTP API server + embedded background worker pool |
| `cvert-ops worker` | Standalone worker pool (no HTTP) |
| `cvert-ops migrate` | Database migrations |
| `cvert-ops import-bulk` | Bulk-import CVE data from a file (dev seed / airgapped loader) |
| `cvert-ops doctor` | System health checks (DB, feeds, config, migrations) |
| `cvert-ops validate-feeds` | Validate feed configuration without running a sync |
| `cvert-ops quota` | Manage per-org AI quota (`set`/`get`/`list`/`delete`) |
| `cvert-ops rotate-encryption-key` | Rotate the at-rest encryption key with re-encrypt pass |
| `healthcheck` | Minimal container liveness/readiness probe |

The background worker handles feed ingestion, alert evaluation, notification delivery, retention cleanup, and report generation — all via an internal job queue in PostgreSQL. No Redis, no RabbitMQ, no external dependencies beyond Postgres.

Expand All @@ -150,21 +164,27 @@ The background worker handles feed ingestion, alert evaluation, notification del

```
cmd/cvert-ops/ CLI entry points (cobra subcommands)
cmd/healthcheck/ Container liveness/readiness probe binary
internal/
ai/ LLM client, quota, sanitization
alert/ Alert DSL compiler and evaluator
api/ HTTP handlers and middleware
api/ HTTP handlers and middleware (REST + SCIM 2.0)
audit/ Audit logging
auth/ JWT, OAuth, API keys, Argon2id
auth/ JWT, OAuth/OIDC, MFA (TOTP + email OTP), API keys, Argon2id
config/ Environment-based configuration
feed/ Feed adapters (NVD, MITRE, KEV, OSV, GHSA, EPSS, ...)
crypto/ Encryption helpers (AES-GCM with AAD binding)
doctor/ System health check framework
feed/ Feed adapters + circuit breaker + SSRF-hardened client
ingest/ Feed ingestion orchestrator
merge/ CVE merge pipeline
metrics/ Prometheus counters and histograms
notify/ Notification channels and delivery
report/ Scheduled report generation
retention/ Data retention policies
search/ Full-text search and facets
store/ Repository layer (sqlc + squirrel)
secure/ Async security event pipeline
store/ Repository layer (sqlc + squirrel) + SCIM store methods
tier/ Subscription tier logic
worker/ Job queue and worker pool
migrations/ SQL migration files (embedded)
templates/ Notification and report templates (embedded)
Expand Down
13 changes: 7 additions & 6 deletions cmd/cvert-ops/rotate.go
Original file line number Diff line number Diff line change
Expand Up @@ -100,21 +100,22 @@ func rotateEncryptionKeys(ctx context.Context, pool *pgxpool.Pool, currentKey, p
return 0, fmt.Errorf("set bypass_rls: %w", err)
}

rows, err := tx.Query(ctx, "SELECT id, client_secret_enc FROM sso_connections")
rows, err := tx.Query(ctx, "SELECT id, org_id, client_secret_enc FROM sso_connections")
if err != nil {
return 0, fmt.Errorf("query sso_connections: %w", err)
}
defer rows.Close()

type pending struct {
id string
enc []byte
id string
orgID [16]byte
enc []byte
}

var updates []pending
for rows.Next() {
var p pending
if err := rows.Scan(&p.id, &p.enc); err != nil {
if err := rows.Scan(&p.id, &p.orgID, &p.enc); err != nil {
return 0, fmt.Errorf("scan row: %w", err)
}
updates = append(updates, p)
Expand All @@ -125,12 +126,12 @@ func rotateEncryptionKeys(ctx context.Context, pool *pgxpool.Pool, currentKey, p

count := 0
for _, u := range updates {
plaintext, err := crypto.DecryptWithFallback(currentKey, previousKey, u.enc)
plaintext, err := crypto.DecryptWithFallback(currentKey, previousKey, u.enc, u.orgID[:])
if err != nil {
return 0, fmt.Errorf("decrypt row %s: %w", u.id, err)
}

newEnc, err := crypto.Encrypt(currentKey, plaintext)
newEnc, err := crypto.Encrypt(currentKey, plaintext, u.orgID[:])
if err != nil {
return 0, fmt.Errorf("re-encrypt row %s: %w", u.id, err)
}
Expand Down
8 changes: 4 additions & 4 deletions cmd/cvert-ops/rotate_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,9 @@ func TestRotateEncryptionKey_ReEncryptsAllValues(t *testing.T) {
}
orgID := org.ID

// Encrypt a secret with the old key.
// Encrypt a secret with the old key, bound to the org.
secret := []byte("my-client-secret")
enc, err := crypto.Encrypt(oldKey, secret)
enc, err := crypto.Encrypt(oldKey, secret, orgID[:])
if err != nil {
t.Fatalf("encrypt with old key: %v", err)
}
Expand Down Expand Up @@ -64,7 +64,7 @@ func TestRotateEncryptionKey_ReEncryptsAllValues(t *testing.T) {
t.Fatalf("read re-encrypted value: %v", err)
}

plaintext, err := crypto.Decrypt(newKey, reEncrypted)
plaintext, err := crypto.Decrypt(newKey, reEncrypted, orgID[:])
if err != nil {
t.Fatalf("decrypt with new key failed: %v", err)
}
Expand All @@ -73,7 +73,7 @@ func TestRotateEncryptionKey_ReEncryptsAllValues(t *testing.T) {
}

// Verify old key alone no longer works.
_, err = crypto.Decrypt(oldKey, reEncrypted)
_, err = crypto.Decrypt(oldKey, reEncrypted, orgID[:])
if err == nil {
t.Error("decrypt with old key should fail on re-encrypted data, but succeeded")
}
Expand Down
Loading
Loading