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:
@@ -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,
|
||||
})
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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",
|
||||
})
|
||||
}
|
||||
@@ -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",
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
Reference in New Issue
Block a user