Compare commits

..
Author SHA1 Message Date
Tomas Dvorak 549790c611 refactor(db): port auth and security handlers to sqlc
- 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]>
2026-10-10 07:36:21 +02:00
Tomáš Dvořák 6bcbc1369e refactor(db): port git providers, repos, and webhook receiver to sqlc (#82)
- git.sql: provider/repo/webhook CRUD, push-dispatch service matcher
- ListServicesForPush folds the dynamic branch clause into a static
  predicate; COALESCE fallbacks match other service loaders
- git_receiver dispatch writes reuse deployment queries
- TestGitLiveInvariants: live coverage for upsert, search, webhook,
  and branch matching semantics

Authored By: TDvorak <[email protected]>
2026-10-10 05:31:21 +02:00
10 changed files with 1169 additions and 155 deletions

No files matched your search

+78 -46
View File
@@ -3,6 +3,8 @@ package api
import ( import (
"bytes" "bytes"
"containr/internal/database" "containr/internal/database"
"containr/internal/database/sqlcdb"
"context"
"database/sql" "database/sql"
"encoding/json" "encoding/json"
"errors" "errors"
@@ -63,11 +65,16 @@ func handleLogin(c *gin.Context) {
// Find user by email // Find user by email
var user User var user User
var hashedPassword string var hashedPassword string
err := db.QueryRow(` urow, err := sqlcdb.New(db.DB).GetUserByEmailForAuth(context.Background(), req.Email)
SELECT id, email, password_hash, name, COALESCE(avatar_url, ''), is_admin, created_at user = User{
FROM users ID: urow.ID.String(),
WHERE email = $1 Email: urow.Email,
`, req.Email).Scan(&user.ID, &user.Email, &hashedPassword, &user.Name, &user.AvatarURL, &user.IsAdmin, &user.CreatedAt) Name: urow.Name,
AvatarURL: urow.AvatarUrl,
IsAdmin: urow.IsAdmin,
CreatedAt: urow.CreatedAt.Time.String(),
}
hashedPassword = urow.PasswordHash
if err == sql.ErrNoRows { if err == sql.ErrNoRows {
authUser, ok := verifyBetterAuthCredentials(c, req.Email, req.Password) 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. // A mirrored first account owns the platform, same as handleRegister.
var total int q := sqlcdb.New(db.DB)
if err := db.QueryRow("SELECT COUNT(*) FROM users").Scan(&total); err != nil { total64, err := q.CountUsers(context.Background())
if err != nil {
return User{}, err return User{}, err
} }
var user User urow, err := q.UpsertLocalUser(context.Background(), sqlcdb.UpsertLocalUserParams{
err = db.QueryRow(` Email: email,
INSERT INTO users (email, password_hash, name, avatar_url, is_admin) PasswordHash: string(hashedPassword),
VALUES ($1, $2, $3, NULLIF($4, ''), $5) Name: name,
ON CONFLICT (email) DO UPDATE Column4: strings.TrimSpace(authUser.Image),
SET name = EXCLUDED.name, IsAdmin: total64 == 0,
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)
if err != nil { if err != nil {
return User{}, err 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 return user, nil
} }
@@ -302,8 +314,8 @@ func handleRegister(c *gin.Context) {
return return
} }
var existing int q := sqlcdb.New(db.DB)
err = db.QueryRow("SELECT COUNT(*) FROM users WHERE email = $1", strings.ToLower(strings.TrimSpace(req.Email))).Scan(&existing) existing, err := q.CountUsersByEmail(context.Background(), strings.ToLower(strings.TrimSpace(req.Email)))
if err != nil { if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Database error"}) c.JSON(http.StatusInternalServerError, gin.H{"error": "Database error"})
return return
@@ -322,12 +334,20 @@ func handleRegister(c *gin.Context) {
} }
// First account owns the platform. // First account owns the platform.
var user User urow, err := q.InsertUserAdmin(context.Background(), sqlcdb.InsertUserAdminParams{
err = db.QueryRow(` Email: req.Email,
INSERT INTO users (email, password_hash, name, is_admin) PasswordHash: string(hashedPassword),
VALUES ($1, $2, $3, $4) Name: req.Name,
RETURNING id, email, name, COALESCE(avatar_url, ''), is_admin, created_at IsAdmin: total == 0,
`, req.Email, string(hashedPassword), req.Name, total == 0).Scan(&user.ID, &user.Email, &user.Name, &user.AvatarURL, &user.IsAdmin, &user.CreatedAt) })
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 { if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create user"}) c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create user"})
@@ -365,8 +385,8 @@ func handleCreateUser(c *gin.Context) {
return return
} }
var existing int existing, err := sqlcdb.New(db.DB).CountUsersByEmail(context.Background(), req.Email)
if err := db.QueryRow("SELECT COUNT(*) FROM users WHERE email = $1", req.Email).Scan(&existing); err != nil { if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Database error"}) c.JSON(http.StatusInternalServerError, gin.H{"error": "Database error"})
return return
} }
@@ -390,12 +410,19 @@ func handleCreateUser(c *gin.Context) {
return return
} }
var user User urow, err := sqlcdb.New(db.DB).InsertUser(context.Background(), sqlcdb.InsertUserParams{
err = db.QueryRow(` Email: req.Email,
INSERT INTO users (email, password_hash, name) PasswordHash: string(hashedPassword),
VALUES ($1, $2, $3) Name: req.Name,
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) 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 { if err != nil {
c.JSON(http.StatusConflict, gin.H{"error": "User already exists"}) c.JSON(http.StatusConflict, gin.H{"error": "User already exists"})
return return
@@ -458,10 +485,11 @@ func createBetterAuthUser(c *gin.Context, name string, email string, password st
} }
func countLocalUsers(db *database.DB) (int, error) { func countLocalUsers(db *database.DB) (int, error) {
var count int count64, err := sqlcdb.New(db.DB).CountUsers(context.Background())
if err := db.QueryRow("SELECT COUNT(*) FROM users").Scan(&count); err != nil { if err != nil {
return 0, err return 0, err
} }
count := int(count64)
// Better Auth writes to auth_users; a row there only mirrors into users on // 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 // the first authenticated call. Count it too or a second public sign-up
// slips through the bootstrap window. UNION by email avoids double-counting // 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) db := c.MustGet("db").(*database.DB)
var user User var user User
err := db.QueryRow(` urow, err := sqlcdb.New(db.DB).GetUserByID(context.Background(), uuid.MustParse(userID))
SELECT id, email, name, COALESCE(avatar_url, ''), is_admin, created_at user = User{
FROM users ID: urow.ID.String(),
WHERE id = $1 Email: urow.Email,
`, userID).Scan(&user.ID, &user.Email, &user.Name, &user.AvatarURL, &user.IsAdmin, &user.CreatedAt) Name: urow.Name,
AvatarURL: urow.AvatarUrl,
IsAdmin: urow.IsAdmin,
CreatedAt: urow.CreatedAt.Time.String(),
}
if err == sql.ErrNoRows { if err == sql.ErrNoRows {
c.JSON(http.StatusNotFound, gin.H{"error": "User not found"}) c.JSON(http.StatusNotFound, gin.H{"error": "User not found"})
@@ -521,11 +553,11 @@ func handleUpdateProfile(c *gin.Context) {
} }
// Update user profile // Update user profile
_, err := db.Exec(` err := sqlcdb.New(db.DB).UpdateUserProfile(context.Background(), sqlcdb.UpdateUserProfileParams{
UPDATE users Column1: req.Name,
SET name = COALESCE($1, name), avatar_url = COALESCE($2, avatar_url) Column2: req.AvatarURL,
WHERE id = $3 ID: uuid.MustParse(userID),
`, req.Name, req.AvatarURL, userID) })
if err != nil { if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update profile"}) c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update profile"})
+70 -109
View File
@@ -2,7 +2,9 @@ package api
import ( import (
"containr/internal/database" "containr/internal/database"
"containr/internal/database/sqlcdb"
"containr/internal/security" "containr/internal/security"
"context"
"database/sql" "database/sql"
"encoding/json" "encoding/json"
"fmt" "fmt"
@@ -33,8 +35,8 @@ func NewSecurityHandler(db *database.DB, encryptionKey string) *SecurityHandler
// Notify the project owner when a background scan completes — criticals // Notify the project owner when a background scan completes — criticals
// earn a distinct title so they stand out in the notification list. // earn a distinct title so they stand out in the notification list.
scanner.OnScanComplete = func(scan *security.SecurityScan) { scanner.OnScanComplete = func(scan *security.SecurityScan) {
var ownerID string ownerID, err := sqlcdb.New(db.DB).GetProjectOwnerText(context.Background(), uuid.MustParse(scan.ProjectID))
if err := db.QueryRow(`SELECT owner_id::text FROM projects WHERE id = $1`, scan.ProjectID).Scan(&ownerID); err != nil || ownerID == "" { if err != nil || ownerID == "" {
return return
} }
title := "Security scan completed" title := "Security scan completed"
@@ -81,14 +83,10 @@ func (sh *SecurityHandler) StartSecurityScan(c *gin.Context) {
return return
} }
var serviceExists bool serviceExists, err := sqlcdb.New(sh.db.DB).ServiceExistsInProject(context.Background(), sqlcdb.ServiceExistsInProjectParams{
err := sh.db.QueryRow( ID: uuid.MustParse(req.ServiceID),
`SELECT EXISTS( ProjectID: uuid.MustParse(req.ProjectID),
SELECT 1 FROM services WHERE id = $1 AND project_id = $2 })
)`,
req.ServiceID,
req.ProjectID,
).Scan(&serviceExists)
if err != nil { if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to validate service"}) c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to validate service"})
return return
@@ -164,40 +162,30 @@ func (sh *SecurityHandler) GetVulnerabilities(c *gin.Context) {
} }
// Query vulnerabilities // Query vulnerabilities
rows, err := sh.db.Query(` rows, err := sqlcdb.New(sh.db.DB).ListVulnerabilities(context.Background(), uuid.MustParse(projectID))
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)
if err != nil { if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get vulnerabilities"}) c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get vulnerabilities"})
return return
} }
defer rows.Close()
var vulnerabilities []security.Vulnerability var vulnerabilities []security.Vulnerability
for rows.Next() { for _, row := range rows {
var vuln security.Vulnerability // NULL description/service_id failed the original scan and dropped
var resolvedAt *time.Time // the row — keep the same filter.
if !row.Description.Valid || !row.ServiceID.Valid {
err := rows.Scan(&vuln.ID, &vuln.Type, &vuln.Severity, &vuln.Title, &vuln.Description,
&vuln.ServiceID, &vuln.Status, &vuln.FoundAt, &resolvedAt)
if err != nil {
continue continue
} }
vulnerabilities = append(vulnerabilities, security.Vulnerability{
vuln.ResolvedAt = resolvedAt ID: row.ID.String(),
vulnerabilities = append(vulnerabilities, vuln) 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}) c.JSON(http.StatusOK, gin.H{"vulnerabilities": vulnerabilities})
@@ -227,11 +215,11 @@ func (sh *SecurityHandler) UpdateVulnerability(c *gin.Context) {
resolvedAt = &now resolvedAt = &now
} }
_, err := sh.db.Exec(` err := sqlcdb.New(sh.db.DB).UpdateVulnerabilityStatus(context.Background(), sqlcdb.UpdateVulnerabilityStatusParams{
UPDATE vulnerabilities Status: req.Status,
SET status = $1, resolved_at = $2 ResolvedAt: ntPtr(resolvedAt),
WHERE id = $3 ID: uuid.MustParse(vulnID),
`, req.Status, resolvedAt, vulnID) })
if err != nil { if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update vulnerability"}) c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update vulnerability"})
@@ -271,13 +259,7 @@ func (sh *SecurityHandler) StartComplianceAssessment(c *gin.Context) {
return return
} }
var frameworkExists bool frameworkExists, err := sqlcdb.New(sh.db.DB).ComplianceFrameworkExists(context.Background(), uuid.MustParse(req.FrameworkID))
err := sh.db.QueryRow(
`SELECT EXISTS(
SELECT 1 FROM compliance_frameworks WHERE id = $1
)`,
req.FrameworkID,
).Scan(&frameworkExists)
if err != nil { if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to validate framework"}) c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to validate framework"})
return return
@@ -321,30 +303,25 @@ func (sh *SecurityHandler) GetComplianceReport(c *gin.Context) {
// GetComplianceFrameworks retrieves available compliance frameworks // GetComplianceFrameworks retrieves available compliance frameworks
func (sh *SecurityHandler) GetComplianceFrameworks(c *gin.Context) { func (sh *SecurityHandler) GetComplianceFrameworks(c *gin.Context) {
rows, err := sh.db.Query(` rows, err := sqlcdb.New(sh.db.DB).ListComplianceFrameworks(context.Background())
SELECT id, name, description, version, enabled, created_at
FROM compliance_frameworks
WHERE enabled = true
ORDER BY name
`)
if err != nil { if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get compliance frameworks"}) c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get compliance frameworks"})
return return
} }
defer rows.Close()
var frameworks []security.ComplianceFramework var frameworks []security.ComplianceFramework
for rows.Next() { for _, row := range rows {
var framework security.ComplianceFramework if !row.Description.Valid {
err := rows.Scan(&framework.ID, &framework.Name, &framework.Description,
&framework.Version, &framework.Enabled, &framework.CreatedAt)
if err != nil {
continue continue
} }
frameworks = append(frameworks, security.ComplianceFramework{
frameworks = append(frameworks, framework) 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}) c.JSON(http.StatusOK, gin.H{"frameworks": frameworks})
@@ -385,23 +362,19 @@ func (sh *SecurityHandler) GetSecurityMetrics(c *gin.Context) {
Resolved int `json:"resolved"` Resolved int `json:"resolved"`
} }
err := sh.db.QueryRow(` q := sqlcdb.New(sh.db.DB)
SELECT mrow, err := q.GetVulnerabilityMetrics(context.Background(), uuid.MustParse(projectID))
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)
if err != nil { if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get vulnerability metrics"}) c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get vulnerability metrics"})
return 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 // Get latest scan
var latestScan struct { var latestScan struct {
@@ -411,13 +384,11 @@ func (sh *SecurityHandler) GetSecurityMetrics(c *gin.Context) {
Status string `json:"status"` Status string `json:"status"`
} }
err = sh.db.QueryRow(` srow, err := q.GetLatestSecurityScan(context.Background(), uuid.MustParse(projectID))
SELECT id, COALESCE((summary->>'score')::int, 0), started_at as scanned_at, status latestScan.ID = srow.ID.String()
FROM security_scans latestScan.Score = int(srow.Score)
WHERE project_id = $1 latestScan.ScannedAt = srow.ScannedAt
ORDER BY started_at DESC latestScan.Status = srow.Status
LIMIT 1
`, projectID).Scan(&latestScan.ID, &latestScan.Score, &latestScan.ScannedAt, &latestScan.Status)
if err == sql.ErrNoRows { if err == sql.ErrNoRows {
latestScan = struct { latestScan = struct {
@@ -438,13 +409,10 @@ func (sh *SecurityHandler) GetSecurityMetrics(c *gin.Context) {
LastAssessed *time.Time `json:"last_assessed"` LastAssessed *time.Time `json:"last_assessed"`
} }
err = sh.db.QueryRow(` crow, err := q.GetLatestComplianceReport(context.Background(), uuid.MustParse(projectID))
SELECT overall_status, score, assessment_date complianceStatus.OverallStatus = crow.OverallStatus
FROM compliance_reports complianceStatus.Score = int(crow.Score)
WHERE project_id = $1 complianceStatus.LastAssessed = &crow.AssessmentDate
ORDER BY assessment_date DESC
LIMIT 1
`, projectID).Scan(&complianceStatus.OverallStatus, &complianceStatus.Score, &complianceStatus.LastAssessed)
if err == sql.ErrNoRows { if err == sql.ErrNoRows {
complianceStatus = struct { complianceStatus = struct {
@@ -614,21 +582,11 @@ func (sh *SecurityHandler) requireProjectAccess(c *gin.Context, projectID string
return "", false return "", false
} }
var hasAccess bool hasAccess, err := sqlcdb.New(sh.db.DB).CheckSecurityProjectAccess(context.Background(), sqlcdb.CheckSecurityProjectAccessParams{
err := sh.db.QueryRow( ID: uuid.MustParse(projectID),
`SELECT EXISTS ( OwnerID: optionalUserUUID(c),
SELECT 1 Column3: contextIsAdmin(c),
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)
if err != nil { if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to verify project access"}) c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to verify project access"})
return "", false return "", false
@@ -649,7 +607,8 @@ func (sh *SecurityHandler) requireSecurityScanAccess(c *gin.Context, scanID stri
} }
var projectID string 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 { if err == sql.ErrNoRows {
c.JSON(http.StatusNotFound, gin.H{"error": "Security scan not found"}) c.JSON(http.StatusNotFound, gin.H{"error": "Security scan not found"})
return false return false
@@ -670,7 +629,8 @@ func (sh *SecurityHandler) requireComplianceReportAccess(c *gin.Context, reportI
} }
var projectID string 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 { if err == sql.ErrNoRows {
c.JSON(http.StatusNotFound, gin.H{"error": "Compliance report not found"}) c.JSON(http.StatusNotFound, gin.H{"error": "Compliance report not found"})
return false return false
@@ -691,7 +651,8 @@ func (sh *SecurityHandler) requireVulnerabilityAccess(c *gin.Context, vulnID str
} }
var projectID string 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 { if err == sql.ErrNoRows {
c.JSON(http.StatusNotFound, gin.H{"error": "Vulnerability not found"}) c.JSON(http.StatusNotFound, gin.H{"error": "Vulnerability not found"})
return "", false 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")
}
}
@@ -0,0 +1,233 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.31.1
// source: auth.sql
package sqlcdb
import (
"context"
"database/sql"
"github.com/google/uuid"
)
const countUsers = `-- name: CountUsers :one
SELECT COUNT(*) FROM users
`
func (q *Queries) CountUsers(ctx context.Context) (int64, error) {
row := q.db.QueryRowContext(ctx, countUsers)
var count int64
err := row.Scan(&count)
return count, err
}
const countUsersByEmail = `-- name: CountUsersByEmail :one
SELECT COUNT(*) FROM users WHERE email = $1
`
func (q *Queries) CountUsersByEmail(ctx context.Context, email string) (int64, error) {
row := q.db.QueryRowContext(ctx, countUsersByEmail, email)
var count int64
err := row.Scan(&count)
return count, err
}
const getUserByEmailForAuth = `-- name: GetUserByEmailForAuth :one
SELECT id, email, password_hash, name, COALESCE(avatar_url, '') AS avatar_url, is_admin, created_at
FROM users WHERE email = $1
`
type GetUserByEmailForAuthRow struct {
ID uuid.UUID `json:"id"`
Email string `json:"email"`
PasswordHash string `json:"password_hash"`
Name string `json:"name"`
AvatarUrl string `json:"avatar_url"`
IsAdmin bool `json:"is_admin"`
CreatedAt sql.NullTime `json:"created_at"`
}
func (q *Queries) GetUserByEmailForAuth(ctx context.Context, email string) (GetUserByEmailForAuthRow, error) {
row := q.db.QueryRowContext(ctx, getUserByEmailForAuth, email)
var i GetUserByEmailForAuthRow
err := row.Scan(
&i.ID,
&i.Email,
&i.PasswordHash,
&i.Name,
&i.AvatarUrl,
&i.IsAdmin,
&i.CreatedAt,
)
return i, err
}
const getUserByID = `-- name: GetUserByID :one
SELECT id, email, name, COALESCE(avatar_url, '') AS avatar_url, is_admin, created_at
FROM users WHERE id = $1
`
type GetUserByIDRow struct {
ID uuid.UUID `json:"id"`
Email string `json:"email"`
Name string `json:"name"`
AvatarUrl string `json:"avatar_url"`
IsAdmin bool `json:"is_admin"`
CreatedAt sql.NullTime `json:"created_at"`
}
func (q *Queries) GetUserByID(ctx context.Context, id uuid.UUID) (GetUserByIDRow, error) {
row := q.db.QueryRowContext(ctx, getUserByID, id)
var i GetUserByIDRow
err := row.Scan(
&i.ID,
&i.Email,
&i.Name,
&i.AvatarUrl,
&i.IsAdmin,
&i.CreatedAt,
)
return i, err
}
const insertUser = `-- name: InsertUser :one
INSERT INTO users (email, password_hash, name)
VALUES ($1, $2, $3)
RETURNING id, email, name, COALESCE(avatar_url, '') AS avatar_url, is_admin, created_at
`
type InsertUserParams struct {
Email string `json:"email"`
PasswordHash string `json:"password_hash"`
Name string `json:"name"`
}
type InsertUserRow struct {
ID uuid.UUID `json:"id"`
Email string `json:"email"`
Name string `json:"name"`
AvatarUrl string `json:"avatar_url"`
IsAdmin bool `json:"is_admin"`
CreatedAt sql.NullTime `json:"created_at"`
}
func (q *Queries) InsertUser(ctx context.Context, arg InsertUserParams) (InsertUserRow, error) {
row := q.db.QueryRowContext(ctx, insertUser, arg.Email, arg.PasswordHash, arg.Name)
var i InsertUserRow
err := row.Scan(
&i.ID,
&i.Email,
&i.Name,
&i.AvatarUrl,
&i.IsAdmin,
&i.CreatedAt,
)
return i, err
}
const insertUserAdmin = `-- name: InsertUserAdmin :one
INSERT INTO users (email, password_hash, name, is_admin)
VALUES ($1, $2, $3, $4)
RETURNING id, email, name, COALESCE(avatar_url, '') AS avatar_url, is_admin, created_at
`
type InsertUserAdminParams struct {
Email string `json:"email"`
PasswordHash string `json:"password_hash"`
Name string `json:"name"`
IsAdmin bool `json:"is_admin"`
}
type InsertUserAdminRow struct {
ID uuid.UUID `json:"id"`
Email string `json:"email"`
Name string `json:"name"`
AvatarUrl string `json:"avatar_url"`
IsAdmin bool `json:"is_admin"`
CreatedAt sql.NullTime `json:"created_at"`
}
func (q *Queries) InsertUserAdmin(ctx context.Context, arg InsertUserAdminParams) (InsertUserAdminRow, error) {
row := q.db.QueryRowContext(ctx, insertUserAdmin,
arg.Email,
arg.PasswordHash,
arg.Name,
arg.IsAdmin,
)
var i InsertUserAdminRow
err := row.Scan(
&i.ID,
&i.Email,
&i.Name,
&i.AvatarUrl,
&i.IsAdmin,
&i.CreatedAt,
)
return i, err
}
const updateUserProfile = `-- name: UpdateUserProfile :exec
UPDATE users
SET name = COALESCE($1::varchar, name), avatar_url = COALESCE($2::varchar, avatar_url)
WHERE id = $3
`
type UpdateUserProfileParams struct {
Column1 string `json:"column_1"`
Column2 string `json:"column_2"`
ID uuid.UUID `json:"id"`
}
func (q *Queries) UpdateUserProfile(ctx context.Context, arg UpdateUserProfileParams) error {
_, err := q.db.ExecContext(ctx, updateUserProfile, arg.Column1, arg.Column2, arg.ID)
return err
}
const upsertLocalUser = `-- name: UpsertLocalUser :one
INSERT INTO users (email, password_hash, name, avatar_url, is_admin)
VALUES ($1, $2, $3, NULLIF($4::varchar, ''), $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, '') AS avatar_url, is_admin, created_at
`
type UpsertLocalUserParams struct {
Email string `json:"email"`
PasswordHash string `json:"password_hash"`
Name string `json:"name"`
Column4 string `json:"column_4"`
IsAdmin bool `json:"is_admin"`
}
type UpsertLocalUserRow struct {
ID uuid.UUID `json:"id"`
Email string `json:"email"`
Name string `json:"name"`
AvatarUrl string `json:"avatar_url"`
IsAdmin bool `json:"is_admin"`
CreatedAt sql.NullTime `json:"created_at"`
}
func (q *Queries) UpsertLocalUser(ctx context.Context, arg UpsertLocalUserParams) (UpsertLocalUserRow, error) {
row := q.db.QueryRowContext(ctx, upsertLocalUser,
arg.Email,
arg.PasswordHash,
arg.Name,
arg.Column4,
arg.IsAdmin,
)
var i UpsertLocalUserRow
err := row.Scan(
&i.ID,
&i.Email,
&i.Name,
&i.AvatarUrl,
&i.IsAdmin,
&i.CreatedAt,
)
return i, err
}
@@ -78,6 +78,28 @@ type Banner struct {
UpdatedAt sql.NullTime `json:"updated_at"` UpdatedAt sql.NullTime `json:"updated_at"`
} }
type ComplianceFramework struct {
ID uuid.UUID `json:"id"`
Name string `json:"name"`
Description sql.NullString `json:"description"`
Version string `json:"version"`
Enabled bool `json:"enabled"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
type ComplianceReport struct {
ID uuid.UUID `json:"id"`
ProjectID uuid.UUID `json:"project_id"`
FrameworkID uuid.UUID `json:"framework_id"`
AssessmentDate time.Time `json:"assessment_date"`
Assessor sql.NullString `json:"assessor"`
OverallStatus string `json:"overall_status"`
Score int32 `json:"score"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
type ContainerInstance struct { type ContainerInstance struct {
ID string `json:"id"` ID string `json:"id"`
Name string `json:"name"` Name string `json:"name"`
@@ -397,6 +419,19 @@ type Registry struct {
UpdatedAt time.Time `json:"updated_at"` UpdatedAt time.Time `json:"updated_at"`
} }
type SecurityScan struct {
ID uuid.UUID `json:"id"`
ProjectID uuid.UUID `json:"project_id"`
ServiceID uuid.NullUUID `json:"service_id"`
ScanType string `json:"scan_type"`
Status string `json:"status"`
StartedAt time.Time `json:"started_at"`
CompletedAt sql.NullTime `json:"completed_at"`
Summary pqtype.NullRawMessage `json:"summary"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
type Service struct { type Service struct {
ID uuid.UUID `json:"id"` ID uuid.UUID `json:"id"`
Name string `json:"name"` Name string `json:"name"`
@@ -500,6 +535,17 @@ type ServiceTemplate struct {
UpdatedAt sql.NullTime `json:"updated_at"` UpdatedAt sql.NullTime `json:"updated_at"`
} }
type User struct {
ID uuid.UUID `json:"id"`
Email string `json:"email"`
PasswordHash string `json:"password_hash"`
Name string `json:"name"`
AvatarUrl sql.NullString `json:"avatar_url"`
CreatedAt sql.NullTime `json:"created_at"`
UpdatedAt sql.NullTime `json:"updated_at"`
IsAdmin bool `json:"is_admin"`
}
type UserInvite struct { type UserInvite struct {
ID uuid.UUID `json:"id"` ID uuid.UUID `json:"id"`
TokenHash string `json:"token_hash"` TokenHash string `json:"token_hash"`
@@ -524,6 +570,21 @@ type UserToken struct {
CreatedAt sql.NullTime `json:"created_at"` CreatedAt sql.NullTime `json:"created_at"`
} }
type Vulnerability struct {
ID uuid.UUID `json:"id"`
Type string `json:"type"`
Severity string `json:"severity"`
Title string `json:"title"`
Description sql.NullString `json:"description"`
ServiceID uuid.NullUUID `json:"service_id"`
ProjectID uuid.UUID `json:"project_id"`
Status string `json:"status"`
FoundAt time.Time `json:"found_at"`
ResolvedAt sql.NullTime `json:"resolved_at"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
type WebhookDelivery struct { type WebhookDelivery struct {
ID uuid.UUID `json:"id"` ID uuid.UUID `json:"id"`
WebhookID uuid.UUID `json:"webhook_id"` WebhookID uuid.UUID `json:"webhook_id"`
@@ -16,6 +16,7 @@ type Querier interface {
// Portable bucket bucketing (no TimescaleDB): floor epoch seconds into // Portable bucket bucketing (no TimescaleDB): floor epoch seconds into
// bucket-sized buckets, then back to timestamptz. // bucket-sized buckets, then back to timestamptz.
AggregateNodeMetrics(ctx context.Context, arg AggregateNodeMetricsParams) ([]AggregateNodeMetricsRow, error) AggregateNodeMetrics(ctx context.Context, arg AggregateNodeMetricsParams) ([]AggregateNodeMetricsRow, error)
CheckSecurityProjectAccess(ctx context.Context, arg CheckSecurityProjectAccessParams) (bool, error)
ClearServiceDomainDefault(ctx context.Context, serviceID uuid.UUID) error ClearServiceDomainDefault(ctx context.Context, serviceID uuid.UUID) error
ClearServiceNodePins(ctx context.Context, nodeID sql.NullString) (int64, error) ClearServiceNodePins(ctx context.Context, nodeID sql.NullString) (int64, error)
CloneServiceDomains(ctx context.Context, arg CloneServiceDomainsParams) error CloneServiceDomains(ctx context.Context, arg CloneServiceDomainsParams) error
@@ -23,6 +24,7 @@ type Querier interface {
CloneServiceVariables(ctx context.Context, arg CloneServiceVariablesParams) error CloneServiceVariables(ctx context.Context, arg CloneServiceVariablesParams) error
CompleteCommand(ctx context.Context, arg CompleteCommandParams) (AgentCommand, error) CompleteCommand(ctx context.Context, arg CompleteCommandParams) (AgentCommand, error)
CompleteDeployment(ctx context.Context, arg CompleteDeploymentParams) error CompleteDeployment(ctx context.Context, arg CompleteDeploymentParams) error
ComplianceFrameworkExists(ctx context.Context, id uuid.UUID) (bool, error)
CountAccessibleDeployments(ctx context.Context, arg CountAccessibleDeploymentsParams) (int64, error) CountAccessibleDeployments(ctx context.Context, arg CountAccessibleDeploymentsParams) (int64, error)
CountActivePreviewsForBranch(ctx context.Context, arg CountActivePreviewsForBranchParams) (int64, error) CountActivePreviewsForBranch(ctx context.Context, arg CountActivePreviewsForBranchParams) (int64, error)
CountDatabaseServicesByUserAndName(ctx context.Context, arg CountDatabaseServicesByUserAndNameParams) (int64, error) CountDatabaseServicesByUserAndName(ctx context.Context, arg CountDatabaseServicesByUserAndNameParams) (int64, error)
@@ -36,6 +38,8 @@ type Querier interface {
CountServicesByProjectAndName(ctx context.Context, arg CountServicesByProjectAndNameParams) (int64, error) CountServicesByProjectAndName(ctx context.Context, arg CountServicesByProjectAndNameParams) (int64, error)
CountUnreadNotificationsByUser(ctx context.Context, userID uuid.UUID) (int64, error) CountUnreadNotificationsByUser(ctx context.Context, userID uuid.UUID) (int64, error)
CountUserRepositories(ctx context.Context, userID uuid.UUID) (int64, error) CountUserRepositories(ctx context.Context, userID uuid.UUID) (int64, error)
CountUsers(ctx context.Context) (int64, error)
CountUsersByEmail(ctx context.Context, email string) (int64, error)
CreateAgent(ctx context.Context, arg CreateAgentParams) (NodeAgent, error) CreateAgent(ctx context.Context, arg CreateAgentParams) (NodeAgent, error)
CreateAgentAuthToken(ctx context.Context, arg CreateAgentAuthTokenParams) (AgentAuthToken, error) CreateAgentAuthToken(ctx context.Context, arg CreateAgentAuthTokenParams) (AgentAuthToken, error)
CreateBackupTarget(ctx context.Context, arg CreateBackupTargetParams) error CreateBackupTarget(ctx context.Context, arg CreateBackupTargetParams) error
@@ -84,6 +88,7 @@ type Querier interface {
GetBackupTargetByIDAndUser(ctx context.Context, arg GetBackupTargetByIDAndUserParams) (BackupTarget, error) GetBackupTargetByIDAndUser(ctx context.Context, arg GetBackupTargetByIDAndUserParams) (BackupTarget, error)
GetBannerByID(ctx context.Context, id uuid.UUID) (Banner, error) GetBannerByID(ctx context.Context, id uuid.UUID) (Banner, error)
GetCommandForAgent(ctx context.Context, arg GetCommandForAgentParams) (AgentCommand, error) GetCommandForAgent(ctx context.Context, arg GetCommandForAgentParams) (AgentCommand, error)
GetComplianceReportProjectID(ctx context.Context, id uuid.UUID) (uuid.UUID, error)
GetContainer(ctx context.Context, id string) (ContainerInstance, error) GetContainer(ctx context.Context, id string) (ContainerInstance, error)
GetContainerForAgent(ctx context.Context, arg GetContainerForAgentParams) (ContainerInstance, error) GetContainerForAgent(ctx context.Context, arg GetContainerForAgentParams) (ContainerInstance, error)
GetCronJob(ctx context.Context, id uuid.UUID) (CronJob, error) GetCronJob(ctx context.Context, id uuid.UUID) (CronJob, error)
@@ -109,6 +114,8 @@ type Querier interface {
GetGitWebhookForPush(ctx context.Context, id uuid.UUID) (GetGitWebhookForPushRow, error) GetGitWebhookForPush(ctx context.Context, id uuid.UUID) (GetGitWebhookForPushRow, error)
GetLastAgentCommandByType(ctx context.Context, arg GetLastAgentCommandByTypeParams) (AgentCommand, error) GetLastAgentCommandByType(ctx context.Context, arg GetLastAgentCommandByTypeParams) (AgentCommand, error)
GetLastDeployedImage(ctx context.Context, serviceID uuid.UUID) (string, error) GetLastDeployedImage(ctx context.Context, serviceID uuid.UUID) (string, error)
GetLatestComplianceReport(ctx context.Context, projectID uuid.UUID) (GetLatestComplianceReportRow, error)
GetLatestSecurityScan(ctx context.Context, projectID uuid.UUID) (GetLatestSecurityScanRow, error)
GetOutboundWebhookByID(ctx context.Context, id uuid.UUID) (OutboundWebhook, error) GetOutboundWebhookByID(ctx context.Context, id uuid.UUID) (OutboundWebhook, error)
GetOutboundWebhookByIDAndUser(ctx context.Context, arg GetOutboundWebhookByIDAndUserParams) (OutboundWebhook, error) GetOutboundWebhookByIDAndUser(ctx context.Context, arg GetOutboundWebhookByIDAndUserParams) (OutboundWebhook, error)
GetPreviewEnvironment(ctx context.Context, arg GetPreviewEnvironmentParams) (GetPreviewEnvironmentRow, error) GetPreviewEnvironment(ctx context.Context, arg GetPreviewEnvironmentParams) (GetPreviewEnvironmentRow, error)
@@ -122,7 +129,9 @@ type Querier interface {
GetProjectOwner(ctx context.Context, id uuid.UUID) (uuid.UUID, error) GetProjectOwner(ctx context.Context, id uuid.UUID) (uuid.UUID, error)
GetProjectOwnerByID(ctx context.Context, projectID uuid.UUID) (uuid.UUID, error) GetProjectOwnerByID(ctx context.Context, projectID uuid.UUID) (uuid.UUID, error)
GetProjectOwnerID(ctx context.Context, id uuid.UUID) (uuid.UUID, error) GetProjectOwnerID(ctx context.Context, id uuid.UUID) (uuid.UUID, error)
GetProjectOwnerText(ctx context.Context, id uuid.UUID) (string, error)
GetProjectRoleForUser(ctx context.Context, arg GetProjectRoleForUserParams) (string, error) GetProjectRoleForUser(ctx context.Context, arg GetProjectRoleForUserParams) (string, error)
GetSecurityScanProjectID(ctx context.Context, id uuid.UUID) (uuid.UUID, error)
GetServiceAccess(ctx context.Context, id uuid.UUID) (GetServiceAccessRow, error) GetServiceAccess(ctx context.Context, id uuid.UUID) (GetServiceAccessRow, error)
GetServiceDomainDefault(ctx context.Context, serviceID uuid.UUID) (string, error) GetServiceDomainDefault(ctx context.Context, serviceID uuid.UUID) (string, error)
GetServiceDomainFirst(ctx context.Context, serviceID uuid.UUID) (string, error) GetServiceDomainFirst(ctx context.Context, serviceID uuid.UUID) (string, error)
@@ -138,8 +147,12 @@ type Querier interface {
GetServiceTemplateByID(ctx context.Context, id string) (ServiceTemplate, error) GetServiceTemplateByID(ctx context.Context, id string) (ServiceTemplate, error)
GetServiceTypeBrief(ctx context.Context, arg GetServiceTypeBriefParams) (GetServiceTypeBriefRow, error) GetServiceTypeBrief(ctx context.Context, arg GetServiceTypeBriefParams) (GetServiceTypeBriefRow, error)
GetServiceVariableValue(ctx context.Context, arg GetServiceVariableValueParams) (string, error) GetServiceVariableValue(ctx context.Context, arg GetServiceVariableValueParams) (string, error)
GetUserByEmailForAuth(ctx context.Context, email string) (GetUserByEmailForAuthRow, error)
GetUserByID(ctx context.Context, id uuid.UUID) (GetUserByIDRow, error)
GetUserInviteByTokenHash(ctx context.Context, tokenHash string) (UserInvite, error) GetUserInviteByTokenHash(ctx context.Context, tokenHash string) (UserInvite, error)
GetUserTokenByHash(ctx context.Context, tokenHash string) (UserToken, error) GetUserTokenByHash(ctx context.Context, tokenHash string) (UserToken, error)
GetVulnerabilityMetrics(ctx context.Context, projectID uuid.UUID) (GetVulnerabilityMetricsRow, error)
GetVulnerabilityProjectID(ctx context.Context, id uuid.UUID) (uuid.UUID, error)
InsertAgentHeartbeat(ctx context.Context, arg InsertAgentHeartbeatParams) error InsertAgentHeartbeat(ctx context.Context, arg InsertAgentHeartbeatParams) error
InsertCronExecution(ctx context.Context, arg InsertCronExecutionParams) error InsertCronExecution(ctx context.Context, arg InsertCronExecutionParams) error
InsertCronJob(ctx context.Context, arg InsertCronJobParams) error InsertCronJob(ctx context.Context, arg InsertCronJobParams) error
@@ -149,6 +162,8 @@ type Querier interface {
InsertNotificationChannel(ctx context.Context, arg InsertNotificationChannelParams) (NotificationChannel, error) InsertNotificationChannel(ctx context.Context, arg InsertNotificationChannelParams) (NotificationChannel, error)
InsertPendingDeployment(ctx context.Context, arg InsertPendingDeploymentParams) error InsertPendingDeployment(ctx context.Context, arg InsertPendingDeploymentParams) error
InsertProjectEnvironment(ctx context.Context, arg InsertProjectEnvironmentParams) error InsertProjectEnvironment(ctx context.Context, arg InsertProjectEnvironmentParams) error
InsertUser(ctx context.Context, arg InsertUserParams) (InsertUserRow, error)
InsertUserAdmin(ctx context.Context, arg InsertUserAdminParams) (InsertUserAdminRow, error)
ListActiveBanners(ctx context.Context) ([]Banner, error) ListActiveBanners(ctx context.Context) ([]Banner, error)
ListAgentAuthTokens(ctx context.Context) ([]AgentAuthToken, error) ListAgentAuthTokens(ctx context.Context) ([]AgentAuthToken, error)
ListAgentHeartbeatsSince(ctx context.Context, arg ListAgentHeartbeatsSinceParams) ([]AgentHeartbeat, error) ListAgentHeartbeatsSince(ctx context.Context, arg ListAgentHeartbeatsSinceParams) ([]AgentHeartbeat, error)
@@ -158,6 +173,7 @@ type Querier interface {
ListBackupTargetsByUser(ctx context.Context, userID string) ([]BackupTarget, error) ListBackupTargetsByUser(ctx context.Context, userID string) ([]BackupTarget, error)
ListBanners(ctx context.Context) ([]Banner, error) ListBanners(ctx context.Context) ([]Banner, error)
ListCommandsForAgent(ctx context.Context, nodeAgentID string) ([]AgentCommand, error) ListCommandsForAgent(ctx context.Context, nodeAgentID string) ([]AgentCommand, error)
ListComplianceFrameworks(ctx context.Context) ([]ListComplianceFrameworksRow, error)
ListConnectedRepositories(ctx context.Context, arg ListConnectedRepositoriesParams) ([]ListConnectedRepositoriesRow, error) ListConnectedRepositories(ctx context.Context, arg ListConnectedRepositoriesParams) ([]ListConnectedRepositoriesRow, error)
ListContainersForAgent(ctx context.Context, nodeAgentID string) ([]ContainerInstance, error) ListContainersForAgent(ctx context.Context, nodeAgentID string) ([]ContainerInstance, error)
ListCronExecutions(ctx context.Context, cronJobID uuid.UUID) ([]CronExecution, error) ListCronExecutions(ctx context.Context, cronJobID uuid.UUID) ([]CronExecution, error)
@@ -207,6 +223,7 @@ type Querier interface {
ListSweepablePreviews(ctx context.Context) ([]ListSweepablePreviewsRow, error) ListSweepablePreviews(ctx context.Context) ([]ListSweepablePreviewsRow, error)
ListUserInvites(ctx context.Context) ([]UserInvite, error) ListUserInvites(ctx context.Context) ([]UserInvite, error)
ListUserTokens(ctx context.Context, userID uuid.UUID) ([]UserToken, error) ListUserTokens(ctx context.Context, userID uuid.UUID) ([]UserToken, error)
ListVulnerabilities(ctx context.Context, projectID uuid.UUID) ([]ListVulnerabilitiesRow, error)
ListWebhookDeliveriesByWebhook(ctx context.Context, webhookID uuid.UUID) ([]WebhookDelivery, error) ListWebhookDeliveriesByWebhook(ctx context.Context, webhookID uuid.UUID) ([]WebhookDelivery, error)
MarkAllNotificationsReadByUser(ctx context.Context, arg MarkAllNotificationsReadByUserParams) error MarkAllNotificationsReadByUser(ctx context.Context, arg MarkAllNotificationsReadByUserParams) error
MarkNotificationReadByIDAndUser(ctx context.Context, arg MarkNotificationReadByIDAndUserParams) error MarkNotificationReadByIDAndUser(ctx context.Context, arg MarkNotificationReadByIDAndUserParams) error
@@ -222,6 +239,7 @@ type Querier interface {
RevokeAgentAuthToken(ctx context.Context, id uuid.UUID) (AgentAuthToken, error) RevokeAgentAuthToken(ctx context.Context, id uuid.UUID) (AgentAuthToken, error)
RevokeUserToken(ctx context.Context, arg RevokeUserTokenParams) (int64, error) RevokeUserToken(ctx context.Context, arg RevokeUserTokenParams) (int64, error)
ScrubCommandPayload(ctx context.Context, arg ScrubCommandPayloadParams) error ScrubCommandPayload(ctx context.Context, arg ScrubCommandPayloadParams) error
ServiceExistsInProject(ctx context.Context, arg ServiceExistsInProjectParams) (bool, error)
ServiceWriteAccess(ctx context.Context, arg ServiceWriteAccessParams) (bool, error) ServiceWriteAccess(ctx context.Context, arg ServiceWriteAccessParams) (bool, error)
SetAgentDefaultDomain(ctx context.Context, arg SetAgentDefaultDomainParams) error SetAgentDefaultDomain(ctx context.Context, arg SetAgentDefaultDomainParams) error
SetAgentSchedulable(ctx context.Context, arg SetAgentSchedulableParams) error SetAgentSchedulable(ctx context.Context, arg SetAgentSchedulableParams) error
@@ -268,13 +286,16 @@ type Querier interface {
UpdatePreviewEnvironment(ctx context.Context, arg UpdatePreviewEnvironmentParams) error UpdatePreviewEnvironment(ctx context.Context, arg UpdatePreviewEnvironmentParams) error
UpdateProjectByID(ctx context.Context, arg UpdateProjectByIDParams) (int64, error) UpdateProjectByID(ctx context.Context, arg UpdateProjectByIDParams) (int64, error)
UpdateServicePreviewBranch(ctx context.Context, arg UpdateServicePreviewBranchParams) error UpdateServicePreviewBranch(ctx context.Context, arg UpdateServicePreviewBranchParams) error
UpdateUserProfile(ctx context.Context, arg UpdateUserProfileParams) error
UpdateUserTemplate(ctx context.Context, arg UpdateUserTemplateParams) (int64, error) UpdateUserTemplate(ctx context.Context, arg UpdateUserTemplateParams) (int64, error)
UpdateVulnerabilityStatus(ctx context.Context, arg UpdateVulnerabilityStatusParams) error
UpdateWebhookDeliveryResult(ctx context.Context, arg UpdateWebhookDeliveryResultParams) error UpdateWebhookDeliveryResult(ctx context.Context, arg UpdateWebhookDeliveryResultParams) error
UpsertEnvironmentVariable(ctx context.Context, arg UpsertEnvironmentVariableParams) error UpsertEnvironmentVariable(ctx context.Context, arg UpsertEnvironmentVariableParams) error
UpsertGitHubAppProvider(ctx context.Context, arg UpsertGitHubAppProviderParams) (UpsertGitHubAppProviderRow, error) UpsertGitHubAppProvider(ctx context.Context, arg UpsertGitHubAppProviderParams) (UpsertGitHubAppProviderRow, error)
UpsertGitRepository(ctx context.Context, arg UpsertGitRepositoryParams) error UpsertGitRepository(ctx context.Context, arg UpsertGitRepositoryParams) error
UpsertGitWebhook(ctx context.Context, arg UpsertGitWebhookParams) (UpsertGitWebhookRow, error) UpsertGitWebhook(ctx context.Context, arg UpsertGitWebhookParams) (UpsertGitWebhookRow, error)
UpsertInstanceMetrics(ctx context.Context, arg UpsertInstanceMetricsParams) error UpsertInstanceMetrics(ctx context.Context, arg UpsertInstanceMetricsParams) error
UpsertLocalUser(ctx context.Context, arg UpsertLocalUserParams) (UpsertLocalUserRow, error)
UpsertNodeMetrics(ctx context.Context, arg UpsertNodeMetricsParams) error UpsertNodeMetrics(ctx context.Context, arg UpsertNodeMetricsParams) error
UpsertServiceContainer(ctx context.Context, arg UpsertServiceContainerParams) error UpsertServiceContainer(ctx context.Context, arg UpsertServiceContainerParams) error
UpsertServiceMetrics(ctx context.Context, arg UpsertServiceMetricsParams) error UpsertServiceMetrics(ctx context.Context, arg UpsertServiceMetricsParams) error
@@ -0,0 +1,318 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.31.1
// source: security.sql
package sqlcdb
import (
"context"
"database/sql"
"time"
"github.com/google/uuid"
)
const checkSecurityProjectAccess = `-- name: CheckSecurityProjectAccess :one
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
))
)
`
type CheckSecurityProjectAccessParams struct {
ID uuid.UUID `json:"id"`
OwnerID uuid.UUID `json:"owner_id"`
Column3 bool `json:"column_3"`
}
func (q *Queries) CheckSecurityProjectAccess(ctx context.Context, arg CheckSecurityProjectAccessParams) (bool, error) {
row := q.db.QueryRowContext(ctx, checkSecurityProjectAccess, arg.ID, arg.OwnerID, arg.Column3)
var exists bool
err := row.Scan(&exists)
return exists, err
}
const complianceFrameworkExists = `-- name: ComplianceFrameworkExists :one
SELECT EXISTS(SELECT 1 FROM compliance_frameworks WHERE id = $1)
`
func (q *Queries) ComplianceFrameworkExists(ctx context.Context, id uuid.UUID) (bool, error) {
row := q.db.QueryRowContext(ctx, complianceFrameworkExists, id)
var exists bool
err := row.Scan(&exists)
return exists, err
}
const getComplianceReportProjectID = `-- name: GetComplianceReportProjectID :one
SELECT project_id FROM compliance_reports WHERE id = $1
`
func (q *Queries) GetComplianceReportProjectID(ctx context.Context, id uuid.UUID) (uuid.UUID, error) {
row := q.db.QueryRowContext(ctx, getComplianceReportProjectID, id)
var project_id uuid.UUID
err := row.Scan(&project_id)
return project_id, err
}
const getLatestComplianceReport = `-- name: GetLatestComplianceReport :one
SELECT overall_status, score, assessment_date
FROM compliance_reports
WHERE project_id = $1
ORDER BY assessment_date DESC
LIMIT 1
`
type GetLatestComplianceReportRow struct {
OverallStatus string `json:"overall_status"`
Score int32 `json:"score"`
AssessmentDate time.Time `json:"assessment_date"`
}
func (q *Queries) GetLatestComplianceReport(ctx context.Context, projectID uuid.UUID) (GetLatestComplianceReportRow, error) {
row := q.db.QueryRowContext(ctx, getLatestComplianceReport, projectID)
var i GetLatestComplianceReportRow
err := row.Scan(&i.OverallStatus, &i.Score, &i.AssessmentDate)
return i, err
}
const getLatestSecurityScan = `-- name: GetLatestSecurityScan :one
SELECT id, COALESCE((summary->>'score')::int, 0)::int AS score, started_at AS scanned_at, status
FROM security_scans
WHERE project_id = $1
ORDER BY started_at DESC
LIMIT 1
`
type GetLatestSecurityScanRow struct {
ID uuid.UUID `json:"id"`
Score int32 `json:"score"`
ScannedAt time.Time `json:"scanned_at"`
Status string `json:"status"`
}
func (q *Queries) GetLatestSecurityScan(ctx context.Context, projectID uuid.UUID) (GetLatestSecurityScanRow, error) {
row := q.db.QueryRowContext(ctx, getLatestSecurityScan, projectID)
var i GetLatestSecurityScanRow
err := row.Scan(
&i.ID,
&i.Score,
&i.ScannedAt,
&i.Status,
)
return i, err
}
const getProjectOwnerText = `-- name: GetProjectOwnerText :one
SELECT owner_id::text FROM projects WHERE id = $1
`
func (q *Queries) GetProjectOwnerText(ctx context.Context, id uuid.UUID) (string, error) {
row := q.db.QueryRowContext(ctx, getProjectOwnerText, id)
var owner_id string
err := row.Scan(&owner_id)
return owner_id, err
}
const getSecurityScanProjectID = `-- name: GetSecurityScanProjectID :one
SELECT project_id FROM security_scans WHERE id = $1
`
func (q *Queries) GetSecurityScanProjectID(ctx context.Context, id uuid.UUID) (uuid.UUID, error) {
row := q.db.QueryRowContext(ctx, getSecurityScanProjectID, id)
var project_id uuid.UUID
err := row.Scan(&project_id)
return project_id, err
}
const getVulnerabilityMetrics = `-- name: GetVulnerabilityMetrics :one
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
`
type GetVulnerabilityMetricsRow struct {
Total int64 `json:"total"`
Critical int64 `json:"critical"`
High int64 `json:"high"`
Medium int64 `json:"medium"`
Low int64 `json:"low"`
Open int64 `json:"open"`
Resolved int64 `json:"resolved"`
}
func (q *Queries) GetVulnerabilityMetrics(ctx context.Context, projectID uuid.UUID) (GetVulnerabilityMetricsRow, error) {
row := q.db.QueryRowContext(ctx, getVulnerabilityMetrics, projectID)
var i GetVulnerabilityMetricsRow
err := row.Scan(
&i.Total,
&i.Critical,
&i.High,
&i.Medium,
&i.Low,
&i.Open,
&i.Resolved,
)
return i, err
}
const getVulnerabilityProjectID = `-- name: GetVulnerabilityProjectID :one
SELECT project_id FROM vulnerabilities WHERE id = $1
`
func (q *Queries) GetVulnerabilityProjectID(ctx context.Context, id uuid.UUID) (uuid.UUID, error) {
row := q.db.QueryRowContext(ctx, getVulnerabilityProjectID, id)
var project_id uuid.UUID
err := row.Scan(&project_id)
return project_id, err
}
const listComplianceFrameworks = `-- name: ListComplianceFrameworks :many
SELECT id, name, description, version, enabled, created_at
FROM compliance_frameworks
WHERE enabled = true
ORDER BY name
`
type ListComplianceFrameworksRow struct {
ID uuid.UUID `json:"id"`
Name string `json:"name"`
Description sql.NullString `json:"description"`
Version string `json:"version"`
Enabled bool `json:"enabled"`
CreatedAt time.Time `json:"created_at"`
}
func (q *Queries) ListComplianceFrameworks(ctx context.Context) ([]ListComplianceFrameworksRow, error) {
rows, err := q.db.QueryContext(ctx, listComplianceFrameworks)
if err != nil {
return nil, err
}
defer rows.Close()
items := []ListComplianceFrameworksRow{}
for rows.Next() {
var i ListComplianceFrameworksRow
if err := rows.Scan(
&i.ID,
&i.Name,
&i.Description,
&i.Version,
&i.Enabled,
&i.CreatedAt,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const listVulnerabilities = `-- name: ListVulnerabilities :many
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
`
type ListVulnerabilitiesRow struct {
ID uuid.UUID `json:"id"`
Type string `json:"type"`
Severity string `json:"severity"`
Title string `json:"title"`
Description sql.NullString `json:"description"`
ServiceID uuid.NullUUID `json:"service_id"`
Status string `json:"status"`
FoundAt time.Time `json:"found_at"`
ResolvedAt sql.NullTime `json:"resolved_at"`
}
func (q *Queries) ListVulnerabilities(ctx context.Context, projectID uuid.UUID) ([]ListVulnerabilitiesRow, error) {
rows, err := q.db.QueryContext(ctx, listVulnerabilities, projectID)
if err != nil {
return nil, err
}
defer rows.Close()
items := []ListVulnerabilitiesRow{}
for rows.Next() {
var i ListVulnerabilitiesRow
if err := rows.Scan(
&i.ID,
&i.Type,
&i.Severity,
&i.Title,
&i.Description,
&i.ServiceID,
&i.Status,
&i.FoundAt,
&i.ResolvedAt,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const serviceExistsInProject = `-- name: ServiceExistsInProject :one
SELECT EXISTS(SELECT 1 FROM services WHERE id = $1 AND project_id = $2)
`
type ServiceExistsInProjectParams struct {
ID uuid.UUID `json:"id"`
ProjectID uuid.UUID `json:"project_id"`
}
func (q *Queries) ServiceExistsInProject(ctx context.Context, arg ServiceExistsInProjectParams) (bool, error) {
row := q.db.QueryRowContext(ctx, serviceExistsInProject, arg.ID, arg.ProjectID)
var exists bool
err := row.Scan(&exists)
return exists, err
}
const updateVulnerabilityStatus = `-- name: UpdateVulnerabilityStatus :exec
UPDATE vulnerabilities SET status = $1, resolved_at = $2 WHERE id = $3
`
type UpdateVulnerabilityStatusParams struct {
Status string `json:"status"`
ResolvedAt sql.NullTime `json:"resolved_at"`
ID uuid.UUID `json:"id"`
}
func (q *Queries) UpdateVulnerabilityStatus(ctx context.Context, arg UpdateVulnerabilityStatusParams) error {
_, err := q.db.ExecContext(ctx, updateVulnerabilityStatus, arg.Status, arg.ResolvedAt, arg.ID)
return err
}
+37
View File
@@ -0,0 +1,37 @@
-- name: GetUserByEmailForAuth :one
SELECT id, email, password_hash, name, COALESCE(avatar_url, '') AS avatar_url, is_admin, created_at
FROM users WHERE email = $1;
-- name: CountUsers :one
SELECT COUNT(*) FROM users;
-- name: CountUsersByEmail :one
SELECT COUNT(*) FROM users WHERE email = $1;
-- name: UpsertLocalUser :one
INSERT INTO users (email, password_hash, name, avatar_url, is_admin)
VALUES ($1, $2, $3, NULLIF($4::varchar, ''), $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, '') AS avatar_url, is_admin, created_at;
-- name: InsertUserAdmin :one
INSERT INTO users (email, password_hash, name, is_admin)
VALUES ($1, $2, $3, $4)
RETURNING id, email, name, COALESCE(avatar_url, '') AS avatar_url, is_admin, created_at;
-- name: InsertUser :one
INSERT INTO users (email, password_hash, name)
VALUES ($1, $2, $3)
RETURNING id, email, name, COALESCE(avatar_url, '') AS avatar_url, is_admin, created_at;
-- name: GetUserByID :one
SELECT id, email, name, COALESCE(avatar_url, '') AS avatar_url, is_admin, created_at
FROM users WHERE id = $1;
-- name: UpdateUserProfile :exec
UPDATE users
SET name = COALESCE($1::varchar, name), avatar_url = COALESCE($2::varchar, avatar_url)
WHERE id = $3;
+78
View File
@@ -0,0 +1,78 @@
-- name: GetProjectOwnerText :one
SELECT owner_id::text FROM projects WHERE id = $1;
-- name: ServiceExistsInProject :one
SELECT EXISTS(SELECT 1 FROM services WHERE id = $1 AND project_id = $2);
-- name: ListVulnerabilities :many
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;
-- name: UpdateVulnerabilityStatus :exec
UPDATE vulnerabilities SET status = $1, resolved_at = $2 WHERE id = $3;
-- name: ComplianceFrameworkExists :one
SELECT EXISTS(SELECT 1 FROM compliance_frameworks WHERE id = $1);
-- name: ListComplianceFrameworks :many
SELECT id, name, description, version, enabled, created_at
FROM compliance_frameworks
WHERE enabled = true
ORDER BY name;
-- name: GetVulnerabilityMetrics :one
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;
-- name: GetLatestSecurityScan :one
SELECT id, COALESCE((summary->>'score')::int, 0)::int AS score, started_at AS scanned_at, status
FROM security_scans
WHERE project_id = $1
ORDER BY started_at DESC
LIMIT 1;
-- name: GetLatestComplianceReport :one
SELECT overall_status, score, assessment_date
FROM compliance_reports
WHERE project_id = $1
ORDER BY assessment_date DESC
LIMIT 1;
-- name: CheckSecurityProjectAccess :one
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
))
);
-- name: GetSecurityScanProjectID :one
SELECT project_id FROM security_scans WHERE id = $1;
-- name: GetComplianceReportProjectID :one
SELECT project_id FROM compliance_reports WHERE id = $1;
-- name: GetVulnerabilityProjectID :one
SELECT project_id FROM vulnerabilities WHERE id = $1;
+61
View File
@@ -529,3 +529,64 @@ CREATE TABLE git_webhooks (
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
); );
CREATE TABLE users (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
email VARCHAR(255) NOT NULL UNIQUE,
password_hash VARCHAR(255) NOT NULL,
name VARCHAR(255) NOT NULL,
avatar_url VARCHAR(500),
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
is_admin BOOLEAN NOT NULL DEFAULT false
);
CREATE TABLE compliance_frameworks (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name VARCHAR(100) NOT NULL UNIQUE,
description TEXT,
version VARCHAR(20) NOT NULL,
enabled BOOLEAN NOT NULL DEFAULT true,
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW()
);
CREATE TABLE security_scans (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
project_id UUID NOT NULL,
service_id UUID,
scan_type VARCHAR(50) NOT NULL,
status VARCHAR(50) NOT NULL DEFAULT 'running',
started_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
completed_at TIMESTAMP WITH TIME ZONE,
summary JSONB DEFAULT '{}',
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW()
);
CREATE TABLE compliance_reports (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
project_id UUID NOT NULL,
framework_id UUID NOT NULL,
assessment_date TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
assessor VARCHAR(255),
overall_status VARCHAR(50) NOT NULL,
score INTEGER NOT NULL DEFAULT 0,
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW()
);
CREATE TABLE vulnerabilities (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
type VARCHAR(50) NOT NULL,
severity VARCHAR(20) NOT NULL,
title VARCHAR(255) NOT NULL,
description TEXT,
service_id UUID,
project_id UUID NOT NULL,
status VARCHAR(50) NOT NULL DEFAULT 'open',
found_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
resolved_at TIMESTAMP WITH TIME ZONE,
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW()
);