mirror of
https://github.com/bitwarden/server.git
synced 2025-05-21 03:24:31 -05:00
Feature/phishing detection cronjob (#5512)
* Added caching to EF implementation. Added error handling and logging * Refactored update method to use sqlbulkcopy instead of performing a round trip for each new insert * Initial implementation for quartz job to get list of phishing domains * Updated phishing domain settings to be its own interface * Add phishing domain detection with checksum-based updates
This commit is contained in:
parent
6e0df19ae4
commit
370a69a86f
@ -14,4 +14,11 @@ public class PhishingDomainsController(IPhishingDomainRepository phishingDomainR
|
|||||||
var domains = await phishingDomainRepository.GetActivePhishingDomainsAsync();
|
var domains = await phishingDomainRepository.GetActivePhishingDomainsAsync();
|
||||||
return Ok(domains);
|
return Ok(domains);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[HttpGet("checksum")]
|
||||||
|
public async Task<ActionResult<string>> GetChecksumAsync()
|
||||||
|
{
|
||||||
|
var checksum = await phishingDomainRepository.GetCurrentChecksumAsync();
|
||||||
|
return Ok(checksum);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -58,6 +58,13 @@ public class JobsHostedService : BaseJobsHostedService
|
|||||||
.StartNow()
|
.StartNow()
|
||||||
.WithCronSchedule("0 0 * * * ?")
|
.WithCronSchedule("0 0 * * * ?")
|
||||||
.Build();
|
.Build();
|
||||||
|
var updatePhishingDomainsTrigger = TriggerBuilder.Create()
|
||||||
|
.WithIdentity("UpdatePhishingDomainsTrigger")
|
||||||
|
.StartNow()
|
||||||
|
.WithSimpleSchedule(x => x
|
||||||
|
.WithIntervalInHours(24)
|
||||||
|
.RepeatForever())
|
||||||
|
.Build();
|
||||||
|
|
||||||
|
|
||||||
var jobs = new List<Tuple<Type, ITrigger>>
|
var jobs = new List<Tuple<Type, ITrigger>>
|
||||||
@ -68,6 +75,7 @@ public class JobsHostedService : BaseJobsHostedService
|
|||||||
new Tuple<Type, ITrigger>(typeof(ValidateUsersJob), everyTopOfTheSixthHourTrigger),
|
new Tuple<Type, ITrigger>(typeof(ValidateUsersJob), everyTopOfTheSixthHourTrigger),
|
||||||
new Tuple<Type, ITrigger>(typeof(ValidateOrganizationsJob), everyTwelfthHourAndThirtyMinutesTrigger),
|
new Tuple<Type, ITrigger>(typeof(ValidateOrganizationsJob), everyTwelfthHourAndThirtyMinutesTrigger),
|
||||||
new Tuple<Type, ITrigger>(typeof(ValidateOrganizationDomainJob), validateOrganizationDomainTrigger),
|
new Tuple<Type, ITrigger>(typeof(ValidateOrganizationDomainJob), validateOrganizationDomainTrigger),
|
||||||
|
new Tuple<Type, ITrigger>(typeof(UpdatePhishingDomainsJob), updatePhishingDomainsTrigger),
|
||||||
};
|
};
|
||||||
|
|
||||||
if (_globalSettings.SelfHosted && _globalSettings.EnableCloudCommunication)
|
if (_globalSettings.SelfHosted && _globalSettings.EnableCloudCommunication)
|
||||||
@ -96,6 +104,7 @@ public class JobsHostedService : BaseJobsHostedService
|
|||||||
services.AddTransient<ValidateUsersJob>();
|
services.AddTransient<ValidateUsersJob>();
|
||||||
services.AddTransient<ValidateOrganizationsJob>();
|
services.AddTransient<ValidateOrganizationsJob>();
|
||||||
services.AddTransient<ValidateOrganizationDomainJob>();
|
services.AddTransient<ValidateOrganizationDomainJob>();
|
||||||
|
services.AddTransient<UpdatePhishingDomainsJob>();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void AddCommercialSecretsManagerJobServices(IServiceCollection services)
|
public static void AddCommercialSecretsManagerJobServices(IServiceCollection services)
|
||||||
|
87
src/Api/Jobs/UpdatePhishingDomainsJob.cs
Normal file
87
src/Api/Jobs/UpdatePhishingDomainsJob.cs
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
using Bit.Core;
|
||||||
|
using Bit.Core.Jobs;
|
||||||
|
using Bit.Core.PhishingDomainFeatures.Interfaces;
|
||||||
|
using Bit.Core.Repositories;
|
||||||
|
using Bit.Core.Settings;
|
||||||
|
using Quartz;
|
||||||
|
|
||||||
|
namespace Bit.Api.Jobs;
|
||||||
|
|
||||||
|
public class UpdatePhishingDomainsJob : BaseJob
|
||||||
|
{
|
||||||
|
private readonly GlobalSettings _globalSettings;
|
||||||
|
private readonly IPhishingDomainRepository _phishingDomainRepository;
|
||||||
|
private readonly ICloudPhishingDomainQuery _cloudPhishingDomainQuery;
|
||||||
|
|
||||||
|
public UpdatePhishingDomainsJob(
|
||||||
|
GlobalSettings globalSettings,
|
||||||
|
IPhishingDomainRepository phishingDomainRepository,
|
||||||
|
ICloudPhishingDomainQuery cloudPhishingDomainQuery,
|
||||||
|
ILogger<UpdatePhishingDomainsJob> logger)
|
||||||
|
: base(logger)
|
||||||
|
{
|
||||||
|
_globalSettings = globalSettings;
|
||||||
|
_phishingDomainRepository = phishingDomainRepository;
|
||||||
|
_cloudPhishingDomainQuery = cloudPhishingDomainQuery;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override async Task ExecuteJobAsync(IJobExecutionContext context)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(_globalSettings.PhishingDomain?.UpdateUrl))
|
||||||
|
{
|
||||||
|
_logger.LogInformation(Constants.BypassFiltersEventId, "Skipping phishing domain update. No URL configured.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_globalSettings.SelfHosted && !_globalSettings.EnableCloudCommunication)
|
||||||
|
{
|
||||||
|
_logger.LogInformation(Constants.BypassFiltersEventId, "Skipping phishing domain update. Cloud communication is disabled in global settings.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the remote checksum
|
||||||
|
var remoteChecksum = await _cloudPhishingDomainQuery.GetRemoteChecksumAsync();
|
||||||
|
if (string.IsNullOrWhiteSpace(remoteChecksum))
|
||||||
|
{
|
||||||
|
_logger.LogWarning(Constants.BypassFiltersEventId, "Could not retrieve remote checksum. Skipping update.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the current checksum from the database
|
||||||
|
var currentChecksum = await _phishingDomainRepository.GetCurrentChecksumAsync();
|
||||||
|
|
||||||
|
// Compare checksums to determine if update is needed
|
||||||
|
if (string.Equals(currentChecksum, remoteChecksum, StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
_logger.LogInformation(Constants.BypassFiltersEventId,
|
||||||
|
"Phishing domains list is up to date (checksum: {Checksum}). Skipping update.",
|
||||||
|
currentChecksum);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_logger.LogInformation(Constants.BypassFiltersEventId,
|
||||||
|
"Checksums differ (current: {CurrentChecksum}, remote: {RemoteChecksum}). Fetching updated domains from {Source}.",
|
||||||
|
currentChecksum, remoteChecksum, _globalSettings.SelfHosted ? "Bitwarden cloud API" : "external source");
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var domains = await _cloudPhishingDomainQuery.GetPhishingDomainsAsync();
|
||||||
|
|
||||||
|
if (domains.Count > 0)
|
||||||
|
{
|
||||||
|
_logger.LogInformation(Constants.BypassFiltersEventId, "Updating {Count} phishing domains with checksum {Checksum}.",
|
||||||
|
domains.Count, remoteChecksum);
|
||||||
|
await _phishingDomainRepository.UpdatePhishingDomainsAsync(domains, remoteChecksum);
|
||||||
|
_logger.LogInformation(Constants.BypassFiltersEventId, "Successfully updated phishing domains.");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_logger.LogWarning(Constants.BypassFiltersEventId, "No valid domains found in the response. Skipping update.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(Constants.BypassFiltersEventId, ex, "Error updating phishing domains.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -177,6 +177,7 @@ public class Startup
|
|||||||
services.AddBillingOperations();
|
services.AddBillingOperations();
|
||||||
services.AddReportingServices();
|
services.AddReportingServices();
|
||||||
services.AddImportServices();
|
services.AddImportServices();
|
||||||
|
services.AddPhishingDomainServices(globalSettings);
|
||||||
|
|
||||||
// Authorization Handlers
|
// Authorization Handlers
|
||||||
services.AddAuthorizationHandlers();
|
services.AddAuthorizationHandlers();
|
||||||
|
@ -2,6 +2,8 @@
|
|||||||
using Bit.Api.Vault.AuthorizationHandlers.Collections;
|
using Bit.Api.Vault.AuthorizationHandlers.Collections;
|
||||||
using Bit.Core.AdminConsole.OrganizationFeatures.Groups.Authorization;
|
using Bit.Core.AdminConsole.OrganizationFeatures.Groups.Authorization;
|
||||||
using Bit.Core.IdentityServer;
|
using Bit.Core.IdentityServer;
|
||||||
|
using Bit.Core.PhishingDomainFeatures;
|
||||||
|
using Bit.Core.PhishingDomainFeatures.Interfaces;
|
||||||
using Bit.Core.Settings;
|
using Bit.Core.Settings;
|
||||||
using Bit.Core.Utilities;
|
using Bit.Core.Utilities;
|
||||||
using Bit.Core.Vault.Authorization.SecurityTasks;
|
using Bit.Core.Vault.Authorization.SecurityTasks;
|
||||||
@ -106,4 +108,22 @@ public static class ServiceCollectionExtensions
|
|||||||
services.AddScoped<IAuthorizationHandler, SecurityTaskAuthorizationHandler>();
|
services.AddScoped<IAuthorizationHandler, SecurityTaskAuthorizationHandler>();
|
||||||
services.AddScoped<IAuthorizationHandler, SecurityTaskOrganizationAuthorizationHandler>();
|
services.AddScoped<IAuthorizationHandler, SecurityTaskOrganizationAuthorizationHandler>();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static void AddPhishingDomainServices(this IServiceCollection services, GlobalSettings globalSettings)
|
||||||
|
{
|
||||||
|
services.AddHttpClient("PhishingDomains", client =>
|
||||||
|
{
|
||||||
|
client.DefaultRequestHeaders.Add("User-Agent", globalSettings.SelfHosted ? "Bitwarden Self-Hosted" : "Bitwarden");
|
||||||
|
client.Timeout = TimeSpan.FromSeconds(30);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (globalSettings.SelfHosted)
|
||||||
|
{
|
||||||
|
services.AddScoped<ICloudPhishingDomainQuery, CloudPhishingDomainRelayQuery>();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
services.AddScoped<ICloudPhishingDomainQuery, CloudPhishingDomainDirectQuery>();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -37,6 +37,10 @@
|
|||||||
},
|
},
|
||||||
"storage": {
|
"storage": {
|
||||||
"connectionString": "UseDevelopmentStorage=true"
|
"connectionString": "UseDevelopmentStorage=true"
|
||||||
|
},
|
||||||
|
"phishingDomain": {
|
||||||
|
"updateUrl": "https://phish.co.za/latest/phishing-domains-ACTIVE.txt",
|
||||||
|
"checksumUrl": "https://raw.githubusercontent.com/Phishing-Database/checksums/refs/heads/master/phishing-domains-ACTIVE.txt.sha256"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -71,6 +71,9 @@
|
|||||||
"accessKeySecret": "SECRET",
|
"accessKeySecret": "SECRET",
|
||||||
"region": "SECRET"
|
"region": "SECRET"
|
||||||
},
|
},
|
||||||
|
"phishingDomain": {
|
||||||
|
"updateUrl": "SECRET"
|
||||||
|
},
|
||||||
"distributedIpRateLimiting": {
|
"distributedIpRateLimiting": {
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"maxRedisTimeoutsThreshold": 10,
|
"maxRedisTimeoutsThreshold": 10,
|
||||||
|
@ -0,0 +1,104 @@
|
|||||||
|
using Bit.Core.PhishingDomainFeatures.Interfaces;
|
||||||
|
using Bit.Core.Settings;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
|
namespace Bit.Core.PhishingDomainFeatures;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Implementation of ICloudPhishingDomainQuery for cloud environments
|
||||||
|
/// that directly calls the external phishing domain source
|
||||||
|
/// </summary>
|
||||||
|
public class CloudPhishingDomainDirectQuery : ICloudPhishingDomainQuery
|
||||||
|
{
|
||||||
|
private readonly IGlobalSettings _globalSettings;
|
||||||
|
private readonly IHttpClientFactory _httpClientFactory;
|
||||||
|
private readonly ILogger<CloudPhishingDomainDirectQuery> _logger;
|
||||||
|
|
||||||
|
public CloudPhishingDomainDirectQuery(
|
||||||
|
IGlobalSettings globalSettings,
|
||||||
|
IHttpClientFactory httpClientFactory,
|
||||||
|
ILogger<CloudPhishingDomainDirectQuery> logger)
|
||||||
|
{
|
||||||
|
_globalSettings = globalSettings;
|
||||||
|
_httpClientFactory = httpClientFactory;
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<List<string>> GetPhishingDomainsAsync()
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(_globalSettings.PhishingDomain?.UpdateUrl))
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("Phishing domain update URL is not configured.");
|
||||||
|
}
|
||||||
|
|
||||||
|
var httpClient = _httpClientFactory.CreateClient("PhishingDomains");
|
||||||
|
var response = await httpClient.GetAsync(_globalSettings.PhishingDomain.UpdateUrl);
|
||||||
|
response.EnsureSuccessStatusCode();
|
||||||
|
|
||||||
|
var content = await response.Content.ReadAsStringAsync();
|
||||||
|
return ParseDomains(content);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the SHA256 checksum of the remote phishing domains list
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>The SHA256 checksum as a lowercase hex string</returns>
|
||||||
|
public async Task<string> GetRemoteChecksumAsync()
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(_globalSettings.PhishingDomain?.ChecksumUrl))
|
||||||
|
{
|
||||||
|
_logger.LogWarning("Phishing domain checksum URL is not configured.");
|
||||||
|
return string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var httpClient = _httpClientFactory.CreateClient("PhishingDomains");
|
||||||
|
var response = await httpClient.GetAsync(_globalSettings.PhishingDomain.ChecksumUrl);
|
||||||
|
response.EnsureSuccessStatusCode();
|
||||||
|
|
||||||
|
var content = await response.Content.ReadAsStringAsync();
|
||||||
|
return ParseChecksumResponse(content);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Error retrieving phishing domain checksum from {Url}",
|
||||||
|
_globalSettings.PhishingDomain.ChecksumUrl);
|
||||||
|
return string.Empty;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Parses a checksum response in the format "hash *filename"
|
||||||
|
/// </summary>
|
||||||
|
private static string ParseChecksumResponse(string checksumContent)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(checksumContent))
|
||||||
|
{
|
||||||
|
return string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Format is typically "hash *filename"
|
||||||
|
var parts = checksumContent.Split(' ', 2);
|
||||||
|
if (parts.Length > 0)
|
||||||
|
{
|
||||||
|
return parts[0].Trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
return string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<string> ParseDomains(string content)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(content))
|
||||||
|
{
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return content
|
||||||
|
.Split(['\r', '\n'], StringSplitOptions.RemoveEmptyEntries)
|
||||||
|
.Select(line => line.Trim())
|
||||||
|
.Where(line => !string.IsNullOrWhiteSpace(line) && !line.StartsWith("#"))
|
||||||
|
.ToList();
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,66 @@
|
|||||||
|
using Bit.Core.PhishingDomainFeatures.Interfaces;
|
||||||
|
using Bit.Core.Services;
|
||||||
|
using Bit.Core.Settings;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
|
namespace Bit.Core.PhishingDomainFeatures;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Implementation of ICloudPhishingDomainQuery for self-hosted environments
|
||||||
|
/// that relays the request to the Bitwarden cloud API
|
||||||
|
/// </summary>
|
||||||
|
public class CloudPhishingDomainRelayQuery : BaseIdentityClientService, ICloudPhishingDomainQuery
|
||||||
|
{
|
||||||
|
private readonly IGlobalSettings _globalSettings;
|
||||||
|
|
||||||
|
public CloudPhishingDomainRelayQuery(
|
||||||
|
IHttpClientFactory httpFactory,
|
||||||
|
IGlobalSettings globalSettings,
|
||||||
|
ILogger<CloudPhishingDomainRelayQuery> logger)
|
||||||
|
: base(
|
||||||
|
httpFactory,
|
||||||
|
globalSettings.Installation.ApiUri,
|
||||||
|
globalSettings.Installation.IdentityUri,
|
||||||
|
"api.installation",
|
||||||
|
$"installation.{globalSettings.Installation.Id}",
|
||||||
|
globalSettings.Installation.Key,
|
||||||
|
logger)
|
||||||
|
{
|
||||||
|
_globalSettings = globalSettings;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<List<string>> GetPhishingDomainsAsync()
|
||||||
|
{
|
||||||
|
if (!_globalSettings.SelfHosted || !_globalSettings.EnableCloudCommunication)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("This query is only for self-hosted installations with cloud communication enabled.");
|
||||||
|
}
|
||||||
|
|
||||||
|
var result = await SendAsync<object, string[]>(HttpMethod.Get, "phishing-domains", null, true);
|
||||||
|
return result?.ToList() ?? new List<string>();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the SHA256 checksum of the remote phishing domains list
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>The SHA256 checksum as a lowercase hex string</returns>
|
||||||
|
public async Task<string> GetRemoteChecksumAsync()
|
||||||
|
{
|
||||||
|
if (!_globalSettings.SelfHosted || !_globalSettings.EnableCloudCommunication)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("This query is only for self-hosted installations with cloud communication enabled.");
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// For self-hosted environments, we get the checksum from the Bitwarden cloud API
|
||||||
|
var result = await SendAsync<object, string>(HttpMethod.Get, "phishing-domains/checksum", null, true);
|
||||||
|
return result ?? string.Empty;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Error retrieving phishing domain checksum from Bitwarden cloud API");
|
||||||
|
return string.Empty;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,7 @@
|
|||||||
|
namespace Bit.Core.PhishingDomainFeatures.Interfaces;
|
||||||
|
|
||||||
|
public interface ICloudPhishingDomainQuery
|
||||||
|
{
|
||||||
|
Task<List<string>> GetPhishingDomainsAsync();
|
||||||
|
Task<string> GetRemoteChecksumAsync();
|
||||||
|
}
|
@ -3,5 +3,6 @@
|
|||||||
public interface IPhishingDomainRepository
|
public interface IPhishingDomainRepository
|
||||||
{
|
{
|
||||||
Task<ICollection<string>> GetActivePhishingDomainsAsync();
|
Task<ICollection<string>> GetActivePhishingDomainsAsync();
|
||||||
Task UpdatePhishingDomainsAsync(IEnumerable<string> domains);
|
Task UpdatePhishingDomainsAsync(IEnumerable<string> domains, string checksum);
|
||||||
|
Task<string> GetCurrentChecksumAsync();
|
||||||
}
|
}
|
||||||
|
@ -84,6 +84,7 @@ public class GlobalSettings : IGlobalSettings
|
|||||||
public virtual ILaunchDarklySettings LaunchDarkly { get; set; } = new LaunchDarklySettings();
|
public virtual ILaunchDarklySettings LaunchDarkly { get; set; } = new LaunchDarklySettings();
|
||||||
public virtual string DevelopmentDirectory { get; set; }
|
public virtual string DevelopmentDirectory { get; set; }
|
||||||
public virtual IWebPushSettings WebPush { get; set; } = new WebPushSettings();
|
public virtual IWebPushSettings WebPush { get; set; } = new WebPushSettings();
|
||||||
|
public virtual IPhishingDomainSettings PhishingDomain { get; set; } = new PhishingDomainSettings();
|
||||||
|
|
||||||
public virtual bool EnableEmailVerification { get; set; }
|
public virtual bool EnableEmailVerification { get; set; }
|
||||||
public virtual string KdfDefaultHashKey { get; set; }
|
public virtual string KdfDefaultHashKey { get; set; }
|
||||||
@ -634,6 +635,12 @@ public class GlobalSettings : IGlobalSettings
|
|||||||
public int MaxNetworkRetries { get; set; } = 2;
|
public int MaxNetworkRetries { get; set; } = 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public class PhishingDomainSettings : IPhishingDomainSettings
|
||||||
|
{
|
||||||
|
public string UpdateUrl { get; set; }
|
||||||
|
public string ChecksumUrl { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
public class DistributedIpRateLimitingSettings
|
public class DistributedIpRateLimitingSettings
|
||||||
{
|
{
|
||||||
public string RedisConnectionString { get; set; }
|
public string RedisConnectionString { get; set; }
|
||||||
|
@ -29,4 +29,5 @@ public interface IGlobalSettings
|
|||||||
string DevelopmentDirectory { get; set; }
|
string DevelopmentDirectory { get; set; }
|
||||||
IWebPushSettings WebPush { get; set; }
|
IWebPushSettings WebPush { get; set; }
|
||||||
GlobalSettings.EventLoggingSettings EventLogging { get; set; }
|
GlobalSettings.EventLoggingSettings EventLogging { get; set; }
|
||||||
|
IPhishingDomainSettings PhishingDomain { get; set; }
|
||||||
}
|
}
|
||||||
|
7
src/Core/Settings/IPhishingDomainSettings.cs
Normal file
7
src/Core/Settings/IPhishingDomainSettings.cs
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
namespace Bit.Core.Settings;
|
||||||
|
|
||||||
|
public interface IPhishingDomainSettings
|
||||||
|
{
|
||||||
|
string UpdateUrl { get; set; }
|
||||||
|
string ChecksumUrl { get; set; }
|
||||||
|
}
|
@ -72,10 +72,4 @@ public static class DapperServiceCollectionExtensions
|
|||||||
services.AddSingleton<IEventRepository, EventRepository>();
|
services.AddSingleton<IEventRepository, EventRepository>();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void AddDapper(this IServiceCollection services)
|
|
||||||
{
|
|
||||||
// Register repositories
|
|
||||||
services.AddSingleton<IPhishingDomainRepository, PhishingDomainRepository>();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -5,6 +5,7 @@ using Bit.Core.Settings;
|
|||||||
using Dapper;
|
using Dapper;
|
||||||
using Microsoft.Data.SqlClient;
|
using Microsoft.Data.SqlClient;
|
||||||
using Microsoft.Extensions.Caching.Distributed;
|
using Microsoft.Extensions.Caching.Distributed;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
namespace Bit.Infrastructure.Dapper.Repositories;
|
namespace Bit.Infrastructure.Dapper.Repositories;
|
||||||
|
|
||||||
@ -12,77 +13,152 @@ public class PhishingDomainRepository : IPhishingDomainRepository
|
|||||||
{
|
{
|
||||||
private readonly string _connectionString;
|
private readonly string _connectionString;
|
||||||
private readonly IDistributedCache _cache;
|
private readonly IDistributedCache _cache;
|
||||||
private const string _cacheKey = "PhishingDomains";
|
private readonly ILogger<PhishingDomainRepository> _logger;
|
||||||
private static readonly DistributedCacheEntryOptions _cacheOptions = new DistributedCacheEntryOptions
|
private const string _cacheKey = "PhishingDomains_v1";
|
||||||
|
private static readonly DistributedCacheEntryOptions _cacheOptions = new()
|
||||||
{
|
{
|
||||||
AbsoluteExpirationRelativeToNow = TimeSpan.FromHours(24) // Cache for 24 hours
|
AbsoluteExpirationRelativeToNow = TimeSpan.FromHours(24),
|
||||||
|
SlidingExpiration = TimeSpan.FromHours(1)
|
||||||
};
|
};
|
||||||
|
|
||||||
public PhishingDomainRepository(GlobalSettings globalSettings, IDistributedCache cache)
|
public PhishingDomainRepository(
|
||||||
: this(globalSettings.SqlServer.ConnectionString, cache)
|
GlobalSettings globalSettings,
|
||||||
|
IDistributedCache cache,
|
||||||
|
ILogger<PhishingDomainRepository> logger)
|
||||||
|
: this(globalSettings.SqlServer.ConnectionString, cache, logger)
|
||||||
{ }
|
{ }
|
||||||
|
|
||||||
public PhishingDomainRepository(string connectionString, IDistributedCache cache)
|
public PhishingDomainRepository(
|
||||||
|
string connectionString,
|
||||||
|
IDistributedCache cache,
|
||||||
|
ILogger<PhishingDomainRepository> logger)
|
||||||
{
|
{
|
||||||
_connectionString = connectionString;
|
_connectionString = connectionString;
|
||||||
_cache = cache;
|
_cache = cache;
|
||||||
|
_logger = logger;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<ICollection<string>> GetActivePhishingDomainsAsync()
|
public async Task<ICollection<string>> GetActivePhishingDomainsAsync()
|
||||||
{
|
{
|
||||||
// Try to get from cache first
|
try
|
||||||
|
{
|
||||||
var cachedDomains = await _cache.GetStringAsync(_cacheKey);
|
var cachedDomains = await _cache.GetStringAsync(_cacheKey);
|
||||||
if (!string.IsNullOrEmpty(cachedDomains))
|
if (!string.IsNullOrEmpty(cachedDomains))
|
||||||
{
|
{
|
||||||
return JsonSerializer.Deserialize<ICollection<string>>(cachedDomains) ?? new List<string>();
|
_logger.LogDebug("Retrieved phishing domains from cache");
|
||||||
|
return JsonSerializer.Deserialize<ICollection<string>>(cachedDomains) ?? [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogWarning(ex, "Failed to retrieve phishing domains from cache");
|
||||||
}
|
}
|
||||||
|
|
||||||
// If not in cache, get from database
|
await using var connection = new SqlConnection(_connectionString);
|
||||||
using (var connection = new SqlConnection(_connectionString))
|
|
||||||
{
|
|
||||||
var results = await connection.QueryAsync<string>(
|
var results = await connection.QueryAsync<string>(
|
||||||
"[dbo].[PhishingDomain_ReadAll]",
|
"[dbo].[PhishingDomain_ReadAll]",
|
||||||
commandType: CommandType.StoredProcedure);
|
commandType: CommandType.StoredProcedure);
|
||||||
|
|
||||||
var domains = results.AsList();
|
var domains = results.AsList();
|
||||||
|
|
||||||
// Store in cache
|
try
|
||||||
|
{
|
||||||
await _cache.SetStringAsync(
|
await _cache.SetStringAsync(
|
||||||
_cacheKey,
|
_cacheKey,
|
||||||
JsonSerializer.Serialize(domains),
|
JsonSerializer.Serialize(domains),
|
||||||
_cacheOptions);
|
_cacheOptions);
|
||||||
|
|
||||||
|
_logger.LogDebug("Stored {Count} phishing domains in cache", domains.Count);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogWarning(ex, "Failed to store phishing domains in cache");
|
||||||
|
}
|
||||||
|
|
||||||
return domains;
|
return domains;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<string> GetCurrentChecksumAsync()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await using var connection = new SqlConnection(_connectionString);
|
||||||
|
|
||||||
|
var checksum = await connection.QueryFirstOrDefaultAsync<string>(
|
||||||
|
"[dbo].[PhishingDomain_ReadChecksum]",
|
||||||
|
commandType: CommandType.StoredProcedure);
|
||||||
|
|
||||||
|
return checksum ?? string.Empty;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Error retrieving phishing domain checksum from database");
|
||||||
|
return string.Empty;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task UpdatePhishingDomainsAsync(IEnumerable<string> domains)
|
public async Task UpdatePhishingDomainsAsync(IEnumerable<string> domains, string checksum)
|
||||||
{
|
{
|
||||||
using (var connection = new SqlConnection(_connectionString))
|
var domainsList = domains.ToList();
|
||||||
|
_logger.LogInformation("Beginning bulk update of {Count} phishing domains with checksum {Checksum}",
|
||||||
|
domainsList.Count, checksum);
|
||||||
|
|
||||||
|
await using var connection = new SqlConnection(_connectionString);
|
||||||
|
await connection.OpenAsync();
|
||||||
|
|
||||||
|
await using var transaction = connection.BeginTransaction();
|
||||||
|
try
|
||||||
{
|
{
|
||||||
await connection.ExecuteAsync(
|
await connection.ExecuteAsync(
|
||||||
"[dbo].[PhishingDomain_DeleteAll]",
|
"[dbo].[PhishingDomain_DeleteAll]",
|
||||||
|
transaction: transaction,
|
||||||
commandType: CommandType.StoredProcedure);
|
commandType: CommandType.StoredProcedure);
|
||||||
|
|
||||||
foreach (var domain in domains)
|
var dataTable = new DataTable();
|
||||||
|
dataTable.Columns.Add("Id", typeof(Guid));
|
||||||
|
dataTable.Columns.Add("Domain", typeof(string));
|
||||||
|
dataTable.Columns.Add("Checksum", typeof(string));
|
||||||
|
|
||||||
|
dataTable.PrimaryKey = [dataTable.Columns["Id"]];
|
||||||
|
|
||||||
|
foreach (var domain in domainsList)
|
||||||
{
|
{
|
||||||
await connection.ExecuteAsync(
|
dataTable.Rows.Add(Guid.NewGuid(), domain, checksum);
|
||||||
"[dbo].[PhishingDomain_Create]",
|
|
||||||
new
|
|
||||||
{
|
|
||||||
Id = Guid.NewGuid(),
|
|
||||||
Domain = domain,
|
|
||||||
CreationDate = DateTime.UtcNow,
|
|
||||||
RevisionDate = DateTime.UtcNow
|
|
||||||
},
|
|
||||||
commandType: CommandType.StoredProcedure);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update cache with new domains
|
using var bulkCopy = new SqlBulkCopy(connection, SqlBulkCopyOptions.Default, transaction);
|
||||||
|
|
||||||
|
bulkCopy.DestinationTableName = "[dbo].[PhishingDomain]";
|
||||||
|
bulkCopy.BatchSize = 10000;
|
||||||
|
|
||||||
|
bulkCopy.ColumnMappings.Add("Id", "Id");
|
||||||
|
bulkCopy.ColumnMappings.Add("Domain", "Domain");
|
||||||
|
bulkCopy.ColumnMappings.Add("Checksum", "Checksum");
|
||||||
|
|
||||||
|
await bulkCopy.WriteToServerAsync(dataTable);
|
||||||
|
await transaction.CommitAsync();
|
||||||
|
|
||||||
|
_logger.LogInformation("Successfully bulk updated {Count} phishing domains", domainsList.Count);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
await transaction.RollbackAsync();
|
||||||
|
_logger.LogError(ex, "Failed to bulk update phishing domains");
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
await _cache.SetStringAsync(
|
await _cache.SetStringAsync(
|
||||||
_cacheKey,
|
_cacheKey,
|
||||||
JsonSerializer.Serialize(domains),
|
JsonSerializer.Serialize(domainsList),
|
||||||
_cacheOptions);
|
_cacheOptions);
|
||||||
|
_logger.LogDebug("Updated phishing domains cache after update operation");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogWarning(ex, "Failed to update phishing domains in cache");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -11,7 +11,6 @@ public class PhishingDomain
|
|||||||
[MaxLength(255)]
|
[MaxLength(255)]
|
||||||
public string Domain { get; set; }
|
public string Domain { get; set; }
|
||||||
|
|
||||||
public DateTime CreationDate { get; set; }
|
[MaxLength(64)]
|
||||||
|
public string Checksum { get; set; }
|
||||||
public DateTime RevisionDate { get; set; }
|
|
||||||
}
|
}
|
||||||
|
@ -111,6 +111,7 @@ public class DatabaseContext : DbContext
|
|||||||
var eOrganizationConnection = builder.Entity<OrganizationConnection>();
|
var eOrganizationConnection = builder.Entity<OrganizationConnection>();
|
||||||
var eOrganizationDomain = builder.Entity<OrganizationDomain>();
|
var eOrganizationDomain = builder.Entity<OrganizationDomain>();
|
||||||
var aWebAuthnCredential = builder.Entity<WebAuthnCredential>();
|
var aWebAuthnCredential = builder.Entity<WebAuthnCredential>();
|
||||||
|
var ePhishingDomain = builder.Entity<PhishingDomain>();
|
||||||
|
|
||||||
// Shadow property configurations go here
|
// Shadow property configurations go here
|
||||||
|
|
||||||
@ -127,6 +128,7 @@ public class DatabaseContext : DbContext
|
|||||||
eOrganizationConnection.Property(c => c.Id).ValueGeneratedNever();
|
eOrganizationConnection.Property(c => c.Id).ValueGeneratedNever();
|
||||||
eOrganizationDomain.Property(ar => ar.Id).ValueGeneratedNever();
|
eOrganizationDomain.Property(ar => ar.Id).ValueGeneratedNever();
|
||||||
aWebAuthnCredential.Property(ar => ar.Id).ValueGeneratedNever();
|
aWebAuthnCredential.Property(ar => ar.Id).ValueGeneratedNever();
|
||||||
|
ePhishingDomain.Property(ar => ar.Id).ValueGeneratedNever();
|
||||||
|
|
||||||
eCollectionCipher.HasKey(cc => new { cc.CollectionId, cc.CipherId });
|
eCollectionCipher.HasKey(cc => new { cc.CollectionId, cc.CipherId });
|
||||||
eCollectionUser.HasKey(cu => new { cu.CollectionId, cu.OrganizationUserId });
|
eCollectionUser.HasKey(cu => new { cu.CollectionId, cu.OrganizationUserId });
|
||||||
@ -167,6 +169,7 @@ public class DatabaseContext : DbContext
|
|||||||
eOrganizationConnection.ToTable(nameof(OrganizationConnection));
|
eOrganizationConnection.ToTable(nameof(OrganizationConnection));
|
||||||
eOrganizationDomain.ToTable(nameof(OrganizationDomain));
|
eOrganizationDomain.ToTable(nameof(OrganizationDomain));
|
||||||
aWebAuthnCredential.ToTable(nameof(WebAuthnCredential));
|
aWebAuthnCredential.ToTable(nameof(WebAuthnCredential));
|
||||||
|
ePhishingDomain.ToTable(nameof(PhishingDomain));
|
||||||
|
|
||||||
ConfigureDateTimeUtcQueries(builder);
|
ConfigureDateTimeUtcQueries(builder);
|
||||||
}
|
}
|
||||||
|
@ -1,50 +1,167 @@
|
|||||||
using Bit.Core.Repositories;
|
using System.Data;
|
||||||
using Bit.Infrastructure.EntityFramework.Models;
|
using System.Text.Json;
|
||||||
|
using Bit.Core.Repositories;
|
||||||
|
using Microsoft.Data.SqlClient;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.Extensions.Caching.Distributed;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
namespace Bit.Infrastructure.EntityFramework.Repositories;
|
namespace Bit.Infrastructure.EntityFramework.Repositories;
|
||||||
|
|
||||||
public class PhishingDomainRepository : IPhishingDomainRepository
|
public class PhishingDomainRepository : IPhishingDomainRepository
|
||||||
{
|
{
|
||||||
private readonly IServiceScopeFactory _serviceScopeFactory;
|
private readonly IServiceScopeFactory _serviceScopeFactory;
|
||||||
|
private readonly IDistributedCache _cache;
|
||||||
|
private readonly ILogger<PhishingDomainRepository> _logger;
|
||||||
|
private const string _cacheKey = "PhishingDomains_v1";
|
||||||
|
private static readonly DistributedCacheEntryOptions _cacheOptions = new()
|
||||||
|
{
|
||||||
|
AbsoluteExpirationRelativeToNow = TimeSpan.FromHours(24),
|
||||||
|
SlidingExpiration = TimeSpan.FromHours(1)
|
||||||
|
};
|
||||||
|
|
||||||
public PhishingDomainRepository(IServiceScopeFactory serviceScopeFactory)
|
public PhishingDomainRepository(
|
||||||
|
IServiceScopeFactory serviceScopeFactory,
|
||||||
|
IDistributedCache cache,
|
||||||
|
ILogger<PhishingDomainRepository> logger)
|
||||||
{
|
{
|
||||||
_serviceScopeFactory = serviceScopeFactory;
|
_serviceScopeFactory = serviceScopeFactory;
|
||||||
|
_cache = cache;
|
||||||
|
_logger = logger;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<ICollection<string>> GetActivePhishingDomainsAsync()
|
public async Task<ICollection<string>> GetActivePhishingDomainsAsync()
|
||||||
{
|
{
|
||||||
using (var scope = _serviceScopeFactory.CreateScope())
|
try
|
||||||
{
|
{
|
||||||
|
var cachedDomains = await _cache.GetStringAsync(_cacheKey);
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(cachedDomains))
|
||||||
|
{
|
||||||
|
_logger.LogDebug("Retrieved phishing domains from cache");
|
||||||
|
return JsonSerializer.Deserialize<ICollection<string>>(cachedDomains) ?? [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogWarning(ex, "Failed to retrieve phishing domains from cache");
|
||||||
|
}
|
||||||
|
|
||||||
|
using var scope = _serviceScopeFactory.CreateScope();
|
||||||
|
|
||||||
var dbContext = scope.ServiceProvider.GetRequiredService<DatabaseContext>();
|
var dbContext = scope.ServiceProvider.GetRequiredService<DatabaseContext>();
|
||||||
var domains = await dbContext.PhishingDomains
|
var domains = await dbContext.PhishingDomains
|
||||||
.Select(d => d.Domain)
|
.Select(d => d.Domain)
|
||||||
.ToListAsync();
|
.ToListAsync();
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await _cache.SetStringAsync(
|
||||||
|
_cacheKey,
|
||||||
|
JsonSerializer.Serialize(domains),
|
||||||
|
_cacheOptions);
|
||||||
|
|
||||||
|
_logger.LogDebug("Stored {Count} phishing domains in cache", domains.Count);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogWarning(ex, "Failed to store phishing domains in cache");
|
||||||
|
}
|
||||||
|
|
||||||
return domains;
|
return domains;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
public async Task UpdatePhishingDomainsAsync(IEnumerable<string> domains)
|
public async Task<string> GetCurrentChecksumAsync()
|
||||||
{
|
{
|
||||||
using (var scope = _serviceScopeFactory.CreateScope())
|
try
|
||||||
{
|
{
|
||||||
|
using var scope = _serviceScopeFactory.CreateScope();
|
||||||
var dbContext = scope.ServiceProvider.GetRequiredService<DatabaseContext>();
|
var dbContext = scope.ServiceProvider.GetRequiredService<DatabaseContext>();
|
||||||
|
|
||||||
// Clear existing domains
|
// Get the first checksum in the database (there should only be one set of domains with the same checksum)
|
||||||
await dbContext.PhishingDomains.ExecuteDeleteAsync();
|
var checksum = await dbContext.PhishingDomains
|
||||||
|
.Select(d => d.Checksum)
|
||||||
|
.FirstOrDefaultAsync();
|
||||||
|
|
||||||
// Add new domains
|
return checksum ?? string.Empty;
|
||||||
var phishingDomains = domains.Select(d => new PhishingDomain
|
}
|
||||||
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
Id = Guid.NewGuid(),
|
_logger.LogError(ex, "Error retrieving phishing domain checksum from database");
|
||||||
Domain = d,
|
return string.Empty;
|
||||||
CreationDate = DateTime.UtcNow,
|
}
|
||||||
RevisionDate = DateTime.UtcNow
|
}
|
||||||
});
|
|
||||||
await dbContext.PhishingDomains.AddRangeAsync(phishingDomains);
|
public async Task UpdatePhishingDomainsAsync(IEnumerable<string> domains, string checksum)
|
||||||
await dbContext.SaveChangesAsync();
|
{
|
||||||
|
var domainsList = domains.ToList();
|
||||||
|
_logger.LogInformation("Beginning bulk update of {Count} phishing domains with checksum {Checksum}",
|
||||||
|
domainsList.Count, checksum);
|
||||||
|
|
||||||
|
using var scope = _serviceScopeFactory.CreateScope();
|
||||||
|
var dbContext = scope.ServiceProvider.GetRequiredService<DatabaseContext>();
|
||||||
|
|
||||||
|
var connection = dbContext.Database.GetDbConnection();
|
||||||
|
var connectionString = connection.ConnectionString;
|
||||||
|
|
||||||
|
await using var sqlConnection = new SqlConnection(connectionString);
|
||||||
|
await sqlConnection.OpenAsync();
|
||||||
|
|
||||||
|
await using var transaction = sqlConnection.BeginTransaction();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await using var command = sqlConnection.CreateCommand();
|
||||||
|
command.Transaction = transaction;
|
||||||
|
command.CommandText = "[dbo].[PhishingDomain_DeleteAll]";
|
||||||
|
command.CommandType = CommandType.StoredProcedure;
|
||||||
|
await command.ExecuteNonQueryAsync();
|
||||||
|
|
||||||
|
var dataTable = new DataTable();
|
||||||
|
dataTable.Columns.Add("Id", typeof(Guid));
|
||||||
|
dataTable.Columns.Add("Domain", typeof(string));
|
||||||
|
dataTable.Columns.Add("Checksum", typeof(string));
|
||||||
|
|
||||||
|
dataTable.PrimaryKey = [dataTable.Columns["Id"]];
|
||||||
|
|
||||||
|
foreach (var domain in domainsList)
|
||||||
|
{
|
||||||
|
dataTable.Rows.Add(Guid.NewGuid(), domain, checksum);
|
||||||
|
}
|
||||||
|
|
||||||
|
using var bulkCopy = new SqlBulkCopy(sqlConnection, SqlBulkCopyOptions.Default, transaction);
|
||||||
|
|
||||||
|
bulkCopy.DestinationTableName = "[dbo].[PhishingDomain]";
|
||||||
|
bulkCopy.BatchSize = 10000;
|
||||||
|
|
||||||
|
bulkCopy.ColumnMappings.Add("Id", "Id");
|
||||||
|
bulkCopy.ColumnMappings.Add("Domain", "Domain");
|
||||||
|
bulkCopy.ColumnMappings.Add("Checksum", "Checksum");
|
||||||
|
|
||||||
|
await bulkCopy.WriteToServerAsync(dataTable);
|
||||||
|
await transaction.CommitAsync();
|
||||||
|
|
||||||
|
_logger.LogInformation("Successfully bulk updated {Count} phishing domains", domainsList.Count);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
await transaction.RollbackAsync();
|
||||||
|
_logger.LogError(ex, "Failed to bulk update phishing domains");
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await _cache.SetStringAsync(
|
||||||
|
_cacheKey,
|
||||||
|
JsonSerializer.Serialize(domainsList),
|
||||||
|
_cacheOptions);
|
||||||
|
|
||||||
|
_logger.LogDebug("Updated phishing domains cache after update operation");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogWarning(ex, "Failed to update phishing domains in cache");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,8 +1,7 @@
|
|||||||
CREATE PROCEDURE [dbo].[PhishingDomain_Create]
|
CREATE PROCEDURE [dbo].[PhishingDomain_Create]
|
||||||
@Id UNIQUEIDENTIFIER,
|
@Id UNIQUEIDENTIFIER,
|
||||||
@Domain NVARCHAR(255),
|
@Domain NVARCHAR(255),
|
||||||
@CreationDate DATETIME2(7),
|
@Checksum NVARCHAR(64)
|
||||||
@RevisionDate DATETIME2(7)
|
|
||||||
AS
|
AS
|
||||||
BEGIN
|
BEGIN
|
||||||
SET NOCOUNT ON
|
SET NOCOUNT ON
|
||||||
@ -11,14 +10,12 @@ BEGIN
|
|||||||
(
|
(
|
||||||
[Id],
|
[Id],
|
||||||
[Domain],
|
[Domain],
|
||||||
[CreationDate],
|
[Checksum]
|
||||||
[RevisionDate]
|
|
||||||
)
|
)
|
||||||
VALUES
|
VALUES
|
||||||
(
|
(
|
||||||
@Id,
|
@Id,
|
||||||
@Domain,
|
@Domain,
|
||||||
@CreationDate,
|
@Checksum
|
||||||
@RevisionDate
|
|
||||||
)
|
)
|
||||||
END
|
END
|
@ -0,0 +1,10 @@
|
|||||||
|
CREATE PROCEDURE [dbo].[PhishingDomain_ReadChecksum]
|
||||||
|
AS
|
||||||
|
BEGIN
|
||||||
|
SET NOCOUNT ON
|
||||||
|
|
||||||
|
SELECT TOP 1
|
||||||
|
[Checksum]
|
||||||
|
FROM
|
||||||
|
[dbo].[PhishingDomain]
|
||||||
|
END
|
@ -1,8 +1,7 @@
|
|||||||
CREATE TABLE [dbo].[PhishingDomain] (
|
CREATE TABLE [dbo].[PhishingDomain] (
|
||||||
[Id] UNIQUEIDENTIFIER NOT NULL,
|
[Id] UNIQUEIDENTIFIER NOT NULL,
|
||||||
[Domain] NVARCHAR(255) NOT NULL,
|
[Domain] NVARCHAR(255) NOT NULL,
|
||||||
[CreationDate] DATETIME2(7) NOT NULL,
|
[Checksum] NVARCHAR(64) NULL,
|
||||||
[RevisionDate] DATETIME2(7) NOT NULL,
|
|
||||||
CONSTRAINT [PK_PhishingDomain] PRIMARY KEY CLUSTERED ([Id] ASC)
|
CONSTRAINT [PK_PhishingDomain] PRIMARY KEY CLUSTERED ([Id] ASC)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -0,0 +1,61 @@
|
|||||||
|
-- Update PhishingDomain table to use Checksum instead of dates
|
||||||
|
IF EXISTS (SELECT * FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME = 'PhishingDomain' AND COLUMN_NAME = 'CreationDate')
|
||||||
|
BEGIN
|
||||||
|
-- Add Checksum column
|
||||||
|
ALTER TABLE [dbo].[PhishingDomain]
|
||||||
|
ADD [Checksum] NVARCHAR(64) NULL;
|
||||||
|
|
||||||
|
-- Drop old columns
|
||||||
|
ALTER TABLE [dbo].[PhishingDomain]
|
||||||
|
DROP COLUMN [CreationDate], [RevisionDate];
|
||||||
|
END
|
||||||
|
GO
|
||||||
|
|
||||||
|
-- Update PhishingDomain_Create stored procedure
|
||||||
|
IF OBJECT_ID('[dbo].[PhishingDomain_Create]') IS NOT NULL
|
||||||
|
BEGIN
|
||||||
|
DROP PROCEDURE [dbo].[PhishingDomain_Create]
|
||||||
|
END
|
||||||
|
GO
|
||||||
|
|
||||||
|
CREATE PROCEDURE [dbo].[PhishingDomain_Create]
|
||||||
|
@Id UNIQUEIDENTIFIER,
|
||||||
|
@Domain NVARCHAR(255),
|
||||||
|
@Checksum NVARCHAR(64)
|
||||||
|
AS
|
||||||
|
BEGIN
|
||||||
|
SET NOCOUNT ON
|
||||||
|
|
||||||
|
INSERT INTO [dbo].[PhishingDomain]
|
||||||
|
(
|
||||||
|
[Id],
|
||||||
|
[Domain],
|
||||||
|
[Checksum]
|
||||||
|
)
|
||||||
|
VALUES
|
||||||
|
(
|
||||||
|
@Id,
|
||||||
|
@Domain,
|
||||||
|
@Checksum
|
||||||
|
)
|
||||||
|
END
|
||||||
|
GO
|
||||||
|
|
||||||
|
-- Create PhishingDomain_ReadChecksum stored procedure
|
||||||
|
IF OBJECT_ID('[dbo].[PhishingDomain_ReadChecksum]') IS NOT NULL
|
||||||
|
BEGIN
|
||||||
|
DROP PROCEDURE [dbo].[PhishingDomain_ReadChecksum]
|
||||||
|
END
|
||||||
|
GO
|
||||||
|
|
||||||
|
CREATE PROCEDURE [dbo].[PhishingDomain_ReadChecksum]
|
||||||
|
AS
|
||||||
|
BEGIN
|
||||||
|
SET NOCOUNT ON
|
||||||
|
|
||||||
|
SELECT TOP 1
|
||||||
|
[Checksum]
|
||||||
|
FROM
|
||||||
|
[dbo].[PhishingDomain]
|
||||||
|
END
|
||||||
|
GO
|
Loading…
x
Reference in New Issue
Block a user