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,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)
}