Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions src/Data/Models/AuditLog.cs
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,12 @@ public enum AuditActionType
// Swap Operations
SwapOut,
SwapIn,
SwapOutInitiated,
SwapOutCompleted,

// Wallet Sweep Operations
WalletSweep,
NodeWalletAssigned,

// Node Operations
AddNode,
Expand Down
18 changes: 18 additions & 0 deletions src/Helpers/Constants.cs
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,18 @@ public class Constants
/// </summary>
public static readonly int AUTO_LIQUIDITY_MANAGEMENT_INTERVAL_MINUTES = 10;

/// <summary>
/// The number of days to retain audit log entries before automatic cleanup.
/// Default is 180 days. Can be configured via AUDIT_LOG_RETENTION_DAYS environment variable.
/// </summary>
public static readonly int AUDIT_LOG_RETENTION_DAYS = 180;

/// <summary>
/// Cron expression for the audit log cleanup job. Default runs daily at 3:00 AM.
/// Can be configured via AUDIT_LOG_CLEANUP_CRON environment variable.
/// </summary>
public static readonly string AUDIT_LOG_CLEANUP_CRON = "0 0 3 * * ?";

// Observability
public static readonly string? OTEL_EXPORTER_ENDPOINT;

Expand Down Expand Up @@ -244,6 +256,12 @@ static Constants()
var autoLiquidityManagementIntervalMinutes = Environment.GetEnvironmentVariable("AUTO_LIQUIDITY_MANAGEMENT_INTERVAL_MINUTES");
if (autoLiquidityManagementIntervalMinutes != null) AUTO_LIQUIDITY_MANAGEMENT_INTERVAL_MINUTES = int.Parse(autoLiquidityManagementIntervalMinutes);

// Audit Log
var auditLogRetentionDays = Environment.GetEnvironmentVariable("AUDIT_LOG_RETENTION_DAYS");
if (auditLogRetentionDays != null) AUDIT_LOG_RETENTION_DAYS = int.Parse(auditLogRetentionDays);

AUDIT_LOG_CLEANUP_CRON = Environment.GetEnvironmentVariable("AUDIT_LOG_CLEANUP_CRON") ?? AUDIT_LOG_CLEANUP_CRON;

// Observability
//We need to expand the env-var with %ENV_VAR% for K8S
var otelCollectorEndpointToBeExpanded = Environment.GetEnvironmentVariable("OTEL_EXPORTER_OTLP_ENDPOINT");
Expand Down
62 changes: 62 additions & 0 deletions src/Jobs/AuditLogCleanupJob.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
/*
* NodeGuard
* Copyright (C) 2023 Elenpay
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see http://www.gnu.org/licenses/.
*
*/

using NodeGuard.Data.Repositories.Interfaces;
using Quartz;

namespace NodeGuard.Jobs;

/// <summary>
/// Job that cleans up old audit log entries based on the configured retention policy
/// </summary>
[DisallowConcurrentExecution]
public class AuditLogCleanupJob : IJob
{
private readonly IAuditLogRepository _auditLogRepository;
private readonly ILogger<AuditLogCleanupJob> _logger;

public AuditLogCleanupJob(IAuditLogRepository auditLogRepository, ILogger<AuditLogCleanupJob> logger)
{
_auditLogRepository = auditLogRepository;
_logger = logger;
}

public async Task Execute(IJobExecutionContext context)
{
_logger.LogInformation("Starting audit log cleanup job");

try
{
var retentionDays = Constants.AUDIT_LOG_RETENTION_DAYS;
var cutoffDate = DateTimeOffset.UtcNow.AddDays(-retentionDays);

_logger.LogInformation("Deleting audit logs older than {CutoffDate} (retention: {RetentionDays} days)",
cutoffDate, retentionDays);

var deletedCount = await _auditLogRepository.DeleteOlderThanAsync(cutoffDate);

_logger.LogInformation("Audit log cleanup completed. Deleted {DeletedCount} entries", deletedCount);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error during audit log cleanup");
throw;
}
}
}
24 changes: 23 additions & 1 deletion src/Jobs/AutoLiquidityManagementJob.cs
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ public class AutoLiquidityManagementJob : IJob
private readonly ILightningService _lightningService;
private readonly IWalletRepository _walletRepository;
private readonly INBXplorerService _nbXplorerService;
private readonly IAuditService _auditService;

public AutoLiquidityManagementJob(
ILogger<AutoLiquidityManagementJob> logger,
Expand All @@ -62,7 +63,8 @@ public AutoLiquidityManagementJob(
IFortySwapService fortySwapService,
ILightningService lightningService,
IWalletRepository walletRepository,
INBXplorerService nbXplorerService)
INBXplorerService nbXplorerService,
IAuditService auditService)
{
_logger = logger;
_nodeRepository = nodeRepository;
Expand All @@ -72,6 +74,7 @@ public AutoLiquidityManagementJob(
_lightningService = lightningService;
_walletRepository = walletRepository;
_nbXplorerService = nbXplorerService;
_auditService = auditService;
}

public async Task Execute(IJobExecutionContext context)
Expand Down Expand Up @@ -290,6 +293,25 @@ public async Task<ManageNodeLiquidityResult> ManageNodeLiquidity(Node node, Canc

_logger.LogInformation("Successfully initiated swap out {SwapId} for node {NodeName}",
swapOut.ProviderId, node.Name);

// Audit successful swap out initiation
await _auditService.LogSystemAsync(
AuditActionType.SwapOutInitiated,
AuditEventType.Success,
AuditObjectType.SwapOut,
swapOut.ProviderId,
new
{
NodeId = node.Id,
NodeName = node.Name,
Provider = selectedProvider.ToString(),
AmountSats = swapAmount,
DestinationAddress = destinationAddress,
DestinationWalletId = node.FundsDestinationWalletId,
ProviderId = swapResponse.Id,
IsAutomatic = true
});

return ManageNodeLiquidityResult.Success;
}
catch (Exception ex)
Expand Down
31 changes: 30 additions & 1 deletion src/Jobs/MonitorSwapsJob.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,16 @@ public class MonitorSwapsJob : IJob
private readonly INodeRepository _nodeRepository;
private readonly ISwapOutRepository _swapOutRepository;
private readonly ISwapsService _swapsService;
private readonly IAuditService _auditService;

public MonitorSwapsJob(ILogger<MonitorSwapsJob> logger, ISchedulerFactory schedulerFactory, INodeRepository nodeRepository, ISwapOutRepository swapOutRepository, ISwapsService swapsService)
public MonitorSwapsJob(ILogger<MonitorSwapsJob> logger, ISchedulerFactory schedulerFactory, INodeRepository nodeRepository, ISwapOutRepository swapOutRepository, ISwapsService swapsService, IAuditService auditService)
{
_logger = logger;
_schedulerFactory = schedulerFactory;
_nodeRepository = nodeRepository;
_swapOutRepository = swapOutRepository;
_swapsService = swapsService;
_auditService = auditService;
}

private void CleanUp(SwapOut swap, string errorMessage)
Expand Down Expand Up @@ -67,6 +69,8 @@ public async Task Execute(IJobExecutionContext context)

if (response.Status != swap.Status)
{
var oldStatus = swap.Status;

if (response.Status == SwapOutStatus.Failed)
{
_logger.LogWarning("Swap {SwapId} status changed from {OldStatus} to {NewStatus}. Error: {ErrorMessage}",
Expand All @@ -89,6 +93,31 @@ public async Task Execute(IJobExecutionContext context)
_logger.LogError("Error updating swap {SwapId}: {Error}", swap.Id, error);
continue;
}

// Audit swap completion (only for successful completions)
if (response.Status == SwapOutStatus.Completed)
{
await _auditService.LogSystemAsync(
AuditActionType.SwapOutCompleted,
AuditEventType.Success,
AuditObjectType.SwapOut,
swap.ProviderId,
new
{
SwapId = swap.Id,
NodeId = swap.NodeId,
Provider = swap.Provider.ToString(),
ProviderId = swap.ProviderId,
AmountSats = swap.SatsAmount,
TotalFeeSats = swap.TotalFees.Satoshi,
ServiceFeeSats = swap.ServiceFeeSats,
LightningFeeSats = swap.LightningFeeSats,
OnChainFeeSats = swap.OnChainFeeSats,
IsManual = swap.IsManual,
OldStatus = oldStatus.ToString(),
NewStatus = response.Status.ToString()
});
}
}
}
}
Expand Down
38 changes: 37 additions & 1 deletion src/Jobs/SweepNodeWalletsJob.cs
Original file line number Diff line number Diff line change
Expand Up @@ -37,19 +37,22 @@ public class SweepNodeWalletsJob : IJob
private readonly INodeRepository _nodeRepository;
private readonly IWalletRepository _walletRepository;
private readonly INBXplorerService _nbXplorerService;
private readonly IAuditService _auditService;

public SweepNodeWalletsJob(ILogger<SweepNodeWalletsJob> logger,
INodeRepository nodeRepository,
IWalletRepository walletRepository,
INBXplorerService nbXplorerService,
ILightningClientService lightningClientService)
ILightningClientService lightningClientService,
IAuditService auditService)
{

_logger = logger;
_nodeRepository = nodeRepository;
_walletRepository = walletRepository;
_nbXplorerService = nbXplorerService;
_lightningClientService = lightningClientService;
_auditService = auditService;
}

public async Task Execute(IJobExecutionContext context)
Expand Down Expand Up @@ -119,6 +122,23 @@ async Task SweepFunds(Node node, Wallet wallet, Lightning.LightningClient lightn
sendManyResponse.Txid,
returningAddress.Address);

// Audit successful wallet sweep
await _auditService.LogSystemAsync(
AuditActionType.WalletSweep,
AuditEventType.Success,
AuditObjectType.Wallet,
wallet.Id.ToString(),
new
{
NodeId = node.Id,
NodeName = node.Name,
WalletId = wallet.Id,
WalletName = wallet.Name,
AmountSats = sweepedFundsAmount,
TxId = sendManyResponse.Txid,
ReturnAddress = returningAddress.Address.ToString()
});

//TODO We need to store the txid somewhere to monitor it..
}
else
Expand Down Expand Up @@ -193,6 +213,22 @@ async Task SweepFunds(Node node, Wallet wallet, Lightning.LightningClient lightn
"Error while adding returning node wallet with id: {WalletId} to node: {NodeName}",
wallet.Id, node.Name);
}
else
{
// Audit successful wallet assignment
await _auditService.LogSystemAsync(
AuditActionType.NodeWalletAssigned,
AuditEventType.Success,
AuditObjectType.Node,
node.Id.ToString(),
new
{
NodeId = node.Id,
NodeName = node.Name,
WalletId = wallet.Id,
WalletName = wallet.Name
});
}
}
else
{
Expand Down
17 changes: 17 additions & 0 deletions src/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -332,6 +332,23 @@ public static async Task Main(string[] args)
}
});
});

// Audit Log Cleanup Job
q.AddJob<AuditLogCleanupJob>(opts =>
{
opts.DisallowConcurrentExecution();
opts.WithIdentity(nameof(AuditLogCleanupJob));
});

q.AddTrigger(opts =>
{
opts.ForJob(nameof(AuditLogCleanupJob))
.WithIdentity($"{nameof(AuditLogCleanupJob)}Trigger")
.StartNow().WithSimpleSchedule(scheduleBuilder =>
{
scheduleBuilder.WithIntervalInHours(24).RepeatForever();
});
});
});

// ASP.NET Core hosting
Expand Down
Loading
Loading