diff --git a/VoidCat/Model/VoidSettings.cs b/VoidCat/Model/VoidSettings.cs index 93e2862..09af0a1 100644 --- a/VoidCat/Model/VoidSettings.cs +++ b/VoidCat/Model/VoidSettings.cs @@ -5,42 +5,55 @@ namespace VoidCat.Model public class VoidSettings { public string DataDirectory { get; init; } = "./data"; - + public TorSettings? TorSettings { get; init; } - public JwtSettings JwtSettings { get; init; } = new("void_cat_internal", "default_key_void_cat_host"); - + public JwtSettings JwtSettings { get; init; } = new() + { + Issuer = "void_cat_internal", + Key = "default_key_void_cat_host" + }; + public string? Redis { get; init; } - + public StrikeApiSettings? Strike { get; init; } - + public SmtpSettings? Smtp { get; init; } public List CorsOrigins { get; init; } = new(); - + public CloudStorageSettings? CloudStorage { get; init; } - + public VirusScannerSettings? VirusScanner { get; init; } } - public sealed record TorSettings(Uri TorControl, string PrivateKey, string ControlPassword); + public sealed class TorSettings + { + public Uri TorControl { get; init; } + public string PrivateKey { get; init; } + public string ControlPassword { get; init; } + } - public sealed record JwtSettings(string Issuer, string Key); + public sealed class JwtSettings + { + public string Issuer { get; init; } + public string Key { get; init; } + } - public sealed record SmtpSettings + public sealed class SmtpSettings { public Uri? Server { get; init; } public string? Username { get; init; } public string? Password { get; init; } } - public sealed record CloudStorageSettings + public sealed class CloudStorageSettings { public bool ServeFromCloud { get; init; } public S3BlobConfig? S3 { get; set; } } - public sealed record S3BlobConfig + public sealed class S3BlobConfig { public string? AccessKey { get; init; } public string? SecretKey { get; init; } @@ -49,14 +62,20 @@ namespace VoidCat.Model public string? BucketName { get; init; } = "void-cat"; } - public sealed record VirusScannerSettings + public sealed class VirusScannerSettings { - public ClamAVSettings? ClamAV { get; init; } + public ClamAVSettings? ClamAV { get; init; } + public VirusTotalConfig? VirusTotal { get; init; } } - public sealed record ClamAVSettings + public sealed class ClamAVSettings { public Uri? Endpoint { get; init; } public long? MaxStreamSize { get; init; } } -} + + public sealed class VirusTotalConfig + { + public string? ApiKey { get; init; } + } +} \ No newline at end of file diff --git a/VoidCat/Program.cs b/VoidCat/Program.cs index 1d6b6a7..1f426eb 100644 --- a/VoidCat/Program.cs +++ b/VoidCat/Program.cs @@ -47,6 +47,7 @@ if (useRedis) services.AddSingleton(cx.GetDatabase()); } +services.AddHttpClient(); services.AddCors(opt => { opt.AddDefaultPolicy(p => diff --git a/VoidCat/Services/VirusScanner/VirusScannerStartup.cs b/VoidCat/Services/VirusScanner/VirusScannerStartup.cs index a2f4afc..e8072a8 100644 --- a/VoidCat/Services/VirusScanner/VirusScannerStartup.cs +++ b/VoidCat/Services/VirusScanner/VirusScannerStartup.cs @@ -1,6 +1,7 @@ using nClam; using VoidCat.Model; using VoidCat.Services.Abstractions; +using VoidCat.Services.VirusScanner.VirusTotal; namespace VoidCat.Services.VirusScanner; @@ -25,6 +26,14 @@ public static class VirusScannerStartup }); services.AddTransient(); } + + // load VirusTotal + if (avSettings.VirusTotal != default) + { + services.AddTransient((svc) => + new VirusTotalClient(svc.GetRequiredService(), avSettings.VirusTotal)); + services.AddTransient(); + } } } } \ No newline at end of file diff --git a/VoidCat/Services/VirusScanner/VirusTotal/VirusTotalClasses.cs b/VoidCat/Services/VirusScanner/VirusTotal/VirusTotalClasses.cs new file mode 100644 index 0000000..dae7857 --- /dev/null +++ b/VoidCat/Services/VirusScanner/VirusTotal/VirusTotalClasses.cs @@ -0,0 +1,327 @@ +using System.Runtime.Serialization; +using Newtonsoft.Json; + +namespace VoidCat.Services.VirusScanner.VirusTotal; + +// Root myDeserializedClass = JsonConvert.DeserializeObject(myJsonResponse); +public class AlertContext +{ + [JsonProperty("proto")] public string Proto { get; set; } + + [JsonProperty("src_ip")] public string SrcIp { get; set; } + + [JsonProperty("src_port")] public int SrcPort { get; set; } +} + +public class CrowdsourcedIdsResult +{ + [JsonProperty("alert_context")] public List AlertContext { get; set; } + + [JsonProperty("alert_severity")] public string AlertSeverity { get; set; } + + [JsonProperty("rule_category")] public string RuleCategory { get; set; } + + [JsonProperty("rule_id")] public string RuleId { get; set; } + + [JsonProperty("rule_msg")] public string RuleMsg { get; set; } + + [JsonProperty("rule_source")] public string RuleSource { get; set; } +} + +public class CrowdsourcedIdsStats +{ + [JsonProperty("high")] public int High { get; set; } + + [JsonProperty("info")] public int Info { get; set; } + + [JsonProperty("low")] public int Low { get; set; } + + [JsonProperty("medium")] public int Medium { get; set; } +} + +public class CrowdsourcedYaraResult +{ + [JsonProperty("description")] public string Description { get; set; } + + [JsonProperty("match_in_subfile")] public bool MatchInSubfile { get; set; } + + [JsonProperty("rule_name")] public string RuleName { get; set; } + + [JsonProperty("ruleset_id")] public string RulesetId { get; set; } + + [JsonProperty("ruleset_name")] public string RulesetName { get; set; } + + [JsonProperty("source")] public string Source { get; set; } +} + +public class ALYac +{ + [JsonProperty("category")] public string Category { get; set; } + + [JsonProperty("engine_name")] public string EngineName { get; set; } + + [JsonProperty("engine_update")] public string EngineUpdate { get; set; } + + [JsonProperty("engine_version")] public string EngineVersion { get; set; } + + [JsonProperty("method")] public string Method { get; set; } + + [JsonProperty("result")] public string Result { get; set; } +} + +public class APEX +{ + [JsonProperty("category")] public string Category { get; set; } + + [JsonProperty("engine_name")] public string EngineName { get; set; } + + [JsonProperty("engine_update")] public string EngineUpdate { get; set; } + + [JsonProperty("engine_version")] public string EngineVersion { get; set; } + + [JsonProperty("method")] public string Method { get; set; } + + [JsonProperty("result")] public string Result { get; set; } +} + +public class AVG +{ + [JsonProperty("category")] public string Category { get; set; } + + [JsonProperty("engine_name")] public string EngineName { get; set; } + + [JsonProperty("engine_update")] public string EngineUpdate { get; set; } + + [JsonProperty("engine_version")] public string EngineVersion { get; set; } + + [JsonProperty("method")] public string Method { get; set; } + + [JsonProperty("result")] public string Result { get; set; } +} + +public class Acronis +{ + [JsonProperty("category")] public string Category { get; set; } + + [JsonProperty("engine_name")] public string EngineName { get; set; } + + [JsonProperty("engine_update")] public string EngineUpdate { get; set; } + + [JsonProperty("engine_version")] public string EngineVersion { get; set; } + + [JsonProperty("method")] public string Method { get; set; } + + [JsonProperty("result")] public object Result { get; set; } +} + +public class LastAnalysisResults +{ + [JsonProperty("ALYac")] public ALYac ALYac { get; set; } + + [JsonProperty("APEX")] public APEX APEX { get; set; } + + [JsonProperty("AVG")] public AVG AVG { get; set; } + + [JsonProperty("Acronis")] public Acronis Acronis { get; set; } +} + +public class LastAnalysisStats +{ + [JsonProperty("confirmed-timeout")] public int ConfirmedTimeout { get; set; } + + [JsonProperty("failure")] public int Failure { get; set; } + + [JsonProperty("harmless")] public int Harmless { get; set; } + + [JsonProperty("malicious")] public int Malicious { get; set; } + + [JsonProperty("suspicious")] public int Suspicious { get; set; } + + [JsonProperty("timeout")] public int Timeout { get; set; } + + [JsonProperty("type-unsupported")] public int TypeUnsupported { get; set; } + + [JsonProperty("undetected")] public int Undetected { get; set; } +} + +public class VirusTotalJujubox +{ + [JsonProperty("category")] public string Category { get; set; } + + [JsonProperty("confidence")] public int Confidence { get; set; } + + [JsonProperty("malware_classification")] + public List MalwareClassification { get; set; } + + [JsonProperty("malware_names")] public List MalwareNames { get; set; } + + [JsonProperty("sandbox_name")] public string SandboxName { get; set; } +} + +public class SandboxVerdicts +{ + [JsonProperty("VirusTotal Jujubox")] public VirusTotalJujubox VirusTotalJujubox { get; set; } +} + +public class SigmaAnalysisStats +{ + [JsonProperty("critical")] public int Critical { get; set; } + + [JsonProperty("high")] public int High { get; set; } + + [JsonProperty("low")] public int Low { get; set; } + + [JsonProperty("medium")] public int Medium { get; set; } +} + +public class SigmaIntegratedRuleSetGitHub +{ + [JsonProperty("critical")] public int Critical { get; set; } + + [JsonProperty("high")] public int High { get; set; } + + [JsonProperty("low")] public int Low { get; set; } + + [JsonProperty("medium")] public int Medium { get; set; } +} + +public class SigmaAnalysisSummary +{ + [JsonProperty("Sigma Integrated Rule Set (GitHub)")] + public SigmaIntegratedRuleSetGitHub SigmaIntegratedRuleSetGitHub { get; set; } +} + +public class TotalVotes +{ + [JsonProperty("harmless")] public int Harmless { get; set; } + + [JsonProperty("malicious")] public int Malicious { get; set; } +} + +public class Attributes +{ + [JsonProperty("capabilities_tags")] public List CapabilitiesTags { get; set; } + + [JsonProperty("creation_date")] public int CreationDate { get; set; } + + [JsonProperty("crowdsourced_ids_results")] + public List CrowdsourcedIdsResults { get; set; } + + [JsonProperty("crowdsourced_ids_stats")] + public CrowdsourcedIdsStats CrowdsourcedIdsStats { get; set; } + + [JsonProperty("crowdsourced_yara_results")] + public List CrowdsourcedYaraResults { get; set; } + + [JsonProperty("downloadable")] public bool Downloadable { get; set; } + + [JsonProperty("first_submission_date")] + public int FirstSubmissionDate { get; set; } + + [JsonProperty("last_analysis_date")] public int LastAnalysisDate { get; set; } + + [JsonProperty("last_analysis_results")] + public LastAnalysisResults LastAnalysisResults { get; set; } + + [JsonProperty("last_analysis_stats")] public LastAnalysisStats LastAnalysisStats { get; set; } + + [JsonProperty("last_modification_date")] + public int LastModificationDate { get; set; } + + [JsonProperty("last_submission_date")] public int LastSubmissionDate { get; set; } + + [JsonProperty("md5")] public string Md5 { get; set; } + + [JsonProperty("meaningful_name")] public string MeaningfulName { get; set; } + + [JsonProperty("names")] public List Names { get; set; } + + [JsonProperty("reputation")] public int Reputation { get; set; } + + [JsonProperty("sandbox_verdicts")] public SandboxVerdicts SandboxVerdicts { get; set; } + + [JsonProperty("sha1")] public string Sha1 { get; set; } + + [JsonProperty("sha256")] public string Sha256 { get; set; } + + [JsonProperty("sigma_analysis_stats")] public SigmaAnalysisStats SigmaAnalysisStats { get; set; } + + [JsonProperty("sigma_analysis_summary")] + public SigmaAnalysisSummary SigmaAnalysisSummary { get; set; } + + [JsonProperty("size")] public int Size { get; set; } + + [JsonProperty("tags")] public List Tags { get; set; } + + [JsonProperty("times_submitted")] public int TimesSubmitted { get; set; } + + [JsonProperty("total_votes")] public TotalVotes TotalVotes { get; set; } + + [JsonProperty("type_description")] public string TypeDescription { get; set; } + + [JsonProperty("type_tag")] public string TypeTag { get; set; } + + [JsonProperty("unique_sources")] public int UniqueSources { get; set; } + + [JsonProperty("vhash")] public string Vhash { get; set; } +} + +public class Links +{ + [JsonProperty("self")] public string Self { get; set; } +} + +public class File +{ + [JsonProperty("attributes")] public Attributes Attributes { get; set; } + + [JsonProperty("id")] public string Id { get; set; } + + [JsonProperty("links")] public Links Links { get; set; } + + [JsonProperty("type")] public string Type { get; set; } +} + +public class Error +{ + [JsonProperty("code")] + public string Code { get; set; } + + [JsonProperty("message")] + public string Message { get; set; } +} + +// ReSharper disable once InconsistentNaming +public class VTResponse +{ + [JsonProperty("data")] + public T Data { get; set; } + + [JsonProperty("error")] + public Error Error { get; set; } +} + +public class VTException : Exception +{ + public VTException(Error error) + { + Error = error; + } + + protected VTException(SerializationInfo info, StreamingContext context, Error error) : base(info, context) + { + Error = error; + } + + public VTException(string? message, Error error) : base(message) + { + Error = error; + } + + public VTException(string? message, Exception? innerException, Error error) : base(message, innerException) + { + Error = error; + } + + public Error Error { get; } +} \ No newline at end of file diff --git a/VoidCat/Services/VirusScanner/VirusTotal/VirusTotalClient.cs b/VoidCat/Services/VirusScanner/VirusTotal/VirusTotalClient.cs new file mode 100644 index 0000000..09e22f1 --- /dev/null +++ b/VoidCat/Services/VirusScanner/VirusTotal/VirusTotalClient.cs @@ -0,0 +1,48 @@ +using System.Text; +using Newtonsoft.Json; +using VoidCat.Model; + +namespace VoidCat.Services.VirusScanner.VirusTotal; + +public class VirusTotalClient +{ + private readonly HttpClient _client; + + public VirusTotalClient(IHttpClientFactory clientFactory, VirusTotalConfig config) + { + _client = clientFactory.CreateClient(); + + _client.BaseAddress = new Uri("https://www.virustotal.com/"); + _client.DefaultRequestHeaders.Add("x-apikey", config.ApiKey); + _client.DefaultRequestHeaders.Add("accept", "application/json"); + } + + public async Task GetReport(string id) + { + return await SendRequest(HttpMethod.Get, $"/api/v3/files/{id}"); + } + + private Task SendRequest(HttpMethod method, string path) + { + return SendRequest(method, path); + } + + private async Task SendRequest(HttpMethod method, string path, TRequest? body = null) + where TRequest : class + { + var req = new HttpRequestMessage(method, path); + if (body != default) + { + var json = JsonConvert.SerializeObject(body); + req.Content = new ByteArrayContent(Encoding.UTF8.GetBytes(json)); + } + + var rsp = await _client.SendAsync(req); + var rspBody = await rsp.Content.ReadAsStringAsync(); + var vtResponse = JsonConvert.DeserializeObject>(rspBody); + if (vtResponse == default) throw new Exception("Failed?"); + if (vtResponse.Error != default) throw new VTException(vtResponse.Error); + + return vtResponse.Data; + } +} \ No newline at end of file diff --git a/VoidCat/Services/VirusScanner/VirusTotal/VirusTotalScanner.cs b/VoidCat/Services/VirusScanner/VirusTotal/VirusTotalScanner.cs new file mode 100644 index 0000000..6d15a2d --- /dev/null +++ b/VoidCat/Services/VirusScanner/VirusTotal/VirusTotalScanner.cs @@ -0,0 +1,38 @@ +using System.Security.Cryptography; +using VoidCat.Model; +using VoidCat.Services.Abstractions; + +namespace VoidCat.Services.VirusScanner.VirusTotal; + +public class VirusTotalScanner : IVirusScanner +{ + private readonly ILogger _logger; + private readonly VirusTotalClient _client; + private readonly IFileStore _fileStore; + + public VirusTotalScanner(ILogger logger, VirusTotalClient client, IFileStore fileStore) + { + _logger = logger; + _client = client; + _fileStore = fileStore; + } + + public async ValueTask ScanFile(Guid id, CancellationToken cts) + { + await using var fs = await _fileStore.Open(new(id, Enumerable.Empty()), cts); + + // hash file and check on VT + var hash = await SHA256.Create().ComputeHashAsync(fs, cts); + + var report = await _client.GetReport(hash.ToHex()); + if (report != default) + { + return new() + { + IsVirus = report.Attributes.Reputation == 0 + }; + } + + throw new InvalidOperationException(); + } +} \ No newline at end of file