refactor(db): port auth and security handlers to sqlc (#83)

- auth.sql: user lookups, counts, register/mirror inserts, profile update
- security.sql: access EXISTS, vulnerability list/metrics/status,
  framework list, latest scan/report, project-id guards
- NULL description/service_id rows stay filtered, matching the original
  scan-error-and-skip behavior
- auth_users probe and filtered audit-log queries stay raw: one is a
  Better Auth table outside the schema, the other is dynamically built
- TestSecurityLiveInvariants covers the access predicate, severity
  ordering, upsert conflict path, and latest-row lookups against live
  Postgres

Authored By: TDvorak <[email protected]>
This commit is contained in:
Tomáš Dvořák authored and GitHub committed 2026-10-10 07:53:06 +02:00
1 parent 6bcbc1369e
commit 66301bb68a
10 files changed
+1169 -155

No files matched your search

+78 -46
View File
@@ -3,6 +3,8 @@ package api
import (
"bytes"
"containr/internal/database"
"containr/internal/database/sqlcdb"
"context"
"database/sql"
"encoding/json"
"errors"
@@ -63,11 +65,16 @@ func handleLogin(c *gin.Context) {
// Find user by email
var user User
var hashedPassword string
err := db.QueryRow(`
SELECT id, email, password_hash, name, COALESCE(avatar_url, ''), is_admin, created_at
FROM users
WHERE email = $1
`, req.Email).Scan(&user.ID, &user.Email, &hashedPassword, &user.Name, &user.AvatarURL, &user.IsAdmin, &user.CreatedAt)
urow, err := sqlcdb.New(db.DB).GetUserByEmailForAuth(context.Background(), req.Email)
user = User{
ID: urow.ID.String(),
Email: urow.Email,
Name: urow.Name,
AvatarURL: urow.AvatarUrl,
IsAdmin: urow.IsAdmin,
CreatedAt: urow.CreatedAt.Time.String(),
}
hashedPassword = urow.PasswordHash
if err == sql.ErrNoRows {
authUser, ok := verifyBetterAuthCredentials(c, req.Email, req.Password)
@@ -257,25 +264,30 @@ func createLocalUserFromBetterAuth(db *database.DB, authUser betterAuthLoginUser
}
// A mirrored first account owns the platform, same as handleRegister.
var total int
if err := db.QueryRow("SELECT COUNT(*) FROM users").Scan(&total); err != nil {
q := sqlcdb.New(db.DB)
total64, err := q.CountUsers(context.Background())
if err != nil {
return User{}, err
}
var user User
err = db.QueryRow(`
INSERT INTO users (email, password_hash, name, avatar_url, is_admin)
VALUES ($1, $2, $3, NULLIF($4, ''), $5)
ON CONFLICT (email) DO UPDATE
SET name = EXCLUDED.name,
avatar_url = COALESCE(EXCLUDED.avatar_url, users.avatar_url),
updated_at = NOW()
RETURNING id, email, name, COALESCE(avatar_url, ''), is_admin, created_at
`, email, string(hashedPassword), name, strings.TrimSpace(authUser.Image), total == 0).
Scan(&user.ID, &user.Email, &user.Name, &user.AvatarURL, &user.IsAdmin, &user.CreatedAt)
urow, err := q.UpsertLocalUser(context.Background(), sqlcdb.UpsertLocalUserParams{
Email: email,
PasswordHash: string(hashedPassword),
Name: name,
Column4: strings.TrimSpace(authUser.Image),
IsAdmin: total64 == 0,
})
if err != nil {
return User{}, err
}
user := User{
ID: urow.ID.String(),
Email: urow.Email,
Name: urow.Name,
AvatarURL: urow.AvatarUrl,
IsAdmin: urow.IsAdmin,
CreatedAt: urow.CreatedAt.Time.String(),
}
return user, nil
}
@@ -302,8 +314,8 @@ func handleRegister(c *gin.Context) {
return
}
var existing int
err = db.QueryRow("SELECT COUNT(*) FROM users WHERE email = $1", strings.ToLower(strings.TrimSpace(req.Email))).Scan(&existing)
q := sqlcdb.New(db.DB)
existing, err := q.CountUsersByEmail(context.Background(), strings.ToLower(strings.TrimSpace(req.Email)))
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Database error"})
return
@@ -322,12 +334,20 @@ func handleRegister(c *gin.Context) {
}
// First account owns the platform.
var user User
err = db.QueryRow(`
INSERT INTO users (email, password_hash, name, is_admin)
VALUES ($1, $2, $3, $4)
RETURNING id, email, name, COALESCE(avatar_url, ''), is_admin, created_at
`, req.Email, string(hashedPassword), req.Name, total == 0).Scan(&user.ID, &user.Email, &user.Name, &user.AvatarURL, &user.IsAdmin, &user.CreatedAt)
urow, err := q.InsertUserAdmin(context.Background(), sqlcdb.InsertUserAdminParams{
Email: req.Email,
PasswordHash: string(hashedPassword),
Name: req.Name,
IsAdmin: total == 0,
})
user := User{
ID: urow.ID.String(),
Email: urow.Email,
Name: urow.Name,
AvatarURL: urow.AvatarUrl,
IsAdmin: urow.IsAdmin,
CreatedAt: urow.CreatedAt.Time.String(),
}
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create user"})
@@ -365,8 +385,8 @@ func handleCreateUser(c *gin.Context) {
return
}
var existing int
if err := db.QueryRow("SELECT COUNT(*) FROM users WHERE email = $1", req.Email).Scan(&existing); err != nil {
existing, err := sqlcdb.New(db.DB).CountUsersByEmail(context.Background(), req.Email)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Database error"})
return
}
@@ -390,12 +410,19 @@ func handleCreateUser(c *gin.Context) {
return
}
var user User
err = db.QueryRow(`
INSERT INTO users (email, password_hash, name)
VALUES ($1, $2, $3)
RETURNING id, email, name, COALESCE(avatar_url, ''), is_admin, created_at
`, req.Email, string(hashedPassword), req.Name).Scan(&user.ID, &user.Email, &user.Name, &user.AvatarURL, &user.IsAdmin, &user.CreatedAt)
urow, err := sqlcdb.New(db.DB).InsertUser(context.Background(), sqlcdb.InsertUserParams{
Email: req.Email,
PasswordHash: string(hashedPassword),
Name: req.Name,
})
user := User{
ID: urow.ID.String(),
Email: urow.Email,
Name: urow.Name,
AvatarURL: urow.AvatarUrl,
IsAdmin: urow.IsAdmin,
CreatedAt: urow.CreatedAt.Time.String(),
}
if err != nil {
c.JSON(http.StatusConflict, gin.H{"error": "User already exists"})
return
@@ -458,10 +485,11 @@ func createBetterAuthUser(c *gin.Context, name string, email string, password st
}
func countLocalUsers(db *database.DB) (int, error) {
var count int
if err := db.QueryRow("SELECT COUNT(*) FROM users").Scan(&count); err != nil {
count64, err := sqlcdb.New(db.DB).CountUsers(context.Background())
if err != nil {
return 0, err
}
count := int(count64)
// Better Auth writes to auth_users; a row there only mirrors into users on
// the first authenticated call. Count it too or a second public sign-up
// slips through the bootstrap window. UNION by email avoids double-counting
@@ -489,11 +517,15 @@ func handleGetProfile(c *gin.Context) {
db := c.MustGet("db").(*database.DB)
var user User
err := db.QueryRow(`
SELECT id, email, name, COALESCE(avatar_url, ''), is_admin, created_at
FROM users
WHERE id = $1
`, userID).Scan(&user.ID, &user.Email, &user.Name, &user.AvatarURL, &user.IsAdmin, &user.CreatedAt)
urow, err := sqlcdb.New(db.DB).GetUserByID(context.Background(), uuid.MustParse(userID))
user = User{
ID: urow.ID.String(),
Email: urow.Email,
Name: urow.Name,
AvatarURL: urow.AvatarUrl,
IsAdmin: urow.IsAdmin,
CreatedAt: urow.CreatedAt.Time.String(),
}
if err == sql.ErrNoRows {
c.JSON(http.StatusNotFound, gin.H{"error": "User not found"})
@@ -521,11 +553,11 @@ func handleUpdateProfile(c *gin.Context) {
}
// Update user profile
_, err := db.Exec(`
UPDATE users
SET name = COALESCE($1, name), avatar_url = COALESCE($2, avatar_url)
WHERE id = $3
`, req.Name, req.AvatarURL, userID)
err := sqlcdb.New(db.DB).UpdateUserProfile(context.Background(), sqlcdb.UpdateUserProfileParams{
Column1: req.Name,
Column2: req.AvatarURL,
ID: uuid.MustParse(userID),
})
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update profile"})
+70 -109
View File
@@ -2,7 +2,9 @@ package api
import (
"containr/internal/database"
"containr/internal/database/sqlcdb"
"containr/internal/security"
"context"
"database/sql"
"encoding/json"
"fmt"
@@ -33,8 +35,8 @@ func NewSecurityHandler(db *database.DB, encryptionKey string) *SecurityHandler
// Notify the project owner when a background scan completes — criticals
// earn a distinct title so they stand out in the notification list.
scanner.OnScanComplete = func(scan *security.SecurityScan) {
var ownerID string
if err := db.QueryRow(`SELECT owner_id::text FROM projects WHERE id = $1`, scan.ProjectID).Scan(&ownerID); err != nil || ownerID == "" {
ownerID, err := sqlcdb.New(db.DB).GetProjectOwnerText(context.Background(), uuid.MustParse(scan.ProjectID))
if err != nil || ownerID == "" {
return
}
title := "Security scan completed"
@@ -81,14 +83,10 @@ func (sh *SecurityHandler) StartSecurityScan(c *gin.Context) {
return
}
var serviceExists bool
err := sh.db.QueryRow(
`SELECT EXISTS(
SELECT 1 FROM services WHERE id = $1 AND project_id = $2
)`,
req.ServiceID,
req.ProjectID,
).Scan(&serviceExists)
serviceExists, err := sqlcdb.New(sh.db.DB).ServiceExistsInProject(context.Background(), sqlcdb.ServiceExistsInProjectParams{
ID: uuid.MustParse(req.ServiceID),
ProjectID: uuid.MustParse(req.ProjectID),
})
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to validate service"})
return
@@ -164,40 +162,30 @@ func (sh *SecurityHandler) GetVulnerabilities(c *gin.Context) {
}
// Query vulnerabilities
rows, err := sh.db.Query(`
SELECT id, type, severity, title, description, service_id, status, found_at, resolved_at
FROM vulnerabilities
WHERE project_id = $1
ORDER BY
CASE severity
WHEN 'critical' THEN 1
WHEN 'high' THEN 2
WHEN 'medium' THEN 3
WHEN 'low' THEN 4
END,
found_at DESC
`, projectID)
rows, err := sqlcdb.New(sh.db.DB).ListVulnerabilities(context.Background(), uuid.MustParse(projectID))
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get vulnerabilities"})
return
}
defer rows.Close()
var vulnerabilities []security.Vulnerability
for rows.Next() {
var vuln security.Vulnerability
var resolvedAt *time.Time
err := rows.Scan(&vuln.ID, &vuln.Type, &vuln.Severity, &vuln.Title, &vuln.Description,
&vuln.ServiceID, &vuln.Status, &vuln.FoundAt, &resolvedAt)
if err != nil {
for _, row := range rows {
// NULL description/service_id failed the original scan and dropped
// the row — keep the same filter.
if !row.Description.Valid || !row.ServiceID.Valid {
continue
}
vuln.ResolvedAt = resolvedAt
vulnerabilities = append(vulnerabilities, vuln)
vulnerabilities = append(vulnerabilities, security.Vulnerability{
ID: row.ID.String(),
Type: row.Type,
Severity: row.Severity,
Title: row.Title,
Description: row.Description.String,
ServiceID: row.ServiceID.UUID.String(),
Status: row.Status,
FoundAt: row.FoundAt,
ResolvedAt: timePtrNT(row.ResolvedAt),
})
}
c.JSON(http.StatusOK, gin.H{"vulnerabilities": vulnerabilities})
@@ -227,11 +215,11 @@ func (sh *SecurityHandler) UpdateVulnerability(c *gin.Context) {
resolvedAt = &now
}
_, err := sh.db.Exec(`
UPDATE vulnerabilities
SET status = $1, resolved_at = $2
WHERE id = $3
`, req.Status, resolvedAt, vulnID)
err := sqlcdb.New(sh.db.DB).UpdateVulnerabilityStatus(context.Background(), sqlcdb.UpdateVulnerabilityStatusParams{
Status: req.Status,
ResolvedAt: ntPtr(resolvedAt),
ID: uuid.MustParse(vulnID),
})
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update vulnerability"})
@@ -271,13 +259,7 @@ func (sh *SecurityHandler) StartComplianceAssessment(c *gin.Context) {
return
}
var frameworkExists bool
err := sh.db.QueryRow(
`SELECT EXISTS(
SELECT 1 FROM compliance_frameworks WHERE id = $1
)`,
req.FrameworkID,
).Scan(&frameworkExists)
frameworkExists, err := sqlcdb.New(sh.db.DB).ComplianceFrameworkExists(context.Background(), uuid.MustParse(req.FrameworkID))
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to validate framework"})
return
@@ -321,30 +303,25 @@ func (sh *SecurityHandler) GetComplianceReport(c *gin.Context) {
// GetComplianceFrameworks retrieves available compliance frameworks
func (sh *SecurityHandler) GetComplianceFrameworks(c *gin.Context) {
rows, err := sh.db.Query(`
SELECT id, name, description, version, enabled, created_at
FROM compliance_frameworks
WHERE enabled = true
ORDER BY name
`)
rows, err := sqlcdb.New(sh.db.DB).ListComplianceFrameworks(context.Background())
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get compliance frameworks"})
return
}
defer rows.Close()
var frameworks []security.ComplianceFramework
for rows.Next() {
var framework security.ComplianceFramework
err := rows.Scan(&framework.ID, &framework.Name, &framework.Description,
&framework.Version, &framework.Enabled, &framework.CreatedAt)
if err != nil {
for _, row := range rows {
if !row.Description.Valid {
continue
}
frameworks = append(frameworks, framework)
frameworks = append(frameworks, security.ComplianceFramework{
ID: row.ID.String(),
Name: row.Name,
Description: row.Description.String,
Version: row.Version,
Enabled: row.Enabled,
CreatedAt: row.CreatedAt,
})
}
c.JSON(http.StatusOK, gin.H{"frameworks": frameworks})
@@ -385,23 +362,19 @@ func (sh *SecurityHandler) GetSecurityMetrics(c *gin.Context) {
Resolved int `json:"resolved"`
}
err := sh.db.QueryRow(`
SELECT
COUNT(*) as total,
COUNT(*) FILTER (WHERE severity = 'critical') as critical,
COUNT(*) FILTER (WHERE severity = 'high') as high,
COUNT(*) FILTER (WHERE severity = 'medium') as medium,
COUNT(*) FILTER (WHERE severity = 'low') as low,
COUNT(*) FILTER (WHERE status = 'open') as open,
COUNT(*) FILTER (WHERE status = 'resolved') as resolved
FROM vulnerabilities
WHERE project_id = $1
`, projectID).Scan(&vulnMetrics.Total, &vulnMetrics.Critical, &vulnMetrics.High,
&vulnMetrics.Medium, &vulnMetrics.Low, &vulnMetrics.Open, &vulnMetrics.Resolved)
q := sqlcdb.New(sh.db.DB)
mrow, err := q.GetVulnerabilityMetrics(context.Background(), uuid.MustParse(projectID))
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get vulnerability metrics"})
return
}
vulnMetrics.Total = int(mrow.Total)
vulnMetrics.Critical = int(mrow.Critical)
vulnMetrics.High = int(mrow.High)
vulnMetrics.Medium = int(mrow.Medium)
vulnMetrics.Low = int(mrow.Low)
vulnMetrics.Open = int(mrow.Open)
vulnMetrics.Resolved = int(mrow.Resolved)
// Get latest scan
var latestScan struct {
@@ -411,13 +384,11 @@ func (sh *SecurityHandler) GetSecurityMetrics(c *gin.Context) {
Status string `json:"status"`
}
err = sh.db.QueryRow(`
SELECT id, COALESCE((summary->>'score')::int, 0), started_at as scanned_at, status
FROM security_scans
WHERE project_id = $1
ORDER BY started_at DESC
LIMIT 1
`, projectID).Scan(&latestScan.ID, &latestScan.Score, &latestScan.ScannedAt, &latestScan.Status)
srow, err := q.GetLatestSecurityScan(context.Background(), uuid.MustParse(projectID))
latestScan.ID = srow.ID.String()
latestScan.Score = int(srow.Score)
latestScan.ScannedAt = srow.ScannedAt
latestScan.Status = srow.Status
if err == sql.ErrNoRows {
latestScan = struct {
@@ -438,13 +409,10 @@ func (sh *SecurityHandler) GetSecurityMetrics(c *gin.Context) {
LastAssessed *time.Time `json:"last_assessed"`
}
err = sh.db.QueryRow(`
SELECT overall_status, score, assessment_date
FROM compliance_reports
WHERE project_id = $1
ORDER BY assessment_date DESC
LIMIT 1
`, projectID).Scan(&complianceStatus.OverallStatus, &complianceStatus.Score, &complianceStatus.LastAssessed)
crow, err := q.GetLatestComplianceReport(context.Background(), uuid.MustParse(projectID))
complianceStatus.OverallStatus = crow.OverallStatus
complianceStatus.Score = int(crow.Score)
complianceStatus.LastAssessed = &crow.AssessmentDate
if err == sql.ErrNoRows {
complianceStatus = struct {
@@ -614,21 +582,11 @@ func (sh *SecurityHandler) requireProjectAccess(c *gin.Context, projectID string
return "", false
}
var hasAccess bool
err := sh.db.QueryRow(
`SELECT EXISTS (
SELECT 1
FROM projects p
WHERE p.id = $1
AND (p.is_approved
OR $3::bool
OR p.owner_id = $2 OR EXISTS (
SELECT 1 FROM project_members pm
WHERE pm.project_id = p.id AND pm.user_id = $2
))
)`,
projectID, optionalUserUUID(c), contextIsAdmin(c),
).Scan(&hasAccess)
hasAccess, err := sqlcdb.New(sh.db.DB).CheckSecurityProjectAccess(context.Background(), sqlcdb.CheckSecurityProjectAccessParams{
ID: uuid.MustParse(projectID),
OwnerID: optionalUserUUID(c),
Column3: contextIsAdmin(c),
})
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to verify project access"})
return "", false
@@ -649,7 +607,8 @@ func (sh *SecurityHandler) requireSecurityScanAccess(c *gin.Context, scanID stri
}
var projectID string
err := sh.db.QueryRow("SELECT project_id FROM security_scans WHERE id = $1", scanID).Scan(&projectID)
pid, err := sqlcdb.New(sh.db.DB).GetSecurityScanProjectID(context.Background(), uuid.MustParse(scanID))
projectID = pid.String()
if err == sql.ErrNoRows {
c.JSON(http.StatusNotFound, gin.H{"error": "Security scan not found"})
return false
@@ -670,7 +629,8 @@ func (sh *SecurityHandler) requireComplianceReportAccess(c *gin.Context, reportI
}
var projectID string
err := sh.db.QueryRow("SELECT project_id FROM compliance_reports WHERE id = $1", reportID).Scan(&projectID)
pid, err := sqlcdb.New(sh.db.DB).GetComplianceReportProjectID(context.Background(), uuid.MustParse(reportID))
projectID = pid.String()
if err == sql.ErrNoRows {
c.JSON(http.StatusNotFound, gin.H{"error": "Compliance report not found"})
return false
@@ -691,7 +651,8 @@ func (sh *SecurityHandler) requireVulnerabilityAccess(c *gin.Context, vulnID str
}
var projectID string
err := sh.db.QueryRow("SELECT project_id FROM vulnerabilities WHERE id = $1", vulnID).Scan(&projectID)
pid, err := sqlcdb.New(sh.db.DB).GetVulnerabilityProjectID(context.Background(), uuid.MustParse(vulnID))
projectID = pid.String()
if err == sql.ErrNoRows {
c.JSON(http.StatusNotFound, gin.H{"error": "Vulnerability not found"})
return "", false
+212
View File
@@ -0,0 +1,212 @@
package api
import (
"context"
"database/sql"
"strings"
"testing"
"time"
"containr/internal/database"
"containr/internal/database/sqlcdb"
"github.com/google/uuid"
)
// Live invariant test against real Postgres — set METRICS_TEST_DSN (or
// DATABASE_URL). Covers the generated auth/security queries: user upsert
// conflict path, project-access EXISTS predicate, vulnerability ordering and
// status update, latest-scan/report lookups, and the framework list.
func TestSecurityLiveInvariants(t *testing.T) {
dsn := testDSNString(t)
db, err := database.NewConnection(dsn)
if err != nil {
t.Fatalf("connect: %v", err)
}
defer db.Close()
ctx := context.Background()
q := sqlcdb.New(db.DB)
userID := uuid.New()
otherID := uuid.New()
projectID := uuid.New()
envID := uuid.New()
serviceID := uuid.New()
scanID := uuid.New()
reportID := uuid.New()
frameworkID := uuid.New()
vulnCrit := uuid.New()
vulnLow := uuid.New()
suffix := strings.ReplaceAll(userID.String()[:8], "-", "")
must := func(err error) {
t.Helper()
if err != nil {
t.Fatalf("setup: %v", err)
}
}
_, err = db.Exec(`INSERT INTO users (id, email, password_hash, name) VALUES ($1, $2, 'x', 'test'), ($3, $4, 'x', 'other')`,
userID, "sec-"+suffix+"@test.local", otherID, "sec-"+suffix+"[email protected]")
must(err)
_, err = db.Exec(`INSERT INTO projects (id, name, owner_id, is_approved) VALUES ($1, $2, $3, false)`,
projectID, "sec-"+suffix, userID)
must(err)
_, err = db.Exec(`INSERT INTO environments (id, name, project_id) VALUES ($1, 'production', $2)`,
envID, projectID)
must(err)
_, err = db.Exec(`INSERT INTO services (id, name, project_id, environment_id, service_type, source_type)
VALUES ($1, $2, $3, $4, 'web', 'image')`,
serviceID, "sec-"+suffix, projectID, envID)
must(err)
defer func() { _, _ = db.Exec(`DELETE FROM users WHERE id IN ($1, $2)`, userID, otherID) }()
// Auth queries: upsert conflicts update rather than error, counts see rows.
_, err = q.InsertUserAdmin(ctx, sqlcdb.InsertUserAdminParams{
Email: "ins-" + suffix + "@test.local", PasswordHash: "x", Name: "ins", IsAdmin: false,
})
must(err)
defer func() { _, _ = db.Exec(`DELETE FROM users WHERE email = $1`, "ins-"+suffix+"@test.local") }()
urow, err := q.UpsertLocalUser(ctx, sqlcdb.UpsertLocalUserParams{
Email: "ins-" + suffix + "@test.local", PasswordHash: "y", Name: "renamed",
Column4: "http://img", IsAdmin: false,
})
must(err)
if urow.Name != "renamed" || urow.AvatarUrl != "http://img" {
t.Fatalf("upsert: %+v", urow)
}
byEmail, err := q.GetUserByEmailForAuth(ctx, "ins-"+suffix+"@test.local")
must(err)
if byEmail.ID != urow.ID || byEmail.PasswordHash != "x" {
t.Fatalf("by email: %+v", byEmail)
}
count, err := q.CountUsersByEmail(ctx, "ins-"+suffix+"@test.local")
must(err)
if count != 1 {
t.Fatalf("count by email: %d", count)
}
byID, err := q.GetUserByID(ctx, urow.ID)
must(err)
if byID.Email != "ins-"+suffix+"@test.local" {
t.Fatalf("by id: %+v", byID)
}
// Access predicate: unapproved project is owner/admin/member only.
ownerAccess, err := q.CheckSecurityProjectAccess(ctx, sqlcdb.CheckSecurityProjectAccessParams{
ID: projectID, OwnerID: userID, Column3: false,
})
must(err)
if !ownerAccess {
t.Fatal("owner should have access")
}
strangerAccess, err := q.CheckSecurityProjectAccess(ctx, sqlcdb.CheckSecurityProjectAccessParams{
ID: projectID, OwnerID: otherID, Column3: false,
})
must(err)
if strangerAccess {
t.Fatal("stranger should not have access")
}
adminAccess, err := q.CheckSecurityProjectAccess(ctx, sqlcdb.CheckSecurityProjectAccessParams{
ID: projectID, OwnerID: otherID, Column3: true,
})
must(err)
if !adminAccess {
t.Fatal("admin should have access")
}
ownerText, err := q.GetProjectOwnerText(ctx, projectID)
must(err)
if ownerText != userID.String() {
t.Fatalf("owner text: %q", ownerText)
}
exists, err := q.ServiceExistsInProject(ctx, sqlcdb.ServiceExistsInProjectParams{
ID: serviceID, ProjectID: projectID,
})
must(err)
if !exists {
t.Fatal("service should exist in project")
}
// Scan/report/vuln rows via raw inserts (fixtures), reads via sqlc.
_, err = db.Exec(`INSERT INTO security_scans (id, project_id, service_id, scan_type, status, summary, started_at)
VALUES ($1, $2, $3, 'dependency', 'completed', '{"score": 85}', NOW() - interval '1 hour')`,
scanID, projectID, serviceID)
must(err)
_, err = db.Exec(`INSERT INTO compliance_frameworks (id, name, description, version, enabled)
VALUES ($1, $2, 'desc', '1.0', true)`,
frameworkID, "fw-"+suffix)
must(err)
defer func() { _, _ = db.Exec(`DELETE FROM compliance_frameworks WHERE id = $1`, frameworkID) }()
_, err = db.Exec(`INSERT INTO compliance_reports (id, project_id, framework_id, overall_status, score, assessment_date)
VALUES ($1, $2, $3, 'compliant', 92, NOW() - interval '30 minutes')`,
reportID, projectID, frameworkID)
must(err)
defer func() { _, _ = db.Exec(`DELETE FROM compliance_reports WHERE id = $1`, reportID) }()
_, err = db.Exec(`INSERT INTO vulnerabilities (id, type, severity, title, description, service_id, project_id, status, found_at)
VALUES ($1, 'dependency', 'low', 'low-v', 'd', $3, $4, 'open', NOW() - interval '2 hours'),
($2, 'dependency', 'critical', 'crit-v', 'd', $3, $4, 'open', NOW() - interval '1 hour')`,
vulnLow, vulnCrit, serviceID, projectID)
must(err)
defer func() { _, _ = db.Exec(`DELETE FROM vulnerabilities WHERE project_id = $1`, projectID) }()
defer func() { _, _ = db.Exec(`DELETE FROM security_scans WHERE id = $1`, scanID) }()
// Severity ordering: critical first.
vulns, err := q.ListVulnerabilities(ctx, projectID)
must(err)
if len(vulns) != 2 || vulns[0].ID != vulnCrit {
t.Fatalf("vuln ordering: %+v", vulns)
}
metrics, err := q.GetVulnerabilityMetrics(ctx, projectID)
must(err)
if metrics.Total != 2 || metrics.Critical != 1 || metrics.Low != 1 || metrics.Open != 2 {
t.Fatalf("metrics: %+v", metrics)
}
must(q.UpdateVulnerabilityStatus(ctx, sqlcdb.UpdateVulnerabilityStatusParams{
Status: "resolved",
ResolvedAt: sql.NullTime{Time: time.Now(), Valid: true},
ID: vulnCrit,
}))
metrics, err = q.GetVulnerabilityMetrics(ctx, projectID)
must(err)
if metrics.Open != 1 || metrics.Resolved != 1 {
t.Fatalf("metrics after resolve: %+v", metrics)
}
// Latest scan/report lookups.
scan, err := q.GetLatestSecurityScan(ctx, projectID)
must(err)
if scan.ID != scanID || scan.Score != 85 || scan.Status != "completed" {
t.Fatalf("latest scan: %+v", scan)
}
report, err := q.GetLatestComplianceReport(ctx, projectID)
must(err)
if report.OverallStatus != "compliant" || report.Score != 92 {
t.Fatalf("latest report: %+v", report)
}
// Project-ID lookups used by the access guards.
scanPID, err := q.GetSecurityScanProjectID(ctx, scanID)
must(err)
reportPID, err := q.GetComplianceReportProjectID(ctx, reportID)
must(err)
vulnPID, err := q.GetVulnerabilityProjectID(ctx, vulnCrit)
must(err)
if scanPID != projectID || reportPID != projectID || vulnPID != projectID {
t.Fatalf("project id lookups: %v %v %v", scanPID, reportPID, vulnPID)
}
// Framework list only returns enabled rows.
frameworks, err := q.ListComplianceFrameworks(ctx)
must(err)
found := false
for _, f := range frameworks {
if f.ID == frameworkID && f.Enabled {
found = true
}
}
if !found {
t.Fatal("enabled framework missing from list")
}
frameworkExists, err := q.ComplianceFrameworkExists(ctx, frameworkID)
must(err)
if !frameworkExists {
t.Fatal("framework should exist")
}
}