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