- Added comprehensive audit logging methods for security events - LogPasswordChange, LogPasswordResetRequest, LogPasswordReset - LogTwoFactorEnabled, LogTwoFactorDisabled, LogTwoFactorVerification - LogAccessDenied, LogRoleChange, LogAccountLocked - LogSecurityEvent for generic security events - Integrated audit logging in password reset handlers - All security events logged with IP, user agent, and metadata
734 lines
19 KiB
Go
734 lines
19 KiB
Go
package services
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"time"
|
|
|
|
"veza-backend-api/internal/database"
|
|
|
|
"github.com/google/uuid"
|
|
"go.uber.org/zap"
|
|
)
|
|
|
|
// AuditService gère les logs d'audit
|
|
type AuditService struct {
|
|
db *database.Database
|
|
logger *zap.Logger
|
|
}
|
|
|
|
// AuditLog représente un log d'audit
|
|
type AuditLog struct {
|
|
ID uuid.UUID `json:"id" db:"id"`
|
|
UserID *uuid.UUID `json:"user_id" db:"user_id"`
|
|
Action string `json:"action" db:"action"`
|
|
Resource string `json:"resource" db:"resource"`
|
|
ResourceID *uuid.UUID `json:"resource_id" db:"resource_id"`
|
|
IPAddress string `json:"ip_address" db:"ip_address"`
|
|
UserAgent string `json:"user_agent" db:"user_agent"`
|
|
Metadata json.RawMessage `json:"metadata" db:"metadata"`
|
|
Timestamp time.Time `json:"timestamp" db:"timestamp"`
|
|
}
|
|
|
|
// AuditLogCreateRequest données pour créer un log d'audit
|
|
type AuditLogCreateRequest struct {
|
|
UserID *uuid.UUID `json:"user_id"`
|
|
Action string `json:"action"`
|
|
Resource string `json:"resource"`
|
|
ResourceID *uuid.UUID `json:"resource_id"`
|
|
IPAddress string `json:"ip_address"`
|
|
UserAgent string `json:"user_agent"`
|
|
Metadata map[string]interface{} `json:"metadata"`
|
|
}
|
|
|
|
// AuditLogSearchRequest paramètres de recherche
|
|
// BE-API-034: Enhanced with better filtering and pagination
|
|
type AuditLogSearchRequest struct {
|
|
UserID *uuid.UUID `json:"user_id"`
|
|
Action string `json:"action"`
|
|
Resource string `json:"resource"`
|
|
ResourceID *uuid.UUID `json:"resource_id"`
|
|
IPAddress string `json:"ip_address"`
|
|
UserAgent string `json:"user_agent"`
|
|
StartDate *time.Time `json:"start_date"`
|
|
EndDate *time.Time `json:"end_date"`
|
|
Limit int `json:"limit"`
|
|
Offset int `json:"offset"`
|
|
Page int `json:"page"` // Alternative to offset (page-based pagination)
|
|
}
|
|
|
|
// AuditStats statistiques d'audit
|
|
type AuditStats struct {
|
|
Action string `json:"action" db:"action"`
|
|
Resource string `json:"resource" db:"resource"`
|
|
ActionCount int64 `json:"action_count" db:"action_count"`
|
|
UniqueUsers int64 `json:"unique_users" db:"unique_users"`
|
|
UniqueIPs int64 `json:"unique_ips" db:"unique_ips"`
|
|
}
|
|
|
|
// SuspiciousActivity activité suspecte détectée
|
|
type SuspiciousActivity struct {
|
|
UserID *uuid.UUID `json:"user_id" db:"user_id"`
|
|
IPAddress string `json:"ip_address" db:"ip_address"`
|
|
ActionCount int64 `json:"action_count" db:"action_count"`
|
|
UniqueActions int64 `json:"unique_actions" db:"unique_actions"`
|
|
RiskScore int `json:"risk_score" db:"risk_score"`
|
|
}
|
|
|
|
// NewAuditService crée un nouveau service d'audit
|
|
func NewAuditService(db *database.Database, logger *zap.Logger) *AuditService {
|
|
return &AuditService{
|
|
db: db,
|
|
logger: logger,
|
|
}
|
|
}
|
|
|
|
// LogAction enregistre une action d'audit
|
|
func (as *AuditService) LogAction(ctx context.Context, req *AuditLogCreateRequest) error {
|
|
// Convertir les métadonnées en JSON
|
|
metadataJSON, err := json.Marshal(req.Metadata)
|
|
if err != nil {
|
|
as.logger.Error("Failed to marshal audit metadata",
|
|
zap.Error(err),
|
|
zap.String("action", req.Action),
|
|
)
|
|
return fmt.Errorf("failed to marshal audit metadata: %w", err)
|
|
}
|
|
|
|
// Insérer le log d'audit
|
|
query := `
|
|
INSERT INTO audit_logs (id, user_id, action, resource, resource_id, ip_address, user_agent, metadata, timestamp)
|
|
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
|
|
`
|
|
|
|
_, err = as.db.ExecContext(ctx, query,
|
|
uuid.New(),
|
|
req.UserID,
|
|
req.Action,
|
|
req.Resource,
|
|
req.ResourceID,
|
|
req.IPAddress,
|
|
req.UserAgent,
|
|
metadataJSON,
|
|
time.Now(),
|
|
)
|
|
|
|
if err != nil {
|
|
as.logger.Error("Failed to log audit action",
|
|
zap.Error(err),
|
|
zap.String("action", req.Action),
|
|
zap.String("resource", req.Resource),
|
|
)
|
|
return fmt.Errorf("failed to log audit action: %w", err)
|
|
}
|
|
|
|
as.logger.Debug("Audit action logged",
|
|
zap.String("action", req.Action),
|
|
zap.String("resource", req.Resource),
|
|
zap.String("user_id", req.UserID.String()),
|
|
)
|
|
|
|
return nil
|
|
}
|
|
|
|
// LogLogin enregistre une tentative de connexion
|
|
func (as *AuditService) LogLogin(ctx context.Context, userID *uuid.UUID, success bool, ipAddress, userAgent string, metadata map[string]interface{}) error {
|
|
action := "login_failed"
|
|
if success {
|
|
action = "login_success"
|
|
}
|
|
|
|
req := &AuditLogCreateRequest{
|
|
UserID: userID,
|
|
Action: action,
|
|
Resource: "user",
|
|
IPAddress: ipAddress,
|
|
UserAgent: userAgent,
|
|
Metadata: metadata,
|
|
}
|
|
|
|
return as.LogAction(ctx, req)
|
|
}
|
|
|
|
// LogLogout enregistre une déconnexion
|
|
func (as *AuditService) LogLogout(ctx context.Context, userID uuid.UUID, ipAddress, userAgent string) error {
|
|
req := &AuditLogCreateRequest{
|
|
UserID: &userID,
|
|
Action: "logout",
|
|
Resource: "user",
|
|
IPAddress: ipAddress,
|
|
UserAgent: userAgent,
|
|
Metadata: map[string]interface{}{},
|
|
}
|
|
|
|
return as.LogAction(ctx, req)
|
|
}
|
|
|
|
// LogUpload enregistre un upload de fichier
|
|
func (as *AuditService) LogUpload(ctx context.Context, userID uuid.UUID, resourceID uuid.UUID, fileName string, fileSize int64, ipAddress, userAgent string) error {
|
|
req := &AuditLogCreateRequest{
|
|
UserID: &userID,
|
|
Action: "upload",
|
|
Resource: "track",
|
|
ResourceID: &resourceID,
|
|
IPAddress: ipAddress,
|
|
UserAgent: userAgent,
|
|
Metadata: map[string]interface{}{
|
|
"file_name": fileName,
|
|
"file_size": fileSize,
|
|
},
|
|
}
|
|
|
|
return as.LogAction(ctx, req)
|
|
}
|
|
|
|
// LogPermissionChange enregistre un changement de permission
|
|
func (as *AuditService) LogPermissionChange(ctx context.Context, userID uuid.UUID, targetUserID uuid.UUID, oldPermissions, newPermissions []string, ipAddress, userAgent string) error {
|
|
req := &AuditLogCreateRequest{
|
|
UserID: &userID,
|
|
Action: "permission_change",
|
|
Resource: "user",
|
|
ResourceID: &targetUserID,
|
|
IPAddress: ipAddress,
|
|
UserAgent: userAgent,
|
|
Metadata: map[string]interface{}{
|
|
"old_permissions": oldPermissions,
|
|
"new_permissions": newPermissions,
|
|
},
|
|
}
|
|
|
|
return as.LogAction(ctx, req)
|
|
}
|
|
|
|
// LogDeletion enregistre une suppression
|
|
func (as *AuditService) LogDeletion(ctx context.Context, userID uuid.UUID, resource string, resourceID uuid.UUID, ipAddress, userAgent string) error {
|
|
req := &AuditLogCreateRequest{
|
|
UserID: &userID,
|
|
Action: "delete",
|
|
Resource: resource,
|
|
ResourceID: &resourceID,
|
|
IPAddress: ipAddress,
|
|
UserAgent: userAgent,
|
|
Metadata: map[string]interface{}{},
|
|
}
|
|
|
|
return as.LogAction(ctx, req)
|
|
}
|
|
|
|
// BE-SEC-013: LogPasswordChange enregistre un changement de mot de passe
|
|
func (as *AuditService) LogPasswordChange(ctx context.Context, userID uuid.UUID, ipAddress, userAgent string) error {
|
|
req := &AuditLogCreateRequest{
|
|
UserID: &userID,
|
|
Action: "password_change",
|
|
Resource: "user",
|
|
IPAddress: ipAddress,
|
|
UserAgent: userAgent,
|
|
Metadata: map[string]interface{}{},
|
|
}
|
|
|
|
return as.LogAction(ctx, req)
|
|
}
|
|
|
|
// BE-SEC-013: LogPasswordResetRequest enregistre une demande de réinitialisation de mot de passe
|
|
func (as *AuditService) LogPasswordResetRequest(ctx context.Context, userID *uuid.UUID, email string, ipAddress, userAgent string) error {
|
|
req := &AuditLogCreateRequest{
|
|
UserID: userID,
|
|
Action: "password_reset_request",
|
|
Resource: "user",
|
|
IPAddress: ipAddress,
|
|
UserAgent: userAgent,
|
|
Metadata: map[string]interface{}{
|
|
"email": email,
|
|
},
|
|
}
|
|
|
|
return as.LogAction(ctx, req)
|
|
}
|
|
|
|
// BE-SEC-013: LogPasswordReset enregistre une réinitialisation de mot de passe
|
|
func (as *AuditService) LogPasswordReset(ctx context.Context, userID uuid.UUID, success bool, ipAddress, userAgent string) error {
|
|
action := "password_reset_failed"
|
|
if success {
|
|
action = "password_reset_success"
|
|
}
|
|
|
|
req := &AuditLogCreateRequest{
|
|
UserID: &userID,
|
|
Action: action,
|
|
Resource: "user",
|
|
IPAddress: ipAddress,
|
|
UserAgent: userAgent,
|
|
Metadata: map[string]interface{}{},
|
|
}
|
|
|
|
return as.LogAction(ctx, req)
|
|
}
|
|
|
|
// BE-SEC-013: LogTwoFactorEnabled enregistre l'activation de 2FA
|
|
func (as *AuditService) LogTwoFactorEnabled(ctx context.Context, userID uuid.UUID, ipAddress, userAgent string) error {
|
|
req := &AuditLogCreateRequest{
|
|
UserID: &userID,
|
|
Action: "2fa_enabled",
|
|
Resource: "user",
|
|
IPAddress: ipAddress,
|
|
UserAgent: userAgent,
|
|
Metadata: map[string]interface{}{},
|
|
}
|
|
|
|
return as.LogAction(ctx, req)
|
|
}
|
|
|
|
// BE-SEC-013: LogTwoFactorDisabled enregistre la désactivation de 2FA
|
|
func (as *AuditService) LogTwoFactorDisabled(ctx context.Context, userID uuid.UUID, ipAddress, userAgent string) error {
|
|
req := &AuditLogCreateRequest{
|
|
UserID: &userID,
|
|
Action: "2fa_disabled",
|
|
Resource: "user",
|
|
IPAddress: ipAddress,
|
|
UserAgent: userAgent,
|
|
Metadata: map[string]interface{}{},
|
|
}
|
|
|
|
return as.LogAction(ctx, req)
|
|
}
|
|
|
|
// BE-SEC-013: LogTwoFactorVerification enregistre une vérification 2FA
|
|
func (as *AuditService) LogTwoFactorVerification(ctx context.Context, userID uuid.UUID, success bool, ipAddress, userAgent string) error {
|
|
action := "2fa_verification_failed"
|
|
if success {
|
|
action = "2fa_verification_success"
|
|
}
|
|
|
|
req := &AuditLogCreateRequest{
|
|
UserID: &userID,
|
|
Action: action,
|
|
Resource: "user",
|
|
IPAddress: ipAddress,
|
|
UserAgent: userAgent,
|
|
Metadata: map[string]interface{}{},
|
|
}
|
|
|
|
return as.LogAction(ctx, req)
|
|
}
|
|
|
|
// BE-SEC-013: LogAccessDenied enregistre un accès refusé
|
|
func (as *AuditService) LogAccessDenied(ctx context.Context, userID *uuid.UUID, resource string, resourceID *uuid.UUID, reason string, ipAddress, userAgent string) error {
|
|
req := &AuditLogCreateRequest{
|
|
UserID: userID,
|
|
Action: "access_denied",
|
|
Resource: resource,
|
|
ResourceID: resourceID,
|
|
IPAddress: ipAddress,
|
|
UserAgent: userAgent,
|
|
Metadata: map[string]interface{}{
|
|
"reason": reason,
|
|
},
|
|
}
|
|
|
|
return as.LogAction(ctx, req)
|
|
}
|
|
|
|
// BE-SEC-013: LogRoleChange enregistre un changement de rôle
|
|
func (as *AuditService) LogRoleChange(ctx context.Context, userID uuid.UUID, targetUserID uuid.UUID, oldRole, newRole string, ipAddress, userAgent string) error {
|
|
req := &AuditLogCreateRequest{
|
|
UserID: &userID,
|
|
Action: "role_change",
|
|
Resource: "user",
|
|
ResourceID: &targetUserID,
|
|
IPAddress: ipAddress,
|
|
UserAgent: userAgent,
|
|
Metadata: map[string]interface{}{
|
|
"old_role": oldRole,
|
|
"new_role": newRole,
|
|
},
|
|
}
|
|
|
|
return as.LogAction(ctx, req)
|
|
}
|
|
|
|
// BE-SEC-013: LogAccountLocked enregistre un verrouillage de compte
|
|
func (as *AuditService) LogAccountLocked(ctx context.Context, email string, reason string, lockedUntil *time.Time, ipAddress, userAgent string) error {
|
|
metadata := map[string]interface{}{
|
|
"email": email,
|
|
"reason": reason,
|
|
}
|
|
if lockedUntil != nil {
|
|
metadata["locked_until"] = lockedUntil.Format(time.RFC3339)
|
|
}
|
|
|
|
req := &AuditLogCreateRequest{
|
|
UserID: nil, // May not have userID if account locked before login
|
|
Action: "account_locked",
|
|
Resource: "user",
|
|
IPAddress: ipAddress,
|
|
UserAgent: userAgent,
|
|
Metadata: metadata,
|
|
}
|
|
|
|
return as.LogAction(ctx, req)
|
|
}
|
|
|
|
// BE-SEC-013: LogSecurityEvent enregistre un événement de sécurité générique
|
|
func (as *AuditService) LogSecurityEvent(ctx context.Context, userID *uuid.UUID, action string, resource string, resourceID *uuid.UUID, details map[string]interface{}, ipAddress, userAgent string) error {
|
|
req := &AuditLogCreateRequest{
|
|
UserID: userID,
|
|
Action: action,
|
|
Resource: resource,
|
|
ResourceID: resourceID,
|
|
IPAddress: ipAddress,
|
|
UserAgent: userAgent,
|
|
Metadata: details,
|
|
}
|
|
|
|
return as.LogAction(ctx, req)
|
|
}
|
|
|
|
// SearchLogs recherche des logs d'audit
|
|
func (as *AuditService) SearchLogs(ctx context.Context, req *AuditLogSearchRequest) ([]*AuditLog, error) {
|
|
// Construire la requête dynamiquement
|
|
query := `
|
|
SELECT id, user_id, action, resource, resource_id, ip_address, user_agent, metadata, timestamp
|
|
FROM audit_logs
|
|
WHERE 1=1
|
|
`
|
|
args := []interface{}{}
|
|
argIndex := 1
|
|
|
|
if req.UserID != nil {
|
|
query += fmt.Sprintf(" AND user_id = $%d", argIndex)
|
|
args = append(args, *req.UserID)
|
|
argIndex++
|
|
}
|
|
|
|
if req.Action != "" {
|
|
query += fmt.Sprintf(" AND action = $%d", argIndex)
|
|
args = append(args, req.Action)
|
|
argIndex++
|
|
}
|
|
|
|
if req.Resource != "" {
|
|
query += fmt.Sprintf(" AND resource = $%d", argIndex)
|
|
args = append(args, req.Resource)
|
|
argIndex++
|
|
}
|
|
|
|
if req.StartDate != nil {
|
|
query += fmt.Sprintf(" AND timestamp >= $%d", argIndex)
|
|
args = append(args, *req.StartDate)
|
|
argIndex++
|
|
}
|
|
|
|
if req.EndDate != nil {
|
|
query += fmt.Sprintf(" AND timestamp <= $%d", argIndex)
|
|
args = append(args, *req.EndDate)
|
|
argIndex++
|
|
}
|
|
|
|
query += " ORDER BY timestamp DESC"
|
|
|
|
if req.Limit > 0 {
|
|
query += fmt.Sprintf(" LIMIT $%d", argIndex)
|
|
args = append(args, req.Limit)
|
|
argIndex++
|
|
}
|
|
|
|
if req.Offset > 0 {
|
|
query += fmt.Sprintf(" OFFSET $%d", argIndex)
|
|
args = append(args, req.Offset)
|
|
}
|
|
|
|
rows, err := as.db.QueryContext(ctx, query, args...)
|
|
if err != nil {
|
|
as.logger.Error("Failed to search audit logs",
|
|
zap.Error(err),
|
|
)
|
|
return nil, fmt.Errorf("failed to search audit logs: %w", err)
|
|
}
|
|
defer rows.Close()
|
|
|
|
var logs []*AuditLog
|
|
for rows.Next() {
|
|
var log AuditLog
|
|
err := rows.Scan(
|
|
&log.ID,
|
|
&log.UserID,
|
|
&log.Action,
|
|
&log.Resource,
|
|
&log.ResourceID,
|
|
&log.IPAddress,
|
|
&log.UserAgent,
|
|
&log.Metadata,
|
|
&log.Timestamp,
|
|
)
|
|
if err != nil {
|
|
as.logger.Error("Failed to scan audit log",
|
|
zap.Error(err),
|
|
)
|
|
continue
|
|
}
|
|
logs = append(logs, &log)
|
|
}
|
|
|
|
return logs, nil
|
|
}
|
|
|
|
// CountLogs compte le nombre total de logs correspondant aux critères de recherche
|
|
// BE-API-034: Added total count for pagination
|
|
func (as *AuditService) CountLogs(ctx context.Context, req *AuditLogSearchRequest) (int64, error) {
|
|
query := `
|
|
SELECT COUNT(*)
|
|
FROM audit_logs
|
|
WHERE 1=1
|
|
`
|
|
args := []interface{}{}
|
|
argIndex := 1
|
|
|
|
if req.UserID != nil {
|
|
query += fmt.Sprintf(" AND user_id = $%d", argIndex)
|
|
args = append(args, *req.UserID)
|
|
argIndex++
|
|
}
|
|
|
|
if req.Action != "" {
|
|
query += fmt.Sprintf(" AND action = $%d", argIndex)
|
|
args = append(args, req.Action)
|
|
argIndex++
|
|
}
|
|
|
|
if req.Resource != "" {
|
|
query += fmt.Sprintf(" AND resource = $%d", argIndex)
|
|
args = append(args, req.Resource)
|
|
argIndex++
|
|
}
|
|
|
|
if req.ResourceID != nil {
|
|
query += fmt.Sprintf(" AND resource_id = $%d", argIndex)
|
|
args = append(args, *req.ResourceID)
|
|
argIndex++
|
|
}
|
|
|
|
if req.IPAddress != "" {
|
|
query += fmt.Sprintf(" AND ip_address = $%d", argIndex)
|
|
args = append(args, req.IPAddress)
|
|
argIndex++
|
|
}
|
|
|
|
if req.UserAgent != "" {
|
|
query += fmt.Sprintf(" AND user_agent LIKE $%d", argIndex)
|
|
args = append(args, "%"+req.UserAgent+"%")
|
|
argIndex++
|
|
}
|
|
|
|
if req.StartDate != nil {
|
|
query += fmt.Sprintf(" AND timestamp >= $%d", argIndex)
|
|
args = append(args, *req.StartDate)
|
|
argIndex++
|
|
}
|
|
|
|
if req.EndDate != nil {
|
|
query += fmt.Sprintf(" AND timestamp <= $%d", argIndex)
|
|
args = append(args, *req.EndDate)
|
|
argIndex++
|
|
}
|
|
|
|
var count int64
|
|
err := as.db.QueryRowContext(ctx, query, args...).Scan(&count)
|
|
if err != nil {
|
|
as.logger.Error("Failed to count audit logs",
|
|
zap.Error(err),
|
|
)
|
|
return 0, fmt.Errorf("failed to count audit logs: %w", err)
|
|
}
|
|
|
|
return count, nil
|
|
}
|
|
|
|
// GetStats récupère les statistiques d'audit
|
|
func (as *AuditService) GetStats(ctx context.Context, startDate, endDate time.Time) ([]*AuditStats, error) {
|
|
query := `
|
|
SELECT action, resource, COUNT(*) as action_count,
|
|
COUNT(DISTINCT user_id) as unique_users,
|
|
COUNT(DISTINCT ip_address) as unique_ips
|
|
FROM audit_logs
|
|
WHERE timestamp BETWEEN $1 AND $2
|
|
GROUP BY action, resource
|
|
ORDER BY action_count DESC
|
|
`
|
|
|
|
rows, err := as.db.QueryContext(ctx, query, startDate, endDate)
|
|
if err != nil {
|
|
as.logger.Error("Failed to get audit stats",
|
|
zap.Error(err),
|
|
)
|
|
return nil, fmt.Errorf("failed to get audit stats: %w", err)
|
|
}
|
|
defer rows.Close()
|
|
|
|
var stats []*AuditStats
|
|
for rows.Next() {
|
|
var stat AuditStats
|
|
err := rows.Scan(
|
|
&stat.Action,
|
|
&stat.Resource,
|
|
&stat.ActionCount,
|
|
&stat.UniqueUsers,
|
|
&stat.UniqueIPs,
|
|
)
|
|
if err != nil {
|
|
as.logger.Error("Failed to scan audit stat",
|
|
zap.Error(err),
|
|
)
|
|
continue
|
|
}
|
|
stats = append(stats, &stat)
|
|
}
|
|
|
|
return stats, nil
|
|
}
|
|
|
|
// DetectSuspiciousActivity détecte les activités suspectes
|
|
func (as *AuditService) DetectSuspiciousActivity(ctx context.Context, hours int) ([]*SuspiciousActivity, error) {
|
|
query := `
|
|
WITH user_activity AS (
|
|
SELECT
|
|
user_id,
|
|
ip_address,
|
|
COUNT(*) as action_count,
|
|
COUNT(DISTINCT action) as unique_actions
|
|
FROM audit_logs
|
|
WHERE timestamp >= NOW() - INTERVAL '%d hours'
|
|
GROUP BY user_id, ip_address
|
|
)
|
|
SELECT
|
|
user_id,
|
|
ip_address,
|
|
action_count,
|
|
unique_actions,
|
|
CASE
|
|
WHEN action_count > 1000 THEN 100
|
|
WHEN action_count > 500 THEN 80
|
|
WHEN action_count > 100 THEN 60
|
|
WHEN action_count > 50 THEN 40
|
|
WHEN action_count > 20 THEN 20
|
|
ELSE 0
|
|
END as risk_score
|
|
FROM user_activity
|
|
WHERE action_count > 20
|
|
ORDER BY risk_score DESC, action_count DESC
|
|
`
|
|
|
|
rows, err := as.db.QueryContext(ctx, fmt.Sprintf(query, hours))
|
|
if err != nil {
|
|
as.logger.Error("Failed to detect suspicious activity",
|
|
zap.Error(err),
|
|
)
|
|
return nil, fmt.Errorf("failed to detect suspicious activity: %w", err)
|
|
}
|
|
defer rows.Close()
|
|
|
|
var activities []*SuspiciousActivity
|
|
for rows.Next() {
|
|
var activity SuspiciousActivity
|
|
err := rows.Scan(
|
|
&activity.UserID,
|
|
&activity.IPAddress,
|
|
&activity.ActionCount,
|
|
&activity.UniqueActions,
|
|
&activity.RiskScore,
|
|
)
|
|
if err != nil {
|
|
as.logger.Error("Failed to scan suspicious activity",
|
|
zap.Error(err),
|
|
)
|
|
continue
|
|
}
|
|
activities = append(activities, &activity)
|
|
}
|
|
|
|
return activities, nil
|
|
}
|
|
|
|
// CleanupOldLogs nettoie les anciens logs d'audit
|
|
func (as *AuditService) CleanupOldLogs(ctx context.Context, retentionDays int) (int64, error) {
|
|
query := `
|
|
DELETE FROM audit_logs
|
|
WHERE timestamp < NOW() - INTERVAL '%d days'
|
|
`
|
|
|
|
result, err := as.db.ExecContext(ctx, fmt.Sprintf(query, retentionDays))
|
|
if err != nil {
|
|
as.logger.Error("Failed to cleanup old audit logs",
|
|
zap.Error(err),
|
|
)
|
|
return 0, fmt.Errorf("failed to cleanup old audit logs: %w", err)
|
|
}
|
|
|
|
rowsAffected, err := result.RowsAffected()
|
|
if err != nil {
|
|
return 0, fmt.Errorf("failed to get rows affected: %w", err)
|
|
}
|
|
|
|
as.logger.Info("Old audit logs cleaned up",
|
|
zap.Int64("deleted_count", rowsAffected),
|
|
zap.Int("retention_days", retentionDays),
|
|
)
|
|
|
|
return rowsAffected, nil
|
|
}
|
|
|
|
// GetUserActivity récupère l'activité d'un utilisateur
|
|
func (as *AuditService) GetUserActivity(ctx context.Context, userID uuid.UUID, limit int) ([]*AuditLog, error) {
|
|
req := &AuditLogSearchRequest{
|
|
UserID: &userID,
|
|
Limit: limit,
|
|
}
|
|
|
|
return as.SearchLogs(ctx, req)
|
|
}
|
|
|
|
// GetIPActivity récupère l'activité d'une IP
|
|
func (as *AuditService) GetIPActivity(ctx context.Context, ipAddress string, limit int) ([]*AuditLog, error) {
|
|
query := `
|
|
SELECT id, user_id, action, resource, resource_id, ip_address, user_agent, metadata, timestamp
|
|
FROM audit_logs
|
|
WHERE ip_address = $1
|
|
ORDER BY timestamp DESC
|
|
LIMIT $2
|
|
`
|
|
|
|
rows, err := as.db.QueryContext(ctx, query, ipAddress, limit)
|
|
if err != nil {
|
|
as.logger.Error("Failed to get IP activity",
|
|
zap.Error(err),
|
|
zap.String("ip_address", ipAddress),
|
|
)
|
|
return nil, fmt.Errorf("failed to get IP activity: %w", err)
|
|
}
|
|
defer rows.Close()
|
|
|
|
var logs []*AuditLog
|
|
for rows.Next() {
|
|
var log AuditLog
|
|
err := rows.Scan(
|
|
&log.ID,
|
|
&log.UserID,
|
|
&log.Action,
|
|
&log.Resource,
|
|
&log.ResourceID,
|
|
&log.IPAddress,
|
|
&log.UserAgent,
|
|
&log.Metadata,
|
|
&log.Timestamp,
|
|
)
|
|
if err != nil {
|
|
as.logger.Error("Failed to scan audit log",
|
|
zap.Error(err),
|
|
)
|
|
continue
|
|
}
|
|
logs = append(logs, &log)
|
|
}
|
|
|
|
return logs, nil
|
|
}
|