mirror of
https://github.com/Dvorinka/Containr.git
synced 2026-06-03 20:12:58 +00:00
421 lines
12 KiB
Go
421 lines
12 KiB
Go
package security
|
|
|
|
import (
|
|
"containr/internal/database"
|
|
"crypto/aes"
|
|
"crypto/cipher"
|
|
"crypto/rand"
|
|
"crypto/sha256"
|
|
"database/sql"
|
|
"encoding/base64"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/google/uuid"
|
|
)
|
|
|
|
// 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
|
|
db *database.DB
|
|
}
|
|
|
|
// NewAuditLogger creates a new audit logger
|
|
func NewAuditLogger(encryptionManager *EncryptionManager, db *database.DB) *AuditLogger {
|
|
return &AuditLogger{
|
|
encryptionManager: encryptionManager,
|
|
db: db,
|
|
}
|
|
}
|
|
|
|
// AuditEvent represents a security audit event
|
|
type AuditEvent struct {
|
|
ID string `json:"id"`
|
|
Timestamp time.Time `json:"timestamp"`
|
|
UserID string `json:"user_id,omitempty"`
|
|
ResourceID string `json:"resource_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 {
|
|
if al == nil || al.db == nil {
|
|
return nil
|
|
}
|
|
|
|
if strings.TrimSpace(event.ID) == "" {
|
|
event.ID = uuid.New().String()
|
|
}
|
|
if event.Timestamp.IsZero() {
|
|
event.Timestamp = time.Now().UTC()
|
|
}
|
|
|
|
// 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
|
|
}
|
|
|
|
detailsJSON, err := json.Marshal(event.Details)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to marshal audit details: %w", err)
|
|
}
|
|
|
|
var userID sql.NullString
|
|
if id := strings.TrimSpace(event.UserID); id != "" {
|
|
if _, err := uuid.Parse(id); err == nil {
|
|
userID = sql.NullString{String: id, Valid: true}
|
|
}
|
|
}
|
|
|
|
var resourceID sql.NullString
|
|
if id := strings.TrimSpace(event.ResourceID); id != "" {
|
|
if _, err := uuid.Parse(id); err == nil {
|
|
resourceID = sql.NullString{String: id, Valid: true}
|
|
}
|
|
}
|
|
|
|
var ipAddress sql.NullString
|
|
if ip := strings.TrimSpace(event.IPAddress); ip != "" && net.ParseIP(ip) != nil {
|
|
ipAddress = sql.NullString{String: ip, Valid: true}
|
|
}
|
|
|
|
var userAgent sql.NullString
|
|
if ua := strings.TrimSpace(event.UserAgent); ua != "" {
|
|
userAgent = sql.NullString{String: ua, Valid: true}
|
|
}
|
|
|
|
_, err = al.db.Exec(
|
|
`INSERT INTO audit_logs (
|
|
id, user_id, resource, resource_id, action, details, ip_address, user_agent, created_at
|
|
) VALUES ($1, $2, $3, $4, $5, $6::jsonb, $7, $8, $9)`,
|
|
event.ID,
|
|
userID,
|
|
event.Resource,
|
|
resourceID,
|
|
event.Action,
|
|
string(detailsJSON),
|
|
ipAddress,
|
|
userAgent,
|
|
event.Timestamp,
|
|
)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to persist audit event: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// LogSecurityEvent logs security-related events
|
|
func (al *AuditLogger) LogSecurityEvent(userID, resourceID, action, resource string, details map[string]interface{}, ipAddress, userAgent string, success bool) error {
|
|
event := AuditEvent{
|
|
UserID: userID,
|
|
ResourceID: resourceID,
|
|
Action: action,
|
|
Resource: resource,
|
|
Details: details,
|
|
IPAddress: ipAddress,
|
|
UserAgent: userAgent,
|
|
Success: success,
|
|
}
|
|
|
|
return al.LogAuditEvent(event)
|
|
}
|