Initial commit: breakpilot-compliance - Compliance SDK Platform

Services: Admin-Compliance, Backend-Compliance,
AI-Compliance-SDK, Consent-SDK, Developer-Portal,
PCA-Platform, DSMS

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Benjamin Boenisch
2026-02-11 23:47:28 +01:00
commit 4435e7ea0a
734 changed files with 251369 additions and 0 deletions

View File

@@ -0,0 +1,42 @@
# Build stage
FROM golang:1.22-alpine AS builder
WORKDIR /app
# Install dependencies
RUN apk add --no-cache git
# Copy go mod files
COPY go.mod go.sum* ./
RUN go mod download
# Copy source code
COPY . .
# Build binary
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o api-gateway .
# Runtime stage
FROM alpine:3.19
WORKDIR /app
# Install ca-certificates for HTTPS
RUN apk --no-cache add ca-certificates
# Copy binary from builder
COPY --from=builder /app/api-gateway .
# Create non-root user
RUN adduser -D -g '' appuser
USER appuser
# Expose port
EXPOSE 8080
# Health check
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD wget --no-verbose --tries=1 --spider http://localhost:8080/health || exit 1
# Run
CMD ["./api-gateway"]

View File

@@ -0,0 +1,15 @@
module github.com/breakpilot/compliance-sdk/services/api-gateway
go 1.22
require (
github.com/gin-gonic/gin v1.9.1
github.com/golang-jwt/jwt/v5 v5.2.0
github.com/google/uuid v1.5.0
github.com/redis/go-redis/v9 v9.3.1
github.com/spf13/viper v1.18.2
go.uber.org/zap v1.26.0
golang.org/x/time v0.5.0
gorm.io/driver/postgres v1.5.4
gorm.io/gorm v1.25.5
)

View File

@@ -0,0 +1,166 @@
// Package api provides HTTP handlers for the API Gateway
package api
import (
"net/http"
"time"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
)
// =============================================================================
// Controls
// =============================================================================
// GetControls retrieves controls for a tenant
func GetControls(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{
"controls": []interface{}{},
"total": 0,
})
}
// CreateControl creates a new control
func CreateControl(c *gin.Context) {
var req map[string]interface{}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusCreated, gin.H{
"id": uuid.New().String(),
"created_at": time.Now().Format(time.RFC3339),
})
}
// UpdateControl updates a control
func UpdateControl(c *gin.Context) {
controlID := c.Param("controlId")
c.JSON(http.StatusOK, gin.H{
"id": controlID,
"updated_at": time.Now().Format(time.RFC3339),
})
}
// DeleteControl deletes a control
func DeleteControl(c *gin.Context) {
c.JSON(http.StatusNoContent, nil)
}
// =============================================================================
// Evidence
// =============================================================================
// GetEvidence retrieves evidence for a tenant
func GetEvidence(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{
"evidence": []interface{}{},
"total": 0,
})
}
// UploadEvidence uploads new evidence
func UploadEvidence(c *gin.Context) {
// Handle file upload
file, err := c.FormFile("file")
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "No file provided"})
return
}
c.JSON(http.StatusCreated, gin.H{
"id": uuid.New().String(),
"filename": file.Filename,
"size": file.Size,
"uploaded_at": time.Now().Format(time.RFC3339),
})
}
// UpdateEvidence updates evidence metadata
func UpdateEvidence(c *gin.Context) {
evidenceID := c.Param("evidenceId")
c.JSON(http.StatusOK, gin.H{
"id": evidenceID,
"updated_at": time.Now().Format(time.RFC3339),
})
}
// DeleteEvidence deletes evidence
func DeleteEvidence(c *gin.Context) {
c.JSON(http.StatusNoContent, nil)
}
// =============================================================================
// Obligations
// =============================================================================
// GetObligations retrieves regulatory obligations
func GetObligations(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{
"obligations": []interface{}{},
"total": 0,
})
}
// RunAssessment runs a compliance assessment
func RunAssessment(c *gin.Context) {
var req map[string]interface{}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
// In production, this would call the compliance engine
c.JSON(http.StatusOK, gin.H{
"assessment_id": uuid.New().String(),
"score": 78,
"trend": "UP",
"by_regulation": gin.H{
"DSGVO": 85,
"NIS2": 72,
"AI_Act": 65,
},
"completed_at": time.Now().Format(time.RFC3339),
})
}
// =============================================================================
// Export
// =============================================================================
// ExportPDF exports a PDF report
func ExportPDF(c *gin.Context) {
reportType := c.Query("type")
if reportType == "" {
reportType = "summary"
}
// In production, generate actual PDF
c.Header("Content-Type", "application/pdf")
c.Header("Content-Disposition", "attachment; filename=compliance-report.pdf")
c.JSON(http.StatusOK, gin.H{
"message": "PDF generation would happen here",
"type": reportType,
})
}
// ExportDOCX exports a Word document
func ExportDOCX(c *gin.Context) {
reportType := c.Query("type")
if reportType == "" {
reportType = "summary"
}
c.Header("Content-Type", "application/vnd.openxmlformats-officedocument.wordprocessingml.document")
c.Header("Content-Disposition", "attachment; filename=compliance-report.docx")
c.JSON(http.StatusOK, gin.H{
"message": "DOCX generation would happen here",
"type": reportType,
})
}

View File

@@ -0,0 +1,363 @@
// Package api provides HTTP handlers for the API Gateway
package api
import (
"net/http"
"time"
"github.com/gin-gonic/gin"
"github.com/golang-jwt/jwt/v5"
"github.com/google/uuid"
)
// TokenRequest represents an OAuth token request
type TokenRequest struct {
GrantType string `json:"grant_type" binding:"required"`
ClientID string `json:"client_id" binding:"required"`
ClientSecret string `json:"client_secret" binding:"required"`
}
// TokenResponse represents an OAuth token response
type TokenResponse struct {
AccessToken string `json:"access_token"`
TokenType string `json:"token_type"`
ExpiresIn int `json:"expires_in"`
RefreshToken string `json:"refresh_token,omitempty"`
}
// GetToken handles OAuth token requests
func GetToken(c *gin.Context) {
var req TokenRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
// Validate credentials (in production, check against database)
// For now, accept any valid-looking credentials
if req.GrantType != "client_credentials" {
c.JSON(http.StatusBadRequest, gin.H{"error": "Unsupported grant type"})
return
}
// Extract tenant ID from client ID (format: sdk_tenantid)
tenantID := req.ClientID
if len(tenantID) > 4 {
tenantID = tenantID[4:] // Remove "sdk_" prefix
}
// Generate JWT
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
"tenant_id": tenantID,
"user_id": req.ClientID,
"scopes": []string{"read", "write"},
"exp": time.Now().Add(24 * time.Hour).Unix(),
"iat": time.Now().Unix(),
})
// Sign token (in production, use config.JWTSecret)
tokenString, _ := token.SignedString([]byte("your-secret-key-change-in-production"))
c.JSON(http.StatusOK, TokenResponse{
AccessToken: tokenString,
TokenType: "Bearer",
ExpiresIn: 86400,
})
}
// RefreshToken handles token refresh requests
func RefreshToken(c *gin.Context) {
// In production, validate refresh token and issue new access token
c.JSON(http.StatusOK, gin.H{"message": "Token refreshed"})
}
// =============================================================================
// State Management
// =============================================================================
// GetState retrieves the SDK state for a tenant
func GetState(c *gin.Context) {
tenantID := c.Param("tenantId")
// In production, fetch from database
c.JSON(http.StatusOK, gin.H{
"tenant_id": tenantID,
"state": gin.H{
"version": 1,
"completedSteps": []string{},
"currentStep": "overview",
},
"updated_at": time.Now().Format(time.RFC3339),
})
}
// SaveState saves the SDK state for a tenant
func SaveState(c *gin.Context) {
var req map[string]interface{}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
// In production, save to database with optimistic locking
c.JSON(http.StatusOK, gin.H{
"success": true,
"version": 1,
"updated_at": time.Now().Format(time.RFC3339),
})
}
// ResetState resets the SDK state for a tenant
func ResetState(c *gin.Context) {
tenantID, _ := c.Get("tenant_id")
c.JSON(http.StatusOK, gin.H{
"success": true,
"tenant_id": tenantID,
"message": "State reset successfully",
})
}
// =============================================================================
// DSGVO / Consent
// =============================================================================
// RecordConsent records a consent decision
func RecordConsent(c *gin.Context) {
var req map[string]interface{}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusCreated, gin.H{
"id": uuid.New().String(),
"created_at": time.Now().Format(time.RFC3339),
"status": "ACTIVE",
})
}
// GetConsents retrieves consents for a user
func GetConsents(c *gin.Context) {
userID := c.Param("userId")
c.JSON(http.StatusOK, gin.H{
"user_id": userID,
"consents": []interface{}{},
})
}
// RevokeConsent revokes a consent
func RevokeConsent(c *gin.Context) {
consentID := c.Param("consentId")
c.JSON(http.StatusOK, gin.H{
"id": consentID,
"status": "REVOKED",
"revoked_at": time.Now().Format(time.RFC3339),
})
}
// =============================================================================
// DSGVO / DSR
// =============================================================================
// SubmitDSR submits a new DSR request
func SubmitDSR(c *gin.Context) {
var req map[string]interface{}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusCreated, gin.H{
"id": uuid.New().String(),
"status": "PENDING",
"submitted_at": time.Now().Format(time.RFC3339),
"deadline": time.Now().AddDate(0, 1, 0).Format(time.RFC3339),
})
}
// ListDSRRequests lists DSR requests for a tenant
func ListDSRRequests(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{
"requests": []interface{}{},
"total": 0,
})
}
// UpdateDSRStatus updates the status of a DSR request
func UpdateDSRStatus(c *gin.Context) {
requestID := c.Param("requestId")
var req map[string]interface{}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{
"id": requestID,
"status": req["status"],
"updated_at": time.Now().Format(time.RFC3339),
})
}
// =============================================================================
// DSGVO / VVT
// =============================================================================
// GetProcessingActivities retrieves processing activities
func GetProcessingActivities(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{
"activities": []interface{}{},
"total": 0,
})
}
// CreateProcessingActivity creates a new processing activity
func CreateProcessingActivity(c *gin.Context) {
var req map[string]interface{}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusCreated, gin.H{
"id": uuid.New().String(),
"created_at": time.Now().Format(time.RFC3339),
})
}
// UpdateProcessingActivity updates a processing activity
func UpdateProcessingActivity(c *gin.Context) {
activityID := c.Param("activityId")
c.JSON(http.StatusOK, gin.H{
"id": activityID,
"updated_at": time.Now().Format(time.RFC3339),
})
}
// DeleteProcessingActivity deletes a processing activity
func DeleteProcessingActivity(c *gin.Context) {
c.JSON(http.StatusNoContent, nil)
}
// =============================================================================
// DSGVO / TOM
// =============================================================================
// GetTOMs retrieves TOMs
func GetTOMs(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{
"toms": []interface{}{},
"total": 0,
})
}
// CreateTOM creates a new TOM
func CreateTOM(c *gin.Context) {
var req map[string]interface{}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusCreated, gin.H{
"id": uuid.New().String(),
"created_at": time.Now().Format(time.RFC3339),
})
}
// UpdateTOM updates a TOM
func UpdateTOM(c *gin.Context) {
tomID := c.Param("tomId")
c.JSON(http.StatusOK, gin.H{
"id": tomID,
"updated_at": time.Now().Format(time.RFC3339),
})
}
// DeleteTOM deletes a TOM
func DeleteTOM(c *gin.Context) {
c.JSON(http.StatusNoContent, nil)
}
// =============================================================================
// DSGVO / DSFA
// =============================================================================
// GetDSFAs retrieves DSFAs
func GetDSFAs(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{
"dsfas": []interface{}{},
"total": 0,
})
}
// CreateDSFA creates a new DSFA
func CreateDSFA(c *gin.Context) {
var req map[string]interface{}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusCreated, gin.H{
"id": uuid.New().String(),
"created_at": time.Now().Format(time.RFC3339),
})
}
// UpdateDSFA updates a DSFA
func UpdateDSFA(c *gin.Context) {
dsfaID := c.Param("dsfaId")
c.JSON(http.StatusOK, gin.H{
"id": dsfaID,
"updated_at": time.Now().Format(time.RFC3339),
})
}
// =============================================================================
// DSGVO / Retention
// =============================================================================
// GetRetentionPolicies retrieves retention policies
func GetRetentionPolicies(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{
"policies": []interface{}{},
"total": 0,
})
}
// CreateRetentionPolicy creates a new retention policy
func CreateRetentionPolicy(c *gin.Context) {
var req map[string]interface{}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusCreated, gin.H{
"id": uuid.New().String(),
"created_at": time.Now().Format(time.RFC3339),
})
}
// UpdateRetentionPolicy updates a retention policy
func UpdateRetentionPolicy(c *gin.Context) {
policyID := c.Param("policyId")
c.JSON(http.StatusOK, gin.H{
"id": policyID,
"updated_at": time.Now().Format(time.RFC3339),
})
}
// DeleteRetentionPolicy deletes a retention policy
func DeleteRetentionPolicy(c *gin.Context) {
c.JSON(http.StatusNoContent, nil)
}

View File

@@ -0,0 +1,115 @@
// Package api provides HTTP handlers for the API Gateway
package api
import (
"net/http"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
)
// =============================================================================
// RAG / Search
// =============================================================================
// SearchRequest represents a RAG search request
type SearchRequest struct {
Query string `json:"query" binding:"required"`
RegulationCodes []string `json:"regulation_codes,omitempty"`
Limit int `json:"limit,omitempty"`
}
// SearchRAG performs a semantic search
func SearchRAG(c *gin.Context) {
var req SearchRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
// In production, forward to RAG service
c.JSON(http.StatusOK, gin.H{
"query": req.Query,
"results": []gin.H{
{
"content": "Art. 9 Abs. 1 DSGVO verbietet grundsätzlich die Verarbeitung besonderer Kategorien personenbezogener Daten...",
"regulationCode": "DSGVO",
"article": "9",
"score": 0.95,
},
},
"total": 1,
})
}
// AskRequest represents a RAG question request
type AskRequest struct {
Question string `json:"question" binding:"required"`
Context string `json:"context,omitempty"`
}
// AskRAG asks a question to the legal assistant
func AskRAG(c *gin.Context) {
var req AskRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
// In production, forward to RAG service
c.JSON(http.StatusOK, gin.H{
"question": req.Question,
"answer": "Art. 9 DSGVO regelt die Verarbeitung besonderer Kategorien personenbezogener Daten...",
"citations": []gin.H{
{
"regulationCode": "DSGVO",
"article": "9",
"relevance": 0.95,
},
},
"confidence": 0.92,
})
}
// GetRegulations returns available regulations
func GetRegulations(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{
"regulations": []gin.H{
{
"code": "DSGVO",
"name": "Datenschutz-Grundverordnung",
"chunks": 99,
"lastUpdated": "2024-01-01",
},
{
"code": "AI_ACT",
"name": "EU AI Act",
"chunks": 85,
"lastUpdated": "2024-01-01",
},
{
"code": "NIS2",
"name": "NIS 2 Directive",
"chunks": 46,
"lastUpdated": "2024-01-01",
},
},
})
}
// UploadDocument uploads a custom document for RAG
func UploadDocument(c *gin.Context) {
file, err := c.FormFile("file")
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "No file provided"})
return
}
c.JSON(http.StatusCreated, gin.H{
"id": uuid.New().String(),
"filename": file.Filename,
"size": file.Size,
"status": "PROCESSING",
"message": "Document uploaded and queued for processing",
})
}

View File

@@ -0,0 +1,206 @@
// Package api provides HTTP handlers for the API Gateway
package api
import (
"net/http"
"time"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
)
// =============================================================================
// SBOM
// =============================================================================
// GenerateSBOM generates a Software Bill of Materials
func GenerateSBOM(c *gin.Context) {
var req map[string]interface{}
c.ShouldBindJSON(&req)
// In production, forward to security scanner service
c.JSON(http.StatusOK, gin.H{
"id": uuid.New().String(),
"format": "cyclonedx",
"version": "1.5",
"generated_at": time.Now().Format(time.RFC3339),
"components": 144,
"licenses": gin.H{
"MIT": 89,
"Apache-2.0": 42,
"BSD-3": 8,
"Other": 5,
},
})
}
// GetSBOMComponents returns SBOM components
func GetSBOMComponents(c *gin.Context) {
category := c.Query("category")
c.JSON(http.StatusOK, gin.H{
"category": category,
"components": []gin.H{
{
"name": "react",
"version": "18.2.0",
"category": "frontend",
"license": "MIT",
"vulnerabilities": 0,
},
{
"name": "express",
"version": "4.18.2",
"category": "backend",
"license": "MIT",
"vulnerabilities": 0,
},
},
"total": 144,
})
}
// ExportSBOM exports SBOM in requested format
func ExportSBOM(c *gin.Context) {
format := c.Param("format")
var contentType string
switch format {
case "cyclonedx":
contentType = "application/json"
case "spdx":
contentType = "application/spdx+json"
default:
c.JSON(http.StatusBadRequest, gin.H{"error": "Unsupported format"})
return
}
c.Header("Content-Type", contentType)
c.Header("Content-Disposition", "attachment; filename=sbom."+format+".json")
c.JSON(http.StatusOK, gin.H{
"bomFormat": "CycloneDX",
"specVersion": "1.5",
"serialNumber": uuid.New().String(),
"version": 1,
"metadata": gin.H{
"timestamp": time.Now().Format(time.RFC3339),
"tools": []gin.H{
{
"vendor": "BreakPilot",
"name": "compliance-sdk",
"version": "0.0.1",
},
},
},
"components": []gin.H{},
})
}
// =============================================================================
// Security Scanning
// =============================================================================
// ScanRequest represents a security scan request
type ScanRequest struct {
Tools []string `json:"tools,omitempty"`
TargetPath string `json:"target_path,omitempty"`
ExcludePaths []string `json:"exclude_paths,omitempty"`
}
// StartSecurityScan starts a security scan
func StartSecurityScan(c *gin.Context) {
var req ScanRequest
c.ShouldBindJSON(&req)
tools := req.Tools
if len(tools) == 0 {
tools = []string{"gitleaks", "semgrep", "trivy", "grype", "syft"}
}
// In production, forward to security scanner service
c.JSON(http.StatusAccepted, gin.H{
"scan_id": uuid.New().String(),
"status": "RUNNING",
"tools": tools,
"started_at": time.Now().Format(time.RFC3339),
"message": "Scan started. Check /findings for results.",
})
}
// GetSecurityFindings returns security findings
func GetSecurityFindings(c *gin.Context) {
severity := c.Query("severity")
tool := c.Query("tool")
c.JSON(http.StatusOK, gin.H{
"filters": gin.H{
"severity": severity,
"tool": tool,
},
"findings": []gin.H{
{
"id": uuid.New().String(),
"tool": "trivy",
"severity": "HIGH",
"title": "CVE-2024-1234",
"description": "Vulnerability in dependency",
"file": "package-lock.json",
"recommendation": "Update to version 2.0.0",
},
},
"summary": gin.H{
"critical": 0,
"high": 1,
"medium": 3,
"low": 5,
"total": 9,
},
})
}
// GetRecommendations returns fix recommendations
func GetRecommendations(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{
"recommendations": []gin.H{
{
"priority": "HIGH",
"category": "DEPENDENCIES",
"title": "Update vulnerable packages",
"description": "Several npm packages have known vulnerabilities. " +
"Run 'npm audit fix' to automatically update compatible versions.",
"affected": []string{"lodash@4.17.20", "axios@0.21.0"},
},
{
"priority": "MEDIUM",
"category": "SECRETS",
"title": "Review detected secrets",
"description": "Gitleaks detected potential secrets in the codebase. " +
"Review and rotate if they are real credentials.",
"affected": []string{".env.example:3"},
},
},
})
}
// GetSecurityReports returns security reports
func GetSecurityReports(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{
"reports": []gin.H{
{
"id": uuid.New().String(),
"name": "Weekly Security Scan",
"generated_at": time.Now().AddDate(0, 0, -7).Format(time.RFC3339),
"findings": 12,
"status": "COMPLETED",
},
{
"id": uuid.New().String(),
"name": "Monthly Compliance Audit",
"generated_at": time.Now().AddDate(0, -1, 0).Format(time.RFC3339),
"findings": 5,
"status": "COMPLETED",
},
},
})
}

View File

@@ -0,0 +1,93 @@
// Package config handles configuration loading for the API Gateway
package config
import (
"os"
)
// Config holds the application configuration
type Config struct {
Environment string
Port string
Version string
// Database
DatabaseURL string
// Redis
RedisURL string
// JWT
JWTSecret string
JWTExpiration int // hours
// Rate limiting
RateLimit RateLimitConfig
// Services
ComplianceEngineURL string
RAGServiceURL string
SecurityScannerURL string
// Storage
MinIOEndpoint string
MinIOAccessKey string
MinIOSecretKey string
MinIOBucket string
}
// RateLimitConfig holds rate limiting configuration
type RateLimitConfig struct {
RequestsPerSecond int
Burst int
}
// Load loads configuration from environment variables
func Load() (*Config, error) {
cfg := &Config{
Environment: getEnv("ENVIRONMENT", "development"),
Port: getEnv("PORT", "8080"),
Version: getEnv("VERSION", "0.0.1"),
DatabaseURL: getEnv("DATABASE_URL", "postgres://breakpilot:breakpilot@localhost:5432/compliance"),
RedisURL: getEnv("REDIS_URL", "redis://localhost:6379"),
JWTSecret: getEnv("JWT_SECRET", "your-secret-key-change-in-production"),
JWTExpiration: getEnvInt("JWT_EXPIRATION", 24),
RateLimit: RateLimitConfig{
RequestsPerSecond: getEnvInt("RATE_LIMIT_RPS", 100),
Burst: getEnvInt("RATE_LIMIT_BURST", 200),
},
ComplianceEngineURL: getEnv("COMPLIANCE_ENGINE_URL", "http://compliance-engine:8081"),
RAGServiceURL: getEnv("RAG_SERVICE_URL", "http://rag-service:8082"),
SecurityScannerURL: getEnv("SECURITY_SCANNER_URL", "http://security-scanner:8083"),
MinIOEndpoint: getEnv("MINIO_ENDPOINT", "minio:9000"),
MinIOAccessKey: getEnv("MINIO_ACCESS_KEY", "breakpilot"),
MinIOSecretKey: getEnv("MINIO_SECRET_KEY", "breakpilot123"),
MinIOBucket: getEnv("MINIO_BUCKET", "compliance"),
}
return cfg, nil
}
func getEnv(key, defaultValue string) string {
if value := os.Getenv(key); value != "" {
return value
}
return defaultValue
}
func getEnvInt(key string, defaultValue int) int {
if value := os.Getenv(key); value != "" {
// Simple conversion, production code should handle errors
var result int
for _, c := range value {
result = result*10 + int(c-'0')
}
return result
}
return defaultValue
}

View File

@@ -0,0 +1,117 @@
// Package middleware provides HTTP middleware for the API Gateway
package middleware
import (
"net/http"
"strings"
"github.com/gin-gonic/gin"
"github.com/golang-jwt/jwt/v5"
)
// Claims represents JWT claims
type Claims struct {
TenantID string `json:"tenant_id"`
UserID string `json:"user_id"`
Scopes []string `json:"scopes"`
jwt.RegisteredClaims
}
// Auth middleware validates JWT tokens or API keys
func Auth(jwtSecret string) gin.HandlerFunc {
return func(c *gin.Context) {
authHeader := c.GetHeader("Authorization")
if authHeader == "" {
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{
"error": "Missing authorization header",
})
return
}
// Check for Bearer token (JWT)
if strings.HasPrefix(authHeader, "Bearer ") {
tokenString := strings.TrimPrefix(authHeader, "Bearer ")
// Check if it's an API key (starts with pk_ or sk_)
if strings.HasPrefix(tokenString, "pk_") || strings.HasPrefix(tokenString, "sk_") {
// API Key authentication
if err := validateAPIKey(c, tokenString); err != nil {
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{
"error": "Invalid API key",
})
return
}
} else {
// JWT authentication
claims, err := validateJWT(tokenString, jwtSecret)
if err != nil {
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{
"error": "Invalid token",
})
return
}
// Store claims in context
c.Set("tenant_id", claims.TenantID)
c.Set("user_id", claims.UserID)
c.Set("scopes", claims.Scopes)
}
} else {
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{
"error": "Invalid authorization format",
})
return
}
c.Next()
}
}
func validateJWT(tokenString, secret string) (*Claims, error) {
token, err := jwt.ParseWithClaims(tokenString, &Claims{}, func(token *jwt.Token) (interface{}, error) {
return []byte(secret), nil
})
if err != nil {
return nil, err
}
if claims, ok := token.Claims.(*Claims); ok && token.Valid {
return claims, nil
}
return nil, jwt.ErrTokenInvalidClaims
}
func validateAPIKey(c *gin.Context, apiKey string) error {
// In production, this would validate against a database of API keys
// For now, we extract tenant info from the X-Tenant-ID header
tenantID := c.GetHeader("X-Tenant-ID")
if tenantID == "" {
tenantID = "default"
}
c.Set("tenant_id", tenantID)
c.Set("user_id", "api-key-user")
c.Set("scopes", []string{"read", "write"})
return nil
}
// TenantIsolation ensures requests only access their own tenant's data
func TenantIsolation() gin.HandlerFunc {
return func(c *gin.Context) {
tenantID, exists := c.Get("tenant_id")
if !exists || tenantID == "" {
c.AbortWithStatusJSON(http.StatusForbidden, gin.H{
"error": "Tenant ID required",
})
return
}
// Add tenant ID to all database queries (handled by handlers)
c.Set("tenant_filter", tenantID)
c.Next()
}
}

View File

@@ -0,0 +1,119 @@
// Package middleware provides HTTP middleware for the API Gateway
package middleware
import (
"net/http"
"time"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
"go.uber.org/zap"
"golang.org/x/time/rate"
)
// Logger middleware logs requests
func Logger(logger *zap.Logger) gin.HandlerFunc {
return func(c *gin.Context) {
start := time.Now()
path := c.Request.URL.Path
raw := c.Request.URL.RawQuery
// Process request
c.Next()
// Log after request
latency := time.Since(start)
clientIP := c.ClientIP()
method := c.Request.Method
statusCode := c.Writer.Status()
if raw != "" {
path = path + "?" + raw
}
logger.Info("request",
zap.String("method", method),
zap.String("path", path),
zap.Int("status", statusCode),
zap.String("ip", clientIP),
zap.Duration("latency", latency),
zap.String("request_id", c.GetString("request_id")),
)
}
}
// CORS middleware handles Cross-Origin Resource Sharing
func CORS() gin.HandlerFunc {
return func(c *gin.Context) {
origin := c.GetHeader("Origin")
if origin == "" {
origin = "*"
}
c.Header("Access-Control-Allow-Origin", origin)
c.Header("Access-Control-Allow-Methods", "GET, POST, PUT, PATCH, DELETE, OPTIONS")
c.Header("Access-Control-Allow-Headers", "Origin, Content-Type, Accept, Authorization, X-Tenant-ID, X-Request-ID")
c.Header("Access-Control-Allow-Credentials", "true")
c.Header("Access-Control-Max-Age", "86400")
if c.Request.Method == "OPTIONS" {
c.AbortWithStatus(http.StatusNoContent)
return
}
c.Next()
}
}
// RequestID middleware adds a unique request ID to each request
func RequestID() gin.HandlerFunc {
return func(c *gin.Context) {
requestID := c.GetHeader("X-Request-ID")
if requestID == "" {
requestID = uuid.New().String()
}
c.Set("request_id", requestID)
c.Header("X-Request-ID", requestID)
c.Next()
}
}
// RateLimitConfig holds rate limiting configuration
type RateLimitConfig struct {
RequestsPerSecond int
Burst int
}
// RateLimiter middleware limits request rate per client
func RateLimiter(config RateLimitConfig) gin.HandlerFunc {
// In production, use a distributed rate limiter with Redis
// This is a simple in-memory rate limiter per IP
limiters := make(map[string]*rate.Limiter)
return func(c *gin.Context) {
clientIP := c.ClientIP()
limiter, exists := limiters[clientIP]
if !exists {
limiter = rate.NewLimiter(rate.Limit(config.RequestsPerSecond), config.Burst)
limiters[clientIP] = limiter
}
if !limiter.Allow() {
c.AbortWithStatusJSON(http.StatusTooManyRequests, gin.H{
"error": "Rate limit exceeded",
"retry_after": "1s",
})
return
}
c.Next()
}
}
// Recovery middleware recovers from panics
func Recovery() gin.HandlerFunc {
return gin.Recovery()
}

View File

@@ -0,0 +1,188 @@
// BreakPilot Compliance SDK - API Gateway
//
// Main entry point for the API Gateway service.
// Handles authentication, rate limiting, and request routing.
package main
import (
"context"
"log"
"net/http"
"os"
"os/signal"
"syscall"
"time"
"github.com/breakpilot/compliance-sdk/services/api-gateway/internal/api"
"github.com/breakpilot/compliance-sdk/services/api-gateway/internal/config"
"github.com/breakpilot/compliance-sdk/services/api-gateway/internal/middleware"
"github.com/gin-gonic/gin"
"go.uber.org/zap"
)
func main() {
// Initialize logger
logger, _ := zap.NewProduction()
defer logger.Sync()
// Load configuration
cfg, err := config.Load()
if err != nil {
logger.Fatal("Failed to load configuration", zap.Error(err))
}
// Set Gin mode
if cfg.Environment == "production" {
gin.SetMode(gin.ReleaseMode)
}
// Initialize router
router := gin.New()
// Global middleware
router.Use(gin.Recovery())
router.Use(middleware.Logger(logger))
router.Use(middleware.CORS())
router.Use(middleware.RequestID())
// Health check (no auth required)
router.GET("/health", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{
"status": "healthy",
"service": "api-gateway",
"version": cfg.Version,
})
})
// API v1 routes
v1 := router.Group("/api/v1")
{
// Public routes (rate limited)
v1.Use(middleware.RateLimiter(cfg.RateLimit))
// Auth routes
auth := v1.Group("/auth")
{
auth.POST("/token", api.GetToken)
auth.POST("/refresh", api.RefreshToken)
}
// Protected routes
protected := v1.Group("")
protected.Use(middleware.Auth(cfg.JWTSecret))
protected.Use(middleware.TenantIsolation())
{
// State management
protected.GET("/state/:tenantId", api.GetState)
protected.POST("/state/save", api.SaveState)
protected.POST("/state/reset", api.ResetState)
// DSGVO
dsgvo := protected.Group("/dsgvo")
{
dsgvo.POST("/consent", api.RecordConsent)
dsgvo.GET("/consent/:userId", api.GetConsents)
dsgvo.DELETE("/consent/:consentId", api.RevokeConsent)
dsgvo.POST("/dsr", api.SubmitDSR)
dsgvo.GET("/dsr", api.ListDSRRequests)
dsgvo.PUT("/dsr/:requestId", api.UpdateDSRStatus)
dsgvo.GET("/vvt", api.GetProcessingActivities)
dsgvo.POST("/vvt", api.CreateProcessingActivity)
dsgvo.PUT("/vvt/:activityId", api.UpdateProcessingActivity)
dsgvo.DELETE("/vvt/:activityId", api.DeleteProcessingActivity)
dsgvo.GET("/tom", api.GetTOMs)
dsgvo.POST("/tom", api.CreateTOM)
dsgvo.PUT("/tom/:tomId", api.UpdateTOM)
dsgvo.DELETE("/tom/:tomId", api.DeleteTOM)
dsgvo.GET("/dsfa", api.GetDSFAs)
dsgvo.POST("/dsfa", api.CreateDSFA)
dsgvo.PUT("/dsfa/:dsfaId", api.UpdateDSFA)
dsgvo.GET("/retention", api.GetRetentionPolicies)
dsgvo.POST("/retention", api.CreateRetentionPolicy)
dsgvo.PUT("/retention/:policyId", api.UpdateRetentionPolicy)
dsgvo.DELETE("/retention/:policyId", api.DeleteRetentionPolicy)
}
// Compliance
compliance := protected.Group("/compliance")
{
compliance.GET("/controls", api.GetControls)
compliance.POST("/controls", api.CreateControl)
compliance.PUT("/controls/:controlId", api.UpdateControl)
compliance.DELETE("/controls/:controlId", api.DeleteControl)
compliance.GET("/evidence", api.GetEvidence)
compliance.POST("/evidence", api.UploadEvidence)
compliance.PUT("/evidence/:evidenceId", api.UpdateEvidence)
compliance.DELETE("/evidence/:evidenceId", api.DeleteEvidence)
compliance.GET("/obligations", api.GetObligations)
compliance.POST("/assessment", api.RunAssessment)
compliance.GET("/export/pdf", api.ExportPDF)
compliance.GET("/export/docx", api.ExportDOCX)
}
// RAG
rag := protected.Group("/rag")
{
rag.POST("/search", api.SearchRAG)
rag.POST("/ask", api.AskRAG)
rag.GET("/regulations", api.GetRegulations)
rag.POST("/documents", api.UploadDocument)
}
// SBOM & Security
security := protected.Group("/security")
{
security.POST("/sbom/generate", api.GenerateSBOM)
security.GET("/sbom/components", api.GetSBOMComponents)
security.GET("/sbom/export/:format", api.ExportSBOM)
security.POST("/scan", api.StartSecurityScan)
security.GET("/findings", api.GetSecurityFindings)
security.GET("/recommendations", api.GetRecommendations)
security.GET("/reports", api.GetSecurityReports)
}
}
}
// Create HTTP server
srv := &http.Server{
Addr: ":" + cfg.Port,
Handler: router,
ReadTimeout: 15 * time.Second,
WriteTimeout: 15 * time.Second,
IdleTimeout: 60 * time.Second,
}
// Start server in goroutine
go func() {
logger.Info("Starting API Gateway", zap.String("port", cfg.Port))
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
logger.Fatal("Failed to start server", zap.Error(err))
}
}()
// Graceful shutdown
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
<-quit
logger.Info("Shutting down server...")
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
if err := srv.Shutdown(ctx); err != nil {
logger.Fatal("Server forced to shutdown", zap.Error(err))
}
logger.Info("Server exited")
}

View File

@@ -0,0 +1,33 @@
# Build stage
FROM golang:1.22-alpine AS builder
WORKDIR /app
RUN apk add --no-cache git
COPY go.mod go.sum* ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o compliance-engine .
# Runtime stage
FROM alpine:3.19
WORKDIR /app
RUN apk --no-cache add ca-certificates
COPY --from=builder /app/compliance-engine .
COPY --from=builder /app/policies ./policies
RUN adduser -D -g '' appuser
USER appuser
EXPOSE 8081
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD wget --no-verbose --tries=1 --spider http://localhost:8081/health || exit 1
CMD ["./compliance-engine"]

View File

@@ -0,0 +1,12 @@
module github.com/breakpilot/compliance-sdk/services/compliance-engine
go 1.22
require (
github.com/gin-gonic/gin v1.9.1
github.com/google/uuid v1.5.0
go.uber.org/zap v1.26.0
gopkg.in/yaml.v3 v3.0.1
gorm.io/driver/postgres v1.5.4
gorm.io/gorm v1.25.5
)

View File

@@ -0,0 +1,329 @@
// Package api provides HTTP handlers for the Compliance Engine
package api
import (
"net/http"
"time"
"github.com/breakpilot/compliance-sdk/services/compliance-engine/internal/ucca"
"github.com/gin-gonic/gin"
)
// Assess performs a full compliance assessment
func Assess(engine *ucca.Engine) gin.HandlerFunc {
return func(c *gin.Context) {
var state map[string]interface{}
if err := c.ShouldBindJSON(&state); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
result := engine.Assess(state)
result.Timestamp = time.Now().Format(time.RFC3339)
c.JSON(http.StatusOK, result)
}
}
// AssessControl assesses a single control
func AssessControl(engine *ucca.Engine) gin.HandlerFunc {
return func(c *gin.Context) {
var req struct {
ControlID string `json:"control_id" binding:"required"`
State map[string]interface{} `json:"state"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
controls := engine.GetControls()
ctrl, exists := controls[req.ControlID]
if !exists {
c.JSON(http.StatusNotFound, gin.H{"error": "Control not found"})
return
}
c.JSON(http.StatusOK, gin.H{
"control": ctrl,
"status": "ASSESSED",
"compliance": true,
"recommendations": []string{},
})
}
}
// AssessRegulation assesses compliance with a specific regulation
func AssessRegulation(engine *ucca.Engine) gin.HandlerFunc {
return func(c *gin.Context) {
var req struct {
RegulationCode string `json:"regulation_code" binding:"required"`
State map[string]interface{} `json:"state"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
regulations := engine.GetRegulations()
reg, exists := regulations[req.RegulationCode]
if !exists {
c.JSON(http.StatusNotFound, gin.H{"error": "Regulation not found"})
return
}
result := engine.Assess(req.State)
c.JSON(http.StatusOK, gin.H{
"regulation": reg,
"score": result.ByRegulation[req.RegulationCode],
"findings": filterFindings(result.Findings, req.RegulationCode),
"assessed_at": time.Now().Format(time.RFC3339),
})
}
}
// CalculateScore calculates the compliance score
func CalculateScore(engine *ucca.Engine) gin.HandlerFunc {
return func(c *gin.Context) {
var state map[string]interface{}
if err := c.ShouldBindJSON(&state); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
result := engine.Assess(state)
c.JSON(http.StatusOK, gin.H{
"overall": result.OverallScore,
"trend": result.Trend,
"by_regulation": result.ByRegulation,
"by_domain": result.ByDomain,
"calculated_at": time.Now().Format(time.RFC3339),
})
}
}
// ScoreBreakdown provides a detailed score breakdown
func ScoreBreakdown(engine *ucca.Engine) gin.HandlerFunc {
return func(c *gin.Context) {
var state map[string]interface{}
if err := c.ShouldBindJSON(&state); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
result := engine.Assess(state)
// Calculate detailed breakdown
breakdown := gin.H{
"overall": gin.H{
"score": result.OverallScore,
"max": 100,
"trend": result.Trend,
"description": getScoreDescription(result.OverallScore),
},
"by_regulation": []gin.H{},
"by_domain": []gin.H{},
"by_category": gin.H{
"documentation": 75,
"technical": 80,
"organizational": 70,
},
}
for reg, score := range result.ByRegulation {
breakdown["by_regulation"] = append(
breakdown["by_regulation"].([]gin.H),
gin.H{"code": reg, "score": score, "status": getStatus(score)},
)
}
for domain, score := range result.ByDomain {
breakdown["by_domain"] = append(
breakdown["by_domain"].([]gin.H),
gin.H{"domain": domain, "score": score, "status": getStatus(score)},
)
}
c.JSON(http.StatusOK, breakdown)
}
}
// GetObligations returns all regulatory obligations
func GetObligations(engine *ucca.Engine) gin.HandlerFunc {
return func(c *gin.Context) {
regulations := engine.GetRegulations()
obligations := []gin.H{}
for code, reg := range regulations {
for _, article := range reg.Articles {
obligations = append(obligations, gin.H{
"id": code + "-" + article,
"regulation_code": code,
"regulation_name": reg.Name,
"article": article,
"status": "PENDING",
})
}
}
c.JSON(http.StatusOK, gin.H{
"obligations": obligations,
"total": len(obligations),
})
}
}
// GetObligationsByRegulation returns obligations for a specific regulation
func GetObligationsByRegulation(engine *ucca.Engine) gin.HandlerFunc {
return func(c *gin.Context) {
regCode := c.Param("regulation")
regulations := engine.GetRegulations()
reg, exists := regulations[regCode]
if !exists {
c.JSON(http.StatusNotFound, gin.H{"error": "Regulation not found"})
return
}
obligations := []gin.H{}
for _, article := range reg.Articles {
obligations = append(obligations, gin.H{
"id": regCode + "-" + article,
"regulation_code": regCode,
"article": article,
"status": "PENDING",
})
}
c.JSON(http.StatusOK, gin.H{
"regulation": reg,
"obligations": obligations,
"total": len(obligations),
})
}
}
// GetControlsCatalog returns the controls catalog
func GetControlsCatalog(engine *ucca.Engine) gin.HandlerFunc {
return func(c *gin.Context) {
controls := engine.GetControls()
result := []gin.H{}
for _, ctrl := range controls {
result = append(result, gin.H{
"id": ctrl.ID,
"name": ctrl.Name,
"description": ctrl.Description,
"domain": ctrl.Domain,
"category": ctrl.Category,
"objective": ctrl.Objective,
})
}
c.JSON(http.StatusOK, gin.H{
"controls": result,
"total": len(result),
})
}
}
// GetControlsByDomain returns controls for a specific domain
func GetControlsByDomain(engine *ucca.Engine) gin.HandlerFunc {
return func(c *gin.Context) {
domain := c.Param("domain")
controls := engine.GetControlsByDomain(domain)
result := []gin.H{}
for _, ctrl := range controls {
result = append(result, gin.H{
"id": ctrl.ID,
"name": ctrl.Name,
"description": ctrl.Description,
"category": ctrl.Category,
"objective": ctrl.Objective,
"guidance": ctrl.Guidance,
"evidence": ctrl.Evidence,
})
}
c.JSON(http.StatusOK, gin.H{
"domain": domain,
"controls": result,
"total": len(result),
})
}
}
// ListPolicies lists all loaded policies
func ListPolicies(engine *ucca.Engine) gin.HandlerFunc {
return func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{
"total_rules": engine.RuleCount(),
"total_regulations": len(engine.GetRegulations()),
"total_controls": len(engine.GetControls()),
"regulations": getRegulationCodes(engine),
})
}
}
// GetPolicy returns details of a specific policy
func GetPolicy(engine *ucca.Engine) gin.HandlerFunc {
return func(c *gin.Context) {
policyID := c.Param("id")
regulations := engine.GetRegulations()
if reg, exists := regulations[policyID]; exists {
c.JSON(http.StatusOK, reg)
return
}
c.JSON(http.StatusNotFound, gin.H{"error": "Policy not found"})
}
}
// Helper functions
func filterFindings(findings []ucca.Finding, regulation string) []ucca.Finding {
result := []ucca.Finding{}
for _, f := range findings {
if f.Regulation == regulation {
result = append(result, f)
}
}
return result
}
func getScoreDescription(score int) string {
switch {
case score >= 90:
return "Excellent - Full compliance achieved"
case score >= 75:
return "Good - Minor improvements needed"
case score >= 50:
return "Fair - Significant work required"
default:
return "Poor - Major compliance gaps exist"
}
}
func getStatus(score int) string {
switch {
case score >= 80:
return "COMPLIANT"
case score >= 60:
return "PARTIAL"
default:
return "NON_COMPLIANT"
}
}
func getRegulationCodes(engine *ucca.Engine) []string {
regulations := engine.GetRegulations()
codes := make([]string, 0, len(regulations))
for code := range regulations {
codes = append(codes, code)
}
return codes
}

View File

@@ -0,0 +1,443 @@
// Package ucca implements the Unified Compliance Control Assessment engine
package ucca
// loadBuiltInRules loads the built-in compliance rules
func (e *Engine) loadBuiltInRules() {
rules := []Rule{
// DSGVO Rules
{
ID: "DSGVO-001",
Name: "Verarbeitungsverzeichnis erforderlich",
Description: "Ein Verzeichnis aller Verarbeitungstätigkeiten muss geführt werden",
Regulation: "DSGVO",
Article: "30",
Severity: "HIGH",
Category: "DOCUMENTATION",
Conditions: []string{"no_processing_activities"},
},
{
ID: "DSGVO-002",
Name: "Technische und organisatorische Maßnahmen",
Description: "Angemessene TOMs müssen implementiert sein",
Regulation: "DSGVO",
Article: "32",
Severity: "HIGH",
Category: "SECURITY",
Conditions: []string{"no_toms"},
},
{
ID: "DSGVO-003",
Name: "Datenschutz-Folgenabschätzung",
Description: "DSFA bei hohem Risiko erforderlich",
Regulation: "DSGVO",
Article: "35",
Severity: "HIGH",
Category: "RISK",
Conditions: []string{"high_risk_processing", "no_dsfa"},
},
{
ID: "DSGVO-004",
Name: "Betroffenenrechte",
Description: "Prozesse für DSR-Anfragen müssen etabliert sein",
Regulation: "DSGVO",
Article: "15-22",
Severity: "CRITICAL",
Category: "RIGHTS",
Conditions: []string{"no_dsr_process"},
},
{
ID: "DSGVO-005",
Name: "Einwilligungsmanagement",
Description: "Einwilligungen müssen dokumentiert und nachweisbar sein",
Regulation: "DSGVO",
Article: "7",
Severity: "HIGH",
Category: "CONSENT",
Conditions: []string{"no_consent_management"},
},
{
ID: "DSGVO-006",
Name: "Datenschutzbeauftragter",
Description: "DSB muss benannt sein wenn erforderlich",
Regulation: "DSGVO",
Article: "37",
Severity: "MEDIUM",
Category: "ORGANIZATION",
Conditions: []string{"dpo_required", "no_dpo"},
},
{
ID: "DSGVO-007",
Name: "Auftragsverarbeitung",
Description: "AVV mit allen Auftragsverarbeitern erforderlich",
Regulation: "DSGVO",
Article: "28",
Severity: "HIGH",
Category: "CONTRACTS",
Conditions: []string{"has_processors", "missing_dpa"},
},
{
ID: "DSGVO-008",
Name: "Löschkonzept",
Description: "Löschfristen und -prozesse müssen definiert sein",
Regulation: "DSGVO",
Article: "17",
Severity: "MEDIUM",
Category: "RETENTION",
Conditions: []string{"no_retention_policies"},
},
// NIS2 Rules
{
ID: "NIS2-001",
Name: "Risikomanagement-Maßnahmen",
Description: "Umfassende Cybersecurity-Risikomanagement-Maßnahmen erforderlich",
Regulation: "NIS2",
Article: "21",
Severity: "CRITICAL",
Category: "RISK",
Conditions: []string{"no_risk_management"},
},
{
ID: "NIS2-002",
Name: "Incident-Meldung",
Description: "Meldepflicht bei Sicherheitsvorfällen",
Regulation: "NIS2",
Article: "23",
Severity: "CRITICAL",
Category: "INCIDENT",
Conditions: []string{"no_incident_process"},
},
{
ID: "NIS2-003",
Name: "Supply Chain Security",
Description: "Sicherheit der Lieferkette muss gewährleistet sein",
Regulation: "NIS2",
Article: "21.2d",
Severity: "HIGH",
Category: "SUPPLY_CHAIN",
Conditions: []string{"no_supply_chain_security"},
},
{
ID: "NIS2-004",
Name: "Business Continuity",
Description: "Geschäftskontinuitätsmanagement erforderlich",
Regulation: "NIS2",
Article: "21.2c",
Severity: "HIGH",
Category: "BCM",
Conditions: []string{"no_bcm"},
},
{
ID: "NIS2-005",
Name: "Kryptografie",
Description: "Richtlinien für Kryptografie und Verschlüsselung",
Regulation: "NIS2",
Article: "21.2h",
Severity: "MEDIUM",
Category: "ENCRYPTION",
Conditions: []string{"no_crypto_policy"},
},
// AI Act Rules
{
ID: "AIACT-001",
Name: "KI-Risikobewertung",
Description: "Risikoeinstufung des KI-Systems erforderlich",
Regulation: "AI_ACT",
Article: "6",
Severity: "CRITICAL",
Category: "RISK",
Conditions: []string{"uses_ai", "no_ai_risk_assessment"},
},
{
ID: "AIACT-002",
Name: "Hochrisiko-KI Dokumentation",
Description: "Technische Dokumentation für Hochrisiko-KI",
Regulation: "AI_ACT",
Article: "11",
Severity: "HIGH",
Category: "DOCUMENTATION",
Conditions: []string{"high_risk_ai", "no_ai_documentation"},
},
{
ID: "AIACT-003",
Name: "Datenqualität",
Description: "Anforderungen an Trainingsdaten",
Regulation: "AI_ACT",
Article: "10",
Severity: "HIGH",
Category: "DATA",
Conditions: []string{"high_risk_ai", "no_data_governance"},
},
{
ID: "AIACT-004",
Name: "Menschliche Aufsicht",
Description: "Menschliche Überwachung muss gewährleistet sein",
Regulation: "AI_ACT",
Article: "14",
Severity: "HIGH",
Category: "OVERSIGHT",
Conditions: []string{"high_risk_ai", "no_human_oversight"},
},
{
ID: "AIACT-005",
Name: "Transparenz",
Description: "Transparenzanforderungen für KI-Systeme",
Regulation: "AI_ACT",
Article: "13",
Severity: "MEDIUM",
Category: "TRANSPARENCY",
Conditions: []string{"uses_ai", "no_ai_transparency"},
},
// Additional cross-regulation rules
{
ID: "CROSS-001",
Name: "Zugriffskontrolle",
Description: "Implementierung von Zugriffskontrollen",
Regulation: "MULTIPLE",
Article: "DSGVO-32, NIS2-21",
Severity: "HIGH",
Category: "ACCESS_CONTROL",
Conditions: []string{"no_access_controls"},
},
{
ID: "CROSS-002",
Name: "Schulungen",
Description: "Regelmäßige Mitarbeiterschulungen",
Regulation: "MULTIPLE",
Article: "DSGVO-39, NIS2-20",
Severity: "MEDIUM",
Category: "TRAINING",
Conditions: []string{"no_training_program"},
},
{
ID: "CROSS-003",
Name: "Audit-Protokollierung",
Description: "Protokollierung sicherheitsrelevanter Ereignisse",
Regulation: "MULTIPLE",
Article: "DSGVO-32, NIS2-21",
Severity: "HIGH",
Category: "LOGGING",
Conditions: []string{"no_audit_logging"},
},
}
for i := range rules {
e.rules[rules[i].ID] = &rules[i]
}
}
// loadBuiltInRegulations loads the built-in regulations
func (e *Engine) loadBuiltInRegulations() {
regulations := []Regulation{
{
Code: "DSGVO",
Name: "Datenschutz-Grundverordnung",
Description: "EU-Verordnung 2016/679 zum Schutz natürlicher Personen bei der Verarbeitung personenbezogener Daten",
Articles: []string{"5", "6", "7", "9", "12-22", "24-32", "33-34", "35-36", "37-39", "44-49"},
Effective: "2018-05-25",
},
{
Code: "NIS2",
Name: "NIS 2 Directive",
Description: "EU-Richtlinie 2022/2555 über Maßnahmen für ein hohes gemeinsames Cybersicherheitsniveau",
Articles: []string{"20", "21", "23", "24", "25", "26", "27", "28", "29", "30", "31", "32"},
Effective: "2024-10-17",
},
{
Code: "AI_ACT",
Name: "EU AI Act",
Description: "EU-Verordnung zur Festlegung harmonisierter Vorschriften für künstliche Intelligenz",
Articles: []string{"5", "6", "8", "9", "10", "11", "12", "13", "14", "15", "16", "17", "52"},
Effective: "2025-02-02",
},
{
Code: "TDDDG",
Name: "Telekommunikation-Digitale-Dienste-Datenschutz-Gesetz",
Description: "Deutsches Gesetz zum Datenschutz bei Telemedien und Telekommunikation",
Articles: []string{"1-30"},
Effective: "2021-12-01",
},
{
Code: "BDSG",
Name: "Bundesdatenschutzgesetz",
Description: "Deutsches Bundesdatenschutzgesetz",
Articles: []string{"1-86"},
Effective: "2018-05-25",
},
}
for i := range regulations {
e.regulations[regulations[i].Code] = &regulations[i]
}
}
// loadBuiltInControls loads the built-in controls catalog
func (e *Engine) loadBuiltInControls() {
controls := []Control{
// Access Control
{
ID: "AC-01",
Name: "Zugriffskontrollrichtlinie",
Description: "Dokumentierte Richtlinie für Zugriffskontrollen",
Domain: "ACCESS_CONTROL",
Category: "POLICY",
Objective: "Etablierung einer konsistenten Zugriffskontrolle",
Guidance: "Definieren Sie Rollen, Verantwortlichkeiten und Prozesse",
Evidence: []string{"Policy-Dokument", "Genehmigungsnachweis"},
},
{
ID: "AC-02",
Name: "Benutzerkontenverwaltung",
Description: "Verwaltung von Benutzerkonten und Zugriffsrechten",
Domain: "ACCESS_CONTROL",
Category: "TECHNICAL",
Objective: "Kontrolle über Benutzerzugriffe",
Guidance: "Implementieren Sie Prozesse für Anlage, Änderung und Löschung",
Evidence: []string{"Prozessdokumentation", "IAM-Konfiguration"},
},
{
ID: "AC-03",
Name: "Multi-Faktor-Authentifizierung",
Description: "Implementierung von MFA für kritische Systeme",
Domain: "ACCESS_CONTROL",
Category: "TECHNICAL",
Objective: "Stärkere Authentifizierung",
Guidance: "MFA für alle privilegierten Zugriffe und externe Zugänge",
Evidence: []string{"MFA-Konfiguration", "Enrollment-Statistik"},
},
// Data Protection
{
ID: "DP-01",
Name: "Datenverschlüsselung",
Description: "Verschlüsselung von Daten at rest und in transit",
Domain: "DATA_PROTECTION",
Category: "TECHNICAL",
Objective: "Schutz der Vertraulichkeit von Daten",
Guidance: "TLS 1.3 für Transit, AES-256 für Rest",
Evidence: []string{"Zertifikate", "Verschlüsselungskonfiguration"},
},
{
ID: "DP-02",
Name: "Datenklassifizierung",
Description: "Schema zur Klassifizierung von Daten",
Domain: "DATA_PROTECTION",
Category: "ORGANIZATIONAL",
Objective: "Angemessener Schutz basierend auf Sensitivität",
Guidance: "Definieren Sie Klassifizierungsstufen und Handhabungsregeln",
Evidence: []string{"Klassifizierungsschema", "Inventar"},
},
{
ID: "DP-03",
Name: "Datensicherung",
Description: "Regelmäßige Backups kritischer Daten",
Domain: "DATA_PROTECTION",
Category: "TECHNICAL",
Objective: "Wiederherstellbarkeit von Daten",
Guidance: "3-2-1 Backup-Regel, regelmäßige Tests",
Evidence: []string{"Backup-Logs", "Restore-Tests"},
},
// Incident Response
{
ID: "IR-01",
Name: "Incident-Response-Plan",
Description: "Dokumentierter Plan für Sicherheitsvorfälle",
Domain: "INCIDENT_RESPONSE",
Category: "ORGANIZATIONAL",
Objective: "Strukturierte Reaktion auf Vorfälle",
Guidance: "Definieren Sie Rollen, Prozesse und Kommunikationswege",
Evidence: []string{"IR-Plan", "Kontaktlisten"},
},
{
ID: "IR-02",
Name: "Incident-Erkennung",
Description: "Systeme zur Erkennung von Sicherheitsvorfällen",
Domain: "INCIDENT_RESPONSE",
Category: "TECHNICAL",
Objective: "Frühzeitige Erkennung von Angriffen",
Guidance: "SIEM, IDS/IPS, Log-Monitoring",
Evidence: []string{"Monitoring-Konfiguration", "Alert-Regeln"},
},
{
ID: "IR-03",
Name: "Meldeprozesse",
Description: "Prozesse für behördliche Meldungen",
Domain: "INCIDENT_RESPONSE",
Category: "ORGANIZATIONAL",
Objective: "Compliance mit Meldepflichten",
Guidance: "72h für DSGVO, 24h für NIS2",
Evidence: []string{"Meldeprozess", "Templates"},
},
// Risk Management
{
ID: "RM-01",
Name: "Risikobeurteilungsmethodik",
Description: "Standardisierte Methodik zur Risikobewertung",
Domain: "RISK_MANAGEMENT",
Category: "ORGANIZATIONAL",
Objective: "Konsistente Risikobewertung",
Guidance: "ISO 27005 oder vergleichbar",
Evidence: []string{"Methodik-Dokument", "Schulungsnachweise"},
},
{
ID: "RM-02",
Name: "Risikoregister",
Description: "Dokumentation aller identifizierten Risiken",
Domain: "RISK_MANAGEMENT",
Category: "DOCUMENTATION",
Objective: "Überblick über Risikolandschaft",
Guidance: "Regelmäßige Aktualisierung, Maßnahmentracking",
Evidence: []string{"Risikoregister", "Review-Protokolle"},
},
{
ID: "RM-03",
Name: "Risikobehandlung",
Description: "Prozess zur Behandlung identifizierter Risiken",
Domain: "RISK_MANAGEMENT",
Category: "ORGANIZATIONAL",
Objective: "Systematische Risikominimierung",
Guidance: "Mitigate, Transfer, Accept, Avoid",
Evidence: []string{"Behandlungspläne", "Statusberichte"},
},
// Business Continuity
{
ID: "BC-01",
Name: "Business-Impact-Analyse",
Description: "Analyse der Geschäftsauswirkungen",
Domain: "BUSINESS_CONTINUITY",
Category: "ORGANIZATIONAL",
Objective: "Identifikation kritischer Prozesse",
Guidance: "RTO/RPO für alle kritischen Systeme",
Evidence: []string{"BIA-Dokument", "Kritikalitätseinstufung"},
},
{
ID: "BC-02",
Name: "Kontinuitätsplan",
Description: "Plan für Geschäftskontinuität",
Domain: "BUSINESS_CONTINUITY",
Category: "ORGANIZATIONAL",
Objective: "Aufrechterhaltung des Betriebs",
Guidance: "Szenarien, Aktivierungskriterien, Ressourcen",
Evidence: []string{"BCP-Dokument", "Ressourcenpläne"},
},
{
ID: "BC-03",
Name: "Disaster-Recovery-Plan",
Description: "Plan für Wiederherstellung nach Katastrophen",
Domain: "BUSINESS_CONTINUITY",
Category: "TECHNICAL",
Objective: "Schnelle Wiederherstellung der IT",
Guidance: "DR-Standort, Failover-Prozesse, Tests",
Evidence: []string{"DRP-Dokument", "Test-Protokolle"},
},
}
for i := range controls {
e.controls[controls[i].ID] = &controls[i]
}
}

View File

@@ -0,0 +1,428 @@
// Package ucca implements the Unified Compliance Control Assessment engine
package ucca
import (
"fmt"
"os"
"path/filepath"
"gopkg.in/yaml.v3"
)
// Engine is the UCCA assessment engine
type Engine struct {
rules map[string]*Rule
regulations map[string]*Regulation
controls map[string]*Control
mappings []ControlMapping
}
// Rule represents a compliance rule
type Rule struct {
ID string `yaml:"id"`
Name string `yaml:"name"`
Description string `yaml:"description"`
Regulation string `yaml:"regulation"`
Article string `yaml:"article"`
Severity string `yaml:"severity"` // CRITICAL, HIGH, MEDIUM, LOW
Category string `yaml:"category"`
Conditions []string `yaml:"conditions"`
Actions []string `yaml:"actions"`
}
// Regulation represents a regulatory framework
type Regulation struct {
Code string `yaml:"code"`
Name string `yaml:"name"`
Description string `yaml:"description"`
Articles []string `yaml:"articles"`
Effective string `yaml:"effective"`
}
// Control represents a compliance control
type Control struct {
ID string `yaml:"id"`
Name string `yaml:"name"`
Description string `yaml:"description"`
Domain string `yaml:"domain"`
Category string `yaml:"category"`
Objective string `yaml:"objective"`
Guidance string `yaml:"guidance"`
Evidence []string `yaml:"evidence"`
}
// ControlMapping maps controls to regulations
type ControlMapping struct {
ControlID string `yaml:"control_id"`
RegulationCode string `yaml:"regulation_code"`
Article string `yaml:"article"`
Requirement string `yaml:"requirement"`
}
// AssessmentResult represents the result of an assessment
type AssessmentResult struct {
TenantID string `json:"tenant_id"`
Timestamp string `json:"timestamp"`
OverallScore int `json:"overall_score"`
Trend string `json:"trend"`
ByRegulation map[string]int `json:"by_regulation"`
ByDomain map[string]int `json:"by_domain"`
Findings []Finding `json:"findings"`
Recommendations []Recommendation `json:"recommendations"`
}
// Finding represents a compliance finding
type Finding struct {
RuleID string `json:"rule_id"`
Severity string `json:"severity"`
Title string `json:"title"`
Description string `json:"description"`
Regulation string `json:"regulation"`
Article string `json:"article"`
Remediation string `json:"remediation"`
}
// Recommendation represents a compliance recommendation
type Recommendation struct {
Priority string `json:"priority"`
Category string `json:"category"`
Title string `json:"title"`
Description string `json:"description"`
Controls []string `json:"controls"`
}
// NewEngine creates a new UCCA engine
func NewEngine(policiesDir string) (*Engine, error) {
engine := &Engine{
rules: make(map[string]*Rule),
regulations: make(map[string]*Regulation),
controls: make(map[string]*Control),
mappings: []ControlMapping{},
}
// Load built-in rules if no policies dir
if policiesDir == "" || !dirExists(policiesDir) {
engine.loadBuiltInRules()
return engine, nil
}
// Load rules from YAML files
if err := engine.loadRules(filepath.Join(policiesDir, "rules")); err != nil {
// Fall back to built-in rules
engine.loadBuiltInRules()
}
// Load regulations
if err := engine.loadRegulations(filepath.Join(policiesDir, "regulations")); err != nil {
engine.loadBuiltInRegulations()
}
// Load controls
if err := engine.loadControls(filepath.Join(policiesDir, "controls")); err != nil {
engine.loadBuiltInControls()
}
return engine, nil
}
// RuleCount returns the number of loaded rules
func (e *Engine) RuleCount() int {
return len(e.rules)
}
// Assess performs a full compliance assessment
func (e *Engine) Assess(state map[string]interface{}) *AssessmentResult {
result := &AssessmentResult{
ByRegulation: make(map[string]int),
ByDomain: make(map[string]int),
Findings: []Finding{},
Recommendations: []Recommendation{},
}
// Evaluate each rule
for _, rule := range e.rules {
finding := e.evaluateRule(rule, state)
if finding != nil {
result.Findings = append(result.Findings, *finding)
}
}
// Calculate scores
result.OverallScore = e.calculateOverallScore(state)
result.ByRegulation = e.calculateRegulationScores(state)
result.ByDomain = e.calculateDomainScores(state)
// Determine trend (would compare with historical data)
result.Trend = "STABLE"
// Generate recommendations
result.Recommendations = e.generateRecommendations(result.Findings)
return result
}
// evaluateRule evaluates a single rule against the state
func (e *Engine) evaluateRule(rule *Rule, state map[string]interface{}) *Finding {
// Simplified rule evaluation
// In production, this would use a proper rule engine
controls, _ := state["controls"].([]interface{})
// Check if related controls are implemented
hasViolation := false
for _, condition := range rule.Conditions {
if condition == "no_controls_implemented" {
if len(controls) == 0 {
hasViolation = true
}
}
// Add more condition evaluations
}
if hasViolation {
return &Finding{
RuleID: rule.ID,
Severity: rule.Severity,
Title: rule.Name,
Description: rule.Description,
Regulation: rule.Regulation,
Article: rule.Article,
Remediation: "Implement the required controls",
}
}
return nil
}
// calculateOverallScore calculates the overall compliance score
func (e *Engine) calculateOverallScore(state map[string]interface{}) int {
controls, ok := state["controls"].([]interface{})
if !ok || len(controls) == 0 {
return 0
}
implemented := 0
partial := 0
total := len(controls)
for _, c := range controls {
ctrl, ok := c.(map[string]interface{})
if !ok {
continue
}
status, _ := ctrl["implementationStatus"].(string)
switch status {
case "IMPLEMENTED":
implemented++
case "PARTIAL":
partial++
}
}
if total == 0 {
return 0
}
// Score = (implemented * 100 + partial * 50) / total
score := (implemented*100 + partial*50) / total
return score
}
// calculateRegulationScores calculates scores per regulation
func (e *Engine) calculateRegulationScores(state map[string]interface{}) map[string]int {
scores := map[string]int{
"DSGVO": 0,
"NIS2": 0,
"AI_ACT": 0,
}
// Simplified calculation
baseScore := e.calculateOverallScore(state)
for reg := range scores {
// Add some variance per regulation
variance := 0
switch reg {
case "DSGVO":
variance = 5
case "NIS2":
variance = -3
case "AI_ACT":
variance = -8
}
score := baseScore + variance
if score < 0 {
score = 0
}
if score > 100 {
score = 100
}
scores[reg] = score
}
return scores
}
// calculateDomainScores calculates scores per control domain
func (e *Engine) calculateDomainScores(state map[string]interface{}) map[string]int {
scores := map[string]int{}
domainCounts := map[string]struct{ implemented, total int }{}
controls, ok := state["controls"].([]interface{})
if !ok {
return scores
}
for _, c := range controls {
ctrl, ok := c.(map[string]interface{})
if !ok {
continue
}
domain, _ := ctrl["domain"].(string)
status, _ := ctrl["implementationStatus"].(string)
counts := domainCounts[domain]
counts.total++
if status == "IMPLEMENTED" {
counts.implemented++
}
domainCounts[domain] = counts
}
for domain, counts := range domainCounts {
if counts.total > 0 {
scores[domain] = (counts.implemented * 100) / counts.total
}
}
return scores
}
// generateRecommendations generates recommendations based on findings
func (e *Engine) generateRecommendations(findings []Finding) []Recommendation {
recs := []Recommendation{}
// Group findings by severity
critical := 0
high := 0
for _, f := range findings {
switch f.Severity {
case "CRITICAL":
critical++
case "HIGH":
high++
}
}
if critical > 0 {
recs = append(recs, Recommendation{
Priority: "CRITICAL",
Category: "COMPLIANCE",
Title: "Address critical compliance gaps",
Description: fmt.Sprintf("%d critical findings require immediate attention", critical),
Controls: []string{},
})
}
if high > 0 {
recs = append(recs, Recommendation{
Priority: "HIGH",
Category: "COMPLIANCE",
Title: "Address high-priority compliance gaps",
Description: fmt.Sprintf("%d high-priority findings should be addressed soon", high),
Controls: []string{},
})
}
return recs
}
// GetRegulations returns all regulations
func (e *Engine) GetRegulations() map[string]*Regulation {
return e.regulations
}
// GetControls returns all controls
func (e *Engine) GetControls() map[string]*Control {
return e.controls
}
// GetControlsByDomain returns controls for a specific domain
func (e *Engine) GetControlsByDomain(domain string) []*Control {
result := []*Control{}
for _, ctrl := range e.controls {
if ctrl.Domain == domain {
result = append(result, ctrl)
}
}
return result
}
// Helper functions
func dirExists(path string) bool {
info, err := os.Stat(path)
if err != nil {
return false
}
return info.IsDir()
}
func (e *Engine) loadRules(dir string) error {
return filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {
if err != nil || info.IsDir() || filepath.Ext(path) != ".yaml" {
return nil
}
data, err := os.ReadFile(path)
if err != nil {
return err
}
var rules []Rule
if err := yaml.Unmarshal(data, &rules); err != nil {
return err
}
for i := range rules {
e.rules[rules[i].ID] = &rules[i]
}
return nil
})
}
func (e *Engine) loadRegulations(dir string) error {
return filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {
if err != nil || info.IsDir() || filepath.Ext(path) != ".yaml" {
return nil
}
data, err := os.ReadFile(path)
if err != nil {
return err
}
var regs []Regulation
if err := yaml.Unmarshal(data, &regs); err != nil {
return err
}
for i := range regs {
e.regulations[regs[i].Code] = &regs[i]
}
return nil
})
}
func (e *Engine) loadControls(dir string) error {
return filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {
if err != nil || info.IsDir() || filepath.Ext(path) != ".yaml" {
return nil
}
data, err := os.ReadFile(path)
if err != nil {
return err
}
var ctrls []Control
if err := yaml.Unmarshal(data, &ctrls); err != nil {
return err
}
for i := range ctrls {
e.controls[ctrls[i].ID] = &ctrls[i]
}
return nil
})
}

View File

@@ -0,0 +1,103 @@
// BreakPilot Compliance SDK - Compliance Engine
//
// UCCA (Unified Compliance Control Assessment) Engine
// Evaluates compliance state against 45+ policy rules
package main
import (
"context"
"log"
"net/http"
"os"
"os/signal"
"syscall"
"time"
"github.com/breakpilot/compliance-sdk/services/compliance-engine/internal/api"
"github.com/breakpilot/compliance-sdk/services/compliance-engine/internal/ucca"
"github.com/gin-gonic/gin"
"go.uber.org/zap"
)
func main() {
logger, _ := zap.NewProduction()
defer logger.Sync()
// Load UCCA policies
engine, err := ucca.NewEngine("policies")
if err != nil {
logger.Fatal("Failed to load UCCA policies", zap.Error(err))
}
logger.Info("UCCA Engine initialized", zap.Int("rules", engine.RuleCount()))
// Set Gin mode
if os.Getenv("ENVIRONMENT") == "production" {
gin.SetMode(gin.ReleaseMode)
}
router := gin.New()
router.Use(gin.Recovery())
// Health check
router.GET("/health", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{
"status": "healthy",
"service": "compliance-engine",
"rules": engine.RuleCount(),
})
})
// API routes
v1 := router.Group("/api/v1")
{
// Assessment
v1.POST("/assess", api.Assess(engine))
v1.POST("/assess/control", api.AssessControl(engine))
v1.POST("/assess/regulation", api.AssessRegulation(engine))
// Score calculation
v1.POST("/score", api.CalculateScore(engine))
v1.POST("/score/breakdown", api.ScoreBreakdown(engine))
// Obligations
v1.GET("/obligations", api.GetObligations(engine))
v1.GET("/obligations/:regulation", api.GetObligationsByRegulation(engine))
// Controls catalog
v1.GET("/controls", api.GetControlsCatalog(engine))
v1.GET("/controls/:domain", api.GetControlsByDomain(engine))
// Policies
v1.GET("/policies", api.ListPolicies(engine))
v1.GET("/policies/:id", api.GetPolicy(engine))
}
port := os.Getenv("PORT")
if port == "" {
port = "8081"
}
srv := &http.Server{
Addr: ":" + port,
Handler: router,
ReadTimeout: 15 * time.Second,
WriteTimeout: 15 * time.Second,
}
go func() {
logger.Info("Starting Compliance Engine", zap.String("port", port))
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
logger.Fatal("Failed to start server", zap.Error(err))
}
}()
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
<-quit
logger.Info("Shutting down...")
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
srv.Shutdown(ctx)
}

View File

@@ -0,0 +1,39 @@
# Build stage
FROM python:3.11-slim AS builder
WORKDIR /app
# Install build dependencies
RUN apt-get update && apt-get install -y --no-install-recommends \
build-essential \
&& rm -rf /var/lib/apt/lists/*
# Install Python dependencies
COPY requirements.txt .
RUN pip install --no-cache-dir --user -r requirements.txt
# Runtime stage
FROM python:3.11-slim
WORKDIR /app
# Copy installed packages from builder
COPY --from=builder /root/.local /root/.local
ENV PATH=/root/.local/bin:$PATH
# Copy application code
COPY . .
# Create non-root user
RUN useradd -m -u 1000 appuser && chown -R appuser:appuser /app
USER appuser
# Expose port
EXPOSE 8082
# Health check
HEALTHCHECK --interval=30s --timeout=3s --start-period=10s --retries=3 \
CMD python -c "import httpx; httpx.get('http://localhost:8082/health')" || exit 1
# Run
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8082"]

View File

@@ -0,0 +1,34 @@
"""
Configuration for RAG Service
"""
from pydantic_settings import BaseSettings
from typing import Optional
class Settings(BaseSettings):
"""Application settings loaded from environment variables."""
# Service
environment: str = "development"
port: int = 8082
# Qdrant
qdrant_url: str = "http://localhost:6333"
qdrant_collection: str = "legal_documents"
# Ollama
ollama_url: str = "http://localhost:11434"
embedding_model: str = "bge-m3"
llm_model: str = "qwen2.5:32b"
# Document Processing
chunk_size: int = 512
chunk_overlap: int = 50
# Legal Corpus
corpus_path: str = "./legal-corpus"
class Config:
env_file = ".env"
env_file_encoding = "utf-8"

View File

@@ -0,0 +1,245 @@
"""
BreakPilot Compliance SDK - RAG Service
Retrieval-Augmented Generation service for legal document search and Q&A.
"""
import os
from contextlib import asynccontextmanager
from fastapi import FastAPI, HTTPException, UploadFile, File
from fastapi.middleware.cors import CORSMiddleware
from pydantic import BaseModel
from typing import List, Optional
import structlog
from rag.search import SearchService
from rag.assistant import AssistantService
from rag.documents import DocumentService
from config import Settings
# Configure logging
structlog.configure(
processors=[
structlog.processors.TimeStamper(fmt="iso"),
structlog.processors.JSONRenderer()
]
)
logger = structlog.get_logger()
# Load settings
settings = Settings()
# Services
search_service: SearchService = None
assistant_service: AssistantService = None
document_service: DocumentService = None
@asynccontextmanager
async def lifespan(app: FastAPI):
"""Application lifespan handler."""
global search_service, assistant_service, document_service
logger.info("Starting RAG Service", version="0.0.1")
# Initialize services
search_service = SearchService(settings)
assistant_service = AssistantService(settings)
document_service = DocumentService(settings)
# Initialize vector store with legal corpus
await search_service.initialize()
logger.info("RAG Service ready",
regulations=len(search_service.regulations),
total_chunks=search_service.total_chunks)
yield
logger.info("Shutting down RAG Service")
app = FastAPI(
title="BreakPilot RAG Service",
description="Legal document search and Q&A service",
version="0.0.1",
lifespan=lifespan
)
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# =============================================================================
# Models
# =============================================================================
class SearchRequest(BaseModel):
query: str
regulation_codes: Optional[List[str]] = None
limit: int = 10
min_score: float = 0.7
class SearchResult(BaseModel):
content: str
regulation_code: str
article: Optional[str] = None
paragraph: Optional[str] = None
score: float
metadata: dict = {}
class SearchResponse(BaseModel):
query: str
results: List[SearchResult]
total: int
class AskRequest(BaseModel):
question: str
context: Optional[str] = None
regulation_codes: Optional[List[str]] = None
include_citations: bool = True
class Citation(BaseModel):
regulation_code: str
article: str
text: str
relevance: float
class AskResponse(BaseModel):
question: str
answer: str
citations: List[Citation]
confidence: float
class RegulationInfo(BaseModel):
code: str
name: str
chunks: int
last_updated: str
# =============================================================================
# Endpoints
# =============================================================================
@app.get("/health")
async def health():
"""Health check endpoint."""
return {
"status": "healthy",
"service": "rag-service",
"version": "0.0.1",
"regulations": len(search_service.regulations) if search_service else 0
}
@app.post("/api/v1/search", response_model=SearchResponse)
async def search(request: SearchRequest):
"""Perform semantic search across legal documents."""
try:
results = await search_service.search(
query=request.query,
regulation_codes=request.regulation_codes,
limit=request.limit,
min_score=request.min_score
)
return SearchResponse(
query=request.query,
results=[SearchResult(**r) for r in results],
total=len(results)
)
except Exception as e:
logger.error("Search failed", error=str(e))
raise HTTPException(status_code=500, detail=str(e))
@app.post("/api/v1/ask", response_model=AskResponse)
async def ask(request: AskRequest):
"""Ask a question about legal requirements."""
try:
response = await assistant_service.ask(
question=request.question,
context=request.context,
regulation_codes=request.regulation_codes,
include_citations=request.include_citations
)
return AskResponse(
question=request.question,
answer=response["answer"],
citations=[Citation(**c) for c in response.get("citations", [])],
confidence=response.get("confidence", 0.9)
)
except Exception as e:
logger.error("Ask failed", error=str(e))
raise HTTPException(status_code=500, detail=str(e))
@app.get("/api/v1/regulations", response_model=List[RegulationInfo])
async def get_regulations():
"""Get list of available regulations."""
return search_service.get_regulations()
@app.get("/api/v1/regulations/{code}")
async def get_regulation(code: str):
"""Get details of a specific regulation."""
regulation = search_service.get_regulation(code)
if not regulation:
raise HTTPException(status_code=404, detail="Regulation not found")
return regulation
@app.post("/api/v1/documents")
async def upload_document(
file: UploadFile = File(...),
regulation_code: Optional[str] = None
):
"""Upload a custom document for indexing."""
try:
result = await document_service.process_upload(
file=file,
regulation_code=regulation_code
)
return {
"id": result["id"],
"filename": file.filename,
"chunks": result["chunks"],
"status": "INDEXED"
}
except Exception as e:
logger.error("Document upload failed", error=str(e))
raise HTTPException(status_code=500, detail=str(e))
@app.delete("/api/v1/documents/{document_id}")
async def delete_document(document_id: str):
"""Delete a custom document."""
try:
await document_service.delete(document_id)
return {"status": "deleted", "id": document_id}
except Exception as e:
logger.error("Document deletion failed", error=str(e))
raise HTTPException(status_code=500, detail=str(e))
if __name__ == "__main__":
import uvicorn
uvicorn.run(
"main:app",
host="0.0.0.0",
port=int(os.getenv("PORT", "8082")),
reload=os.getenv("ENVIRONMENT") != "production"
)

View File

@@ -0,0 +1,7 @@
"""RAG module for BreakPilot Compliance SDK."""
from .search import SearchService
from .assistant import AssistantService
from .documents import DocumentService
__all__ = ["SearchService", "AssistantService", "DocumentService"]

View File

@@ -0,0 +1,139 @@
"""
Assistant Service for RAG
Handles Q&A using LLM with retrieved context.
"""
import httpx
from typing import List, Optional, Dict, Any
import structlog
from .search import SearchService
logger = structlog.get_logger()
SYSTEM_PROMPT = """Du bist ein Experte für Datenschutz- und Compliance-Recht.
Beantworte Fragen basierend auf den bereitgestellten Rechtstexten.
Zitiere immer die relevanten Artikel und Paragraphen.
Antworte auf Deutsch.
Wenn du dir nicht sicher bist, sage das klar.
"""
class AssistantService:
"""Service for legal Q&A using RAG."""
def __init__(self, settings):
self.settings = settings
self.search_service = SearchService(settings)
async def ask(
self,
question: str,
context: Optional[str] = None,
regulation_codes: Optional[List[str]] = None,
include_citations: bool = True
) -> Dict[str, Any]:
"""Answer a legal question using RAG."""
# Search for relevant context
search_results = await self.search_service.search(
query=question,
regulation_codes=regulation_codes,
limit=5,
min_score=0.6
)
# Build context from search results
retrieved_context = "\n\n".join([
f"[{r['regulation_code']} Art. {r['article']}]: {r['content']}"
for r in search_results
])
# Add user-provided context if any
if context:
retrieved_context = f"{context}\n\n{retrieved_context}"
# Build prompt
prompt = f"""Kontext aus Rechtstexten:
{retrieved_context}
Frage: {question}
Beantworte die Frage basierend auf dem Kontext. Zitiere relevante Artikel."""
# Generate answer
answer = await self._generate_response(prompt)
# Extract citations
citations = []
if include_citations:
for result in search_results:
citations.append({
"regulation_code": result["regulation_code"],
"article": result.get("article", ""),
"text": result["content"][:200] + "...",
"relevance": result["score"]
})
return {
"answer": answer,
"citations": citations,
"confidence": self._calculate_confidence(search_results)
}
async def _generate_response(self, prompt: str) -> str:
"""Generate response using Ollama."""
try:
async with httpx.AsyncClient() as client:
response = await client.post(
f"{self.settings.ollama_url}/api/generate",
json={
"model": self.settings.llm_model,
"prompt": prompt,
"system": SYSTEM_PROMPT,
"stream": False,
"options": {
"temperature": 0.3,
"top_p": 0.9
}
},
timeout=120.0
)
response.raise_for_status()
return response.json()["response"]
except httpx.TimeoutException:
logger.error("LLM request timed out")
return "Die Anfrage hat zu lange gedauert. Bitte versuchen Sie es erneut."
except Exception as e:
logger.error("LLM generation failed", error=str(e))
# Return fallback response
return self._generate_fallback_response(prompt)
def _generate_fallback_response(self, prompt: str) -> str:
"""Generate a fallback response without LLM."""
return """Basierend auf den verfügbaren Rechtstexten:
Die relevanten Regelungen finden sich in den zitierten Artikeln.
Für eine detaillierte rechtliche Bewertung empfehle ich die Konsultation
der vollständigen Gesetzestexte oder eines Rechtsbeistands.
Hinweis: Dies ist eine automatisch generierte Antwort.
Der LLM-Dienst war nicht verfügbar."""
def _calculate_confidence(self, search_results: List[Dict]) -> float:
"""Calculate confidence score based on search results."""
if not search_results:
return 0.3
# Average relevance score
avg_score = sum(r["score"] for r in search_results) / len(search_results)
# Adjust based on number of results
if len(search_results) >= 3:
confidence = avg_score * 1.1
else:
confidence = avg_score * 0.9
return min(confidence, 1.0)

View File

@@ -0,0 +1,153 @@
"""
Document Service for RAG
Handles document upload, processing, and indexing.
"""
import uuid
from typing import Optional, Dict, Any
from fastapi import UploadFile
import structlog
logger = structlog.get_logger()
class DocumentService:
"""Service for document processing and indexing."""
def __init__(self, settings):
self.settings = settings
self.documents: Dict[str, Dict] = {}
async def process_upload(
self,
file: UploadFile,
regulation_code: Optional[str] = None
) -> Dict[str, Any]:
"""Process and index an uploaded document."""
doc_id = str(uuid.uuid4())
# Read file content
content = await file.read()
# Determine file type and extract text
filename = file.filename or "unknown"
if filename.endswith(".pdf"):
text = await self._extract_pdf(content)
elif filename.endswith(".docx"):
text = await self._extract_docx(content)
elif filename.endswith(".md"):
text = await self._extract_markdown(content)
else:
text = content.decode("utf-8", errors="ignore")
# Chunk the text
chunks = self._chunk_text(text)
# Store document metadata
self.documents[doc_id] = {
"id": doc_id,
"filename": filename,
"regulation_code": regulation_code or "CUSTOM",
"chunks": len(chunks),
"text_length": len(text)
}
# TODO: Index chunks in Qdrant
logger.info("Document processed",
doc_id=doc_id,
filename=filename,
chunks=len(chunks))
return {
"id": doc_id,
"filename": filename,
"chunks": len(chunks)
}
async def _extract_pdf(self, content: bytes) -> str:
"""Extract text from PDF."""
try:
from pypdf import PdfReader
from io import BytesIO
reader = PdfReader(BytesIO(content))
text = ""
for page in reader.pages:
text += page.extract_text() + "\n"
return text
except Exception as e:
logger.error("PDF extraction failed", error=str(e))
return ""
async def _extract_docx(self, content: bytes) -> str:
"""Extract text from DOCX."""
try:
from docx import Document
from io import BytesIO
doc = Document(BytesIO(content))
text = ""
for para in doc.paragraphs:
text += para.text + "\n"
return text
except Exception as e:
logger.error("DOCX extraction failed", error=str(e))
return ""
async def _extract_markdown(self, content: bytes) -> str:
"""Extract text from Markdown."""
try:
import markdown
from bs4 import BeautifulSoup
html = markdown.markdown(content.decode("utf-8"))
soup = BeautifulSoup(html, "html.parser")
return soup.get_text()
except Exception as e:
logger.error("Markdown extraction failed", error=str(e))
return content.decode("utf-8", errors="ignore")
def _chunk_text(self, text: str) -> list:
"""Split text into chunks."""
chunk_size = self.settings.chunk_size
chunk_overlap = self.settings.chunk_overlap
chunks = []
start = 0
while start < len(text):
end = start + chunk_size
# Try to break at sentence boundary
if end < len(text):
# Look for sentence end within overlap window
search_start = max(end - chunk_overlap, start)
search_text = text[search_start:end + chunk_overlap]
for sep in [". ", ".\n", "! ", "? "]:
last_sep = search_text.rfind(sep)
if last_sep > 0:
end = search_start + last_sep + len(sep)
break
chunk = text[start:end].strip()
if chunk:
chunks.append(chunk)
start = end - chunk_overlap
return chunks
async def delete(self, document_id: str) -> bool:
"""Delete a document and its chunks."""
if document_id in self.documents:
del self.documents[document_id]
# TODO: Delete from Qdrant
logger.info("Document deleted", doc_id=document_id)
return True
return False
def get_document(self, document_id: str) -> Optional[Dict]:
"""Get document metadata."""
return self.documents.get(document_id)

View File

@@ -0,0 +1,235 @@
"""
Search Service for RAG
Handles semantic search across legal documents using Qdrant and embeddings.
"""
import httpx
from typing import List, Optional, Dict, Any
from qdrant_client import QdrantClient
from qdrant_client.models import (
Distance, VectorParams, PointStruct,
Filter, FieldCondition, MatchValue
)
import structlog
logger = structlog.get_logger()
class SearchService:
"""Service for semantic search across legal documents."""
def __init__(self, settings):
self.settings = settings
self.qdrant = QdrantClient(url=settings.qdrant_url)
self.collection = settings.qdrant_collection
self.regulations: Dict[str, Dict] = {}
self.total_chunks = 0
async def initialize(self):
"""Initialize the search service and load legal corpus."""
# Ensure collection exists
try:
self.qdrant.get_collection(self.collection)
logger.info("Using existing collection", collection=self.collection)
except Exception:
# Create collection
self.qdrant.create_collection(
collection_name=self.collection,
vectors_config=VectorParams(
size=1024, # bge-m3 dimension
distance=Distance.COSINE
)
)
logger.info("Created collection", collection=self.collection)
# Load built-in regulations metadata
self._load_regulations_metadata()
# Index legal corpus if empty
info = self.qdrant.get_collection(self.collection)
if info.points_count == 0:
await self._index_legal_corpus()
self.total_chunks = info.points_count
def _load_regulations_metadata(self):
"""Load metadata for available regulations."""
self.regulations = {
"DSGVO": {
"code": "DSGVO",
"name": "Datenschutz-Grundverordnung",
"full_name": "Verordnung (EU) 2016/679",
"effective": "2018-05-25",
"chunks": 99,
"articles": list(range(1, 100))
},
"AI_ACT": {
"code": "AI_ACT",
"name": "EU AI Act",
"full_name": "Verordnung über Künstliche Intelligenz",
"effective": "2025-02-02",
"chunks": 85,
"articles": list(range(1, 114))
},
"NIS2": {
"code": "NIS2",
"name": "NIS 2 Directive",
"full_name": "Richtlinie (EU) 2022/2555",
"effective": "2024-10-17",
"chunks": 46,
"articles": list(range(1, 47))
},
"TDDDG": {
"code": "TDDDG",
"name": "TDDDG",
"full_name": "Telekommunikation-Digitale-Dienste-Datenschutz-Gesetz",
"effective": "2021-12-01",
"chunks": 30,
"articles": list(range(1, 31))
},
"BDSG": {
"code": "BDSG",
"name": "BDSG",
"full_name": "Bundesdatenschutzgesetz",
"effective": "2018-05-25",
"chunks": 86,
"articles": list(range(1, 87))
}
}
async def _index_legal_corpus(self):
"""Index the legal corpus into Qdrant."""
logger.info("Indexing legal corpus...")
# Sample chunks for demonstration
# In production, this would load actual legal documents
sample_chunks = [
{
"content": "Art. 9 Abs. 1 DSGVO: Die Verarbeitung personenbezogener Daten, aus denen die rassische und ethnische Herkunft, politische Meinungen, religiöse oder weltanschauliche Überzeugungen oder die Gewerkschaftszugehörigkeit hervorgehen, sowie die Verarbeitung von genetischen Daten, biometrischen Daten zur eindeutigen Identifizierung einer natürlichen Person, Gesundheitsdaten oder Daten zum Sexualleben oder der sexuellen Orientierung einer natürlichen Person ist untersagt.",
"regulation_code": "DSGVO",
"article": "9",
"paragraph": "1"
},
{
"content": "Art. 6 Abs. 1 DSGVO: Die Verarbeitung ist nur rechtmäßig, wenn mindestens eine der nachstehenden Bedingungen erfüllt ist: a) Die betroffene Person hat ihre Einwilligung zu der Verarbeitung der sie betreffenden personenbezogenen Daten für einen oder mehrere bestimmte Zwecke gegeben.",
"regulation_code": "DSGVO",
"article": "6",
"paragraph": "1"
},
{
"content": "Art. 32 DSGVO: Unter Berücksichtigung des Stands der Technik, der Implementierungskosten und der Art, des Umfangs, der Umstände und der Zwecke der Verarbeitung sowie der unterschiedlichen Eintrittswahrscheinlichkeit und Schwere des Risikos für die Rechte und Freiheiten natürlicher Personen treffen der Verantwortliche und der Auftragsverarbeiter geeignete technische und organisatorische Maßnahmen.",
"regulation_code": "DSGVO",
"article": "32",
"paragraph": "1"
},
{
"content": "Art. 6 AI Act: Hochrisiko-KI-Systeme. Als Hochrisiko-KI-Systeme gelten KI-Systeme, die als Sicherheitskomponente eines Produkts oder selbst als Produkt bestimmungsgemäß verwendet werden sollen.",
"regulation_code": "AI_ACT",
"article": "6",
"paragraph": "1"
},
{
"content": "Art. 21 NIS2: Risikomanagementmaßnahmen im Bereich der Cybersicherheit. Die Mitgliedstaaten stellen sicher, dass wesentliche und wichtige Einrichtungen geeignete und verhältnismäßige technische, operative und organisatorische Maßnahmen ergreifen.",
"regulation_code": "NIS2",
"article": "21",
"paragraph": "1"
}
]
# Generate embeddings and index
points = []
for i, chunk in enumerate(sample_chunks):
embedding = await self._get_embedding(chunk["content"])
points.append(PointStruct(
id=i,
vector=embedding,
payload=chunk
))
self.qdrant.upsert(
collection_name=self.collection,
points=points
)
logger.info("Indexed legal corpus", chunks=len(points))
async def _get_embedding(self, text: str) -> List[float]:
"""Get embedding for text using Ollama."""
try:
async with httpx.AsyncClient() as client:
response = await client.post(
f"{self.settings.ollama_url}/api/embeddings",
json={
"model": self.settings.embedding_model,
"prompt": text
},
timeout=30.0
)
response.raise_for_status()
return response.json()["embedding"]
except Exception as e:
logger.error("Embedding failed", error=str(e))
# Return zero vector as fallback
return [0.0] * 1024
async def search(
self,
query: str,
regulation_codes: Optional[List[str]] = None,
limit: int = 10,
min_score: float = 0.7
) -> List[Dict[str, Any]]:
"""Perform semantic search."""
# Get query embedding
query_embedding = await self._get_embedding(query)
# Build filter
search_filter = None
if regulation_codes:
search_filter = Filter(
should=[
FieldCondition(
key="regulation_code",
match=MatchValue(value=code)
)
for code in regulation_codes
]
)
# Search
results = self.qdrant.search(
collection_name=self.collection,
query_vector=query_embedding,
query_filter=search_filter,
limit=limit,
score_threshold=min_score
)
return [
{
"content": hit.payload.get("content", ""),
"regulation_code": hit.payload.get("regulation_code", ""),
"article": hit.payload.get("article"),
"paragraph": hit.payload.get("paragraph"),
"score": hit.score,
"metadata": hit.payload
}
for hit in results
]
def get_regulations(self) -> List[Dict]:
"""Get list of available regulations."""
return [
{
"code": reg["code"],
"name": reg["name"],
"chunks": reg["chunks"],
"last_updated": reg["effective"]
}
for reg in self.regulations.values()
]
def get_regulation(self, code: str) -> Optional[Dict]:
"""Get details of a specific regulation."""
return self.regulations.get(code)

View File

@@ -0,0 +1,31 @@
# BreakPilot Compliance SDK - RAG Service Dependencies
# Web Framework
fastapi==0.109.0
uvicorn[standard]==0.27.0
python-multipart==0.0.6
# Vector Database
qdrant-client==1.7.0
# LLM & Embeddings
httpx==0.26.0
ollama==0.1.6
# Document Processing
pypdf==3.17.4
python-docx==1.1.0
beautifulsoup4==4.12.3
markdown==3.5.2
# Text Processing
tiktoken==0.5.2
langchain-text-splitters==0.0.1
# Utilities
pydantic==2.5.3
pydantic-settings==2.1.0
python-dotenv==1.0.0
# Logging & Monitoring
structlog==24.1.0

View File

@@ -0,0 +1,45 @@
# Build stage
FROM golang:1.22-alpine AS builder
WORKDIR /app
RUN apk add --no-cache git
COPY go.mod go.sum* ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o security-scanner .
# Runtime stage with security tools
FROM alpine:3.19
WORKDIR /app
# Install security tools
RUN apk --no-cache add ca-certificates curl git python3 py3-pip nodejs npm && \
# Install gitleaks
curl -sSfL https://github.com/gitleaks/gitleaks/releases/download/v8.18.0/gitleaks_8.18.0_linux_x64.tar.gz | tar xz -C /usr/local/bin && \
# Install trivy
curl -sfL https://raw.githubusercontent.com/aquasecurity/trivy/main/contrib/install.sh | sh -s -- -b /usr/local/bin && \
# Install grype
curl -sSfL https://raw.githubusercontent.com/anchore/grype/main/install.sh | sh -s -- -b /usr/local/bin && \
# Install syft
curl -sSfL https://raw.githubusercontent.com/anchore/syft/main/install.sh | sh -s -- -b /usr/local/bin && \
# Install semgrep
pip3 install --break-system-packages semgrep bandit && \
# Cleanup
rm -rf /var/cache/apk/*
COPY --from=builder /app/security-scanner .
RUN adduser -D -g '' appuser
USER appuser
EXPOSE 8083
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD wget --no-verbose --tries=1 --spider http://localhost:8083/health || exit 1
CMD ["./security-scanner"]

View File

@@ -0,0 +1,9 @@
module github.com/breakpilot/compliance-sdk/services/security-scanner
go 1.22
require (
github.com/gin-gonic/gin v1.9.1
github.com/google/uuid v1.5.0
go.uber.org/zap v1.26.0
)

View File

@@ -0,0 +1,216 @@
// Package api provides HTTP handlers for the Security Scanner
package api
import (
"net/http"
"github.com/breakpilot/compliance-sdk/services/security-scanner/internal/scanner"
"github.com/gin-gonic/gin"
)
// ScanRequest represents a scan request
type ScanRequest struct {
Tools []string `json:"tools"`
TargetPath string `json:"target_path"`
ExcludePaths []string `json:"exclude_paths"`
}
// StartScan starts a new security scan
func StartScan(manager *scanner.Manager) gin.HandlerFunc {
return func(c *gin.Context) {
var req ScanRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
tools := req.Tools
if len(tools) == 0 {
tools = manager.AvailableTools()
}
scan := manager.StartScan(tools, req.TargetPath)
c.JSON(http.StatusAccepted, gin.H{
"scan_id": scan.ID,
"status": scan.Status,
"tools": scan.Tools,
"started_at": scan.StartedAt,
})
}
}
// GetScanStatus returns the status of a scan
func GetScanStatus(manager *scanner.Manager) gin.HandlerFunc {
return func(c *gin.Context) {
scanID := c.Param("scanId")
scan := manager.GetScan(scanID)
if scan == nil {
c.JSON(http.StatusNotFound, gin.H{"error": "Scan not found"})
return
}
c.JSON(http.StatusOK, gin.H{
"scan_id": scan.ID,
"status": scan.Status,
"started_at": scan.StartedAt,
"completed_at": scan.CompletedAt,
"summary": scan.Summary,
})
}
}
// GetScanResults returns the results of a scan
func GetScanResults(manager *scanner.Manager) gin.HandlerFunc {
return func(c *gin.Context) {
scanID := c.Param("scanId")
scan := manager.GetScan(scanID)
if scan == nil {
c.JSON(http.StatusNotFound, gin.H{"error": "Scan not found"})
return
}
c.JSON(http.StatusOK, scan)
}
}
// GetFindings returns all findings
func GetFindings(manager *scanner.Manager) gin.HandlerFunc {
return func(c *gin.Context) {
findings := manager.GetAllFindings()
severity := c.Query("severity")
tool := c.Query("tool")
status := c.Query("status")
// Filter findings
filtered := []scanner.Finding{}
for _, f := range findings {
if severity != "" && f.Severity != severity {
continue
}
if tool != "" && f.Tool != tool {
continue
}
if status != "" && f.Status != status {
continue
}
filtered = append(filtered, f)
}
c.JSON(http.StatusOK, gin.H{
"findings": filtered,
"total": len(filtered),
})
}
}
// GetFinding returns a specific finding
func GetFinding(manager *scanner.Manager) gin.HandlerFunc {
return func(c *gin.Context) {
findingID := c.Param("findingId")
findings := manager.GetAllFindings()
for _, f := range findings {
if f.ID == findingID {
c.JSON(http.StatusOK, f)
return
}
}
c.JSON(http.StatusNotFound, gin.H{"error": "Finding not found"})
}
}
// UpdateFindingStatus updates the status of a finding
func UpdateFindingStatus(manager *scanner.Manager) gin.HandlerFunc {
return func(c *gin.Context) {
findingID := c.Param("findingId")
var req struct {
Status string `json:"status" binding:"required"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
if manager.UpdateFindingStatus(findingID, req.Status) {
c.JSON(http.StatusOK, gin.H{
"id": findingID,
"status": req.Status,
})
} else {
c.JSON(http.StatusNotFound, gin.H{"error": "Finding not found"})
}
}
}
// GenerateSBOM generates a Software Bill of Materials
func GenerateSBOM(manager *scanner.Manager) gin.HandlerFunc {
return func(c *gin.Context) {
var req struct {
TargetPath string `json:"target_path"`
}
c.ShouldBindJSON(&req)
sbom := manager.GenerateSBOM(req.TargetPath)
c.JSON(http.StatusOK, sbom)
}
}
// GetSBOM returns an SBOM by ID
func GetSBOM(manager *scanner.Manager) gin.HandlerFunc {
return func(c *gin.Context) {
// In production, retrieve from storage
sbom := manager.GenerateSBOM("")
c.JSON(http.StatusOK, sbom)
}
}
// ExportSBOM exports an SBOM in the specified format
func ExportSBOM(manager *scanner.Manager) gin.HandlerFunc {
return func(c *gin.Context) {
format := c.Param("format")
sbom := manager.GenerateSBOM("")
sbom.Format = format
var contentType string
switch format {
case "cyclonedx":
contentType = "application/vnd.cyclonedx+json"
case "spdx":
contentType = "application/spdx+json"
default:
c.JSON(http.StatusBadRequest, gin.H{"error": "Unsupported format"})
return
}
c.Header("Content-Type", contentType)
c.Header("Content-Disposition", "attachment; filename=sbom."+format+".json")
c.JSON(http.StatusOK, sbom)
}
}
// GetRecommendations returns security recommendations
func GetRecommendations(manager *scanner.Manager) gin.HandlerFunc {
return func(c *gin.Context) {
recs := manager.GetRecommendations()
c.JSON(http.StatusOK, gin.H{
"recommendations": recs,
"total": len(recs),
})
}
}
// GetToolsStatus returns the status of all tools
func GetToolsStatus(manager *scanner.Manager) gin.HandlerFunc {
return func(c *gin.Context) {
tools := manager.GetTools()
c.JSON(http.StatusOK, gin.H{"tools": tools})
}
}

View File

@@ -0,0 +1,409 @@
// Package scanner manages security scanning tools
package scanner
import (
"sync"
"time"
"github.com/google/uuid"
"go.uber.org/zap"
)
// Manager orchestrates security scanning tools
type Manager struct {
logger *zap.Logger
tools map[string]*Tool
scans map[string]*Scan
mu sync.RWMutex
}
// Tool represents a security scanning tool
type Tool struct {
Name string `json:"name"`
Version string `json:"version"`
Available bool `json:"available"`
Description string `json:"description"`
Category string `json:"category"` // secrets, sast, container, dependencies, sbom
}
// Scan represents a security scan
type Scan struct {
ID string `json:"id"`
Status string `json:"status"` // PENDING, RUNNING, COMPLETED, FAILED
Tools []string `json:"tools"`
TargetPath string `json:"target_path"`
StartedAt time.Time `json:"started_at"`
CompletedAt time.Time `json:"completed_at,omitempty"`
Findings []Finding `json:"findings"`
Summary Summary `json:"summary"`
}
// Finding represents a security finding
type Finding struct {
ID string `json:"id"`
Tool string `json:"tool"`
Severity string `json:"severity"` // CRITICAL, HIGH, MEDIUM, LOW
Title string `json:"title"`
Description string `json:"description"`
File string `json:"file,omitempty"`
Line int `json:"line,omitempty"`
Recommendation string `json:"recommendation"`
Status string `json:"status"` // OPEN, IN_PROGRESS, RESOLVED, FALSE_POSITIVE
CreatedAt time.Time `json:"created_at"`
CVE string `json:"cve,omitempty"`
}
// Summary represents scan summary
type Summary struct {
Critical int `json:"critical"`
High int `json:"high"`
Medium int `json:"medium"`
Low int `json:"low"`
Total int `json:"total"`
}
// SBOM represents a Software Bill of Materials
type SBOM struct {
ID string `json:"id"`
Format string `json:"format"` // cyclonedx, spdx
Version string `json:"version"`
GeneratedAt time.Time `json:"generated_at"`
Components []Component `json:"components"`
Dependencies []string `json:"dependencies"`
}
// Component represents an SBOM component
type Component struct {
Name string `json:"name"`
Version string `json:"version"`
Type string `json:"type"` // library, application, framework
License string `json:"license"`
Category string `json:"category"`
Vulnerabilities []string `json:"vulnerabilities,omitempty"`
}
// NewManager creates a new scanner manager
func NewManager(logger *zap.Logger) *Manager {
m := &Manager{
logger: logger,
tools: make(map[string]*Tool),
scans: make(map[string]*Scan),
}
// Initialize tools
m.initializeTools()
return m
}
func (m *Manager) initializeTools() {
m.tools = map[string]*Tool{
"gitleaks": {
Name: "Gitleaks",
Version: "8.18.0",
Available: true,
Description: "Detect secrets and sensitive data in git repositories",
Category: "secrets",
},
"semgrep": {
Name: "Semgrep",
Version: "1.51.0",
Available: true,
Description: "Static analysis for multiple languages",
Category: "sast",
},
"bandit": {
Name: "Bandit",
Version: "1.7.6",
Available: true,
Description: "Security linter for Python code",
Category: "sast",
},
"trivy": {
Name: "Trivy",
Version: "0.48.0",
Available: true,
Description: "Vulnerability scanner for containers and filesystems",
Category: "container",
},
"grype": {
Name: "Grype",
Version: "0.73.0",
Available: true,
Description: "Vulnerability scanner for dependencies",
Category: "dependencies",
},
"syft": {
Name: "Syft",
Version: "0.98.0",
Available: true,
Description: "SBOM generator",
Category: "sbom",
},
}
}
// AvailableTools returns list of available tools
func (m *Manager) AvailableTools() []string {
tools := make([]string, 0, len(m.tools))
for name, tool := range m.tools {
if tool.Available {
tools = append(tools, name)
}
}
return tools
}
// GetTools returns all tools with their status
func (m *Manager) GetTools() map[string]*Tool {
return m.tools
}
// StartScan starts a new security scan
func (m *Manager) StartScan(tools []string, targetPath string) *Scan {
scan := &Scan{
ID: uuid.New().String(),
Status: "RUNNING",
Tools: tools,
TargetPath: targetPath,
StartedAt: time.Now(),
Findings: []Finding{},
}
m.mu.Lock()
m.scans[scan.ID] = scan
m.mu.Unlock()
// Run scan asynchronously
go m.runScan(scan)
return scan
}
func (m *Manager) runScan(scan *Scan) {
m.logger.Info("Starting scan", zap.String("id", scan.ID), zap.Strings("tools", scan.Tools))
// Simulate scanning
time.Sleep(3 * time.Second)
// Generate sample findings
findings := m.generateSampleFindings(scan.Tools)
// Update scan
m.mu.Lock()
scan.Status = "COMPLETED"
scan.CompletedAt = time.Now()
scan.Findings = findings
scan.Summary = m.calculateSummary(findings)
m.mu.Unlock()
m.logger.Info("Scan completed",
zap.String("id", scan.ID),
zap.Int("findings", len(findings)))
}
func (m *Manager) generateSampleFindings(tools []string) []Finding {
findings := []Finding{}
for _, tool := range tools {
switch tool {
case "gitleaks":
// Usually no findings in clean repos
case "trivy":
findings = append(findings, Finding{
ID: uuid.New().String(),
Tool: "trivy",
Severity: "HIGH",
Title: "CVE-2024-1234 in lodash",
Description: "Prototype pollution vulnerability in lodash < 4.17.21",
Recommendation: "Upgrade lodash to version 4.17.21 or higher",
Status: "OPEN",
CreatedAt: time.Now(),
CVE: "CVE-2024-1234",
})
case "semgrep":
findings = append(findings, Finding{
ID: uuid.New().String(),
Tool: "semgrep",
Severity: "MEDIUM",
Title: "Potential SQL injection",
Description: "User input used in SQL query without proper sanitization",
File: "src/db/queries.ts",
Line: 42,
Recommendation: "Use parameterized queries",
Status: "OPEN",
CreatedAt: time.Now(),
})
case "grype":
findings = append(findings, Finding{
ID: uuid.New().String(),
Tool: "grype",
Severity: "LOW",
Title: "Outdated dependency",
Description: "axios@0.21.0 has known vulnerabilities",
Recommendation: "Update to axios@1.6.0 or higher",
Status: "OPEN",
CreatedAt: time.Now(),
})
}
}
return findings
}
func (m *Manager) calculateSummary(findings []Finding) Summary {
summary := Summary{}
for _, f := range findings {
switch f.Severity {
case "CRITICAL":
summary.Critical++
case "HIGH":
summary.High++
case "MEDIUM":
summary.Medium++
case "LOW":
summary.Low++
}
summary.Total++
}
return summary
}
// GetScan returns a scan by ID
func (m *Manager) GetScan(scanID string) *Scan {
m.mu.RLock()
defer m.mu.RUnlock()
return m.scans[scanID]
}
// GetAllFindings returns all findings across scans
func (m *Manager) GetAllFindings() []Finding {
m.mu.RLock()
defer m.mu.RUnlock()
findings := []Finding{}
for _, scan := range m.scans {
findings = append(findings, scan.Findings...)
}
return findings
}
// UpdateFindingStatus updates the status of a finding
func (m *Manager) UpdateFindingStatus(findingID, status string) bool {
m.mu.Lock()
defer m.mu.Unlock()
for _, scan := range m.scans {
for i := range scan.Findings {
if scan.Findings[i].ID == findingID {
scan.Findings[i].Status = status
return true
}
}
}
return false
}
// GenerateSBOM generates a Software Bill of Materials
func (m *Manager) GenerateSBOM(targetPath string) *SBOM {
sbom := &SBOM{
ID: uuid.New().String(),
Format: "cyclonedx",
Version: "1.5",
GeneratedAt: time.Now(),
Components: m.generateSampleComponents(),
}
return sbom
}
func (m *Manager) generateSampleComponents() []Component {
return []Component{
{
Name: "react",
Version: "18.2.0",
Type: "library",
License: "MIT",
Category: "frontend",
},
{
Name: "typescript",
Version: "5.3.3",
Type: "library",
License: "Apache-2.0",
Category: "tooling",
},
{
Name: "express",
Version: "4.18.2",
Type: "framework",
License: "MIT",
Category: "backend",
},
{
Name: "lodash",
Version: "4.17.21",
Type: "library",
License: "MIT",
Category: "utility",
},
}
}
// GetRecommendations generates recommendations based on findings
func (m *Manager) GetRecommendations() []Recommendation {
findings := m.GetAllFindings()
recs := []Recommendation{}
// Group by category
hasVulns := false
hasSecrets := false
hasSAST := false
for _, f := range findings {
switch m.tools[f.Tool].Category {
case "dependencies", "container":
hasVulns = true
case "secrets":
hasSecrets = true
case "sast":
hasSAST = true
}
}
if hasVulns {
recs = append(recs, Recommendation{
Priority: "HIGH",
Category: "DEPENDENCIES",
Title: "Update vulnerable dependencies",
Description: "Several dependencies have known vulnerabilities. Run 'npm audit fix' or equivalent.",
})
}
if hasSecrets {
recs = append(recs, Recommendation{
Priority: "CRITICAL",
Category: "SECRETS",
Title: "Remove exposed secrets",
Description: "Secrets detected in codebase. Remove and rotate immediately.",
})
}
if hasSAST {
recs = append(recs, Recommendation{
Priority: "MEDIUM",
Category: "CODE_QUALITY",
Title: "Address code quality issues",
Description: "Static analysis found potential security issues in code.",
})
}
return recs
}
// Recommendation represents a security recommendation
type Recommendation struct {
Priority string `json:"priority"`
Category string `json:"category"`
Title string `json:"title"`
Description string `json:"description"`
}

View File

@@ -0,0 +1,96 @@
// BreakPilot Compliance SDK - Security Scanner Service
//
// Orchestrates security scanning tools and aggregates results.
package main
import (
"context"
"net/http"
"os"
"os/signal"
"syscall"
"time"
"github.com/breakpilot/compliance-sdk/services/security-scanner/internal/api"
"github.com/breakpilot/compliance-sdk/services/security-scanner/internal/scanner"
"github.com/gin-gonic/gin"
"go.uber.org/zap"
)
func main() {
logger, _ := zap.NewProduction()
defer logger.Sync()
// Initialize scanner manager
scannerManager := scanner.NewManager(logger)
if os.Getenv("ENVIRONMENT") == "production" {
gin.SetMode(gin.ReleaseMode)
}
router := gin.New()
router.Use(gin.Recovery())
// Health check
router.GET("/health", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{
"status": "healthy",
"service": "security-scanner",
"tools": scannerManager.AvailableTools(),
})
})
// API routes
v1 := router.Group("/api/v1")
{
// Scanning
v1.POST("/scan", api.StartScan(scannerManager))
v1.GET("/scan/:scanId", api.GetScanStatus(scannerManager))
v1.GET("/scan/:scanId/results", api.GetScanResults(scannerManager))
// Findings
v1.GET("/findings", api.GetFindings(scannerManager))
v1.GET("/findings/:findingId", api.GetFinding(scannerManager))
v1.PUT("/findings/:findingId/status", api.UpdateFindingStatus(scannerManager))
// SBOM
v1.POST("/sbom/generate", api.GenerateSBOM(scannerManager))
v1.GET("/sbom/:sbomId", api.GetSBOM(scannerManager))
v1.GET("/sbom/:sbomId/export/:format", api.ExportSBOM(scannerManager))
// Recommendations
v1.GET("/recommendations", api.GetRecommendations(scannerManager))
// Tool status
v1.GET("/tools", api.GetToolsStatus(scannerManager))
}
port := os.Getenv("PORT")
if port == "" {
port = "8083"
}
srv := &http.Server{
Addr: ":" + port,
Handler: router,
ReadTimeout: 15 * time.Second,
WriteTimeout: 300 * time.Second, // Long timeout for scans
}
go func() {
logger.Info("Starting Security Scanner", zap.String("port", port))
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
logger.Fatal("Failed to start server", zap.Error(err))
}
}()
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
<-quit
logger.Info("Shutting down...")
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
srv.Shutdown(ctx)
}