Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ The Vite dev server proxies `/api` requests to `localhost:8088` (the Go backend)
- **`internal/s3/`** — AWS SDK v2 client for fetching snapshot data from S3/Garage object storage.
- **`internal/jira/`** — JIRA REST API client. Discovers active releases, syncs issues by fixVersion.
- **`internal/model/`** — Shared data types used across packages.
- **`internal/junit/`** — JUnit XML test result parser.
- **`internal/ctrf/`** — CTRF (Common Test Report Format) JSON types.

### Frontend (`web/`)
- React 19 + TypeScript, built with Vite 6
Expand Down
33 changes: 33 additions & 0 deletions internal/clair/clair.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package clair

// Report is a Clair vulnerability report for a single container image.
type Report struct {
Vulnerabilities map[string]Vulnerability `json:"vulnerabilities"`
PackageVulnerabilities map[string][]string `json:"package_vulnerabilities"`
Packages map[string]Package `json:"packages"`
}

// Vulnerability describes a single CVE found by Clair.
type Vulnerability struct {
Name string `json:"name"`
NormalizedSeverity string `json:"normalized_severity"`
Description string `json:"description"`
Links string `json:"links"`
FixedInVersion string `json:"fixed_in_version"`
Package struct {
Name string `json:"name"`
} `json:"package"`
}

// Package describes a software package detected in the image.
type Package struct {
Name string `json:"name"`
Version string `json:"version"`
}

// ScanSummaryEntry is one element of the scans/summary.json array.
type ScanSummaryEntry struct {
Component string `json:"component"`
Status string `json:"status"`
Reports int `json:"reports"`
}
45 changes: 45 additions & 0 deletions internal/ctrf/ctrf.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package ctrf

// Report is the top-level CTRF JSON structure.
type Report struct {
Results Results `json:"results"`
}

// Results contains the tool info, summary, and individual test outcomes.
type Results struct {
Tool Tool `json:"tool"`
Summary Summary `json:"summary"`
Tests []Test `json:"tests"`
}

// Tool identifies the test runner that produced the report.
type Tool struct {
Name string `json:"name"`
Version string `json:"version"`
}

// Summary holds aggregate counts from a CTRF report.
type Summary struct {
Tests int `json:"tests"`
Passed int `json:"passed"`
Failed int `json:"failed"`
Skipped int `json:"skipped"`
Pending int `json:"pending"`
Other int `json:"other"`
Flaky int `json:"flaky"`
Start int64 `json:"start"`
Stop int64 `json:"stop"`
}

// Test represents a single test case outcome in a CTRF report.
type Test struct {
Name string `json:"name"`
Status string `json:"status"`
Duration float64 `json:"duration"`
Message string `json:"message,omitempty"`
Trace string `json:"trace,omitempty"`
FilePath string `json:"filePath,omitempty"`
Suite string `json:"suite,omitempty"`
Retries int `json:"retries,omitempty"`
Flaky bool `json:"flaky,omitempty"`
}
70 changes: 52 additions & 18 deletions internal/db/queries/snapshots.sql
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
-- name: CreateSnapshot :execlastid
INSERT INTO snapshots (application, name, trigger_component, trigger_git_sha, trigger_pipeline_run, tests_passed, released, release_blocked_reason, created_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?);
INSERT INTO snapshots (application, name, tests_passed, created_at)
VALUES (?, ?, ?, ?);

-- name: SnapshotExistsByName :one
SELECT COUNT(*) FROM snapshots WHERE name = ?;

-- name: GetSnapshotRow :one
SELECT id, application, name, trigger_component, trigger_git_sha, trigger_pipeline_run,
tests_passed, released, release_blocked_reason, created_at
SELECT id, application, name, tests_passed, created_at
FROM snapshots WHERE name = ?;

-- name: CreateSnapshotComponent :exec
Expand All @@ -21,22 +20,19 @@ WHERE snapshot_id = ?
ORDER BY component;

-- name: ListAllSnapshots :many
SELECT id, application, name, trigger_component, trigger_git_sha, trigger_pipeline_run,
tests_passed, released, release_blocked_reason, created_at
SELECT id, application, name, tests_passed, created_at
FROM snapshots
ORDER BY id DESC LIMIT ? OFFSET ?;

-- name: ListSnapshotsByApplication :many
SELECT id, application, name, trigger_component, trigger_git_sha, trigger_pipeline_run,
tests_passed, released, release_blocked_reason, created_at
SELECT id, application, name, tests_passed, created_at
FROM snapshots
WHERE application = ?
ORDER BY id DESC LIMIT ? OFFSET ?;

-- name: LatestSnapshotPerApplication :many
SELECT s.id, s.application, s.name, s.trigger_component, s.trigger_git_sha, s.trigger_pipeline_run,
s.tests_passed, s.released, s.release_blocked_reason, s.created_at, CAST(counts.cnt AS INTEGER) AS cnt,
(SELECT COUNT(*) FROM snapshot_test_results WHERE snapshot_id = s.id) AS test_count
SELECT s.id, s.application, s.name, s.tests_passed, s.created_at, CAST(counts.cnt AS INTEGER) AS cnt,
(SELECT COUNT(*) FROM test_suites WHERE snapshot_id = s.id) AS test_count
FROM snapshots s
JOIN (
SELECT application, MAX(id) AS max_id, COUNT(*) AS cnt
Expand All @@ -45,12 +41,50 @@ JOIN (
) counts ON s.id = counts.max_id
ORDER BY s.application;

-- name: CreateSnapshotTestResult :exec
INSERT INTO snapshot_test_results (snapshot_id, scenario, status, pipeline_run, total, passed, failed, skipped, duration_sec)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?);
-- name: CreateTestSuite :execlastid
INSERT INTO test_suites (snapshot_id, name, status, pipeline_run, tool_name, tool_version, tests, passed, failed, skipped, pending, other, flaky, start_time, stop_time, duration_ms)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?);

-- name: ListSnapshotTestResults :many
SELECT id, snapshot_id, scenario, status, pipeline_run, total, passed, failed, skipped, duration_sec, created_at
FROM snapshot_test_results
-- name: CreateTestCase :exec
INSERT INTO test_cases (test_suite_id, name, status, duration_ms, message, trace, file_path, suite, retries, flaky)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?);

-- name: ListTestSuitesBySnapshot :many
SELECT id, snapshot_id, name, status, pipeline_run, tool_name, tool_version, tests, passed, failed, skipped, pending, other, flaky, start_time, stop_time, duration_ms, created_at
FROM test_suites
WHERE snapshot_id = ?
ORDER BY scenario;
ORDER BY name;

-- name: ListTestCasesBySuite :many
SELECT id, test_suite_id, name, status, duration_ms, message, trace, file_path, suite, retries, flaky
FROM test_cases
WHERE test_suite_id = ?
ORDER BY name;

-- name: CreateVulnerabilityReport :execlastid
INSERT INTO vulnerability_reports (snapshot_id, component, arch, total, critical, high, medium, low, unknown, fixable)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?);

-- name: CreateVulnerability :exec
INSERT INTO vulnerabilities (report_id, name, severity, package_name, package_version, fixed_in_version, description, link)
VALUES (?, ?, ?, ?, ?, ?, ?, ?);

-- name: ListVulnerabilityReportsBySnapshot :many
SELECT id, snapshot_id, component, arch, total, critical, high, medium, low, unknown, fixable, created_at
FROM vulnerability_reports
WHERE snapshot_id = ?
ORDER BY component, arch;

-- name: ListVulnerabilitiesByReport :many
SELECT id, report_id, name, severity, package_name, package_version, fixed_in_version, description, link
FROM vulnerabilities
WHERE report_id = ?
ORDER BY
CASE severity
WHEN 'Critical' THEN 0
WHEN 'High' THEN 1
WHEN 'Medium' THEN 2
WHEN 'Low' THEN 3
ELSE 4
END,
name;
79 changes: 64 additions & 15 deletions internal/db/schema.sql
Original file line number Diff line number Diff line change
Expand Up @@ -6,36 +6,54 @@ CREATE TABLE IF NOT EXISTS components (
);

CREATE TABLE IF NOT EXISTS snapshots (
id INTEGER PRIMARY KEY AUTOINCREMENT,
application TEXT NOT NULL,
name TEXT NOT NULL UNIQUE,
trigger_component TEXT NOT NULL DEFAULT '',
trigger_git_sha TEXT NOT NULL DEFAULT '',
trigger_pipeline_run TEXT NOT NULL DEFAULT '',
tests_passed INTEGER NOT NULL DEFAULT 0,
released INTEGER NOT NULL DEFAULT 0,
release_blocked_reason TEXT NOT NULL DEFAULT '',
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ','now'))
id INTEGER PRIMARY KEY AUTOINCREMENT,
application TEXT NOT NULL,
name TEXT NOT NULL UNIQUE,
tests_passed INTEGER NOT NULL DEFAULT 0,
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ','now'))
);

CREATE INDEX IF NOT EXISTS idx_snapshots_application ON snapshots(application);
CREATE INDEX IF NOT EXISTS idx_snapshots_created ON snapshots(created_at DESC);

CREATE TABLE IF NOT EXISTS snapshot_test_results (
CREATE TABLE IF NOT EXISTS test_suites (
id INTEGER PRIMARY KEY AUTOINCREMENT,
snapshot_id INTEGER NOT NULL REFERENCES snapshots(id) ON DELETE CASCADE,
scenario TEXT NOT NULL,
name TEXT NOT NULL,
status TEXT NOT NULL DEFAULT 'unknown',
pipeline_run TEXT NOT NULL DEFAULT '',
total INTEGER NOT NULL DEFAULT 0,
tool_name TEXT NOT NULL DEFAULT '',
tool_version TEXT NOT NULL DEFAULT '',
tests INTEGER NOT NULL DEFAULT 0,
passed INTEGER NOT NULL DEFAULT 0,
failed INTEGER NOT NULL DEFAULT 0,
skipped INTEGER NOT NULL DEFAULT 0,
duration_sec REAL NOT NULL DEFAULT 0.0,
pending INTEGER NOT NULL DEFAULT 0,
other INTEGER NOT NULL DEFAULT 0,
flaky INTEGER NOT NULL DEFAULT 0,
start_time INTEGER NOT NULL DEFAULT 0,
stop_time INTEGER NOT NULL DEFAULT 0,
duration_ms INTEGER NOT NULL DEFAULT 0,
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ','now'))
);

CREATE INDEX IF NOT EXISTS idx_snapshot_test_results_snapshot ON snapshot_test_results(snapshot_id);
CREATE INDEX IF NOT EXISTS idx_test_suites_snapshot ON test_suites(snapshot_id);

CREATE TABLE IF NOT EXISTS test_cases (
id INTEGER PRIMARY KEY AUTOINCREMENT,
test_suite_id INTEGER NOT NULL REFERENCES test_suites(id) ON DELETE CASCADE,
name TEXT NOT NULL,
status TEXT NOT NULL DEFAULT 'unknown',
duration_ms REAL NOT NULL DEFAULT 0.0,
message TEXT NOT NULL DEFAULT '',
trace TEXT NOT NULL DEFAULT '',
file_path TEXT NOT NULL DEFAULT '',
suite TEXT NOT NULL DEFAULT '',
retries INTEGER NOT NULL DEFAULT 0,
flaky INTEGER NOT NULL DEFAULT 0
);

CREATE INDEX IF NOT EXISTS idx_test_cases_suite ON test_cases(test_suite_id);

CREATE TABLE IF NOT EXISTS snapshot_components (
id INTEGER PRIMARY KEY AUTOINCREMENT,
Expand Down Expand Up @@ -64,6 +82,37 @@ CREATE TABLE IF NOT EXISTS jira_issues (
);

CREATE UNIQUE INDEX IF NOT EXISTS idx_jira_issues_key_version ON jira_issues(key, fix_version);

CREATE TABLE IF NOT EXISTS vulnerability_reports (
id INTEGER PRIMARY KEY AUTOINCREMENT,
snapshot_id INTEGER NOT NULL REFERENCES snapshots(id) ON DELETE CASCADE,
component TEXT NOT NULL,
arch TEXT NOT NULL,
total INTEGER NOT NULL DEFAULT 0,
critical INTEGER NOT NULL DEFAULT 0,
high INTEGER NOT NULL DEFAULT 0,
medium INTEGER NOT NULL DEFAULT 0,
low INTEGER NOT NULL DEFAULT 0,
unknown INTEGER NOT NULL DEFAULT 0,
fixable INTEGER NOT NULL DEFAULT 0,
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ','now'))
);

CREATE INDEX IF NOT EXISTS idx_vuln_reports_snapshot ON vulnerability_reports(snapshot_id);

CREATE TABLE IF NOT EXISTS vulnerabilities (
id INTEGER PRIMARY KEY AUTOINCREMENT,
report_id INTEGER NOT NULL REFERENCES vulnerability_reports(id) ON DELETE CASCADE,
name TEXT NOT NULL,
severity TEXT NOT NULL DEFAULT '',
package_name TEXT NOT NULL DEFAULT '',
package_version TEXT NOT NULL DEFAULT '',
fixed_in_version TEXT NOT NULL DEFAULT '',
description TEXT NOT NULL DEFAULT '',
link TEXT NOT NULL DEFAULT ''
);

CREATE INDEX IF NOT EXISTS idx_vulns_report ON vulnerabilities(report_id);
CREATE INDEX IF NOT EXISTS idx_jira_issues_fix_version ON jira_issues(fix_version);

CREATE TABLE IF NOT EXISTS release_versions (
Expand Down
Loading
Loading