16 changed files with 0 additions and 593 deletions
@ -1,34 +0,0 @@
@@ -1,34 +0,0 @@
|
||||
using Bit.Core; |
||||
using Bit.Core.Repositories; |
||||
using Bit.Core.Services; |
||||
using Microsoft.AspNetCore.Mvc; |
||||
|
||||
namespace Bit.Api.Controllers; |
||||
|
||||
[Route("phishing-domains")] |
||||
public class PhishingDomainsController(IPhishingDomainRepository phishingDomainRepository, IFeatureService featureService) : Controller |
||||
{ |
||||
[HttpGet] |
||||
public async Task<ActionResult<ICollection<string>>> GetPhishingDomainsAsync() |
||||
{ |
||||
if (!featureService.IsEnabled(FeatureFlagKeys.PhishingDetection)) |
||||
{ |
||||
return NotFound(); |
||||
} |
||||
|
||||
var domains = await phishingDomainRepository.GetActivePhishingDomainsAsync(); |
||||
return Ok(domains); |
||||
} |
||||
|
||||
[HttpGet("checksum")] |
||||
public async Task<ActionResult<string>> GetChecksumAsync() |
||||
{ |
||||
if (!featureService.IsEnabled(FeatureFlagKeys.PhishingDetection)) |
||||
{ |
||||
return NotFound(); |
||||
} |
||||
|
||||
var checksum = await phishingDomainRepository.GetCurrentChecksumAsync(); |
||||
return Ok(checksum); |
||||
} |
||||
} |
||||
@ -1,97 +0,0 @@
@@ -1,97 +0,0 @@
|
||||
using Bit.Core; |
||||
using Bit.Core.Jobs; |
||||
using Bit.Core.PhishingDomainFeatures.Interfaces; |
||||
using Bit.Core.Repositories; |
||||
using Bit.Core.Services; |
||||
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; |
||||
private readonly IFeatureService _featureService; |
||||
public UpdatePhishingDomainsJob( |
||||
GlobalSettings globalSettings, |
||||
IPhishingDomainRepository phishingDomainRepository, |
||||
ICloudPhishingDomainQuery cloudPhishingDomainQuery, |
||||
IFeatureService featureService, |
||||
ILogger<UpdatePhishingDomainsJob> logger) |
||||
: base(logger) |
||||
{ |
||||
_globalSettings = globalSettings; |
||||
_phishingDomainRepository = phishingDomainRepository; |
||||
_cloudPhishingDomainQuery = cloudPhishingDomainQuery; |
||||
_featureService = featureService; |
||||
} |
||||
|
||||
protected override async Task ExecuteJobAsync(IJobExecutionContext context) |
||||
{ |
||||
if (!_featureService.IsEnabled(FeatureFlagKeys.PhishingDetection)) |
||||
{ |
||||
_logger.LogInformation(Constants.BypassFiltersEventId, "Skipping phishing domain update. Feature flag is disabled."); |
||||
return; |
||||
} |
||||
|
||||
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; |
||||
} |
||||
|
||||
var remoteChecksum = await _cloudPhishingDomainQuery.GetRemoteChecksumAsync(); |
||||
if (string.IsNullOrWhiteSpace(remoteChecksum)) |
||||
{ |
||||
_logger.LogWarning(Constants.BypassFiltersEventId, "Could not retrieve remote checksum. Skipping update."); |
||||
return; |
||||
} |
||||
|
||||
var currentChecksum = await _phishingDomainRepository.GetCurrentChecksumAsync(); |
||||
|
||||
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.Contains("phishing.testcategory.com", StringComparer.OrdinalIgnoreCase)) |
||||
{ |
||||
domains.Add("phishing.testcategory.com"); |
||||
} |
||||
|
||||
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."); |
||||
} |
||||
} |
||||
} |
||||
@ -1,95 +0,0 @@
@@ -1,95 +0,0 @@
|
||||
// FIXME: Update this file to be null safe and then delete the line below |
||||
#nullable disable |
||||
|
||||
using System.Text; |
||||
using Azure.Storage.Blobs; |
||||
using Azure.Storage.Blobs.Models; |
||||
using Bit.Core.Settings; |
||||
using Microsoft.Extensions.Logging; |
||||
|
||||
namespace Bit.Core.PhishingDomainFeatures; |
||||
|
||||
public class AzurePhishingDomainStorageService |
||||
{ |
||||
private const string _containerName = "phishingdomains"; |
||||
private const string _domainsFileName = "domains.txt"; |
||||
private const string _checksumFileName = "checksum.txt"; |
||||
|
||||
private readonly BlobServiceClient _blobServiceClient; |
||||
private readonly ILogger<AzurePhishingDomainStorageService> _logger; |
||||
private BlobContainerClient _containerClient; |
||||
|
||||
public AzurePhishingDomainStorageService( |
||||
GlobalSettings globalSettings, |
||||
ILogger<AzurePhishingDomainStorageService> logger) |
||||
{ |
||||
_blobServiceClient = new BlobServiceClient(globalSettings.Storage.ConnectionString); |
||||
_logger = logger; |
||||
} |
||||
|
||||
public async Task<ICollection<string>> GetDomainsAsync() |
||||
{ |
||||
await InitAsync(); |
||||
|
||||
var blobClient = _containerClient.GetBlobClient(_domainsFileName); |
||||
if (!await blobClient.ExistsAsync()) |
||||
{ |
||||
return []; |
||||
} |
||||
|
||||
var response = await blobClient.DownloadAsync(); |
||||
using var streamReader = new StreamReader(response.Value.Content); |
||||
var content = await streamReader.ReadToEndAsync(); |
||||
|
||||
return [.. content |
||||
.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries) |
||||
.Select(line => line.Trim()) |
||||
.Where(line => !string.IsNullOrWhiteSpace(line) && !line.StartsWith('#'))]; |
||||
} |
||||
|
||||
public async Task<string> GetChecksumAsync() |
||||
{ |
||||
await InitAsync(); |
||||
|
||||
var blobClient = _containerClient.GetBlobClient(_checksumFileName); |
||||
if (!await blobClient.ExistsAsync()) |
||||
{ |
||||
return string.Empty; |
||||
} |
||||
|
||||
var response = await blobClient.DownloadAsync(); |
||||
using var streamReader = new StreamReader(response.Value.Content); |
||||
return (await streamReader.ReadToEndAsync()).Trim(); |
||||
} |
||||
|
||||
public async Task UpdateDomainsAsync(IEnumerable<string> domains, string checksum) |
||||
{ |
||||
await InitAsync(); |
||||
|
||||
var domainsContent = string.Join(Environment.NewLine, domains); |
||||
var domainsStream = new MemoryStream(Encoding.UTF8.GetBytes(domainsContent)); |
||||
var domainsBlobClient = _containerClient.GetBlobClient(_domainsFileName); |
||||
|
||||
await domainsBlobClient.UploadAsync(domainsStream, new BlobUploadOptions |
||||
{ |
||||
HttpHeaders = new BlobHttpHeaders { ContentType = "text/plain" } |
||||
}, CancellationToken.None); |
||||
|
||||
var checksumStream = new MemoryStream(Encoding.UTF8.GetBytes(checksum)); |
||||
var checksumBlobClient = _containerClient.GetBlobClient(_checksumFileName); |
||||
|
||||
await checksumBlobClient.UploadAsync(checksumStream, new BlobUploadOptions |
||||
{ |
||||
HttpHeaders = new BlobHttpHeaders { ContentType = "text/plain" } |
||||
}, CancellationToken.None); |
||||
} |
||||
|
||||
private async Task InitAsync() |
||||
{ |
||||
if (_containerClient is null) |
||||
{ |
||||
_containerClient = _blobServiceClient.GetBlobContainerClient(_containerName); |
||||
await _containerClient.CreateIfNotExistsAsync(); |
||||
} |
||||
} |
||||
} |
||||
@ -1,100 +0,0 @@
@@ -1,100 +0,0 @@
|
||||
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); |
||||
|
||||
return parts.Length > 0 ? parts[0].Trim() : string.Empty; |
||||
} |
||||
|
||||
private static List<string> ParseDomains(string content) |
||||
{ |
||||
if (string.IsNullOrWhiteSpace(content)) |
||||
{ |
||||
return []; |
||||
} |
||||
|
||||
return content |
||||
.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries) |
||||
.Select(line => line.Trim()) |
||||
.Where(line => !string.IsNullOrWhiteSpace(line) && !line.StartsWith("#")) |
||||
.ToList(); |
||||
} |
||||
} |
||||
@ -1,69 +0,0 @@
@@ -1,69 +0,0 @@
|
||||
// FIXME: Update this file to be null safe and then delete the line below |
||||
#nullable disable |
||||
|
||||
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.licensing", |
||||
$"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; |
||||
} |
||||
} |
||||
} |
||||
@ -1,7 +0,0 @@
@@ -1,7 +0,0 @@
|
||||
namespace Bit.Core.PhishingDomainFeatures.Interfaces; |
||||
|
||||
public interface ICloudPhishingDomainQuery |
||||
{ |
||||
Task<List<string>> GetPhishingDomainsAsync(); |
||||
Task<string> GetRemoteChecksumAsync(); |
||||
} |
||||
@ -1,8 +0,0 @@
@@ -1,8 +0,0 @@
|
||||
namespace Bit.Core.Repositories; |
||||
|
||||
public interface IPhishingDomainRepository |
||||
{ |
||||
Task<ICollection<string>> GetActivePhishingDomainsAsync(); |
||||
Task UpdatePhishingDomainsAsync(IEnumerable<string> domains, string checksum); |
||||
Task<string> GetCurrentChecksumAsync(); |
||||
} |
||||
@ -1,126 +0,0 @@
@@ -1,126 +0,0 @@
|
||||
using System.Text.Json; |
||||
using Bit.Core.PhishingDomainFeatures; |
||||
using Microsoft.Extensions.Caching.Distributed; |
||||
using Microsoft.Extensions.Logging; |
||||
|
||||
namespace Bit.Core.Repositories.Implementations; |
||||
|
||||
public class AzurePhishingDomainRepository : IPhishingDomainRepository |
||||
{ |
||||
private readonly AzurePhishingDomainStorageService _storageService; |
||||
private readonly IDistributedCache _cache; |
||||
private readonly ILogger<AzurePhishingDomainRepository> _logger; |
||||
private const string _domainsCacheKey = "PhishingDomains_v1"; |
||||
private const string _checksumCacheKey = "PhishingDomains_Checksum_v1"; |
||||
private static readonly DistributedCacheEntryOptions _cacheOptions = new() |
||||
{ |
||||
AbsoluteExpirationRelativeToNow = TimeSpan.FromHours(24), |
||||
SlidingExpiration = TimeSpan.FromHours(1) |
||||
}; |
||||
|
||||
public AzurePhishingDomainRepository( |
||||
AzurePhishingDomainStorageService storageService, |
||||
IDistributedCache cache, |
||||
ILogger<AzurePhishingDomainRepository> logger) |
||||
{ |
||||
_storageService = storageService; |
||||
_cache = cache; |
||||
_logger = logger; |
||||
} |
||||
|
||||
public async Task<ICollection<string>> GetActivePhishingDomainsAsync() |
||||
{ |
||||
try |
||||
{ |
||||
var cachedDomains = await _cache.GetStringAsync(_domainsCacheKey); |
||||
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"); |
||||
} |
||||
|
||||
var domains = await _storageService.GetDomainsAsync(); |
||||
|
||||
try |
||||
{ |
||||
await _cache.SetStringAsync( |
||||
_domainsCacheKey, |
||||
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; |
||||
} |
||||
|
||||
public async Task<string> GetCurrentChecksumAsync() |
||||
{ |
||||
try |
||||
{ |
||||
var cachedChecksum = await _cache.GetStringAsync(_checksumCacheKey); |
||||
if (!string.IsNullOrEmpty(cachedChecksum)) |
||||
{ |
||||
_logger.LogDebug("Retrieved phishing domain checksum from cache"); |
||||
return cachedChecksum; |
||||
} |
||||
} |
||||
catch (Exception ex) |
||||
{ |
||||
_logger.LogWarning(ex, "Failed to retrieve phishing domain checksum from cache"); |
||||
} |
||||
|
||||
var checksum = await _storageService.GetChecksumAsync(); |
||||
|
||||
try |
||||
{ |
||||
if (!string.IsNullOrEmpty(checksum)) |
||||
{ |
||||
await _cache.SetStringAsync( |
||||
_checksumCacheKey, |
||||
checksum, |
||||
_cacheOptions); |
||||
_logger.LogDebug("Stored phishing domain checksum in cache"); |
||||
} |
||||
} |
||||
catch (Exception ex) |
||||
{ |
||||
_logger.LogWarning(ex, "Failed to store phishing domain checksum in cache"); |
||||
} |
||||
|
||||
return checksum; |
||||
} |
||||
|
||||
public async Task UpdatePhishingDomainsAsync(IEnumerable<string> domains, string checksum) |
||||
{ |
||||
var domainsList = domains.ToList(); |
||||
await _storageService.UpdateDomainsAsync(domainsList, checksum); |
||||
|
||||
try |
||||
{ |
||||
await _cache.SetStringAsync( |
||||
_domainsCacheKey, |
||||
JsonSerializer.Serialize(domainsList), |
||||
_cacheOptions); |
||||
|
||||
await _cache.SetStringAsync( |
||||
_checksumCacheKey, |
||||
checksum, |
||||
_cacheOptions); |
||||
|
||||
_logger.LogDebug("Updated phishing domains cache after update operation"); |
||||
} |
||||
catch (Exception ex) |
||||
{ |
||||
_logger.LogWarning(ex, "Failed to update phishing domains in cache"); |
||||
} |
||||
} |
||||
} |
||||
Loading…
Reference in new issue