Files
breakpilot-compliance/ai-compliance-sdk/internal/usecase/store.go
T
Benjamin Admin 06bfbd1dca
Build + Deploy / build-admin-compliance (push) Successful in 2m46s
Build + Deploy / build-backend-compliance (push) Successful in 26s
Build + Deploy / build-ai-sdk (push) Successful in 52s
Build + Deploy / build-developer-portal (push) Successful in 22s
Build + Deploy / build-tts (push) Successful in 16s
Build + Deploy / build-document-crawler (push) Successful in 12s
Build + Deploy / build-dsms-gateway (push) Successful in 20s
Build + Deploy / build-dsms-node (push) Successful in 16s
CI / branch-name (push) Has been skipped
CI / guardrail-integrity (push) Has been skipped
CI / loc-budget (push) Failing after 18s
CI / secret-scan (push) Has been skipped
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / nodejs-build (push) Successful in 3m16s
CI / dep-audit (push) Has been skipped
CI / sbom-scan (push) Has been skipped
CI / test-go (push) Successful in 1m0s
CI / test-python-backend (push) Successful in 41s
CI / test-python-document-crawler (push) Successful in 29s
CI / test-python-dsms-gateway (push) Successful in 23s
CI / validate-canonical-controls (push) Successful in 16s
Build + Deploy / trigger-orca (push) Successful in 2m36s
feat(use-case-compiler): MC-based compliance questionnaires with scoring
Implements the Use-Case Compiler that turns Master Controls into
interactive compliance audits. 5 templates (Vendor Check, SAST/DAST,
DSGVO, NIS2, CRA), deterministic + LLM question generation, scoring
engine with regulation/severity breakdown, and gap detection.

- Backend: 9 API endpoints, 22 unit tests (all pass)
- Frontend: Template selector, questionnaire, result dashboard
- Migration 027: usecase_audits + usecase_answers tables

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-12 13:49:16 +02:00

348 lines
9.7 KiB
Go

package usecase
import (
"context"
"encoding/json"
"fmt"
"time"
"github.com/google/uuid"
"github.com/jackc/pgx/v5/pgxpool"
)
// Store handles database operations for use-case audits.
type Store struct {
pool *pgxpool.Pool
}
// NewStore creates a new Store.
func NewStore(pool *pgxpool.Pool) *Store {
return &Store{pool: pool}
}
// ── Audit CRUD ─────────────────────────────────────────────────────
// CreateAudit inserts a new audit.
func (s *Store) CreateAudit(a *Audit) error {
ctx := context.Background()
a.ID = uuid.New()
a.CreatedAt = time.Now()
a.UpdatedAt = time.Now()
a.Status = StatusDraft
questionsJSON, err := json.Marshal(a.Questions)
if err != nil {
return fmt.Errorf("marshal questions: %w", err)
}
_, err = s.pool.Exec(ctx, `
INSERT INTO compliance.usecase_audits
(id, tenant_id, template_id, name, target_name, status,
total_questions, answered_questions, compliance_score,
questions, created_at, updated_at)
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12)`,
a.ID, a.TenantID, a.TemplateID, a.Name, a.TargetName,
a.Status, a.TotalQuestions, a.AnsweredQuestions, a.ComplianceScore,
questionsJSON, a.CreatedAt, a.UpdatedAt,
)
return err
}
// GetAudit loads an audit by ID.
func (s *Store) GetAudit(id uuid.UUID) (*Audit, error) {
ctx := context.Background()
a := &Audit{}
var questionsJSON []byte
err := s.pool.QueryRow(ctx, `
SELECT id, tenant_id, template_id, name, target_name, status,
total_questions, answered_questions, compliance_score,
questions, created_at, updated_at, completed_at
FROM compliance.usecase_audits WHERE id = $1`, id,
).Scan(
&a.ID, &a.TenantID, &a.TemplateID, &a.Name, &a.TargetName,
&a.Status, &a.TotalQuestions, &a.AnsweredQuestions,
&a.ComplianceScore, &questionsJSON,
&a.CreatedAt, &a.UpdatedAt, &a.CompletedAt,
)
if err != nil {
return nil, err
}
if len(questionsJSON) > 0 {
json.Unmarshal(questionsJSON, &a.Questions)
}
return a, nil
}
// ListAudits returns all audits for a tenant.
func (s *Store) ListAudits(tenantID uuid.UUID) ([]Audit, error) {
ctx := context.Background()
rows, err := s.pool.Query(ctx, `
SELECT id, template_id, name, target_name, status,
total_questions, answered_questions, compliance_score,
created_at, updated_at, completed_at
FROM compliance.usecase_audits
WHERE tenant_id = $1
ORDER BY created_at DESC`, tenantID)
if err != nil {
return nil, err
}
defer rows.Close()
var audits []Audit
for rows.Next() {
var a Audit
a.TenantID = tenantID
if err := rows.Scan(
&a.ID, &a.TemplateID, &a.Name, &a.TargetName, &a.Status,
&a.TotalQuestions, &a.AnsweredQuestions, &a.ComplianceScore,
&a.CreatedAt, &a.UpdatedAt, &a.CompletedAt,
); err != nil {
return nil, err
}
audits = append(audits, a)
}
return audits, nil
}
// UpdateAuditScore updates the score and status of an audit.
func (s *Store) UpdateAuditScore(id uuid.UUID, answered int, score float64, status AuditStatus) error {
ctx := context.Background()
now := time.Now()
query := `
UPDATE compliance.usecase_audits
SET answered_questions = $2, compliance_score = $3,
status = $4, updated_at = $5`
args := []interface{}{id, answered, score, status, now}
if status == StatusCompleted {
query += `, completed_at = $6 WHERE id = $1`
args = append(args, now)
} else {
query += ` WHERE id = $1`
}
_, err := s.pool.Exec(ctx, query, args...)
return err
}
// ── Answer CRUD ────────────────────────────────────────────────────
// SaveAnswer upserts an answer (INSERT ... ON CONFLICT UPDATE).
func (s *Store) SaveAnswer(a *Answer) error {
ctx := context.Background()
a.ID = uuid.New()
a.AnsweredAt = time.Now()
answerJSON, err := json.Marshal(map[string]interface{}{
"value": a.Value,
"comment": a.Comment,
})
if err != nil {
return fmt.Errorf("marshal answer: %w", err)
}
evidenceJSON, _ := json.Marshal(a.EvidenceIDs)
_, err = s.pool.Exec(ctx, `
INSERT INTO compliance.usecase_answers
(id, audit_id, question_id, mc_id, answer, evidence_ids, status, answered_at)
VALUES ($1,$2,$3,$4,$5,$6,$7,$8)
ON CONFLICT (audit_id, question_id)
DO UPDATE SET answer = $5, evidence_ids = $6, status = $7, answered_at = $8`,
a.ID, a.AuditID, a.QuestionID, a.MCID,
answerJSON, evidenceJSON, a.Status, a.AnsweredAt,
)
return err
}
// ListAnswers returns all answers for an audit.
func (s *Store) ListAnswers(auditID uuid.UUID) ([]Answer, error) {
ctx := context.Background()
rows, err := s.pool.Query(ctx, `
SELECT id, audit_id, question_id, mc_id, answer, evidence_ids, status, answered_at
FROM compliance.usecase_answers
WHERE audit_id = $1
ORDER BY answered_at`, auditID)
if err != nil {
return nil, err
}
defer rows.Close()
var answers []Answer
for rows.Next() {
var a Answer
var answerJSON, evidenceJSON []byte
if err := rows.Scan(
&a.ID, &a.AuditID, &a.QuestionID, &a.MCID,
&answerJSON, &evidenceJSON, &a.Status, &a.AnsweredAt,
); err != nil {
return nil, err
}
var payload map[string]interface{}
if json.Unmarshal(answerJSON, &payload) == nil {
a.Value = payload["value"]
if c, ok := payload["comment"].(string); ok {
a.Comment = c
}
}
json.Unmarshal(evidenceJSON, &a.EvidenceIDs)
answers = append(answers, a)
}
return answers, nil
}
// ── MC Queries ─────────────────────────────────────────────────────
// MCInfo holds minimal data about a Master Control for compilation.
type MCInfo struct {
MasterControlID string `json:"master_control_id"`
CanonicalName string `json:"canonical_name"`
TotalControls int `json:"total_controls"`
RegSource string `json:"regulation_source"`
}
// FetchMCsByFilters returns MCs whose canonical_name matches any filter pattern.
func (s *Store) FetchMCsByFilters(filters []string) ([]MCInfo, error) {
if len(filters) == 0 {
return nil, nil
}
ctx := context.Background()
// Build LIKE conditions from filter patterns (support trailing *)
conditions := make([]string, len(filters))
args := make([]interface{}, len(filters))
for i, f := range filters {
// Convert "third_party_management_*" → "third_party_management_%"
pattern := f
if len(pattern) > 0 && pattern[len(pattern)-1] == '*' {
pattern = pattern[:len(pattern)-1] + "%"
}
conditions[i] = fmt.Sprintf("mc.canonical_name LIKE $%d", i+1)
args[i] = pattern
}
query := fmt.Sprintf(`
SELECT DISTINCT mc.master_control_id, mc.canonical_name, mc.total_controls,
COALESCE(
(SELECT pc.source_citation::jsonb->>'source'
FROM compliance.master_control_members mcm2
JOIN compliance.canonical_controls cc2 ON cc2.id = mcm2.control_uuid
LEFT JOIN compliance.canonical_controls pc ON pc.id = cc2.parent_control_uuid
WHERE mcm2.master_control_uuid = mc.id
AND pc.source_citation IS NOT NULL
LIMIT 1), ''
) as regulation_source
FROM compliance.master_controls mc
WHERE %s
ORDER BY mc.total_controls DESC
LIMIT 200`,
joinOr(conditions))
rows, err := s.pool.Query(ctx, query, args...)
if err != nil {
return nil, fmt.Errorf("fetch MCs: %w", err)
}
defer rows.Close()
var mcs []MCInfo
for rows.Next() {
var m MCInfo
if err := rows.Scan(&m.MasterControlID, &m.CanonicalName,
&m.TotalControls, &m.RegSource); err != nil {
return nil, err
}
mcs = append(mcs, m)
}
return mcs, nil
}
// FetchCheckQuestions loads existing doc_check_controls for MCs.
func (s *Store) FetchCheckQuestions(mcIDs []string) (map[string][]CheckQuestion, error) {
if len(mcIDs) == 0 {
return nil, nil
}
ctx := context.Background()
rows, err := s.pool.Query(ctx, `
SELECT control_id, check_question, pass_criteria, fail_criteria, severity
FROM compliance.doc_check_controls
WHERE control_id = ANY($1)`, mcIDs)
if err != nil {
return nil, err
}
defer rows.Close()
result := make(map[string][]CheckQuestion)
for rows.Next() {
var cq CheckQuestion
if err := rows.Scan(&cq.ControlID, &cq.Question,
&cq.PassCriteria, &cq.FailCriteria, &cq.Severity); err != nil {
return nil, err
}
result[cq.ControlID] = append(result[cq.ControlID], cq)
}
return result, nil
}
// CheckQuestion holds an existing doc_check_control question.
type CheckQuestion struct {
ControlID string `json:"control_id"`
Question string `json:"check_question"`
PassCriteria string `json:"pass_criteria"`
FailCriteria string `json:"fail_criteria"`
Severity string `json:"severity"`
}
// CountMCSourceCitations counts controls with source_citation per MC.
func (s *Store) CountMCSourceCitations(mcIDs []string) (map[string]int, error) {
if len(mcIDs) == 0 {
return nil, nil
}
ctx := context.Background()
rows, err := s.pool.Query(ctx, `
SELECT mc.master_control_id,
COUNT(CASE WHEN cc.source_citation IS NOT NULL
AND cc.source_citation != '' THEN 1 END)
FROM compliance.master_controls mc
JOIN compliance.master_control_members mcm ON mcm.master_control_uuid = mc.id
JOIN compliance.canonical_controls cc ON cc.id = mcm.control_uuid
WHERE mc.master_control_id = ANY($1)
GROUP BY mc.master_control_id`, mcIDs)
if err != nil {
return nil, err
}
defer rows.Close()
result := make(map[string]int)
for rows.Next() {
var id string
var count int
if err := rows.Scan(&id, &count); err != nil {
return nil, err
}
result[id] = count
}
return result, nil
}
func joinOr(conditions []string) string {
if len(conditions) == 1 {
return conditions[0]
}
result := "("
for i, c := range conditions {
if i > 0 {
result += " OR "
}
result += c
}
return result + ")"
}