feat: initial implementation of container management platform

This commit is contained in:
Tomas Dvorak
2026-02-16 10:18:05 +01:00
commit ffa5489dc1
167 changed files with 55910 additions and 0 deletions
+488
View File
@@ -0,0 +1,488 @@
package security
import (
"containr/internal/database"
"context"
"database/sql"
"encoding/json"
"fmt"
"log"
"strings"
"time"
"github.com/google/uuid"
)
// ComplianceFramework represents a compliance framework
type ComplianceFramework struct {
ID string `json:"id"`
Name string `json:"name"`
Description string `json:"description"`
Version string `json:"version"`
Enabled bool `json:"enabled"`
CreatedAt time.Time `json:"created_at"`
}
// ComplianceControl represents a compliance control
type ComplianceControl struct {
ID string `json:"id"`
FrameworkID string `json:"framework_id"`
Code string `json:"code"`
Title string `json:"title"`
Description string `json:"description"`
Category string `json:"category"`
Requirement string `json:"requirement"`
TestProcedure string `json:"test_procedure"`
Status string `json:"status"` // "compliant", "non_compliant", "not_applicable", "pending"
LastAssessed *time.Time `json:"last_assessed,omitempty"`
Evidence string `json:"evidence"`
Metadata string `json:"metadata"`
}
// ComplianceReport represents a compliance assessment report
type ComplianceReport struct {
ID string `json:"id"`
ProjectID string `json:"project_id"`
FrameworkID string `json:"framework_id"`
AssessmentDate time.Time `json:"assessment_date"`
Assessor string `json:"assessor"`
OverallStatus string `json:"overall_status"`
Score int `json:"score"` // 0-100
Controls []ComplianceControl `json:"controls"`
Risks []ComplianceRisk `json:"risks"`
Recommendations []string `json:"recommendations"`
}
// ComplianceRisk represents a compliance risk
type ComplianceRisk struct {
ID string `json:"id"`
ControlID string `json:"control_id"`
Title string `json:"title"`
Description string `json:"description"`
Impact string `json:"impact"` // "high", "medium", "low"
Likelihood string `json:"likelihood"` // "high", "medium", "low"
Mitigation string `json:"mitigation"`
}
// ComplianceManager handles compliance operations
type ComplianceManager struct {
db *database.DB
}
// NewComplianceManager creates a new compliance manager
func NewComplianceManager(db *database.DB) *ComplianceManager {
return &ComplianceManager{db: db}
}
// InitializeGDPRFramework initializes GDPR compliance framework
func (cm *ComplianceManager) InitializeGDPRFramework() error {
framework := ComplianceFramework{
ID: uuid.New().String(),
Name: "GDPR",
Description: "General Data Protection Regulation compliance framework",
Version: "1.0",
Enabled: true,
CreatedAt: time.Now(),
}
// Insert framework
_, err := cm.db.Exec(`
INSERT INTO compliance_frameworks (id, name, description, version, enabled, created_at)
VALUES ($1, $2, $3, $4, $5, $6)
ON CONFLICT (name) DO UPDATE SET version = $4, enabled = $5
`, framework.ID, framework.Name, framework.Description, framework.Version, framework.Enabled, framework.CreatedAt)
if err != nil {
return fmt.Errorf("failed to create GDPR framework: %w", err)
}
// Add GDPR controls
controls := []ComplianceControl{
{
ID: uuid.New().String(),
FrameworkID: framework.ID,
Code: "GDPR-Art-32",
Title: "Security of Processing",
Description: "Technical and organizational measures to ensure data security",
Category: "Security",
Requirement: "Implement appropriate technical and organizational measures to ensure a level of security appropriate to the risk",
TestProcedure: "Review security controls, encryption policies, access controls, and incident response procedures",
Status: "pending",
Evidence: "",
Metadata: `{"risk_level": "high", "review_frequency": "quarterly"}`,
},
{
ID: uuid.New().String(),
FrameworkID: framework.ID,
Code: "GDPR-Art-25",
Title: "Data Protection by Design and by Default",
Description: "Implement data protection measures in system design",
Category: "Privacy by Design",
Requirement: "Implement data protection principles in system design and default settings",
TestProcedure: "Review system architecture, privacy settings, and data minimization practices",
Status: "pending",
Evidence: "",
Metadata: `{"risk_level": "medium", "review_frequency": "biannual"}`,
},
{
ID: uuid.New().String(),
FrameworkID: framework.ID,
Code: "GDPR-Art-24",
Title: "Responsibility of the Controller",
Description: "Data controller responsibility and compliance demonstration",
Category: "Governance",
Requirement: "Implement measures to ensure and demonstrate compliance with GDPR",
TestProcedure: "Review governance policies, documentation, and compliance monitoring",
Status: "pending",
Evidence: "",
Metadata: `{"risk_level": "medium", "review_frequency": "annual"}`,
},
{
ID: uuid.New().String(),
FrameworkID: framework.ID,
Code: "GDPR-Art-33",
Title: "Notification of Personal Data Breach",
Description: "Procedures for notifying data breaches to authorities",
Category: "Incident Response",
Requirement: "Implement procedures for notifying personal data breaches within 72 hours",
TestProcedure: "Review incident response procedures, notification templates, and breach detection mechanisms",
Status: "pending",
Evidence: "",
Metadata: `{"risk_level": "high", "review_frequency": "quarterly"}`,
},
}
for _, control := range controls {
_, err := cm.db.Exec(`
INSERT INTO compliance_controls (id, framework_id, code, title, description, category, requirement, test_procedure, status, last_assessed, evidence, metadata)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, NULL, $10, $11)
ON CONFLICT (framework_id, code) DO UPDATE SET title = $4, description = $5, requirement = $7, test_procedure = $8
`, control.ID, control.FrameworkID, control.Code, control.Title, control.Description,
control.Category, control.Requirement, control.TestProcedure, control.Status, control.Evidence, control.Metadata)
if err != nil {
log.Printf("Failed to insert GDPR control %s: %v", control.Code, err)
}
}
return nil
}
// AssessCompliance performs a compliance assessment
func (cm *ComplianceManager) AssessCompliance(projectID, frameworkID, assessor string) (*ComplianceReport, error) {
reportID := uuid.New().String()
report := &ComplianceReport{
ID: reportID,
ProjectID: projectID,
FrameworkID: frameworkID,
AssessmentDate: time.Now(),
Assessor: assessor,
OverallStatus: "in_progress",
Score: 0,
Controls: []ComplianceControl{},
Risks: []ComplianceRisk{},
Recommendations: []string{},
}
// Insert report record
_, err := cm.db.Exec(`
INSERT INTO compliance_reports (id, project_id, framework_id, assessment_date, assessor, overall_status, score)
VALUES ($1, $2, $3, $4, $5, $6, $7)
`, report.ID, report.ProjectID, report.FrameworkID, report.AssessmentDate, report.Assessor, report.OverallStatus, report.Score)
if err != nil {
return nil, fmt.Errorf("failed to create compliance report: %w", err)
}
// Start assessment in background
go cm.performAssessment(report)
return report, nil
}
// performAssessment executes the compliance assessment
func (cm *ComplianceManager) performAssessment(report *ComplianceReport) {
ctx := context.Background()
// Get framework controls
controls, err := cm.getFrameworkControls(report.FrameworkID)
if err != nil {
log.Printf("Failed to get framework controls: %v", err)
return
}
var assessedControls []ComplianceControl
var risks []ComplianceRisk
var recommendations []string
compliantCount := 0
for _, control := range controls {
assessedControl := cm.assessControl(ctx, report.ProjectID, control)
assessedControls = append(assessedControls, assessedControl)
if assessedControl.Status == "compliant" {
compliantCount++
} else if assessedControl.Status == "non_compliant" {
// Generate risk for non-compliant controls
risk := ComplianceRisk{
ID: uuid.New().String(),
ControlID: assessedControl.ID,
Title: fmt.Sprintf("Non-compliance: %s", assessedControl.Title),
Description: fmt.Sprintf("Control %s is not compliant", assessedControl.Code),
Impact: cm.getRiskImpact(assessedControl),
Likelihood: cm.getRiskLikelihood(assessedControl),
Mitigation: cm.generateMitigation(assessedControl),
}
risks = append(risks, risk)
// Generate recommendation
rec := fmt.Sprintf("Implement controls to achieve compliance for %s: %s", assessedControl.Code, assessedControl.Title)
recommendations = append(recommendations, rec)
}
// Update control status in database
_, err := cm.db.Exec(`
UPDATE compliance_controls
SET status = $1, last_assessed = $2, evidence = $3
WHERE id = $4
`, assessedControl.Status, assessedControl.LastAssessed, assessedControl.Evidence, assessedControl.ID)
if err != nil {
log.Printf("Failed to update control %s: %v", assessedControl.ID, err)
}
}
// Calculate overall score
score := int((float64(compliantCount) / float64(len(controls))) * 100)
// Determine overall status
overallStatus := "non_compliant"
if score >= 90 {
overallStatus = "compliant"
} else if score >= 70 {
overallStatus = "partially_compliant"
}
// Update report with results
_, err = cm.db.Exec(`
UPDATE compliance_reports
SET overall_status = $1, score = $2
WHERE id = $3
`, overallStatus, score, report.ID)
if err != nil {
log.Printf("Failed to update compliance report %s: %v", report.ID, err)
return
}
// Store risks and recommendations
for _, risk := range risks {
_, err := cm.db.Exec(`
INSERT INTO compliance_risks (id, report_id, control_id, title, description, impact, likelihood, mitigation)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
`, risk.ID, report.ID, risk.ControlID, risk.Title, risk.Description, risk.Impact, risk.Likelihood, risk.Mitigation)
if err != nil {
log.Printf("Failed to store risk %s: %v", risk.ID, err)
}
}
}
// getFrameworkControls retrieves all controls for a framework
func (cm *ComplianceManager) getFrameworkControls(frameworkID string) ([]ComplianceControl, error) {
rows, err := cm.db.Query(`
SELECT id, framework_id, code, title, description, category, requirement, test_procedure, status, last_assessed, evidence, metadata
FROM compliance_controls WHERE framework_id = $1
`, frameworkID)
if err != nil {
return nil, err
}
defer rows.Close()
var controls []ComplianceControl
for rows.Next() {
var control ComplianceControl
var lastAssessed sql.NullTime
err := rows.Scan(&control.ID, &control.FrameworkID, &control.Code, &control.Title, &control.Description,
&control.Category, &control.Requirement, &control.TestProcedure, &control.Status, &lastAssessed, &control.Evidence, &control.Metadata)
if err != nil {
continue
}
if lastAssessed.Valid {
control.LastAssessed = &lastAssessed.Time
}
controls = append(controls, control)
}
return controls, nil
}
// assessControl assesses a single compliance control
func (cm *ComplianceManager) assessControl(ctx context.Context, projectID string, control ComplianceControl) ComplianceControl {
assessed := control
now := time.Now()
assessed.LastAssessed = &now
// Simulate assessment logic (in real implementation, this would check actual configurations)
switch control.Code {
case "GDPR-Art-32":
// Check security measures
hasEncryption := cm.checkDataEncryption(projectID)
hasAccessControl := cm.checkAccessControl(projectID)
hasIncidentResponse := cm.checkIncidentResponse(projectID)
if hasEncryption && hasAccessControl && hasIncidentResponse {
assessed.Status = "compliant"
assessed.Evidence = "Encryption enabled, access controls configured, incident response procedures documented"
} else {
assessed.Status = "non_compliant"
missing := []string{}
if !hasEncryption {
missing = append(missing, "data encryption")
}
if !hasAccessControl {
missing = append(missing, "access controls")
}
if !hasIncidentResponse {
missing = append(missing, "incident response procedures")
}
assessed.Evidence = fmt.Sprintf("Missing controls: %s", strings.Join(missing, ", "))
}
case "GDPR-Art-25":
// Check privacy by design
hasDataMinimization := cm.checkDataMinimization(projectID)
hasPrivacySettings := cm.checkPrivacySettings(projectID)
if hasDataMinimization && hasPrivacySettings {
assessed.Status = "compliant"
assessed.Evidence = "Privacy by design principles implemented, data minimization configured"
} else {
assessed.Status = "non_compliant"
assessed.Evidence = "Privacy by design principles not fully implemented"
}
default:
// Default assessment for other controls
assessed.Status = "pending"
assessed.Evidence = "Assessment pending manual review"
}
return assessed
}
// Helper functions for assessment checks (simulated)
func (cm *ComplianceManager) checkDataEncryption(projectID string) bool {
// Simulate checking encryption settings
// In real implementation, this would check actual configurations
return true
}
func (cm *ComplianceManager) checkAccessControl(projectID string) bool {
// Simulate checking access control
return true
}
func (cm *ComplianceManager) checkIncidentResponse(projectID string) bool {
// Simulate checking incident response procedures
return false // Simulate missing for demo
}
func (cm *ComplianceManager) checkDataMinimization(projectID string) bool {
return true
}
func (cm *ComplianceManager) checkPrivacySettings(projectID string) bool {
return false // Simulate missing for demo
}
func (cm *ComplianceManager) getRiskImpact(control ComplianceControl) string {
// Extract impact from metadata or default based on category
var metadata map[string]interface{}
json.Unmarshal([]byte(control.Metadata), &metadata)
if impact, ok := metadata["risk_level"].(string); ok {
return impact
}
// Default impact based on category
switch control.Category {
case "Security", "Incident Response":
return "high"
case "Privacy by Design":
return "medium"
default:
return "low"
}
}
func (cm *ComplianceManager) getRiskLikelihood(control ComplianceControl) string {
// Default likelihood based on control complexity
if strings.Contains(control.Requirement, "implement") || strings.Contains(control.Requirement, "procedures") {
return "medium"
}
return "low"
}
func (cm *ComplianceManager) generateMitigation(control ComplianceControl) string {
return fmt.Sprintf("Implement and document controls for %s as specified in the requirements", control.Title)
}
// GetComplianceReport retrieves a compliance report by ID
func (cm *ComplianceManager) GetComplianceReport(reportID string) (*ComplianceReport, error) {
var report ComplianceReport
err := cm.db.QueryRow(`
SELECT id, project_id, framework_id, assessment_date, assessor, overall_status, score
FROM compliance_reports WHERE id = $1
`, reportID).Scan(&report.ID, &report.ProjectID, &report.FrameworkID, &report.AssessmentDate, &report.Assessor, &report.OverallStatus, &report.Score)
if err != nil {
return nil, err
}
// Load controls
controls, err := cm.getFrameworkControls(report.FrameworkID)
if err == nil {
report.Controls = controls
}
// Load risks
risks, err := cm.getReportRisks(report.ID)
if err == nil {
report.Risks = risks
}
return &report, nil
}
// getReportRisks retrieves risks for a compliance report
func (cm *ComplianceManager) getReportRisks(reportID string) ([]ComplianceRisk, error) {
rows, err := cm.db.Query(`
SELECT id, control_id, title, description, impact, likelihood, mitigation
FROM compliance_risks WHERE report_id = $1
`, reportID)
if err != nil {
return nil, err
}
defer rows.Close()
var risks []ComplianceRisk
for rows.Next() {
var risk ComplianceRisk
err := rows.Scan(&risk.ID, &risk.ControlID, &risk.Title, &risk.Description, &risk.Impact, &risk.Likelihood, &risk.Mitigation)
if err != nil {
continue
}
risks = append(risks, risk)
}
return risks, nil
}
+358
View File
@@ -0,0 +1,358 @@
package security
import (
"crypto/aes"
"crypto/cipher"
"crypto/rand"
"crypto/sha256"
"encoding/base64"
"encoding/json"
"fmt"
"io"
"strings"
"time"
)
// EncryptionManager handles data encryption and decryption
type EncryptionManager struct {
gcm cipher.AEAD
}
// NewEncryptionManager creates a new encryption manager
func NewEncryptionManager(key string) (*EncryptionManager, error) {
// Convert key to 32 bytes for AES-256
keyHash := sha256.Sum256([]byte(key))
block, err := aes.NewCipher(keyHash[:])
if err != nil {
return nil, fmt.Errorf("failed to create cipher: %w", err)
}
gcm, err := cipher.NewGCM(block)
if err != nil {
return nil, fmt.Errorf("failed to create GCM: %w", err)
}
return &EncryptionManager{gcm: gcm}, nil
}
// Encrypt encrypts data using AES-256 GCM
func (em *EncryptionManager) Encrypt(plaintext string) (string, error) {
if plaintext == "" {
return "", nil
}
nonce := make([]byte, em.gcm.NonceSize())
if _, err := io.ReadFull(rand.Reader, nonce); err != nil {
return "", fmt.Errorf("failed to generate nonce: %w", err)
}
ciphertext := em.gcm.Seal(nonce, nonce, []byte(plaintext), nil)
return base64.StdEncoding.EncodeToString(ciphertext), nil
}
// Decrypt decrypts data using AES-256 GCM
func (em *EncryptionManager) Decrypt(ciphertext string) (string, error) {
if ciphertext == "" {
return "", nil
}
data, err := base64.StdEncoding.DecodeString(ciphertext)
if err != nil {
return "", fmt.Errorf("failed to decode base64: %w", err)
}
nonceSize := em.gcm.NonceSize()
if len(data) < nonceSize {
return "", fmt.Errorf("ciphertext too short")
}
nonce, ciphertext_bytes := data[:nonceSize], data[nonceSize:]
plaintext, err := em.gcm.Open(nil, nonce, ciphertext_bytes, nil)
if err != nil {
return "", fmt.Errorf("failed to decrypt: %w", err)
}
return string(plaintext), nil
}
// EncryptSensitiveData encrypts sensitive data fields
func (em *EncryptionManager) EncryptSensitiveData(data map[string]interface{}) (map[string]interface{}, error) {
result := make(map[string]interface{})
for key, value := range data {
if em.isSensitiveField(key) {
strValue, ok := value.(string)
if ok {
encrypted, err := em.Encrypt(strValue)
if err != nil {
return nil, fmt.Errorf("failed to encrypt field %s: %w", key, err)
}
result[key] = encrypted
} else {
result[key] = value
}
} else {
result[key] = value
}
}
return result, nil
}
// DecryptSensitiveData decrypts sensitive data fields
func (em *EncryptionManager) DecryptSensitiveData(data map[string]interface{}) (map[string]interface{}, error) {
result := make(map[string]interface{})
for key, value := range data {
if em.isSensitiveField(key) {
strValue, ok := value.(string)
if ok {
decrypted, err := em.Decrypt(strValue)
if err != nil {
return nil, fmt.Errorf("failed to decrypt field %s: %w", key, err)
}
result[key] = decrypted
} else {
result[key] = value
}
} else {
result[key] = value
}
}
return result, nil
}
// isSensitiveField determines if a field contains sensitive data
func (em *EncryptionManager) isSensitiveField(fieldName string) bool {
sensitiveFields := []string{
"password", "secret", "token", "key", "api_key", "private_key",
"database_url", "connection_string", "credit_card", "ssn",
"social_security", "bank_account", "auth_token", "jwt_secret",
"encryption_key", "webhook_secret", "oauth_secret", "access_token",
"refresh_token", "client_secret", "private", "confidential",
}
fieldName = strings.ToLower(fieldName)
for _, sensitive := range sensitiveFields {
if strings.Contains(fieldName, sensitive) {
return true
}
}
return false
}
// DataRetentionManager handles data retention policies
type DataRetentionManager struct {
encryptionManager *EncryptionManager
}
// NewDataRetentionManager creates a new data retention manager
func NewDataRetentionManager(encryptionManager *EncryptionManager) *DataRetentionManager {
return &DataRetentionManager{
encryptionManager: encryptionManager,
}
}
// RetentionPolicy defines data retention rules
type RetentionPolicy struct {
ID string `json:"id"`
Name string `json:"name"`
DataType string `json:"data_type"`
RetentionPeriod time.Duration `json:"retention_period"`
Action string `json:"action"` // "delete", "anonymize", "archive"
Enabled bool `json:"enabled"`
CreatedAt time.Time `json:"created_at"`
}
// AnonymizedData represents anonymized user data
type AnonymizedData struct {
OriginalID string `json:"original_id"`
AnonymizedID string `json:"anonymized_id"`
DataType string `json:"data_type"`
AnonymizedAt time.Time `json:"anonymized_at"`
RetainedData string `json:"retained_data"` // Encrypted non-sensitive data
}
// AnonymizeUserData anonymizes user data for GDPR compliance
func (drm *DataRetentionManager) AnonymizeUserData(userData map[string]interface{}) (*AnonymizedData, error) {
anonymizedID := fmt.Sprintf("anon_%d", time.Now().UnixNano())
// Separate sensitive and non-sensitive data
sensitiveData := make(map[string]interface{})
nonSensitiveData := make(map[string]interface{})
for key, value := range userData {
if drm.isPersonalData(key) {
sensitiveData[key] = value
} else {
nonSensitiveData[key] = value
}
}
// Encrypt non-sensitive data for retention
nonSensitiveJSON, _ := json.Marshal(nonSensitiveData)
encryptedRetainedData, err := drm.encryptionManager.Encrypt(string(nonSensitiveJSON))
if err != nil {
return nil, fmt.Errorf("failed to encrypt retained data: %w", err)
}
// Create anonymized record
anonymized := &AnonymizedData{
OriginalID: fmt.Sprintf("%v", userData["id"]),
AnonymizedID: anonymizedID,
DataType: "user",
AnonymizedAt: time.Now(),
RetainedData: encryptedRetainedData,
}
return anonymized, nil
}
// isPersonalData determines if data is personal information under GDPR
func (drm *DataRetentionManager) isPersonalData(fieldName string) bool {
personalDataFields := []string{
"name", "email", "phone", "address", "birthdate", "gender",
"ip_address", "user_agent", "location", "biometric", "health",
"political", "religious", "sexual", "criminal", "financial",
"education", "employment", "family", "social", "behavioral",
"identifier", "cookie", "tracking", "profile", "preferences",
}
fieldName = strings.ToLower(fieldName)
for _, personal := range personalDataFields {
if strings.Contains(fieldName, personal) {
return true
}
}
return false
}
// ApplyRetentionPolicy applies retention policies to data
func (drm *DataRetentionManager) ApplyRetentionPolicy(dataType string, dataTimestamp time.Time, policy RetentionPolicy) string {
if !policy.Enabled {
return "retain"
}
expiryDate := dataTimestamp.Add(policy.RetentionPeriod)
if time.Now().Before(expiryDate) {
return "retain"
}
return policy.Action
}
// GenerateDataSubjectReport generates a report of all data held about a user
func (drm *DataRetentionManager) GenerateDataSubjectReport(userID string, userData map[string]interface{}) (map[string]interface{}, error) {
report := map[string]interface{}{
"user_id": userID,
"report_generated": time.Now(),
"data_categories": drm.categorizeUserData(userData),
"retention_policies": drm.getApplicablePolicies(userData),
"data_sources": []string{"database", "logs", "analytics"},
}
return report, nil
}
// categorizeUserData categorizes user data by type
func (drm *DataRetentionManager) categorizeUserData(userData map[string]interface{}) map[string][]string {
categories := map[string][]string{
"identity": {},
"contact": {},
"technical": {},
"behavioral": {},
"preferences": {},
}
for key := range userData {
lowerKey := strings.ToLower(key)
switch {
case strings.Contains(lowerKey, "name") || strings.Contains(lowerKey, "id"):
categories["identity"] = append(categories["identity"], key)
case strings.Contains(lowerKey, "email") || strings.Contains(lowerKey, "phone"):
categories["contact"] = append(categories["contact"], key)
case strings.Contains(lowerKey, "ip") || strings.Contains(lowerKey, "agent"):
categories["technical"] = append(categories["technical"], key)
case strings.Contains(lowerKey, "activity") || strings.Contains(lowerKey, "behavior"):
categories["behavioral"] = append(categories["behavioral"], key)
case strings.Contains(lowerKey, "preference") || strings.Contains(lowerKey, "setting"):
categories["preferences"] = append(categories["preferences"], key)
}
}
return categories
}
// getApplicablePolicies returns applicable retention policies
func (drm *DataRetentionManager) getApplicablePolicies(userData map[string]interface{}) []string {
policies := []string{
"user_data_2_years",
"analytics_data_6_months",
"logs_data_90_days",
"deleted_users_30_days",
}
return policies
}
// AuditLogger handles security audit logging
type AuditLogger struct {
encryptionManager *EncryptionManager
}
// NewAuditLogger creates a new audit logger
func NewAuditLogger(encryptionManager *EncryptionManager) *AuditLogger {
return &AuditLogger{
encryptionManager: encryptionManager,
}
}
// AuditEvent represents a security audit event
type AuditEvent struct {
ID string `json:"id"`
Timestamp time.Time `json:"timestamp"`
UserID string `json:"user_id,omitempty"`
Action string `json:"action"`
Resource string `json:"resource"`
Details map[string]interface{} `json:"details"`
IPAddress string `json:"ip_address"`
UserAgent string `json:"user_agent"`
Success bool `json:"success"`
}
// LogAuditEvent logs a security audit event
func (al *AuditLogger) LogAuditEvent(event AuditEvent) error {
event.ID = fmt.Sprintf("audit_%d", time.Now().UnixNano())
event.Timestamp = time.Now()
// Encrypt sensitive details
if event.Details != nil {
encryptedDetails, err := al.encryptionManager.EncryptSensitiveData(event.Details)
if err != nil {
return fmt.Errorf("failed to encrypt audit details: %w", err)
}
event.Details = encryptedDetails
}
// In a real implementation, this would be stored in a secure audit database
// For now, we'll just return success
return nil
}
// LogSecurityEvent logs security-related events
func (al *AuditLogger) LogSecurityEvent(userID, action, resource string, details map[string]interface{}, ipAddress, userAgent string, success bool) error {
event := AuditEvent{
UserID: userID,
Action: action,
Resource: resource,
Details: details,
IPAddress: ipAddress,
UserAgent: userAgent,
Success: success,
}
return al.LogAuditEvent(event)
}
+404
View File
@@ -0,0 +1,404 @@
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
rows, err := s.db.Query(`
SELECT id, name FROM services WHERE project_id = $1
`, scan.ProjectID)
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)
vulnerabilities = append(vulnerabilities, serviceVulns...)
}
return vulnerabilities
}
// simulateDependencyScan simulates scanning for vulnerable dependencies
func (s *Scanner) simulateDependencyScan(serviceID, serviceName 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: "", // Will be filled by caller
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
}