mirror of
https://github.com/Dvorinka/Containr.git
synced 2026-06-03 20:12:58 +00:00
410 lines
11 KiB
Go
410 lines
11 KiB
Go
package security
|
|
|
|
import (
|
|
"containr/internal/database"
|
|
"context"
|
|
"database/sql"
|
|
"encoding/json"
|
|
"fmt"
|
|
"log"
|
|
"time"
|
|
|
|
"github.com/google/uuid"
|
|
)
|
|
|
|
// Vulnerability represents a security vulnerability
|
|
type Vulnerability struct {
|
|
ID string `json:"id"`
|
|
Type string `json:"type"` // "dependency", "configuration", "code"
|
|
Severity string `json:"severity"` // "critical", "high", "medium", "low"
|
|
Title string `json:"title"`
|
|
Description string `json:"description"`
|
|
ServiceID string `json:"service_id"`
|
|
ProjectID string `json:"project_id"`
|
|
Status string `json:"status"` // "open", "resolved", "ignored"
|
|
FoundAt time.Time `json:"found_at"`
|
|
ResolvedAt *time.Time `json:"resolved_at,omitempty"`
|
|
Metadata string `json:"metadata"` // JSON string for additional data
|
|
}
|
|
|
|
// SecurityScan represents a security scan result
|
|
type SecurityScan struct {
|
|
ID string `json:"id"`
|
|
ProjectID string `json:"project_id"`
|
|
ServiceID *string `json:"service_id,omitempty"`
|
|
ScanType string `json:"scan_type"` // "dependency", "configuration", "comprehensive"
|
|
Status string `json:"status"` // "running", "completed", "failed"
|
|
StartedAt time.Time `json:"started_at"`
|
|
CompletedAt *time.Time `json:"completed_at,omitempty"`
|
|
Vulnerabilities []Vulnerability `json:"vulnerabilities"`
|
|
Summary ScanSummary `json:"summary"`
|
|
}
|
|
|
|
// ScanSummary provides a summary of scan results
|
|
type ScanSummary struct {
|
|
Total int `json:"total"`
|
|
Critical int `json:"critical"`
|
|
High int `json:"high"`
|
|
Medium int `json:"medium"`
|
|
Low int `json:"low"`
|
|
Score int `json:"score"` // 0-100 security score
|
|
}
|
|
|
|
// Scanner handles security scanning operations
|
|
type Scanner struct {
|
|
db *database.DB
|
|
}
|
|
|
|
// NewScanner creates a new security scanner
|
|
func NewScanner(db *database.DB) *Scanner {
|
|
return &Scanner{db: db}
|
|
}
|
|
|
|
// StartSecurityScan initiates a security scan
|
|
func (s *Scanner) StartSecurityScan(projectID, serviceID, scanType string) (*SecurityScan, error) {
|
|
scanID := uuid.New().String()
|
|
|
|
scan := &SecurityScan{
|
|
ID: scanID,
|
|
ProjectID: projectID,
|
|
ScanType: scanType,
|
|
Status: "running",
|
|
StartedAt: time.Now(),
|
|
Summary: ScanSummary{},
|
|
}
|
|
|
|
if serviceID != "" {
|
|
scan.ServiceID = &serviceID
|
|
}
|
|
|
|
// Insert scan record
|
|
_, err := s.db.Exec(`
|
|
INSERT INTO security_scans (id, project_id, service_id, scan_type, status, started_at)
|
|
VALUES ($1, $2, $3, $4, $5, $6)
|
|
`, scan.ID, scan.ProjectID, scan.ServiceID, scan.ScanType, scan.Status, scan.StartedAt)
|
|
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to create security scan: %w", err)
|
|
}
|
|
|
|
// Start scan in background
|
|
go s.performScan(scan)
|
|
|
|
return scan, nil
|
|
}
|
|
|
|
// performScan executes the actual security scan
|
|
func (s *Scanner) performScan(scan *SecurityScan) {
|
|
ctx := context.Background()
|
|
var vulnerabilities []Vulnerability
|
|
|
|
switch scan.ScanType {
|
|
case "dependency":
|
|
vulnerabilities = s.scanDependencies(ctx, scan)
|
|
case "configuration":
|
|
vulnerabilities = s.scanConfiguration(ctx, scan)
|
|
case "comprehensive":
|
|
vulnerabilities = s.scanComprehensive(ctx, scan)
|
|
default:
|
|
vulnerabilities = []Vulnerability{}
|
|
}
|
|
|
|
// Calculate summary
|
|
summary := s.calculateSummary(vulnerabilities)
|
|
|
|
// Update scan with results
|
|
completedAt := time.Now()
|
|
_, err := s.db.Exec(`
|
|
UPDATE security_scans
|
|
SET status = $1, completed_at = $2, summary = $3
|
|
WHERE id = $4
|
|
`, "completed", completedAt, summaryToJSON(summary), scan.ID)
|
|
|
|
if err != nil {
|
|
log.Printf("Failed to update security scan %s: %v", scan.ID, err)
|
|
return
|
|
}
|
|
|
|
// Store vulnerabilities
|
|
for _, vuln := range vulnerabilities {
|
|
_, err := s.db.Exec(`
|
|
INSERT INTO vulnerabilities (id, type, severity, title, description, service_id, project_id, status, found_at, metadata)
|
|
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)
|
|
`, vuln.ID, vuln.Type, vuln.Severity, vuln.Title, vuln.Description, vuln.ServiceID, vuln.ProjectID, vuln.Status, vuln.FoundAt, vuln.Metadata)
|
|
|
|
if err != nil {
|
|
log.Printf("Failed to store vulnerability %s: %v", vuln.ID, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
// scanDependencies scans for known vulnerable dependencies
|
|
func (s *Scanner) scanDependencies(ctx context.Context, scan *SecurityScan) []Vulnerability {
|
|
var vulnerabilities []Vulnerability
|
|
|
|
// Get project services
|
|
query := `SELECT id, name FROM services WHERE project_id = $1`
|
|
args := []interface{}{scan.ProjectID}
|
|
if scan.ServiceID != nil {
|
|
query += ` AND id = $2`
|
|
args = append(args, *scan.ServiceID)
|
|
}
|
|
|
|
rows, err := s.db.Query(query, args...)
|
|
|
|
if err != nil {
|
|
log.Printf("Failed to query services for scan: %v", err)
|
|
return vulnerabilities
|
|
}
|
|
defer rows.Close()
|
|
|
|
for rows.Next() {
|
|
var serviceID, serviceName string
|
|
if err := rows.Scan(&serviceID, &serviceName); err != nil {
|
|
continue
|
|
}
|
|
|
|
// Simulate dependency scanning (in real implementation, this would check package.json, go.mod, etc.)
|
|
serviceVulns := s.simulateDependencyScan(serviceID, serviceName, scan.ProjectID)
|
|
vulnerabilities = append(vulnerabilities, serviceVulns...)
|
|
}
|
|
|
|
return vulnerabilities
|
|
}
|
|
|
|
// simulateDependencyScan simulates scanning for vulnerable dependencies
|
|
func (s *Scanner) simulateDependencyScan(serviceID, serviceName, projectID string) []Vulnerability {
|
|
var vulns []Vulnerability
|
|
|
|
// Simulate finding some common vulnerabilities
|
|
commonVulns := []struct {
|
|
title string
|
|
description string
|
|
severity string
|
|
}{
|
|
{"Outdated OpenSSL version", "Service uses OpenSSL version with known vulnerabilities", "high"},
|
|
{"Vulnerable npm package", "Package 'lodash' version < 4.17.21 has prototype pollution vulnerability", "medium"},
|
|
{"Outdated Go module", "Go module 'net/http' version has security issues", "low"},
|
|
}
|
|
|
|
for i, vuln := range commonVulns {
|
|
vulns = append(vulns, Vulnerability{
|
|
ID: uuid.New().String(),
|
|
Type: "dependency",
|
|
Severity: vuln.severity,
|
|
Title: vuln.title,
|
|
Description: vuln.description,
|
|
ServiceID: serviceID,
|
|
ProjectID: projectID,
|
|
Status: "open",
|
|
FoundAt: time.Now(),
|
|
Metadata: fmt.Sprintf(`{"service": "%s", "package": "example-package-%d"}`, serviceName, i+1),
|
|
})
|
|
}
|
|
|
|
return vulns
|
|
}
|
|
|
|
// scanConfiguration scans for security configuration issues
|
|
func (s *Scanner) scanConfiguration(ctx context.Context, scan *SecurityScan) []Vulnerability {
|
|
var vulnerabilities []Vulnerability
|
|
|
|
// Check for common configuration issues
|
|
configIssues := []struct {
|
|
title string
|
|
description string
|
|
severity string
|
|
}{
|
|
{"Debug mode enabled", "Application is running in debug mode in production", "high"},
|
|
{"No rate limiting", "API endpoints lack rate limiting protection", "medium"},
|
|
{"CORS too permissive", "CORS configuration allows all origins", "medium"},
|
|
{"Missing security headers", "Security headers (CSP, HSTS) not configured", "low"},
|
|
}
|
|
|
|
for _, issue := range configIssues {
|
|
vulnerabilities = append(vulnerabilities, Vulnerability{
|
|
ID: uuid.New().String(),
|
|
Type: "configuration",
|
|
Severity: issue.severity,
|
|
Title: issue.title,
|
|
Description: issue.description,
|
|
ServiceID: "", // Project-level issue
|
|
ProjectID: scan.ProjectID,
|
|
Status: "open",
|
|
FoundAt: time.Now(),
|
|
Metadata: "{}",
|
|
})
|
|
}
|
|
|
|
return vulnerabilities
|
|
}
|
|
|
|
// scanComprehensive performs a comprehensive security scan
|
|
func (s *Scanner) scanComprehensive(ctx context.Context, scan *SecurityScan) []Vulnerability {
|
|
var allVulnerabilities []Vulnerability
|
|
|
|
// Run all scan types
|
|
allVulnerabilities = append(allVulnerabilities, s.scanDependencies(ctx, scan)...)
|
|
allVulnerabilities = append(allVulnerabilities, s.scanConfiguration(ctx, scan)...)
|
|
|
|
return allVulnerabilities
|
|
}
|
|
|
|
// calculateSummary calculates scan summary from vulnerabilities
|
|
func (s *Scanner) calculateSummary(vulnerabilities []Vulnerability) ScanSummary {
|
|
summary := ScanSummary{
|
|
Total: len(vulnerabilities),
|
|
}
|
|
|
|
for _, vuln := range vulnerabilities {
|
|
switch vuln.Severity {
|
|
case "critical":
|
|
summary.Critical++
|
|
case "high":
|
|
summary.High++
|
|
case "medium":
|
|
summary.Medium++
|
|
case "low":
|
|
summary.Low++
|
|
}
|
|
}
|
|
|
|
// Calculate security score (0-100, higher is better)
|
|
if summary.Total == 0 {
|
|
summary.Score = 100
|
|
} else {
|
|
deduction := (summary.Critical * 25) + (summary.High * 15) + (summary.Medium * 8) + (summary.Low * 3)
|
|
summary.Score = max(0, 100-deduction)
|
|
}
|
|
|
|
return summary
|
|
}
|
|
|
|
// GetSecurityScan retrieves a security scan by ID
|
|
func (s *Scanner) GetSecurityScan(scanID string) (*SecurityScan, error) {
|
|
var scan SecurityScan
|
|
var summaryJSON sql.NullString
|
|
var completedAt sql.NullTime
|
|
|
|
err := s.db.QueryRow(`
|
|
SELECT id, project_id, service_id, scan_type, status, started_at, completed_at, summary
|
|
FROM security_scans WHERE id = $1
|
|
`, scanID).Scan(&scan.ID, &scan.ProjectID, &scan.ServiceID, &scan.ScanType, &scan.Status, &scan.StartedAt, &completedAt, &summaryJSON)
|
|
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if completedAt.Valid {
|
|
scan.CompletedAt = &completedAt.Time
|
|
}
|
|
|
|
if summaryJSON.Valid {
|
|
scan.Summary = jsonToSummary(summaryJSON.String)
|
|
}
|
|
|
|
// Load vulnerabilities
|
|
vulns, err := s.getVulnerabilitiesForScan(scan.ID)
|
|
if err == nil {
|
|
scan.Vulnerabilities = vulns
|
|
}
|
|
|
|
return &scan, nil
|
|
}
|
|
|
|
// getVulnerabilitiesForScan retrieves vulnerabilities for a scan
|
|
func (s *Scanner) getVulnerabilitiesForScan(scanID string) ([]Vulnerability, error) {
|
|
rows, err := s.db.Query(`
|
|
SELECT id, type, severity, title, description, service_id, project_id, status, found_at, resolved_at, metadata
|
|
FROM vulnerabilities WHERE project_id = (SELECT project_id FROM security_scans WHERE id = $1)
|
|
ORDER BY severity DESC, found_at DESC
|
|
`, scanID)
|
|
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
|
|
var vulnerabilities []Vulnerability
|
|
for rows.Next() {
|
|
var vuln Vulnerability
|
|
var resolvedAt sql.NullTime
|
|
|
|
err := rows.Scan(&vuln.ID, &vuln.Type, &vuln.Severity, &vuln.Title, &vuln.Description,
|
|
&vuln.ServiceID, &vuln.ProjectID, &vuln.Status, &vuln.FoundAt, &resolvedAt, &vuln.Metadata)
|
|
|
|
if err != nil {
|
|
continue
|
|
}
|
|
|
|
if resolvedAt.Valid {
|
|
vuln.ResolvedAt = &resolvedAt.Time
|
|
}
|
|
|
|
vulnerabilities = append(vulnerabilities, vuln)
|
|
}
|
|
|
|
return vulnerabilities, nil
|
|
}
|
|
|
|
// GetProjectSecurityHistory retrieves security scan history for a project
|
|
func (s *Scanner) GetProjectSecurityHistory(projectID string, limit int) ([]SecurityScan, error) {
|
|
rows, err := s.db.Query(`
|
|
SELECT id, project_id, service_id, scan_type, status, started_at, completed_at, summary
|
|
FROM security_scans
|
|
WHERE project_id = $1
|
|
ORDER BY started_at DESC
|
|
LIMIT $2
|
|
`, projectID, limit)
|
|
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
|
|
var scans []SecurityScan
|
|
for rows.Next() {
|
|
var scan SecurityScan
|
|
var summaryJSON sql.NullString
|
|
var completedAt sql.NullTime
|
|
|
|
err := rows.Scan(&scan.ID, &scan.ProjectID, &scan.ServiceID, &scan.ScanType, &scan.Status,
|
|
&scan.StartedAt, &completedAt, &summaryJSON)
|
|
|
|
if err != nil {
|
|
continue
|
|
}
|
|
|
|
if completedAt.Valid {
|
|
scan.CompletedAt = &completedAt.Time
|
|
}
|
|
|
|
if summaryJSON.Valid {
|
|
scan.Summary = jsonToSummary(summaryJSON.String)
|
|
}
|
|
|
|
scans = append(scans, scan)
|
|
}
|
|
|
|
return scans, nil
|
|
}
|
|
|
|
// Helper functions
|
|
func summaryToJSON(summary ScanSummary) string {
|
|
data, _ := json.Marshal(summary)
|
|
return string(data)
|
|
}
|
|
|
|
func jsonToSummary(jsonStr string) ScanSummary {
|
|
var summary ScanSummary
|
|
json.Unmarshal([]byte(jsonStr), &summary)
|
|
return summary
|
|
}
|
|
|
|
func max(a, b int) int {
|
|
if a > b {
|
|
return a
|
|
}
|
|
return b
|
|
}
|