├── .gitattributes ├── .gitignore ├── LICENSE.txt ├── Properties.targets ├── README.md └── src ├── VirusTotalNet.Examples ├── Program.cs └── VirusTotalNet.Examples.csproj ├── VirusTotalNet.Tests ├── CommentTests.cs ├── DateTimeParserTests.cs ├── DomainReportTests.cs ├── FileReportTests.cs ├── FileRescanTests.cs ├── FileScanTests.cs ├── GeneralTests.cs ├── IPReportTests.cs ├── Properties │ └── AssemblyInfo.cs ├── ResourceHelperTests.cs ├── TestDataTests.cs ├── TestInternals │ ├── FailingContractResolver.cs │ ├── TestBase.cs │ └── TestData.cs ├── UrlReportTests.cs ├── UrlScanTests.cs └── VirusTotalNet.Tests.csproj ├── VirusTotalNet.sln └── VirusTotalNet ├── Enums └── ResourceType.cs ├── Exceptions ├── AccessDeniedException.cs ├── InvalidDateTimeException.cs ├── InvalidResourceException.cs ├── RateLimitException.cs ├── ResourceLimitException.cs └── SizeLimitException.cs ├── Helpers ├── HashHelper.cs └── ResourcesHelper.cs ├── Internal ├── DateTimeParsers │ ├── UnixTimeConverter.cs │ └── YearMonthDayConverter.cs ├── Helpers │ └── EnumHelper.cs ├── Objects │ └── LargeFileUpload.cs └── Other │ └── CustomURLEncodedContent.cs ├── Objects ├── DetectedUrl.cs ├── DomainResolution.cs ├── IPResolution.cs ├── Sample.cs ├── SampleWithDate.cs ├── ScanEngine.cs ├── UrlScanEngine.cs ├── UserComment.cs ├── WebutationInfo.cs └── WotInfo.cs ├── Properties └── AssemblyInfo.cs ├── ResponseCodes ├── CommentResponseCode.cs ├── DomainResponseCode.cs ├── FileReportResponseCode.cs ├── IPReportResponseCode.cs ├── RescanResponseCode.cs ├── ScanFileResponseCode.cs ├── UrlReportResponseCode.cs └── UrlScanResponseCode.cs ├── Results ├── CommentResult.cs ├── CreateCommentResult.cs ├── DomainReport.cs ├── FileReport.cs ├── IPReport.cs ├── RescanResult.cs ├── ScanResult.cs ├── UrlReport.cs └── UrlScanResult.cs ├── VirusTotal.cs └── VirusTotalNet.csproj /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | 4 | # Custom for Visual Studio 5 | *.cs diff=csharp 6 | *.sln merge=union 7 | *.csproj merge=union 8 | *.vbproj merge=union 9 | *.fsproj merge=union 10 | *.dbproj merge=union 11 | 12 | # Standard to msysgit 13 | *.doc diff=astextplain 14 | *.DOC diff=astextplain 15 | *.docx diff=astextplain 16 | *.DOCX diff=astextplain 17 | *.dot diff=astextplain 18 | *.DOT diff=astextplain 19 | *.pdf diff=astextplain 20 | *.PDF diff=astextplain 21 | *.rtf diff=astextplain 22 | *.RTF diff=astextplain 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ################# 2 | ## Visual Studio 3 | ################# 4 | 5 | ## Ignore Visual Studio temporary files, build results, and 6 | ## files generated by popular Visual Studio add-ons. 7 | 8 | # User-specific files 9 | *.suo 10 | *.user 11 | *.sln.docstates 12 | 13 | # Build results 14 | 15 | [Dd]ebug/ 16 | [Rr]elease/ 17 | x64/ 18 | build/ 19 | [Bb]in/ 20 | [Oo]bj/ 21 | 22 | # MSTest test Results 23 | [Tt]est[Rr]esult*/ 24 | [Bb]uild[Ll]og.* 25 | 26 | *_i.c 27 | *_p.c 28 | *.ilk 29 | *.meta 30 | *.obj 31 | *.pch 32 | *.pdb 33 | *.pgc 34 | *.pgd 35 | *.rsp 36 | *.sbr 37 | *.tlb 38 | *.tli 39 | *.tlh 40 | *.tmp 41 | *.tmp_proj 42 | *.log 43 | *.vspscc 44 | *.vssscc 45 | .builds 46 | *.pidb 47 | *.log 48 | *.scc 49 | 50 | # Visual C++ cache files 51 | ipch/ 52 | *.aps 53 | *.ncb 54 | *.opensdf 55 | *.sdf 56 | *.cachefile 57 | 58 | # Visual Studio profiler 59 | *.psess 60 | *.vsp 61 | *.vspx 62 | 63 | # Guidance Automation Toolkit 64 | *.gpState 65 | 66 | # ReSharper is a .NET coding add-in 67 | _ReSharper*/ 68 | *.[Rr]e[Ss]harper 69 | 70 | # TeamCity is a build add-in 71 | _TeamCity* 72 | 73 | # DotCover is a Code Coverage Tool 74 | *.dotCover 75 | 76 | # NCrunch 77 | *.ncrunch* 78 | .*crunch*.local.xml 79 | 80 | # Installshield output folder 81 | [Ee]xpress/ 82 | 83 | # DocProject is a documentation generator add-in 84 | DocProject/buildhelp/ 85 | DocProject/Help/*.HxT 86 | DocProject/Help/*.HxC 87 | DocProject/Help/*.hhc 88 | DocProject/Help/*.hhk 89 | DocProject/Help/*.hhp 90 | DocProject/Help/Html2 91 | DocProject/Help/html 92 | 93 | # Click-Once directory 94 | publish/ 95 | 96 | # Publish Web Output 97 | *.Publish.xml 98 | *.pubxml 99 | *.publishproj 100 | 101 | # NuGet Packages Directory 102 | ## TODO: If you have NuGet Package Restore enabled, uncomment the next line 103 | #packages/ 104 | 105 | # Windows Azure Build Output 106 | csx 107 | *.build.csdef 108 | 109 | # Windows Store app package directory 110 | AppPackages/ 111 | 112 | # Others 113 | sql/ 114 | *.Cache 115 | ClientBin/ 116 | [Ss]tyle[Cc]op.* 117 | ~$* 118 | *~ 119 | *.dbmdl 120 | *.[Pp]ublish.xml 121 | *.pfx 122 | *.publishsettings 123 | 124 | # RIA/Silverlight projects 125 | Generated_Code/ 126 | 127 | # Backup & report files from converting an old project file to a newer 128 | # Visual Studio version. Backup files are not needed, because we have git ;-) 129 | _UpgradeReport_Files/ 130 | Backup*/ 131 | UpgradeLog*.XML 132 | UpgradeLog*.htm 133 | 134 | # SQL Server files 135 | App_Data/*.mdf 136 | App_Data/*.ldf 137 | 138 | ############# 139 | ## Windows detritus 140 | ############# 141 | 142 | # Windows image file caches 143 | Thumbs.db 144 | ehthumbs.db 145 | 146 | # Folder config file 147 | Desktop.ini 148 | 149 | # Recycle Bin used on file shares 150 | $RECYCLE.BIN/ 151 | 152 | # Mac crap 153 | .DS_Store 154 | 155 | BenchmarkDotNet.Artifacts/ 156 | .vs/ 157 | .idea/ -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Ian Qvist 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Properties.targets: -------------------------------------------------------------------------------- 1 | 2 | 3 | 2.2.0 4 | latest 5 | Copyright 2023, by Ian Qvist. All rights reserved. 6 | Ian Qvist 7 | git 8 | https://github.com/Genbox/VirusTotalNet/ 9 | https://github.com/Genbox/VirusTotalNet/ 10 | MIT 11 | 12 | true 13 | true 14 | true 15 | snupkg 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # VirusTotal.NET - A full implementation of the VirusTotal 2.0 API 2 | 3 | [![NuGet](https://img.shields.io/nuget/v/VirusTotalNet.svg?style=flat-square&label=nuget)](https://www.nuget.org/packages/VirusTotalNet/) 4 | 5 | ### Features 6 | 7 | * Fully asynchronous API 8 | * Scan, rescan and get reports of scanned files and URLs 9 | * Get reports for IP addresses, URLs, and domains 10 | * Batch support for APIs that support it 11 | * Size and resource limits built in for better performance 12 | * Configurable limits to accommodate some VT private API features. However, this API does not officially support the private VT API. 13 | 14 | ### Examples 15 | 16 | ```csharp 17 | VirusTotal virusTotal = new VirusTotal("YOUR API KEY HERE"); 18 | 19 | //Use HTTPS instead of HTTP 20 | virusTotal.UseTLS = true; 21 | 22 | //Create the EICAR test virus. See http://www.eicar.org/86-0-Intended-use.html 23 | byte[] eicar = Encoding.ASCII.GetBytes(@"X5O!P%@AP[4\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!$H+H*"); 24 | 25 | //Check if the file has been scanned before. 26 | FileReport report = await virusTotal.GetFileReportAsync(eicar); 27 | 28 | Console.WriteLine("Seen before: " + (report.ResponseCode == FileReportResponseCode.Present ? "Yes" : "No")); 29 | ``` 30 | 31 | Output: 32 | ``` 33 | Seen before: True 34 | ``` 35 | 36 | Take a look at the VirusTotal.Examples project for more examples. -------------------------------------------------------------------------------- /src/VirusTotalNet.Examples/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | using System.Threading.Tasks; 5 | using VirusTotalNet.Objects; 6 | using VirusTotalNet.ResponseCodes; 7 | using VirusTotalNet.Results; 8 | 9 | namespace VirusTotalNet.Examples; 10 | 11 | internal class Program 12 | { 13 | private static async Task Main(string[] args) 14 | { 15 | using VirusTotal virusTotal = new VirusTotal("YOUR API KEY HERE"); 16 | 17 | //Use HTTPS instead of HTTP 18 | virusTotal.UseTLS = true; 19 | 20 | //Create the EICAR test virus. See http://www.eicar.org/86-0-Intended-use.html 21 | byte[] eicar = Encoding.ASCII.GetBytes(@"X5O!P%@AP[4\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!$H+H*"); 22 | 23 | //Check if the file has been scanned before. 24 | FileReport fileReport = await virusTotal.GetFileReportAsync(eicar); 25 | 26 | bool hasFileBeenScannedBefore = fileReport.ResponseCode == FileReportResponseCode.Present; 27 | 28 | Console.WriteLine("File has been scanned before: " + (hasFileBeenScannedBefore ? "Yes" : "No")); 29 | 30 | //If the file has been scanned before, the results are embedded inside the report. 31 | if (hasFileBeenScannedBefore) 32 | { 33 | PrintScan(fileReport); 34 | } 35 | else 36 | { 37 | ScanResult fileResult = await virusTotal.ScanFileAsync(eicar, "EICAR.txt"); 38 | PrintScan(fileResult); 39 | } 40 | 41 | Console.WriteLine(); 42 | 43 | string scanUrl = "http://www.google.com/"; 44 | 45 | UrlReport urlReport = await virusTotal.GetUrlReportAsync(scanUrl); 46 | 47 | bool hasUrlBeenScannedBefore = urlReport.ResponseCode == UrlReportResponseCode.Present; 48 | Console.WriteLine("URL has been scanned before: " + (hasUrlBeenScannedBefore ? "Yes" : "No")); 49 | 50 | //If the url has been scanned before, the results are embedded inside the report. 51 | if (hasUrlBeenScannedBefore) 52 | { 53 | PrintScan(urlReport); 54 | } 55 | else 56 | { 57 | UrlScanResult urlResult = await virusTotal.ScanUrlAsync(scanUrl); 58 | PrintScan(urlResult); 59 | } 60 | } 61 | 62 | private static void PrintScan(UrlScanResult scanResult) 63 | { 64 | Console.WriteLine("Scan ID: " + scanResult.ScanId); 65 | Console.WriteLine("Message: " + scanResult.VerboseMsg); 66 | Console.WriteLine(); 67 | } 68 | 69 | private static void PrintScan(ScanResult scanResult) 70 | { 71 | Console.WriteLine("Scan ID: " + scanResult.ScanId); 72 | Console.WriteLine("Message: " + scanResult.VerboseMsg); 73 | Console.WriteLine(); 74 | } 75 | 76 | private static void PrintScan(FileReport fileReport) 77 | { 78 | Console.WriteLine("Scan ID: " + fileReport.ScanId); 79 | Console.WriteLine("Message: " + fileReport.VerboseMsg); 80 | 81 | if (fileReport.ResponseCode == FileReportResponseCode.Present) 82 | { 83 | foreach (KeyValuePair scan in fileReport.Scans) 84 | { 85 | Console.WriteLine("{0,-25} Detected: {1}", scan.Key, scan.Value.Detected); 86 | } 87 | } 88 | 89 | Console.WriteLine(); 90 | } 91 | 92 | private static void PrintScan(UrlReport urlReport) 93 | { 94 | Console.WriteLine("Scan ID: " + urlReport.ScanId); 95 | Console.WriteLine("Message: " + urlReport.VerboseMsg); 96 | 97 | if (urlReport.ResponseCode == UrlReportResponseCode.Present) 98 | { 99 | foreach (KeyValuePair scan in urlReport.Scans) 100 | { 101 | Console.WriteLine("{0,-25} Detected: {1}", scan.Key, scan.Value.Detected); 102 | } 103 | } 104 | 105 | Console.WriteLine(); 106 | } 107 | } -------------------------------------------------------------------------------- /src/VirusTotalNet.Examples/VirusTotalNet.Examples.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | net7.0 6 | Exe 7 | false 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/VirusTotalNet.Tests/CommentTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using System.Threading.Tasks; 4 | using VirusTotalNet.Helpers; 5 | using VirusTotalNet.ResponseCodes; 6 | using VirusTotalNet.Results; 7 | using VirusTotalNet.Tests.TestInternals; 8 | using Xunit; 9 | 10 | namespace VirusTotalNet.Tests; 11 | 12 | public class CommentTests : TestBase 13 | { 14 | [Fact] 15 | public async Task CreateValidComment() 16 | { 17 | CreateCommentResult comment = await VirusTotal.CreateCommentAsync(TestData.TestHash, "VirusTotal.NET test - " + DateTime.UtcNow.ToString("O")); 18 | Assert.Equal(CommentResponseCode.Success, comment.ResponseCode); 19 | Assert.Equal("Your comment was successfully posted", comment.VerboseMsg); 20 | } 21 | 22 | [Fact] 23 | public async Task CreateCommentOnUnknownResource() 24 | { 25 | CreateCommentResult comment = await VirusTotal.CreateCommentAsync(TestData.GetRandomSHA1s(1).First(), "VirusTotal.NET test - " + DateTime.UtcNow.ToString("O")); 26 | Assert.Equal(CommentResponseCode.Error, comment.ResponseCode); 27 | Assert.Equal("Could not find resource", comment.VerboseMsg); 28 | } 29 | 30 | [Fact] 31 | public async Task CreateDuplicateComment() 32 | { 33 | //Create the comment. This might fail with an error, but it does not matter. 34 | await VirusTotal.CreateCommentAsync(TestData.TestHash, "VirusTotal.NET test"); 35 | 36 | CreateCommentResult comment = await VirusTotal.CreateCommentAsync(TestData.TestHash, "VirusTotal.NET test"); 37 | Assert.Equal(CommentResponseCode.Error, comment.ResponseCode); 38 | Assert.Equal("Duplicate comment", comment.VerboseMsg); 39 | } 40 | 41 | [Fact] 42 | public async Task CreateLargeComment() 43 | { 44 | byte[] content = new byte[1024 * 4]; 45 | string contentInHex = HashHelper.ByteArrayToHex(content);//2x size now 46 | 47 | await Assert.ThrowsAsync(async () => await VirusTotal.CreateCommentAsync(TestData.TestHash, contentInHex)); 48 | } 49 | 50 | [Fact] 51 | public async Task CreateEmptyComment() 52 | { 53 | await Assert.ThrowsAsync(async () => await VirusTotal.CreateCommentAsync(TestData.TestHash, string.Empty)); 54 | } 55 | 56 | //[Fact] 57 | //public async Task GetComment() 58 | //{ 59 | // CommentResult comment = await VirusTotal.GetCommentAsync(TestData.TestHash); 60 | //} 61 | 62 | //[Fact] 63 | //public async Task GetCommentOnUnknownResource() 64 | //{ 65 | // CommentResult comment = await VirusTotal.GetCommentAsync(TestData.GetRandomSHA1s(1).First()); 66 | //} 67 | 68 | //[Fact] 69 | //public async Task GetCommentWithBefore() 70 | //{ 71 | // CommentResult comment = await VirusTotal.GetCommentAsync(TestData.TestHash, DateTime.UtcNow); //TODO: before 72 | //} 73 | } -------------------------------------------------------------------------------- /src/VirusTotalNet.Tests/DateTimeParserTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using Newtonsoft.Json; 4 | using VirusTotalNet.Objects; 5 | using VirusTotalNet.Results; 6 | using VirusTotalNet.Tests.TestInternals; 7 | using Xunit; 8 | 9 | namespace VirusTotalNet.Tests; 10 | 11 | public class DateTimeParserTests : TestBase 12 | { 13 | [Fact] 14 | public void YearMonthDayConverterTest() 15 | { 16 | FileReport fileReport = new FileReport 17 | { 18 | ScanDate = new DateTime(2017, 08, 10), 19 | Scans = new Dictionary() 20 | }; 21 | 22 | ScanEngine scan1 = new ScanEngine { Detected = true, Result = "Malware.Undefined!8.C (cloud:nWdia2XyY0T)", Version = "25.0.0.1", Update = new DateTime(2017, 07, 26) }; 23 | ScanEngine scan2 = new ScanEngine { Detected = false, Result = null, Version = "5.5.1.3", Update = new DateTime(2017, 07, 25) }; 24 | ScanEngine scan3 = new ScanEngine { Detected = false, Result = null, Version = "1.0.1.223", Update = new DateTime(2017, 07, 18) }; 25 | 26 | fileReport.Scans.Add("Rising", scan1); 27 | fileReport.Scans.Add("Yandex", scan2); 28 | fileReport.Scans.Add("SentinelOne", scan3); 29 | 30 | string fileReportJson = JsonConvert.SerializeObject(fileReport.Scans); 31 | Assert.Contains("\"Update\":\"20170726\"", fileReportJson); 32 | Assert.Contains("\"Update\":\"20170725\"", fileReportJson); 33 | Assert.Contains("\"Update\":\"20170718\"", fileReportJson); 34 | } 35 | } -------------------------------------------------------------------------------- /src/VirusTotalNet.Tests/DomainReportTests.cs: -------------------------------------------------------------------------------- 1 | using System.Linq; 2 | using System.Threading.Tasks; 3 | using VirusTotalNet.ResponseCodes; 4 | using VirusTotalNet.Results; 5 | using VirusTotalNet.Tests.TestInternals; 6 | using Xunit; 7 | 8 | namespace VirusTotalNet.Tests; 9 | 10 | public class DomainReportTests : TestBase 11 | { 12 | [Fact] 13 | public async Task GetDomainReportKnownDomain() 14 | { 15 | IgnoreMissingJson(" / Alexa category", " / alphaMountain.ai category", " / Categories", " / Comodo Valkyrie Verdict category", " / Dr.Web category", " / TrendMicro category", " / Websense ThreatSeeker category"); 16 | 17 | DomainReport report = await VirusTotal.GetDomainReportAsync(TestData.KnownDomains[0]); 18 | Assert.Equal(DomainResponseCode.Present, report.ResponseCode); 19 | } 20 | 21 | //[Fact] 22 | //public async Task GetDomainReportInvalidDomain() 23 | //{ 24 | // //TODO: I can't find a domain that VT does not think is valid. 25 | // //Domains tried: 26 | // //- 27 | // //. 28 | // //%20 29 | // //%2F 30 | //} 31 | 32 | [Fact] 33 | public async Task GetDomainReportUnknownDomain() 34 | { 35 | //Reports don't contain all these fields when it is unknown 36 | IgnoreMissingJson(" / Alexa category", " / Alexa domain info", " / Alexa rank", " / alphaMountain.ai category", " / BitDefender category", " / BitDefender domain info", " / Categories", " / Comodo Valkyrie Verdict category", " / detected_communicating_samples", " / detected_downloaded_samples", " / detected_referrer_samples", " / detected_urls", " / domain_siblings", " / Dr.Web category", " / Forcepoint ThreatSeeker category", " / Opera domain info", " / Pcaps", " / Resolutions", " / Sophos category", " / subdomains", " / TrendMicro category", " / undetected_communicating_samples", " / undetected_downloaded_samples", " / undetected_referrer_samples", " / undetected_urls", " / Websense ThreatSeeker category", " / Webutation domain info", " / whois", " / whois_timestamp", " / WOT domain info", " / Xcitium Verdict Cloud category"); 37 | 38 | DomainReport report = await VirusTotal.GetDomainReportAsync(TestData.GetUnknownDomains(1).First()); 39 | Assert.Equal(DomainResponseCode.NotPresent, report.ResponseCode); 40 | } 41 | } -------------------------------------------------------------------------------- /src/VirusTotalNet.Tests/FileReportTests.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | using System.Threading.Tasks; 4 | using VirusTotalNet.Exceptions; 5 | using VirusTotalNet.ResponseCodes; 6 | using VirusTotalNet.Results; 7 | using VirusTotalNet.Tests.TestInternals; 8 | using Xunit; 9 | 10 | namespace VirusTotalNet.Tests; 11 | 12 | public class FileReportTests : TestBase 13 | { 14 | [Fact] 15 | public async Task GetReportForKnownFile() 16 | { 17 | FileReport fileReport = await VirusTotal.GetFileReportAsync(TestData.EICARMalware); 18 | 19 | //It should always be in the VirusTotal database. 20 | Assert.Equal(FileReportResponseCode.Present, fileReport.ResponseCode); 21 | } 22 | 23 | //[Fact] 24 | //public async Task GetReportForInvalidFile() 25 | //{ 26 | // //TODO: I can't seem to provoke an error response code by sending resources that are invalid. 27 | // //They just seem to either give code 0 (notpresent) or an empty JSON response 28 | //} 29 | 30 | [Fact] 31 | public async Task GetMultipleReportForKnownFiles() 32 | { 33 | IEnumerable results = await VirusTotal.GetFileReportsAsync(TestData.KnownHashes); 34 | 35 | foreach (FileReport fileReport in results) 36 | { 37 | //It should always be in the VirusTotal database. 38 | Assert.Equal(FileReportResponseCode.Present, fileReport.ResponseCode); 39 | } 40 | } 41 | 42 | [Fact] 43 | public async Task GetReportForUnknownFile() 44 | { 45 | //Reports for unknown files do not have these fields 46 | IgnoreMissingJson(" / MD5", " / Permalink", " / Positives", " / scan_date", " / scan_id", " / Scans", " / SHA1", " / SHA256", " / Total"); 47 | 48 | FileReport fileReport = await VirusTotal.GetFileReportAsync(TestData.GetRandomSHA1s(1).First()); 49 | 50 | //It should not be in the VirusTotal database already, which means it should return error. 51 | Assert.Equal(FileReportResponseCode.NotPresent, fileReport.ResponseCode); 52 | } 53 | 54 | [Fact] 55 | public async Task GetMultipleReportForUnknownFiles() 56 | { 57 | //Reports for unknown files do not have these fields 58 | IgnoreMissingJson("[array] / MD5", "[array] / Permalink", "[array] / Positives", "[array] / scan_date", "[array] / scan_id", "[array] / Scans", "[array] / SHA1", "[array] / SHA256", "[array] / Total"); 59 | 60 | IEnumerable results = await VirusTotal.GetFileReportsAsync(TestData.GetRandomSHA1s(3)); 61 | 62 | foreach (FileReport fileReport in results) 63 | { 64 | //It should never be in the VirusTotal database. 65 | Assert.Equal(FileReportResponseCode.NotPresent, fileReport.ResponseCode); 66 | } 67 | } 68 | 69 | [Fact] 70 | public async void GetReportForRecentFile() 71 | { 72 | //We ignore these fields due to unknown file 73 | IgnoreMissingJson(" / MD5", " / Permalink", " / Positives", " / scan_date", " / Scans", " / SHA1", " / SHA256", " / Total"); 74 | 75 | ScanResult result = await VirusTotal.ScanFileAsync(TestData.GetRandomFile(128, 1).First(), TestData.TestFileName); 76 | 77 | FileReport fileReport = await VirusTotal.GetFileReportAsync(result.ScanId); 78 | 79 | Assert.Equal(FileReportResponseCode.Queued, fileReport.ResponseCode); 80 | } 81 | 82 | [Fact] 83 | public async void GetReportForInvalidResource() 84 | { 85 | await Assert.ThrowsAsync(async () => await VirusTotal.GetFileReportAsync("aaaaaaaaaaa")); 86 | } 87 | 88 | [Fact] 89 | public async Task FileReportBatchLimit() 90 | { 91 | IgnoreMissingJson("[array] / MD5", "[array] / Permalink", "[array] / Positives", "[array] / scan_date", "[array] / scan_id", "[array] / Scans", "[array] / SHA1", "[array] / SHA256", "[array] / Total"); 92 | 93 | VirusTotal.RestrictNumberOfResources = false; 94 | 95 | IEnumerable results = await VirusTotal.GetFileReportsAsync(TestData.GetRandomSHA1s(10)); 96 | 97 | //We only expect 4 as VT simply returns 4 results no matter the batch size. 98 | Assert.Equal(VirusTotal.FileReportBatchSizeLimit, results.Count()); 99 | } 100 | } -------------------------------------------------------------------------------- /src/VirusTotalNet.Tests/FileRescanTests.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | using System.Threading.Tasks; 4 | using VirusTotalNet.ResponseCodes; 5 | using VirusTotalNet.Results; 6 | using VirusTotalNet.Tests.TestInternals; 7 | using Xunit; 8 | 9 | namespace VirusTotalNet.Tests; 10 | 11 | public class FileRescanTests : TestBase 12 | { 13 | [Fact(Skip = "Public keys no longer have access to this")] 14 | public async Task RescanKnownFile() 15 | { 16 | RescanResult fileResult = await VirusTotal.RescanFileAsync(TestData.EICARMalware); 17 | 18 | //It should always be in the VirusTotal database. We expect it to rescan it 19 | Assert.Equal(RescanResponseCode.Queued, fileResult.ResponseCode); 20 | } 21 | 22 | //[Fact] 23 | //public async Task RescanInvalidFile() 24 | //{ 25 | // //TODO: Can't seem to provoke an error response code. 26 | //} 27 | 28 | [Fact(Skip = "Public keys no longer have access to this")] 29 | public async Task RescanMultipleKnownFile() 30 | { 31 | IEnumerable fileResult = await VirusTotal.RescanFilesAsync(TestData.KnownHashes); 32 | 33 | foreach (RescanResult rescanResult in fileResult) 34 | { 35 | //It should always be in the VirusTotal database. We expect it to rescan it 36 | Assert.Equal(RescanResponseCode.Queued, rescanResult.ResponseCode); 37 | } 38 | } 39 | 40 | [Fact(Skip = "Public keys no longer have access to this")] 41 | public async Task RescanUnknownFile() 42 | { 43 | IgnoreMissingJson(" / Permalink", " / scan_id", " / SHA256"); 44 | 45 | RescanResult fileResult = await VirusTotal.RescanFileAsync(TestData.GetRandomSHA1s(1).First()); 46 | 47 | //It should not be in the VirusTotal database already, which means it should return error. 48 | Assert.Equal(RescanResponseCode.ResourceNotFound, fileResult.ResponseCode); 49 | } 50 | 51 | [Fact(Skip = "Public keys no longer have access to this")] 52 | public async Task RescanSmallFile() 53 | { 54 | RescanResult fileResult = await VirusTotal.RescanFileAsync(new byte[1]); 55 | 56 | //It has been scanned before, we expect it to return queued. 57 | Assert.Equal(RescanResponseCode.Queued, fileResult.ResponseCode); 58 | } 59 | 60 | [Fact(Skip = "Public keys no longer have access to this")] 61 | public async Task RescanBatchLimit() 62 | { 63 | IgnoreMissingJson("[array] / Permalink", "[array] / scan_id", "[array] / SHA256"); 64 | 65 | VirusTotal.RestrictNumberOfResources = false; 66 | 67 | IEnumerable results = await VirusTotal.RescanFilesAsync(TestData.GetRandomSHA1s(50)); 68 | 69 | //We only expect 25 as VT simply returns 25 results no matter the batch size. 70 | Assert.Equal(VirusTotal.RescanBatchSizeLimit, results.Count()); 71 | } 72 | } -------------------------------------------------------------------------------- /src/VirusTotalNet.Tests/FileScanTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using System.Threading.Tasks; 4 | using VirusTotalNet.Exceptions; 5 | using VirusTotalNet.ResponseCodes; 6 | using VirusTotalNet.Results; 7 | using VirusTotalNet.Tests.TestInternals; 8 | using Xunit; 9 | 10 | namespace VirusTotalNet.Tests; 11 | 12 | public class FileScanTests : TestBase 13 | { 14 | [Fact] 15 | public async Task ScanKnownFile() 16 | { 17 | ScanResult fileResult = await VirusTotal.ScanFileAsync(TestData.EICARMalware, TestData.EICARFilename); 18 | 19 | //It should always be in the VirusTotal database. 20 | Assert.Equal(ScanFileResponseCode.Queued, fileResult.ResponseCode); 21 | } 22 | 23 | [Fact] 24 | public async Task ScanTestFile() 25 | { 26 | ScanResult fileResult = await VirusTotal.ScanFileAsync(TestData.TestFile, TestData.TestFileName); 27 | 28 | //It should always be in the VirusTotal database. 29 | Assert.Equal(ScanFileResponseCode.Queued, fileResult.ResponseCode); 30 | } 31 | 32 | [Fact] 33 | public async Task ScanUnknownFile() 34 | { 35 | ScanResult fileResult = await VirusTotal.ScanFileAsync(TestData.GetRandomFile(128, 1).First(), TestData.TestFileName); 36 | 37 | //It should never be in the VirusTotal database. 38 | Assert.Equal(ScanFileResponseCode.Queued, fileResult.ResponseCode); 39 | } 40 | 41 | [Fact] 42 | public async Task ScanSmallFile() 43 | { 44 | ScanResult fileResult = await VirusTotal.ScanFileAsync(new byte[1], TestData.TestFileName); 45 | Assert.Equal(ScanFileResponseCode.Queued, fileResult.ResponseCode); 46 | } 47 | 48 | [Fact] 49 | public async Task ScanLargeFile() 50 | { 51 | VirusTotal.Timeout = TimeSpan.FromSeconds(500); 52 | ScanResult result = await VirusTotal.ScanFileAsync(new byte[VirusTotal.FileSizeLimit], TestData.TestFileName); 53 | 54 | Assert.Equal(ScanFileResponseCode.Queued, result.ResponseCode); 55 | } 56 | 57 | [Fact] 58 | public async Task ScanLargeFileOverLimit() 59 | { 60 | //We expect it to throw a SizeLimitException because the file is above the legal limit 61 | await Assert.ThrowsAsync(async () => await VirusTotal.ScanFileAsync(new byte[VirusTotal.FileSizeLimit + 1], TestData.TestFileName)); 62 | } 63 | 64 | [Fact(Skip = "Public keys don't have access to this")] 65 | public async Task ScanVeryLargeFile() 66 | { 67 | VirusTotal.Timeout = TimeSpan.FromSeconds(500); 68 | ScanResult result = await VirusTotal.ScanLargeFileAsync(new byte[VirusTotal.LargeFileSizeLimit], TestData.TestFileName); 69 | 70 | Assert.Equal(ScanFileResponseCode.Queued, result.ResponseCode); 71 | } 72 | 73 | [Fact] 74 | public async Task ScanVeryLargeFileOverLimit() 75 | { 76 | //We expect it to throw a SizeLimitException because the file is above the legal limit 77 | await Assert.ThrowsAsync(async () => await VirusTotal.ScanFileAsync(new byte[VirusTotal.LargeFileSizeLimit + 1], TestData.TestFileName)); 78 | } 79 | 80 | [Fact] 81 | public async Task ScanLargeFileOverLimitCheckDisabled() 82 | { 83 | VirusTotal.RestrictSizeLimits = false; 84 | VirusTotal.Timeout = TimeSpan.FromSeconds(500); 85 | 86 | //4KB over the limit should be enough. it is difficult to test since VT measures the limit on total request size. 87 | await Assert.ThrowsAsync(async () => await VirusTotal.ScanFileAsync(new byte[VirusTotal.FileSizeLimit + 1024 * 4], TestData.TestFileName)); 88 | } 89 | } -------------------------------------------------------------------------------- /src/VirusTotalNet.Tests/GeneralTests.cs: -------------------------------------------------------------------------------- 1 | using System.Linq; 2 | using System.Threading.Tasks; 3 | using VirusTotalNet.Exceptions; 4 | using VirusTotalNet.Tests.TestInternals; 5 | using Xunit; 6 | 7 | namespace VirusTotalNet.Tests; 8 | 9 | public class GeneralTests : TestBase 10 | { 11 | [Fact] 12 | public async Task UnauthorizedScan() 13 | { 14 | using VirusTotal virusTotal = new VirusTotal("xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"); //64 characters 15 | await Assert.ThrowsAsync(async () => await virusTotal.GetFileReportAsync(TestData.KnownHashes.First())); 16 | } 17 | 18 | [Fact] 19 | public async Task GetRawResponse() 20 | { 21 | bool completedRaised = false; 22 | 23 | VirusTotal.OnRawResponseReceived += response => 24 | { 25 | Assert.NotEmpty(response); 26 | completedRaised = true; 27 | }; 28 | 29 | await VirusTotal.GetFileReportAsync(TestData.KnownHashes.First()); 30 | Assert.True(completedRaised); 31 | } 32 | 33 | [Fact] 34 | public async Task OnHTTPRequest() 35 | { 36 | bool completedRaised = false; 37 | 38 | VirusTotal.OnHTTPRequestSending += request => 39 | { 40 | Assert.NotNull(request); 41 | completedRaised = true; 42 | }; 43 | 44 | await VirusTotal.GetFileReportAsync(TestData.KnownHashes.First()); 45 | Assert.True(completedRaised); 46 | } 47 | 48 | [Fact] 49 | public async Task OnHTTPResponse() 50 | { 51 | bool completedRaised = false; 52 | 53 | VirusTotal.OnHTTPResponseReceived += response => 54 | { 55 | Assert.NotNull(response); 56 | completedRaised = true; 57 | }; 58 | 59 | await VirusTotal.GetFileReportAsync(TestData.KnownHashes.First()); 60 | Assert.True(completedRaised); 61 | } 62 | 63 | [Fact] 64 | public void PublicFileLink() 65 | { 66 | //By default, VirusTotal redirects to http:// based links 67 | VirusTotal.UseTLS = false; 68 | 69 | Assert.Equal("http://www.virustotal.com/#/file/99017f6eebbac24f351415dd410d522d/detection", VirusTotal.GetPublicFileScanLink("99017f6eebbac24f351415dd410d522d")); 70 | Assert.Equal("http://www.virustotal.com/#/file/4d1740485713a2ab3a4f5822a01f645fe8387f92/detection", VirusTotal.GetPublicFileScanLink("4d1740485713a2ab3a4f5822a01f645fe8387f92")); 71 | Assert.Equal("http://www.virustotal.com/#/file/52d3df0ed60c46f336c131bf2ca454f73bafdc4b04dfa2aea80746f5ba9e6d1c/detection", VirusTotal.GetPublicFileScanLink("52d3df0ed60c46f336c131bf2ca454f73bafdc4b04dfa2aea80746f5ba9e6d1c")); 72 | } 73 | 74 | [Fact] 75 | public void PublicUrlLink() 76 | { 77 | //By default, VirusTotal redirects to http:// based links 78 | VirusTotal.UseTLS = false; 79 | 80 | Assert.Equal("http://www.virustotal.com/#/url/cf4b367e49bf0b22041c6f065f4aa19f3cfe39c8d5abc0617343d1a66c6a26f5/detection", VirusTotal.GetPublicUrlScanLink("google.com")); 81 | Assert.Equal("http://www.virustotal.com/#/url/cf4b367e49bf0b22041c6f065f4aa19f3cfe39c8d5abc0617343d1a66c6a26f5/detection", VirusTotal.GetPublicUrlScanLink("http://google.com")); 82 | Assert.Equal("http://www.virustotal.com/#/url/9d116b1b0c1200ca75016e4c010bc94836366881b021a658ea7f8548b6543c1e/detection", VirusTotal.GetPublicUrlScanLink("https://google.com")); 83 | } 84 | } -------------------------------------------------------------------------------- /src/VirusTotalNet.Tests/IPReportTests.cs: -------------------------------------------------------------------------------- 1 | using System.Linq; 2 | using System.Threading.Tasks; 3 | using VirusTotalNet.Exceptions; 4 | using VirusTotalNet.ResponseCodes; 5 | using VirusTotalNet.Results; 6 | using VirusTotalNet.Tests.TestInternals; 7 | using Xunit; 8 | 9 | namespace VirusTotalNet.Tests; 10 | 11 | public class IPReportTests : TestBase 12 | { 13 | [Fact] 14 | public async Task GetIPReportKnownIPv4() 15 | { 16 | IgnoreMissingJson("detected_referrer_samples[array] / Date", " / Continent", " / Network"); 17 | 18 | IPReport report = await VirusTotal.GetIPReportAsync(TestData.KnownIPv4s.First()); 19 | Assert.Equal(IPReportResponseCode.Present, report.ResponseCode); 20 | } 21 | 22 | [Fact] 23 | public async Task GetIPReportRandomIPv6() 24 | { 25 | //IPv6 is not supported 26 | await Assert.ThrowsAsync(async () => await VirusTotal.GetIPReportAsync(TestData.GetRandomIPv6s(1).First())); 27 | } 28 | } -------------------------------------------------------------------------------- /src/VirusTotalNet.Tests/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using Xunit; 2 | 3 | [assembly: CollectionBehavior(MaxParallelThreads = 1)] -------------------------------------------------------------------------------- /src/VirusTotalNet.Tests/ResourceHelperTests.cs: -------------------------------------------------------------------------------- 1 | using VirusTotalNet.Enums; 2 | using VirusTotalNet.Exceptions; 3 | using VirusTotalNet.Helpers; 4 | using VirusTotalNet.Tests.TestInternals; 5 | using Xunit; 6 | 7 | namespace VirusTotalNet.Tests; 8 | 9 | public class ResourceHelperTests : TestBase 10 | { 11 | [Fact] 12 | public void ValidResources() 13 | { 14 | string[] values = 15 | { 16 | "99017f6eebbac24f351415dd410d522d", 17 | "4d1740485713a2ab3a4f5822a01f645fe8387f92", 18 | "52d3df0ed60c46f336c131bf2ca454f73bafdc4b04dfa2aea80746f5ba9e6d1c" 19 | }; 20 | 21 | ResourcesHelper.ValidateResourcea(values, ResourceType.AnyHash); 22 | 23 | ResourcesHelper.ValidateResourcea("99017f6eebbac24f351415dd410d522d", ResourceType.MD5); 24 | ResourcesHelper.ValidateResourcea("4d1740485713a2ab3a4f5822a01f645fe8387f92", ResourceType.SHA1); 25 | ResourcesHelper.ValidateResourcea("52d3df0ed60c46f336c131bf2ca454f73bafdc4b04dfa2aea80746f5ba9e6d1c", ResourceType.SHA256); 26 | ResourcesHelper.ValidateResourcea("52d3df0ed60c46f336c131bf2ca454f73bafdc4b04dfa2aea80746f5ba9e6d1c-1273894724", ResourceType.ScanId); 27 | ResourcesHelper.ValidateResourcea("https://developers.virustotal.com/v2.0/reference#file-report", ResourceType.URL); 28 | ResourcesHelper.ValidateResourcea("http://google.com/?data=fake¬true=%20", ResourceType.URL); 29 | ResourcesHelper.ValidateResourcea("domainonly.com", ResourceType.URL); 30 | } 31 | 32 | [Fact] 33 | public void InvalidResources() 34 | { 35 | Assert.Throws(() => ResourcesHelper.ValidateResourcea("99017f6eebbac24f35-1415dd410d522d", ResourceType.AnyType)); 36 | Assert.Throws(() => ResourcesHelper.ValidateResourcea("1r1", ResourceType.AnyType)); 37 | Assert.Throws(() => ResourcesHelper.ValidateResourcea("", ResourceType.AnyType)); 38 | Assert.Throws(() => ResourcesHelper.ValidateResourcea("52d3df0ed60c46f336c131bf-2ca454f73bafdc4b04dfa2aea80746f5ba9e6d1c-1273894724", ResourceType.AnyType)); 39 | Assert.Throws(() => ResourcesHelper.ValidateResourcea("https://", ResourceType.AnyType)); 40 | Assert.Throws(() => ResourcesHelper.ValidateResourcea("https:///google.com", ResourceType.AnyType)); 41 | } 42 | } -------------------------------------------------------------------------------- /src/VirusTotalNet.Tests/TestDataTests.cs: -------------------------------------------------------------------------------- 1 | using System.Linq; 2 | using VirusTotalNet.Tests.TestInternals; 3 | using Xunit; 4 | 5 | namespace VirusTotalNet.Tests; 6 | 7 | public class TestDataTests 8 | { 9 | [Fact] 10 | public void TestUnknownIPv4() 11 | { 12 | string[] unknownIPv4S = TestData.GetUnknownIPv4s(3).ToArray(); 13 | 14 | Assert.Equal("0.0.0.0", unknownIPv4S[0]); 15 | Assert.Equal("0.0.0.1", unknownIPv4S[1]); 16 | Assert.Equal("0.0.0.2", unknownIPv4S[2]); 17 | } 18 | } -------------------------------------------------------------------------------- /src/VirusTotalNet.Tests/TestInternals/FailingContractResolver.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using Newtonsoft.Json; 3 | using Newtonsoft.Json.Serialization; 4 | 5 | namespace VirusTotalNet.Tests.TestInternals; 6 | //This code comes from: https://github.com/LordMike/TMDbLib/blob/master/TMDbLibTests/JsonHelpers/FailingContractResolver.cs 7 | 8 | public class FailingContractResolver : DefaultContractResolver 9 | { 10 | protected override JsonProperty CreateProperty(MemberInfo member, MemberSerialization memberSerialization) 11 | { 12 | JsonProperty res = base.CreateProperty(member, memberSerialization); 13 | 14 | // If we haven't explicitly stated that a field is not needed, we require it for compliance 15 | if (!res.Ignored) 16 | res.Required = Required.AllowNull; 17 | 18 | return res; 19 | } 20 | } -------------------------------------------------------------------------------- /src/VirusTotalNet.Tests/TestInternals/TestBase.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Diagnostics; 4 | using System.Linq; 5 | using System.Text; 6 | using System.Text.RegularExpressions; 7 | using System.Threading; 8 | using Newtonsoft.Json; 9 | using Newtonsoft.Json.Serialization; 10 | 11 | namespace VirusTotalNet.Tests.TestInternals; 12 | //This code comes from https://github.com/LordMike/TMDbLib/blob/master/TMDbLibTests/JsonHelpers/TestBase.cs 13 | 14 | public abstract class TestBase : IDisposable 15 | { 16 | private static readonly Regex _normalizeRegex = new Regex(@"\[[\d]+\]", RegexOptions.Compiled); 17 | private readonly List _errors = new List(); 18 | private readonly List _ignoreMissingCSharp; 19 | private readonly List _ignoreMissingJson; 20 | 21 | protected TestBase() 22 | { 23 | _ignoreMissingJson = new List(); 24 | _ignoreMissingCSharp = new List(); 25 | 26 | ThrowOnMissingContract = true; 27 | 28 | JsonSerializerSettings settings = new JsonSerializerSettings(); 29 | settings.MissingMemberHandling = MissingMemberHandling.Error; 30 | settings.ContractResolver = new FailingContractResolver(); 31 | settings.Error = Error; 32 | 33 | VirusTotal = new VirusTotal("YOUR API KEY HERE", settings); 34 | VirusTotal.UserAgent = "VirusTotal.NET unit tests"; 35 | VirusTotal.UseTLS = false; 36 | 37 | VirusTotal.OnRawResponseReceived += bytes => { LastCallInJSON = Encoding.UTF8.GetString(bytes); }; 38 | 39 | //Hack to only make 4 requests pr. sec. with public API key 40 | if (!Debugger.IsAttached) 41 | Thread.Sleep(15000); 42 | } 43 | 44 | protected VirusTotal VirusTotal { get; } 45 | 46 | protected bool ThrowOnMissingContract { get; set; } 47 | 48 | protected string LastCallInJSON { get; set; } 49 | 50 | /// 51 | /// Ignores errors about missing JSON properties (Where C# properties are not set) 52 | /// 53 | protected void IgnoreMissingJson(params string[] keys) 54 | { 55 | _ignoreMissingJson.AddRange(keys); 56 | } 57 | 58 | /// 59 | /// Ignores errors about missing C# properties (Where new or unknown JSON properties are present) 60 | /// 61 | protected void IgnoreMissingCSharp(params string[] keys) 62 | { 63 | _ignoreMissingCSharp.AddRange(keys); 64 | } 65 | 66 | private void Error(object sender, ErrorEventArgs errorEventArgs) 67 | { 68 | _errors.Add(errorEventArgs); 69 | errorEventArgs.ErrorContext.Handled = true; 70 | } 71 | 72 | public virtual void Dispose() 73 | { 74 | VirusTotal.Dispose(); 75 | 76 | if (_errors.Count == 0) 77 | return; 78 | 79 | // Sort the errors 80 | // Also de-duplicate them, as there is no point in blasting us with multiple instances of the "same" error 81 | Dictionary missingFieldInCSharp = new Dictionary(StringComparer.OrdinalIgnoreCase); 82 | Dictionary missingPropertyInJson = new Dictionary(StringComparer.OrdinalIgnoreCase); 83 | Dictionary other = new Dictionary(StringComparer.OrdinalIgnoreCase); 84 | 85 | foreach (ErrorEventArgs error in _errors) 86 | { 87 | string key = error.ErrorContext.Path + " / " + error.ErrorContext.Member; 88 | string errorMessage = error.ErrorContext.Error.Message; 89 | 90 | key = _normalizeRegex.Replace(key, "[array]"); 91 | 92 | if (errorMessage.StartsWith("Could not find member", StringComparison.OrdinalIgnoreCase)) 93 | { 94 | // Field in JSON is missing in C# 95 | if (!_ignoreMissingCSharp.Contains(key) && !missingFieldInCSharp.ContainsKey(key)) 96 | missingFieldInCSharp.Add(key, error); 97 | } 98 | else if (errorMessage.StartsWith("Required property", StringComparison.OrdinalIgnoreCase)) 99 | { 100 | // Field in C# is missing in JSON 101 | if (!_ignoreMissingJson.Contains(key, StringComparer.OrdinalIgnoreCase) && !missingPropertyInJson.ContainsKey(key)) 102 | missingPropertyInJson.Add(key, error); 103 | } 104 | else 105 | other.TryAdd(key, error); 106 | } 107 | 108 | // Combine all errors into a nice text 109 | StringBuilder sb = new StringBuilder(); 110 | 111 | if (missingFieldInCSharp.Count > 0) 112 | { 113 | sb.AppendLine("Fields missing in C# (Present in JSON)"); 114 | foreach (KeyValuePair pair in missingFieldInCSharp) 115 | sb.AppendLine($"[{pair.Value.CurrentObject?.GetType().Name}] {pair.Key}: {pair.Value.ErrorContext.Error.Message}"); 116 | 117 | sb.AppendLine(); 118 | } 119 | 120 | if (missingPropertyInJson.Count > 0) 121 | { 122 | sb.AppendLine("Fields missing in JSON (Present in C#)"); 123 | foreach (KeyValuePair pair in missingPropertyInJson) 124 | sb.AppendLine($"[{pair.Value.CurrentObject?.GetType().Name}] {pair.Key}: {pair.Value.ErrorContext.Error.Message}"); 125 | 126 | sb.AppendLine(); 127 | } 128 | 129 | if (other.Count > 0) 130 | { 131 | sb.AppendLine("Other errors"); 132 | foreach (KeyValuePair pair in other) 133 | sb.AppendLine($"[{pair.Value.CurrentObject?.GetType().Name}] {pair.Key}: {pair.Value.ErrorContext.Error.Message}"); 134 | 135 | sb.AppendLine(); 136 | } 137 | 138 | if (missingFieldInCSharp.Count > 0) 139 | { 140 | // Helper line of properties that can be ignored 141 | sb.AppendLine("Ignore JSON props missing from C#:"); 142 | sb.AppendLine($"{nameof(IgnoreMissingCSharp)}({string.Join(", ", missingFieldInCSharp.OrderBy(s => s.Key).Select(s => $"\"{s.Key}\""))});"); 143 | 144 | sb.AppendLine(); 145 | } 146 | 147 | if (missingPropertyInJson.Count > 0) 148 | { 149 | // Helper line of properties that can be ignored 150 | sb.AppendLine("Ignore C# props missing from JSON:"); 151 | sb.AppendLine(nameof(IgnoreMissingJson) + "(" + string.Join(", ", missingPropertyInJson.OrderBy(s => s.Key).Select(s => $"\"{s.Key}\"")) + ");"); 152 | 153 | sb.AppendLine(); 154 | } 155 | 156 | if (!ThrowOnMissingContract) 157 | return; 158 | 159 | if (missingFieldInCSharp.Count > 0 || missingPropertyInJson.Count > 0 || other.Count > 0) 160 | { 161 | sb.AppendLine(); 162 | sb.AppendLine("Raw JSON: "); 163 | sb.AppendLine(LastCallInJSON); 164 | throw new InvalidOperationException(sb.ToString()); 165 | } 166 | 167 | GC.SuppressFinalize(this); 168 | } 169 | } -------------------------------------------------------------------------------- /src/VirusTotalNet.Tests/TestInternals/TestData.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Net; 5 | using System.Text; 6 | using VirusTotalNet.Helpers; 7 | 8 | namespace VirusTotalNet.Tests.TestInternals; 9 | 10 | internal static class TestData 11 | { 12 | private static readonly Random _random = new Random(); 13 | 14 | internal static readonly string[] KnownDomains = { "google.com", "facebook.com", "virustotal.com" }; 15 | 16 | internal static readonly string[] KnownUrls = { "http://google.se", "http://google.com", "https://virustotal.com" }; 17 | internal static readonly string[] KnownIPv4s = { "8.8.8.8", "8.8.4.4", "216.58.211.142" }; //Google DNS and Google.com 18 | 19 | internal static readonly string[] KnownHashes = 20 | { 21 | "bf531b602b823473f09c7102b3baabd1848bef03", //conficker 22 | "e1112134b6dcc8bed54e0e34d8ac272795e73d74", //fake AV 23 | "5b63d3bf46aec2126932d8a683ca971c56f7d717" //IRC bot 24 | }; 25 | 26 | internal const string TestFileName = "VirusTotal.NET"; 27 | internal static readonly byte[] TestFile = Encoding.ASCII.GetBytes("VirusTotal.NET test file"); 28 | internal static readonly string TestHash = ResourcesHelper.GetResourceIdentifier(TestFile); 29 | 30 | /// 31 | /// EICAR test virus. See http://www.EICARMalware.org/86-0-Intended-use.html 32 | /// 33 | internal const string EICARFilename = "EICAR.txt"; 34 | 35 | internal static readonly byte[] EICARMalware = Encoding.ASCII.GetBytes(@"X5O!P%@AP[4\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!$H+H*"); 36 | 37 | internal static IEnumerable GetUnknownDomains(int count) 38 | { 39 | for (int i = 0; i < count; i++) 40 | { 41 | yield return $"VirusTotal.NET-{Guid.NewGuid()}.com"; 42 | } 43 | } 44 | 45 | internal static IEnumerable GetUnknownUrls(int count) 46 | { 47 | foreach (string unknownDomain in GetUnknownDomains(count)) 48 | { 49 | yield return "http://" + unknownDomain; 50 | } 51 | } 52 | 53 | internal static IEnumerable GetUnknownIPv4s(int count) 54 | { 55 | for (int i = 0; i < count; i++) 56 | { 57 | IPAddress d = new IPAddress(BitConverter.GetBytes(i).Reverse().ToArray()); 58 | yield return d.ToString(); 59 | } 60 | } 61 | 62 | internal static IEnumerable GetRandomIPv6s(int count) 63 | { 64 | byte[] bytes = new byte[16]; 65 | 66 | for (int i = 0; i < count; i++) 67 | { 68 | _random.NextBytes(bytes); 69 | IPAddress ipv6Address = new IPAddress(bytes); 70 | yield return ipv6Address.ToString(); 71 | } 72 | } 73 | 74 | internal static IEnumerable GetRandomSHA1s(int count) 75 | { 76 | byte[] bytes = new byte[20]; 77 | 78 | for (int i = 0; i < count; i++) 79 | { 80 | _random.NextBytes(bytes); 81 | yield return HashHelper.ByteArrayToHex(bytes); 82 | } 83 | } 84 | 85 | internal static IEnumerable GetRandomFile(int size, int count) 86 | { 87 | for (int i = 0; i < count; i++) 88 | { 89 | byte[] bytes = new byte[size]; 90 | _random.NextBytes(bytes); 91 | yield return bytes; 92 | } 93 | } 94 | } -------------------------------------------------------------------------------- /src/VirusTotalNet.Tests/UrlReportTests.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | using System.Threading.Tasks; 4 | using VirusTotalNet.Exceptions; 5 | using VirusTotalNet.ResponseCodes; 6 | using VirusTotalNet.Results; 7 | using VirusTotalNet.Tests.TestInternals; 8 | using Xunit; 9 | 10 | namespace VirusTotalNet.Tests; 11 | 12 | public class UrlReportTests : TestBase 13 | { 14 | [Fact] 15 | public async Task GetReportKnownUrl() 16 | { 17 | IgnoreMissingJson("scans.0xSI_f33d / Detail", "scans.Abusix / Detail", "scans.Acronis / Detail", "scans.ADMINUSLabs / Detail", "scans.AlienVault / Detail", "scans.AlphaSOC / Detail", "scans.Antiy-AVL / Detail", "scans.AutoShun / Detail", "scans.Avira / Detail", "scans.BitDefender / Detail", "scans.Bkav / Detail", "scans.BlockList / Detail", "scans.Blueliv / Detail", "scans.Certego / Detail", "scans.Cluster25 / Detail", "scans.CRDF / Detail", "scans.CrowdSec / Detail", "scans.Cyan / Detail", "scans.Cyble / Detail", "scans.CyRadar / Detail", "scans.DNS8 / Detail", "scans.EmergingThreats / Detail", "scans.Emsisoft / Detail", "scans.ESET / Detail", "scans.ESTsecurity / Detail", "scans.Fortinet / Detail", "scans.G-Data / Detail", "scans.GreenSnow / Detail", "scans.IPsum / Detail", "scans.K7AntiVirus / Detail", "scans.Kaspersky / Detail", "scans.Lionic / Detail", "scans.Lumu / Detail", "scans.Malwared / Detail", "scans.MalwarePatrol / Detail", "scans.Netcraft / Detail", "scans.OpenPhish / Detail", "scans.PhishFort / Detail", "scans.PhishLabs / Detail", "scans.Phishtank / Detail", "scans.PREBYTES / Detail", "scans.PrecisionSec / Detail", "scans.Quttera / Detail", "scans.Rising / Detail", "scans.SafeToOpen / Detail", "scans.Sangfor / Detail", "scans.Scantitan / Detail", "scans.Seclookup / Detail", "scans.SecureBrain / Detail", "scans.securolytics / Detail", "scans.Sophos / Detail", "scans.Spam404 / Detail", "scans.StopForumSpam / Detail", "scans.ThreatHive / Detail", "scans.Threatsourcing / Detail", "scans.Trustwave / Detail", "scans.URLhaus / Detail", "scans.URLQuery / Detail", "scans.VIPRE / Detail", "scans.ViriBack / Detail", "scans.Webroot / Detail", "scans.ZeroCERT / Detail", "scans['AICC (MONITORAPP)'] / Detail", "scans['alphaMountain.ai'] / Detail", "scans['ArcSight Threat Intelligence'] / Detail", "scans['Artists Against 419'] / Detail", "scans['benkow.cc'] / Detail", "scans['Bfore.Ai PreCrime'] / Detail", "scans['Chong Lua Dao'] / Detail", "scans['CINS Army'] / Detail", "scans['CMC Threat Intelligence'] / Detail", "scans['Criminal IP'] / Detail", "scans['desenmascara.me'] / Detail", "scans['Dr.Web'] / Detail", "scans['Feodo Tracker'] / Detail", "scans['Forcepoint ThreatSeeker'] / Detail", "scans['Google Safebrowsing'] / Detail", "scans['Heimdal Security'] / Detail", "scans['Juniper Networks'] / Detail", "scans['malwares.com URL checker'] / Detail", "scans['Phishing Database'] / Detail", "scans['Quick Heal'] / Detail", "scans['SCUMWARE.org'] / Detail", "scans['Snort IP sample list'] / Detail", "scans['Sucuri SiteCheck'] / Detail", "scans['Viettel Threat Intelligence'] / Detail", "scans['VX Vault'] / Detail", "scans['Xcitium Verdict Cloud'] / Detail"); 18 | UrlReport urlReport = await VirusTotal.GetUrlReportAsync(TestData.KnownUrls.First()); 19 | Assert.Equal(UrlReportResponseCode.Present, urlReport.ResponseCode); 20 | } 21 | 22 | [Fact] 23 | public async Task GetMultipleReportKnownUrl() 24 | { 25 | IgnoreMissingJson("[array].scans.0xSI_f33d / Detail", "[array].scans.Abusix / Detail", "[array].scans.Acronis / Detail", "[array].scans.ADMINUSLabs / Detail", "[array].scans.AlienVault / Detail", "[array].scans.AlphaSOC / Detail", "[array].scans.Antiy-AVL / Detail", "[array].scans.AutoShun / Detail", "[array].scans.Avira / Detail", "[array].scans.BitDefender / Detail", "[array].scans.Bkav / Detail", "[array].scans.BlockList / Detail", "[array].scans.Blueliv / Detail", "[array].scans.Certego / Detail", "[array].scans.Cluster25 / Detail", "[array].scans.CRDF / Detail", "[array].scans.CrowdSec / Detail", "[array].scans.Cyan / Detail", "[array].scans.Cyble / Detail", "[array].scans.CyRadar / Detail", "[array].scans.DNS8 / Detail", "[array].scans.EmergingThreats / Detail", "[array].scans.Emsisoft / Detail", "[array].scans.ESET / Detail", "[array].scans.ESTsecurity / Detail", "[array].scans.Fortinet / Detail", "[array].scans.G-Data / Detail", "[array].scans.GreenSnow / Detail", "[array].scans.IPsum / Detail", "[array].scans.K7AntiVirus / Detail", "[array].scans.Kaspersky / Detail", "[array].scans.Lionic / Detail", "[array].scans.Lumu / Detail", "[array].scans.Malwared / Detail", "[array].scans.MalwarePatrol / Detail", "[array].scans.Netcraft / Detail", "[array].scans.OpenPhish / Detail", "[array].scans.PhishFort / Detail", "[array].scans.PhishLabs / Detail", "[array].scans.Phishtank / Detail", "[array].scans.PREBYTES / Detail", "[array].scans.PrecisionSec / Detail", "[array].scans.Quttera / Detail", "[array].scans.Rising / Detail", "[array].scans.SafeToOpen / Detail", "[array].scans.Sangfor / Detail", "[array].scans.Scantitan / Detail", "[array].scans.Seclookup / Detail", "[array].scans.SecureBrain / Detail", "[array].scans.securolytics / Detail", "[array].scans.Sophos / Detail", "[array].scans.Spam404 / Detail", "[array].scans.StopForumSpam / Detail", "[array].scans.ThreatHive / Detail", "[array].scans.Threatsourcing / Detail", "[array].scans.Trustwave / Detail", "[array].scans.URLhaus / Detail", "[array].scans.URLQuery / Detail", "[array].scans.VIPRE / Detail", "[array].scans.ViriBack / Detail", "[array].scans.Webroot / Detail", "[array].scans.ZeroCERT / Detail", "[array].scans['AICC (MONITORAPP)'] / Detail", "[array].scans['alphaMountain.ai'] / Detail", "[array].scans['ArcSight Threat Intelligence'] / Detail", "[array].scans['Artists Against 419'] / Detail", "[array].scans['benkow.cc'] / Detail", "[array].scans['Bfore.Ai PreCrime'] / Detail", "[array].scans['Chong Lua Dao'] / Detail", "[array].scans['CINS Army'] / Detail", "[array].scans['CMC Threat Intelligence'] / Detail", "[array].scans['Criminal IP'] / Detail", "[array].scans['desenmascara.me'] / Detail", "[array].scans['Dr.Web'] / Detail", "[array].scans['Feodo Tracker'] / Detail", "[array].scans['Forcepoint ThreatSeeker'] / Detail", "[array].scans['Google Safebrowsing'] / Detail", "[array].scans['Heimdal Security'] / Detail", "[array].scans['Juniper Networks'] / Detail", "[array].scans['malwares.com URL checker'] / Detail", "[array].scans['Phishing Database'] / Detail", "[array].scans['Quick Heal'] / Detail", "[array].scans['SCUMWARE.org'] / Detail", "[array].scans['Snort IP sample list'] / Detail", "[array].scans['Sucuri SiteCheck'] / Detail", "[array].scans['Viettel Threat Intelligence'] / Detail", "[array].scans['VX Vault'] / Detail", "[array].scans['Xcitium Verdict Cloud'] / Detail"); 26 | 27 | IEnumerable urlReports = await VirusTotal.GetUrlReportsAsync(TestData.KnownUrls); 28 | 29 | foreach (UrlReport urlReport in urlReports) 30 | { 31 | Assert.Equal(UrlReportResponseCode.Present, urlReport.ResponseCode); 32 | } 33 | } 34 | 35 | [Fact] 36 | public async Task GetReportUnknownUrl() 37 | { 38 | IgnoreMissingJson(" / filescan_id", " / Permalink", " / Positives", " / scan_date", " / scan_id", " / Scans", " / Total", " / URL"); 39 | 40 | UrlReport urlReport = await VirusTotal.GetUrlReportAsync(TestData.GetUnknownUrls(1).First()); 41 | Assert.Equal(UrlReportResponseCode.NotPresent, urlReport.ResponseCode); 42 | 43 | //We are not supposed to have a scan id 44 | Assert.True(string.IsNullOrWhiteSpace(urlReport.ScanId)); 45 | } 46 | 47 | [Fact] 48 | public async Task GetMultipleReportUnknownUrl() 49 | { 50 | IgnoreMissingJson("[array] / filescan_id", "[array] / Permalink", "[array] / Positives", "[array] / scan_date", "[array] / scan_id", "[array] / Scans", "[array] / Total", "[array] / URL"); 51 | 52 | IEnumerable urlReports = await VirusTotal.GetUrlReportsAsync(TestData.GetUnknownUrls(4)); 53 | 54 | foreach (UrlReport urlReport in urlReports) 55 | { 56 | Assert.Equal(UrlReportResponseCode.NotPresent, urlReport.ResponseCode); 57 | } 58 | } 59 | 60 | [Fact] 61 | public async Task GetReportForUnknownUrlAndScan() 62 | { 63 | IgnoreMissingJson(" / filescan_id", " / Positives", " / Scans", " / Total"); 64 | 65 | UrlReport urlReport = await VirusTotal.GetUrlReportAsync(TestData.GetUnknownUrls(1).First(), true); 66 | 67 | //It return "present" because we told it to scan it 68 | Assert.Equal(UrlReportResponseCode.Present, urlReport.ResponseCode); 69 | 70 | //We are supposed to have a scan id because we scanned it 71 | Assert.False(string.IsNullOrWhiteSpace(urlReport.ScanId)); 72 | } 73 | 74 | [Fact] 75 | public async Task GetReportInvalidUrl() 76 | { 77 | await Assert.ThrowsAsync(async () => await VirusTotal.GetUrlReportAsync(".")); 78 | } 79 | 80 | [Fact] 81 | public async Task UrlReportBatchLimit() 82 | { 83 | IgnoreMissingJson("[array] / filescan_id", "[array] / Permalink", "[array] / Positives", "[array] / scan_date", "[array] / scan_id", "[array] / Scans", "[array] / Total", "[array] / URL"); 84 | 85 | VirusTotal.RestrictNumberOfResources = false; 86 | 87 | IEnumerable results = await VirusTotal.GetUrlReportsAsync(TestData.GetUnknownUrls(5)); 88 | 89 | //We only expect 4 as VT simply returns 4 results no matter the batch size. 90 | Assert.Equal(VirusTotal.UrlReportBatchSizeLimit, results.Count()); 91 | } 92 | } -------------------------------------------------------------------------------- /src/VirusTotalNet.Tests/UrlScanTests.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | using System.Threading.Tasks; 4 | using VirusTotalNet.ResponseCodes; 5 | using VirusTotalNet.Results; 6 | using VirusTotalNet.Tests.TestInternals; 7 | using Xunit; 8 | 9 | namespace VirusTotalNet.Tests; 10 | 11 | public class UrlScanTests : TestBase 12 | { 13 | [Fact] 14 | public async Task ScanKnownUrl() 15 | { 16 | UrlScanResult fileResult = await VirusTotal.ScanUrlAsync(TestData.KnownUrls.First()); 17 | Assert.Equal(UrlScanResponseCode.Queued, fileResult.ResponseCode); 18 | } 19 | 20 | [Fact] 21 | public async Task ScanMultipleKnownUrls() 22 | { 23 | IEnumerable urlScans = await VirusTotal.ScanUrlsAsync(TestData.KnownUrls); 24 | 25 | foreach (UrlScanResult urlScan in urlScans) 26 | { 27 | Assert.Equal(UrlScanResponseCode.Queued, urlScan.ResponseCode); 28 | } 29 | } 30 | 31 | [Fact] 32 | public async Task ScanUnknownUrl() 33 | { 34 | UrlScanResult fileResult = await VirusTotal.ScanUrlAsync(TestData.GetUnknownUrls(1).First()); 35 | Assert.Equal(UrlScanResponseCode.Queued, fileResult.ResponseCode); 36 | } 37 | 38 | [Fact] 39 | public async Task ScanMultipleUnknownUrl() 40 | { 41 | IEnumerable urlScans = await VirusTotal.ScanUrlsAsync(TestData.GetUnknownUrls(5)); 42 | 43 | foreach (UrlScanResult urlScan in urlScans) 44 | { 45 | Assert.Equal(UrlScanResponseCode.Queued, urlScan.ResponseCode); 46 | } 47 | } 48 | 49 | [Fact] 50 | public async Task UrlScanBatchLimit() 51 | { 52 | VirusTotal.RestrictNumberOfResources = false; 53 | 54 | IEnumerable results = await VirusTotal.ScanUrlsAsync(TestData.GetUnknownUrls(50)); 55 | 56 | //We only expect 25 as VT simply returns 25 results no matter the batch size. 57 | Assert.Equal(VirusTotal.UrlScanBatchSizeLimit, results.Count()); 58 | } 59 | } -------------------------------------------------------------------------------- /src/VirusTotalNet.Tests/VirusTotalNet.Tests.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | net7.0 6 | false 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | all 17 | runtime; build; native; contentfiles; analyzers; buildtransitive 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /src/VirusTotalNet.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 16 4 | VisualStudioVersion = 16.0.29009.5 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "VirusTotalNet", "VirusTotalNet\VirusTotalNet.csproj", "{E98F21E1-E613-4380-841B-58690AFBA620}" 7 | EndProject 8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "VirusTotalNet.Examples", "VirusTotalNet.Examples\VirusTotalNet.Examples.csproj", "{744105A6-A97A-44AB-A294-375A9AEE7D6F}" 9 | EndProject 10 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "VirusTotalNet.Tests", "VirusTotalNet.Tests\VirusTotalNet.Tests.csproj", "{1A6CAB28-AE7A-4B7B-A4B5-06123D2E4B64}" 11 | EndProject 12 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "_Imports", "_Imports", "{527837F9-33C1-4332-B64D-D7CD0594CC75}" 13 | ProjectSection(SolutionItems) = preProject 14 | ..\Properties.targets = ..\Properties.targets 15 | EndProjectSection 16 | EndProject 17 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "_Items", "_Items", "{C2EA2D8C-2576-4F12-929E-3AD3E531AAC1}" 18 | ProjectSection(SolutionItems) = preProject 19 | ..\.gitattributes = ..\.gitattributes 20 | ..\.gitignore = ..\.gitignore 21 | ..\README.md = ..\README.md 22 | EndProjectSection 23 | EndProject 24 | Global 25 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 26 | Debug|Any CPU = Debug|Any CPU 27 | Release|Any CPU = Release|Any CPU 28 | EndGlobalSection 29 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 30 | {E98F21E1-E613-4380-841B-58690AFBA620}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 31 | {E98F21E1-E613-4380-841B-58690AFBA620}.Debug|Any CPU.Build.0 = Debug|Any CPU 32 | {E98F21E1-E613-4380-841B-58690AFBA620}.Release|Any CPU.ActiveCfg = Release|Any CPU 33 | {E98F21E1-E613-4380-841B-58690AFBA620}.Release|Any CPU.Build.0 = Release|Any CPU 34 | {744105A6-A97A-44AB-A294-375A9AEE7D6F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 35 | {744105A6-A97A-44AB-A294-375A9AEE7D6F}.Debug|Any CPU.Build.0 = Debug|Any CPU 36 | {744105A6-A97A-44AB-A294-375A9AEE7D6F}.Release|Any CPU.ActiveCfg = Release|Any CPU 37 | {744105A6-A97A-44AB-A294-375A9AEE7D6F}.Release|Any CPU.Build.0 = Release|Any CPU 38 | {1A6CAB28-AE7A-4B7B-A4B5-06123D2E4B64}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 39 | {1A6CAB28-AE7A-4B7B-A4B5-06123D2E4B64}.Debug|Any CPU.Build.0 = Debug|Any CPU 40 | {1A6CAB28-AE7A-4B7B-A4B5-06123D2E4B64}.Release|Any CPU.ActiveCfg = Release|Any CPU 41 | {1A6CAB28-AE7A-4B7B-A4B5-06123D2E4B64}.Release|Any CPU.Build.0 = Release|Any CPU 42 | EndGlobalSection 43 | GlobalSection(SolutionProperties) = preSolution 44 | HideSolutionNode = FALSE 45 | EndGlobalSection 46 | GlobalSection(ExtensibilityGlobals) = postSolution 47 | SolutionGuid = {7F8514BD-D1C4-42C8-8281-6C78B021347D} 48 | EndGlobalSection 49 | EndGlobal 50 | -------------------------------------------------------------------------------- /src/VirusTotalNet/Enums/ResourceType.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace VirusTotalNet.Enums; 4 | 5 | [Flags] 6 | public enum ResourceType : long 7 | { 8 | MD5 = 1 << 0, 9 | SHA1 = 1 << 1, 10 | SHA256 = 1 << 2, 11 | ScanId = 1 << 3, 12 | URL = 1 << 4, 13 | IP = 1 << 5, 14 | Domain = 1 << 6, 15 | AnyHash = MD5 | SHA1 | SHA256, 16 | AnyType = AnyHash | ScanId | URL | IP | Domain 17 | } -------------------------------------------------------------------------------- /src/VirusTotalNet/Exceptions/AccessDeniedException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace VirusTotalNet.Exceptions; 4 | 5 | /// 6 | /// Exception that is thrown when you don't have access to the service. 7 | /// 8 | public class AccessDeniedException : Exception 9 | { 10 | public AccessDeniedException(string message) 11 | : base(message) { } 12 | } -------------------------------------------------------------------------------- /src/VirusTotalNet/Exceptions/InvalidDateTimeException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace VirusTotalNet.Exceptions; 4 | 5 | public class InvalidDateTimeException : Exception 6 | { 7 | public InvalidDateTimeException(string message) : base(message) { } 8 | } -------------------------------------------------------------------------------- /src/VirusTotalNet/Exceptions/InvalidResourceException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace VirusTotalNet.Exceptions; 4 | 5 | public class InvalidResourceException : Exception 6 | { 7 | public InvalidResourceException(string message) : base(message) { } 8 | } -------------------------------------------------------------------------------- /src/VirusTotalNet/Exceptions/RateLimitException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace VirusTotalNet.Exceptions; 4 | 5 | /// 6 | /// Exception that is thrown when the rate limit has been hit. 7 | /// 8 | public class RateLimitException : Exception 9 | { 10 | public RateLimitException(string message) 11 | : base(message) { } 12 | } -------------------------------------------------------------------------------- /src/VirusTotalNet/Exceptions/ResourceLimitException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace VirusTotalNet.Exceptions; 4 | 5 | /// 6 | /// Exception that is thrown when the number of resources exceed the allowed. 7 | /// 8 | public class ResourceLimitException : Exception 9 | { 10 | public ResourceLimitException(string message) 11 | : base(message) { } 12 | } -------------------------------------------------------------------------------- /src/VirusTotalNet/Exceptions/SizeLimitException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace VirusTotalNet.Exceptions; 4 | 5 | /// 6 | /// Exception that is thrown when the file size exceeds the allowed. 7 | /// 8 | public class SizeLimitException : Exception 9 | { 10 | public SizeLimitException(long vtLimitBytes, long actualBytes) 11 | : base($"The file size limit on VirusTotal is {vtLimitBytes / 1024} KB. Your file is {actualBytes / 1024} KB") { } 12 | 13 | public SizeLimitException(long vtLimitBytes) 14 | : base($"The file size limit on VirusTotal is {vtLimitBytes / 1024} KB.") { } 15 | } -------------------------------------------------------------------------------- /src/VirusTotalNet/Helpers/HashHelper.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Security.Cryptography; 4 | using System.Text; 5 | 6 | namespace VirusTotalNet.Helpers; 7 | 8 | public static class HashHelper 9 | { 10 | public static string GetSha256(byte[] buffer) 11 | { 12 | using MemoryStream ms = new MemoryStream(buffer); 13 | return GetSha256(ms); 14 | } 15 | 16 | public static string GetSha256(string content, Encoding? encoding = null) 17 | { 18 | encoding ??= Encoding.UTF8; 19 | 20 | using MemoryStream ms = new MemoryStream(encoding.GetBytes(content)); 21 | return GetSha256(ms); 22 | } 23 | 24 | public static string GetSha256(FileInfo file) 25 | { 26 | if (!file.Exists) 27 | throw new FileNotFoundException("File not found.", file.FullName); 28 | 29 | using FileStream stream = file.OpenRead(); 30 | return GetSha256(stream); 31 | } 32 | 33 | public static string GetSha256(Stream stream) 34 | { 35 | if (stream == null || stream.Length == 0) 36 | throw new ArgumentException("You must provide a valid stream.", nameof(stream)); 37 | 38 | using SHA256 sha256 = SHA256.Create(); 39 | byte[] hashBytes = sha256.ComputeHash(stream); 40 | return ByteArrayToHex(hashBytes); 41 | } 42 | 43 | public static string GetSha1(byte[] buffer) 44 | { 45 | using MemoryStream ms = new MemoryStream(buffer); 46 | return GetSha1(ms); 47 | } 48 | 49 | public static string GetSha1(string content, Encoding? encoding = null) 50 | { 51 | encoding ??= Encoding.UTF8; 52 | 53 | using MemoryStream ms = new MemoryStream(encoding.GetBytes(content)); 54 | return GetSha1(ms); 55 | } 56 | 57 | public static string GetSha1(FileInfo file) 58 | { 59 | if (!file.Exists) 60 | throw new FileNotFoundException("File not found.", file.FullName); 61 | 62 | using FileStream stream = file.OpenRead(); 63 | return GetSha1(stream); 64 | } 65 | 66 | public static string GetSha1(Stream stream) 67 | { 68 | if (stream == null || stream.Length == 0) 69 | throw new ArgumentException("You must provide a valid stream.", nameof(stream)); 70 | 71 | using SHA1 sha1 = SHA1.Create(); 72 | byte[] hashBytes = sha1.ComputeHash(stream); 73 | return ByteArrayToHex(hashBytes); 74 | } 75 | 76 | public static string GetMd5(byte[] buffer) 77 | { 78 | using MemoryStream ms = new MemoryStream(buffer); 79 | return GetMd5(ms); 80 | } 81 | 82 | public static string GetMd5(string content, Encoding? encoding = null) 83 | { 84 | encoding ??= Encoding.UTF8; 85 | 86 | using MemoryStream ms = new MemoryStream(encoding.GetBytes(content)); 87 | return GetMd5(ms); 88 | } 89 | 90 | public static string GetMd5(FileInfo file) 91 | { 92 | if (!file.Exists) 93 | throw new FileNotFoundException("File not found.", file.FullName); 94 | 95 | using FileStream stream = file.OpenRead(); 96 | return GetMd5(stream); 97 | } 98 | 99 | public static string GetMd5(Stream stream) 100 | { 101 | if (stream == null || stream.Length == 0) 102 | throw new ArgumentException("You must provide a valid stream.", nameof(stream)); 103 | 104 | using MD5 md5 = MD5.Create(); 105 | byte[] md5Result = md5.ComputeHash(stream); 106 | return ByteArrayToHex(md5Result); 107 | } 108 | 109 | public static string ByteArrayToHex(byte[] buffer) 110 | { 111 | StringBuilder hex = new StringBuilder(buffer.Length * 2); 112 | 113 | foreach (byte b in buffer) 114 | hex.AppendFormat("{0:x2}", b); 115 | 116 | return hex.ToString(); 117 | } 118 | } -------------------------------------------------------------------------------- /src/VirusTotalNet/Helpers/ResourcesHelper.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Linq; 5 | using System.Net; 6 | using System.Net.Sockets; 7 | using VirusTotalNet.Enums; 8 | using VirusTotalNet.Exceptions; 9 | using VirusTotalNet.Internal.Helpers; 10 | 11 | namespace VirusTotalNet.Helpers; 12 | 13 | public static class ResourcesHelper 14 | { 15 | public static IEnumerable GetResourceIdentifier(IEnumerable files) 16 | { 17 | foreach (FileInfo fileInfo in files) 18 | { 19 | yield return GetResourceIdentifier(fileInfo); 20 | } 21 | } 22 | 23 | public static string GetResourceIdentifier(FileInfo file) 24 | { 25 | return HashHelper.GetSha256(file); 26 | } 27 | 28 | public static IEnumerable GetResourceIdentifier(IEnumerable files) 29 | { 30 | foreach (byte[] fileBytes in files) 31 | { 32 | yield return GetResourceIdentifier(fileBytes); 33 | } 34 | } 35 | 36 | public static string GetResourceIdentifier(byte[] file) 37 | { 38 | return HashHelper.GetSha256(file); 39 | } 40 | 41 | public static IEnumerable GetResourceIdentifier(IEnumerable streams) 42 | { 43 | foreach (Stream stream in streams) 44 | { 45 | yield return GetResourceIdentifier(stream); 46 | } 47 | } 48 | 49 | public static string GetResourceIdentifier(Stream stream) 50 | { 51 | return HashHelper.GetSha256(stream); 52 | } 53 | 54 | public static string GetResourceIdentifier(string url) 55 | { 56 | if (!IsValidURL(url, out url)) 57 | throw new InvalidResourceException($"The url '{url}' is in the wrong format"); 58 | 59 | return HashHelper.GetSha256(url); 60 | } 61 | 62 | public static string GetResourceIdentifier(Uri url) 63 | { 64 | return GetResourceIdentifier(url.ToString()); 65 | } 66 | 67 | public static IEnumerable ValidateResourcea(IEnumerable resources, ResourceType type) 68 | { 69 | if (resources == null) 70 | throw new InvalidResourceException("No resources given"); 71 | 72 | IEnumerable array = resources as string[] ?? resources.ToArray(); 73 | 74 | if (!array.Any()) 75 | throw new InvalidResourceException("No resources given"); 76 | 77 | foreach (string resource in array) 78 | { 79 | yield return ValidateResourcea(resource, type); 80 | } 81 | } 82 | 83 | public static string ValidateResourcea(string resource, ResourceType type) 84 | { 85 | if (string.IsNullOrWhiteSpace(resource)) 86 | throw new InvalidResourceException("Resource is invalid"); 87 | 88 | string sanitized = resource; 89 | bool valid = false; 90 | 91 | if (type.HasFlag(ResourceType.MD5)) 92 | valid |= IsValidMD5(resource); 93 | if (type.HasFlag(ResourceType.SHA1)) 94 | valid |= IsValidSHA1(resource); 95 | if (type.HasFlag(ResourceType.SHA256)) 96 | valid |= IsValidSHA256(resource); 97 | if (type.HasFlag(ResourceType.ScanId)) 98 | valid |= IsValidScanId(resource); 99 | if (type.HasFlag(ResourceType.URL)) 100 | valid |= IsValidURL(resource, out sanitized); 101 | if (type.HasFlag(ResourceType.IP)) 102 | valid |= IsValidIP(resource, out sanitized); 103 | if (type.HasFlag(ResourceType.Domain)) 104 | valid |= IsValidDomain(resource, out sanitized); 105 | 106 | if (!valid) 107 | throw new InvalidResourceException($"Resource '{resource}' has to be one of the following: {string.Join(", ", type.GetIndividualFlags())}"); 108 | 109 | return sanitized; 110 | } 111 | 112 | public static bool IsValidScanId(string resource) 113 | { 114 | if (resource.Length != 75) 115 | return false; 116 | 117 | string[] parts = resource.Split('-'); 118 | 119 | if (parts.Length != 2) 120 | return false; 121 | 122 | if (parts[0].Length != 64 || parts[1].Length != 10) 123 | return false; 124 | 125 | return IsAlphaNumeric(parts[0]) && IsNumeric(parts[1]); 126 | } 127 | 128 | public static bool IsValidURL(string resource, out string sanitized) 129 | { 130 | sanitized = resource; 131 | 132 | if (!resource.Contains('.')) 133 | return false; 134 | 135 | if (!Uri.TryCreate(NormalizeUrl(resource, false), UriKind.Absolute, out Uri uri)) 136 | return false; 137 | 138 | sanitized = uri.ToString(); 139 | return true; 140 | } 141 | 142 | public static bool IsValidIP(string resource, out string sanitized) 143 | { 144 | sanitized = resource; 145 | 146 | if (!IPAddress.TryParse(resource, out IPAddress ip)) 147 | return false; 148 | 149 | if (ip.AddressFamily != AddressFamily.InterNetwork) 150 | return false; 151 | 152 | sanitized = ip.ToString(); 153 | return true; 154 | } 155 | 156 | public static bool IsValidDomain(string resource, out string sanitized) 157 | { 158 | sanitized = resource; 159 | 160 | if (!resource.Contains('.')) 161 | return false; 162 | 163 | if (!Uri.TryCreate(NormalizeUrl(resource, false), UriKind.Absolute, out Uri uri)) 164 | return false; 165 | 166 | sanitized = uri.Host; 167 | return true; 168 | } 169 | 170 | public static bool IsValidMD5(string resource) 171 | { 172 | return resource.Length == 32 && IsAlphaNumeric(resource); 173 | } 174 | 175 | public static bool IsValidSHA1(string resource) 176 | { 177 | return resource.Length == 40 && IsAlphaNumeric(resource); 178 | } 179 | 180 | public static bool IsValidSHA256(string resource) 181 | { 182 | return resource.Length == 64 && IsAlphaNumeric(resource); 183 | } 184 | 185 | public static bool IsAlphaNumeric(string input) 186 | { 187 | return input.All(char.IsLetterOrDigit); 188 | } 189 | 190 | public static bool IsNumeric(string input) 191 | { 192 | return input != string.Empty && input.All(x => x >= 48 && x <= 57); 193 | } 194 | 195 | public static string NormalizeUrl(string url, bool useTls) 196 | { 197 | string tempUri = url.Trim(); 198 | 199 | if (tempUri.StartsWith("http://", StringComparison.OrdinalIgnoreCase) || tempUri.StartsWith("https://", StringComparison.OrdinalIgnoreCase)) 200 | return tempUri; 201 | 202 | if (useTls) 203 | { 204 | if (!tempUri.StartsWith("https://", StringComparison.OrdinalIgnoreCase)) 205 | tempUri = "https://" + tempUri; 206 | } 207 | else 208 | { 209 | if (!tempUri.StartsWith("http://", StringComparison.OrdinalIgnoreCase)) 210 | tempUri = "http://" + tempUri; 211 | } 212 | 213 | return tempUri; 214 | } 215 | } -------------------------------------------------------------------------------- /src/VirusTotalNet/Internal/DateTimeParsers/UnixTimeConverter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Newtonsoft.Json; 3 | using Newtonsoft.Json.Converters; 4 | using VirusTotalNet.Helpers; 5 | 6 | namespace VirusTotalNet.Internal.DateTimeParsers; 7 | 8 | internal class UnixTimeConverter : DateTimeConverterBase 9 | { 10 | private static DateTime _epoc = new DateTime(1970, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc); 11 | 12 | private static DateTime FromUnix(long unixTime) 13 | { 14 | return _epoc.AddSeconds(unixTime).ToLocalTime(); 15 | } 16 | 17 | public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer) 18 | { 19 | throw new NotSupportedException(); 20 | } 21 | 22 | public override object? ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer) 23 | { 24 | if (reader.Value == null) 25 | return null; 26 | 27 | string stringVal = reader.Value.ToString(); 28 | 29 | if (string.IsNullOrWhiteSpace(stringVal)) 30 | return DateTime.MinValue; 31 | 32 | if (!ResourcesHelper.IsNumeric(stringVal)) 33 | return DateTime.MinValue; 34 | 35 | return FromUnix(long.Parse(stringVal)); 36 | } 37 | } -------------------------------------------------------------------------------- /src/VirusTotalNet/Internal/DateTimeParsers/YearMonthDayConverter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Globalization; 3 | using Newtonsoft.Json; 4 | using Newtonsoft.Json.Converters; 5 | using VirusTotalNet.Exceptions; 6 | using VirusTotalNet.Helpers; 7 | 8 | namespace VirusTotalNet.Internal.DateTimeParsers; 9 | 10 | internal class YearMonthDayConverter : DateTimeConverterBase 11 | { 12 | private readonly CultureInfo _culture = new CultureInfo("en-us"); 13 | private const string _newDateTimeFormat = "yyyyMMdd"; 14 | private const string _oldDateTimeFormat = "yyyyMMddHHmmss"; 15 | 16 | public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer) 17 | { 18 | writer.DateFormatString = _newDateTimeFormat; 19 | writer.WriteValue(value); 20 | } 21 | 22 | public override object ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer) 23 | { 24 | if (reader.Value is not string valueStr) 25 | return DateTime.MinValue; 26 | 27 | if (!ResourcesHelper.IsNumeric(valueStr)) 28 | return DateTime.MinValue; 29 | 30 | //New format 31 | if (DateTime.TryParseExact(valueStr, _newDateTimeFormat, _culture, DateTimeStyles.AllowWhiteSpaces, out DateTime result)) 32 | return result; 33 | 34 | //Old format 35 | if (DateTime.TryParseExact(valueStr, _oldDateTimeFormat, _culture, DateTimeStyles.AllowWhiteSpaces, out result)) 36 | return result; 37 | 38 | throw new InvalidDateTimeException("Invalid date/time from VirusTotal. Tried to parse: " + valueStr); 39 | } 40 | } -------------------------------------------------------------------------------- /src/VirusTotalNet/Internal/Helpers/EnumHelper.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | 5 | namespace VirusTotalNet.Internal.Helpers; 6 | 7 | internal static class EnumHelper 8 | { 9 | public static IEnumerable GetIndividualFlags(this Enum value) 10 | { 11 | return GetFlags(value, GetFlagValues(value.GetType()).ToArray()); 12 | } 13 | 14 | private static IEnumerable GetFlags(Enum value, Enum[] values) 15 | { 16 | ulong bits = Convert.ToUInt64(value); 17 | List results = new List(); 18 | for (int i = values.Length - 1; i >= 0; i--) 19 | { 20 | ulong mask = Convert.ToUInt64(values[i]); 21 | if (i == 0 && mask == 0L) 22 | break; 23 | if ((bits & mask) == mask) 24 | { 25 | results.Add(values[i]); 26 | bits -= mask; 27 | } 28 | } 29 | if (bits != 0L) 30 | return Enumerable.Empty(); 31 | if (Convert.ToUInt64(value) != 0L) 32 | return results.Reverse(); 33 | if (bits == Convert.ToUInt64(value) && values.Length > 0 && Convert.ToUInt64(values[0]) == 0L) 34 | return values.Take(1); 35 | return Enumerable.Empty(); 36 | } 37 | 38 | private static IEnumerable GetFlagValues(Type enumType) 39 | { 40 | ulong flag = 0x1; 41 | foreach (Enum value in Enum.GetValues(enumType).Cast()) 42 | { 43 | ulong bits = Convert.ToUInt64(value); 44 | if (bits == 0L) 45 | continue; // skip the zero value 46 | while (flag < bits) 47 | flag <<= 1; 48 | if (flag == bits) 49 | yield return value; 50 | } 51 | } 52 | } -------------------------------------------------------------------------------- /src/VirusTotalNet/Internal/Objects/LargeFileUpload.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | 3 | namespace VirusTotalNet.Internal.Objects; 4 | 5 | internal class LargeFileUpload 6 | { 7 | [JsonProperty("upload_url")] 8 | public string UploadUrl { get; set; } 9 | } -------------------------------------------------------------------------------- /src/VirusTotalNet/Internal/Other/CustomURLEncodedContent.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Net; 3 | using System.Net.Http; 4 | using System.Net.Http.Headers; 5 | using System.Text; 6 | 7 | namespace VirusTotalNet.Internal.Other; 8 | 9 | /// 10 | /// I use this class instead of FormUrlEncodedContent due to a bug in .NET core regarding the length of values 11 | /// 12 | internal class CustomURLEncodedContent : ByteArrayContent 13 | { 14 | public CustomURLEncodedContent(IEnumerable> pairs) : base(GetContentByteArray(pairs)) 15 | { 16 | Headers.ContentType = new MediaTypeHeaderValue("application/x-www-form-urlencoded"); 17 | } 18 | 19 | private static byte[] GetContentByteArray(IEnumerable> pairs) 20 | { 21 | // Encode and concatenate data 22 | StringBuilder builder = new StringBuilder(); 23 | foreach (KeyValuePair pair in pairs) 24 | { 25 | if (builder.Length > 0) 26 | builder.Append('&'); 27 | 28 | builder.Append(Encode(pair.Key)); 29 | builder.Append('='); 30 | builder.Append(Encode(pair.Value)); 31 | } 32 | return Encoding.UTF8.GetBytes(builder.ToString()); 33 | } 34 | 35 | private static string Encode(string data) 36 | { 37 | if (string.IsNullOrEmpty(data)) 38 | return string.Empty; 39 | 40 | return WebUtility.UrlEncode(data); 41 | } 42 | } -------------------------------------------------------------------------------- /src/VirusTotalNet/Objects/DetectedUrl.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Newtonsoft.Json; 3 | 4 | namespace VirusTotalNet.Objects; 5 | 6 | public class DetectedUrl 7 | { 8 | public string Url { get; set; } 9 | 10 | public int Positives { get; set; } 11 | 12 | public int Total { get; set; } 13 | 14 | [JsonProperty("scan_date")] 15 | public DateTime ScanDate { get; set; } 16 | } -------------------------------------------------------------------------------- /src/VirusTotalNet/Objects/DomainResolution.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Newtonsoft.Json; 3 | 4 | namespace VirusTotalNet.Objects; 5 | 6 | public class DomainResolution 7 | { 8 | [JsonProperty("last_resolved", NullValueHandling = NullValueHandling.Ignore)] 9 | public DateTime LastResolved { get; set; } 10 | 11 | [JsonProperty("ip_address")] 12 | public string IPAddress { get; set; } 13 | } -------------------------------------------------------------------------------- /src/VirusTotalNet/Objects/IPResolution.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Newtonsoft.Json; 3 | 4 | namespace VirusTotalNet.Objects; 5 | 6 | public class IPResolution 7 | { 8 | [JsonProperty("last_resolved", NullValueHandling = NullValueHandling.Ignore)] 9 | public DateTime LastResolved { get; set; } 10 | 11 | public string Hostname { get; set; } 12 | } -------------------------------------------------------------------------------- /src/VirusTotalNet/Objects/Sample.cs: -------------------------------------------------------------------------------- 1 | namespace VirusTotalNet.Objects; 2 | 3 | public class Sample 4 | { 5 | public int Positives { get; set; } 6 | public int Total { get; set; } 7 | public string Sha256 { get; set; } 8 | } -------------------------------------------------------------------------------- /src/VirusTotalNet/Objects/SampleWithDate.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace VirusTotalNet.Objects; 4 | 5 | public class SampleWithDate 6 | { 7 | public DateTime Date { get; set; } 8 | public int Positives { get; set; } 9 | public int Total { get; set; } 10 | public string Sha256 { get; set; } 11 | } -------------------------------------------------------------------------------- /src/VirusTotalNet/Objects/ScanEngine.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Newtonsoft.Json; 3 | using VirusTotalNet.Internal.DateTimeParsers; 4 | 5 | namespace VirusTotalNet.Objects; 6 | 7 | public class ScanEngine 8 | { 9 | /// 10 | /// True if the engine flagged the resource. 11 | /// 12 | public bool Detected { get; set; } 13 | 14 | /// 15 | /// Version of the engine. 16 | /// 17 | public string Version { get; set; } 18 | 19 | /// 20 | /// Contains the name of the malware, if any. 21 | /// 22 | public string Result { get; set; } 23 | 24 | /// 25 | /// The date of the latest signatures of the engine. 26 | /// 27 | [JsonConverter(typeof(YearMonthDayConverter))] 28 | public DateTime Update { get; set; } 29 | } -------------------------------------------------------------------------------- /src/VirusTotalNet/Objects/UrlScanEngine.cs: -------------------------------------------------------------------------------- 1 | namespace VirusTotalNet.Objects; 2 | 3 | public class UrlScanEngine 4 | { 5 | /// 6 | /// True if the engine flagged the resource. 7 | /// 8 | public bool Detected { get; set; } 9 | 10 | /// 11 | /// Details about the detection 12 | /// 13 | public string Detail { get; set; } 14 | 15 | /// 16 | /// Contains the name of the malware, if any. 17 | /// 18 | public string Result { get; set; } 19 | } -------------------------------------------------------------------------------- /src/VirusTotalNet/Objects/UserComment.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace VirusTotalNet.Objects; 4 | 5 | public class UserComment 6 | { 7 | public string Comment { get; set; } 8 | public DateTime Date { get; set; } 9 | } -------------------------------------------------------------------------------- /src/VirusTotalNet/Objects/WebutationInfo.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | 3 | namespace VirusTotalNet.Objects; 4 | 5 | public class WebutationInfo 6 | { 7 | [JsonProperty("Adult content")] 8 | public string AdultContent { get; set; } 9 | 10 | [JsonProperty("Safety score")] 11 | public int SafetyScore { get; set; } 12 | 13 | public string Verdict { get; set; } 14 | } -------------------------------------------------------------------------------- /src/VirusTotalNet/Objects/WotInfo.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | 3 | namespace VirusTotalNet.Objects; 4 | 5 | public class WOTInfo 6 | { 7 | [JsonProperty("Child safety")] 8 | public string ChildSafety { get; set; } 9 | 10 | public string Privacy { get; set; } 11 | 12 | public string Trustworthiness { get; set; } 13 | 14 | [JsonProperty("Vendor reliability")] 15 | public string VendorReliability { get; set; } 16 | } -------------------------------------------------------------------------------- /src/VirusTotalNet/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.CompilerServices; 2 | 3 | [assembly: InternalsVisibleTo("VirusTotalNet.Tests")] -------------------------------------------------------------------------------- /src/VirusTotalNet/ResponseCodes/CommentResponseCode.cs: -------------------------------------------------------------------------------- 1 | namespace VirusTotalNet.ResponseCodes; 2 | 3 | public enum CommentResponseCode 4 | { 5 | Error = 0, 6 | Success = 1 7 | } -------------------------------------------------------------------------------- /src/VirusTotalNet/ResponseCodes/DomainResponseCode.cs: -------------------------------------------------------------------------------- 1 | namespace VirusTotalNet.ResponseCodes; 2 | 3 | public enum DomainResponseCode 4 | { 5 | /// 6 | /// The item you searched for was not present in VirusTotal's dataset. 7 | /// 8 | NotPresent = 0, 9 | 10 | /// 11 | /// The item was present and it could be retrieved. 12 | /// 13 | Present = 1 14 | } -------------------------------------------------------------------------------- /src/VirusTotalNet/ResponseCodes/FileReportResponseCode.cs: -------------------------------------------------------------------------------- 1 | namespace VirusTotalNet.ResponseCodes; 2 | 3 | public enum FileReportResponseCode 4 | { 5 | /// 6 | /// The resource is still being scanned 7 | /// 8 | Queued = -2, 9 | 10 | /// 11 | /// The item you searched for was not present in VirusTotal's dataset. 12 | /// 13 | NotPresent = 0, 14 | 15 | /// 16 | /// The item was present and it could be retrieved. 17 | /// 18 | Present = 1 19 | } -------------------------------------------------------------------------------- /src/VirusTotalNet/ResponseCodes/IPReportResponseCode.cs: -------------------------------------------------------------------------------- 1 | namespace VirusTotalNet.ResponseCodes; 2 | 3 | public enum IPReportResponseCode 4 | { 5 | /// 6 | /// The item you searched for was not present in VirusTotal's dataset. 7 | /// 8 | NotPresent = 0, 9 | 10 | /// 11 | /// The item was present and it could be retrieved. 12 | /// 13 | Present = 1 14 | } -------------------------------------------------------------------------------- /src/VirusTotalNet/ResponseCodes/RescanResponseCode.cs: -------------------------------------------------------------------------------- 1 | namespace VirusTotalNet.ResponseCodes; 2 | 3 | public enum RescanResponseCode 4 | { 5 | /// 6 | /// There was an error in the request 7 | /// 8 | ResourceNotFound = 0, 9 | 10 | /// 11 | /// The requested item is still queued for analysis. 12 | /// 13 | Queued = 1 14 | } -------------------------------------------------------------------------------- /src/VirusTotalNet/ResponseCodes/ScanFileResponseCode.cs: -------------------------------------------------------------------------------- 1 | namespace VirusTotalNet.ResponseCodes; 2 | 3 | public enum ScanFileResponseCode 4 | { 5 | //Note: I don't think Error can happen. 6 | 7 | /// 8 | /// An error happened in the request. 9 | /// 10 | Error = 0, 11 | 12 | /// 13 | /// The requested item is still queued for analysis. 14 | /// 15 | Queued = 1 16 | } -------------------------------------------------------------------------------- /src/VirusTotalNet/ResponseCodes/UrlReportResponseCode.cs: -------------------------------------------------------------------------------- 1 | namespace VirusTotalNet.ResponseCodes; 2 | 3 | public enum UrlReportResponseCode 4 | { 5 | /// 6 | /// The item you searched for was not present in VirusTotal's dataset. 7 | /// 8 | NotPresent = 0, 9 | 10 | /// 11 | /// The item was indeed present and it could be retrieved. 12 | /// 13 | Present = 1 14 | } -------------------------------------------------------------------------------- /src/VirusTotalNet/ResponseCodes/UrlScanResponseCode.cs: -------------------------------------------------------------------------------- 1 | namespace VirusTotalNet.ResponseCodes; 2 | 3 | public enum UrlScanResponseCode 4 | { 5 | /// 6 | /// The requested item is queued for analysis. 7 | /// 8 | Queued = 1 9 | } -------------------------------------------------------------------------------- /src/VirusTotalNet/Results/CommentResult.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using Newtonsoft.Json; 3 | using VirusTotalNet.Objects; 4 | 5 | namespace VirusTotalNet.Results; 6 | 7 | public class CommentResult 8 | { 9 | /// 10 | /// A list of comments on the resource 11 | /// 12 | public List Comments { get; set; } 13 | 14 | /// 15 | /// Contains the message that corresponds to the response code. 16 | /// 17 | [JsonProperty("verbose_msg")] 18 | public string VerboseMsg { get; set; } 19 | } -------------------------------------------------------------------------------- /src/VirusTotalNet/Results/CreateCommentResult.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | using VirusTotalNet.ResponseCodes; 3 | 4 | namespace VirusTotalNet.Results; 5 | 6 | public class CreateCommentResult 7 | { 8 | [JsonProperty("response_code")] 9 | public CommentResponseCode ResponseCode { get; set; } 10 | 11 | /// 12 | /// Contains the message that corresponds to the response code. 13 | /// 14 | [JsonProperty("verbose_msg")] 15 | public string VerboseMsg { get; set; } 16 | } -------------------------------------------------------------------------------- /src/VirusTotalNet/Results/DomainReport.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using Newtonsoft.Json; 4 | using VirusTotalNet.Internal.DateTimeParsers; 5 | using VirusTotalNet.Objects; 6 | using VirusTotalNet.ResponseCodes; 7 | 8 | namespace VirusTotalNet.Results; 9 | 10 | public class DomainReport 11 | { 12 | [JsonProperty("Sophos category")] 13 | public string SophosCategory { get; set; } 14 | 15 | [JsonProperty("alphaMountain.ai category")] 16 | public string AlphaMountainCategory { get; set; } 17 | 18 | [JsonProperty("Comodo Valkyrie Verdict category")] 19 | public string ComodoValkyrieCategory { get; set; } 20 | 21 | [JsonProperty("Alexa category")] 22 | public string AlexaCategory { get; set; } 23 | 24 | [JsonProperty("Alexa domain info")] 25 | public string AlexaDomainInfo { get; set; } 26 | 27 | [JsonProperty("Alexa rank")] 28 | public int AlexaRank { get; set; } 29 | 30 | [JsonProperty("BitDefender category")] 31 | public string BitDefenderCategory { get; set; } 32 | 33 | [JsonProperty("BitDefender domain info")] 34 | public string BitDefenderDomainInfo { get; set; } 35 | 36 | public List Categories { get; set; } 37 | 38 | [JsonProperty("detected_communicating_samples")] 39 | public List DetectedCommunicatingSamples { get; set; } 40 | 41 | [JsonProperty("detected_downloaded_samples")] 42 | public List DetectedDownloadedSamples { get; set; } 43 | 44 | [JsonProperty("detected_referrer_samples")] 45 | public List DetectedReferrerSamples { get; set; } 46 | 47 | [JsonProperty("detected_urls")] 48 | public List DetectedUrls { get; set; } 49 | 50 | [JsonProperty("Dr.Web category")] 51 | public string DrWebCategory { get; set; } 52 | 53 | [JsonProperty("Forcepoint ThreatSeeker category")] 54 | public string ForcePointThreatSeekerCategory { get; set; } 55 | 56 | [JsonProperty("Opera domain info")] 57 | public string OperaDomainInfo { get; set; } 58 | 59 | public List Pcaps { get; set; } 60 | 61 | public List Resolutions { get; set; } 62 | 63 | [JsonProperty("domain_siblings")] 64 | public List DomainSiblings { get; set; } 65 | 66 | [JsonProperty("subdomains")] 67 | public List SubDomains { get; set; } 68 | 69 | [JsonProperty("TrendMicro category")] 70 | public string TrendMicroCategory { get; set; } 71 | 72 | [JsonProperty("undetected_communicating_samples")] 73 | public List UndetectedCommunicatingSamples { get; set; } 74 | 75 | [JsonProperty("undetected_downloaded_samples")] 76 | public List UndetectedDownloadedSamples { get; set; } 77 | 78 | [JsonProperty("undetected_referrer_samples")] 79 | public List UndetectedReferrerSamples { get; set; } 80 | 81 | [JsonProperty("undetected_urls")] 82 | public List> UndetectedUrls { get; set; } 83 | 84 | [JsonProperty("Websense ThreatSeeker category")] 85 | public string WebsenseThreatSeekerCategory { get; set; } 86 | 87 | [JsonProperty("Webutation domain info")] 88 | public WebutationInfo WebutationDomainInfo { get; set; } 89 | 90 | [JsonProperty("whois")] 91 | public string WhoIs { get; set; } 92 | 93 | [JsonProperty("whois_timestamp", NullValueHandling = NullValueHandling.Ignore)] 94 | [JsonConverter(typeof(UnixTimeConverter))] 95 | public DateTime WhoIsTimestamp { get; set; } 96 | 97 | [JsonProperty("WOT domain info")] 98 | public WOTInfo WOTDomainInfo { get; set; } 99 | 100 | [JsonProperty("Xcitium Verdict Cloud category")] 101 | public string XcitiumCloudCategory { get; set; } 102 | 103 | [JsonProperty("response_code")] 104 | public DomainResponseCode ResponseCode { get; set; } 105 | 106 | /// 107 | /// Contains the message that corresponds to the response code. 108 | /// 109 | [JsonProperty("verbose_msg")] 110 | public string VerboseMsg { get; set; } 111 | } -------------------------------------------------------------------------------- /src/VirusTotalNet/Results/FileReport.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using Newtonsoft.Json; 4 | using VirusTotalNet.Objects; 5 | using VirusTotalNet.ResponseCodes; 6 | 7 | namespace VirusTotalNet.Results; 8 | 9 | public class FileReport 10 | { 11 | /// 12 | /// MD5 hash of the resource. 13 | /// 14 | public string MD5 { get; set; } 15 | 16 | /// 17 | /// A permanent link that points to this specific scan. 18 | /// 19 | public string Permalink { get; set; } 20 | 21 | /// 22 | /// How many engines flagged this resource. 23 | /// 24 | public int Positives { get; set; } 25 | 26 | /// 27 | /// Contains the id of the resource. Can be a SHA256, MD5 or other hash type. 28 | /// 29 | public string Resource { get; set; } 30 | 31 | /// 32 | /// The date the resource was last scanned. 33 | /// 34 | [JsonProperty("scan_date")] 35 | public DateTime ScanDate { get; set; } 36 | 37 | /// 38 | /// Contains the scan id for this result. 39 | /// 40 | [JsonProperty("scan_id")] 41 | public string ScanId { get; set; } 42 | 43 | /// 44 | /// The scan results from each engine. 45 | /// 46 | public Dictionary Scans { get; set; } 47 | 48 | /// 49 | /// SHA1 hash of the resource. 50 | /// 51 | public string SHA1 { get; set; } 52 | 53 | /// 54 | /// SHA256 hash of the resource. 55 | /// 56 | public string SHA256 { get; set; } 57 | 58 | /// 59 | /// How many engines scanned this resource. 60 | /// 61 | public int Total { get; set; } 62 | 63 | /// 64 | /// The response code. Use this to determine the status of the report. 65 | /// 66 | [JsonProperty("response_code")] 67 | public FileReportResponseCode ResponseCode { get; set; } 68 | 69 | /// 70 | /// Contains the message that corresponds to the response code. 71 | /// 72 | [JsonProperty("verbose_msg")] 73 | public string VerboseMsg { get; set; } 74 | } -------------------------------------------------------------------------------- /src/VirusTotalNet/Results/IPReport.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using Newtonsoft.Json; 3 | using VirusTotalNet.Objects; 4 | using VirusTotalNet.ResponseCodes; 5 | 6 | namespace VirusTotalNet.Results; 7 | 8 | public class IPReport 9 | { 10 | [JsonProperty("as_owner")] 11 | public string AsOwner { get; set; } 12 | 13 | public int ASN { get; set; } 14 | 15 | public string Country { get; set; } 16 | public string Continent { get; set; } 17 | public string Network { get; set; } 18 | 19 | [JsonProperty("detected_communicating_samples")] 20 | public List DetectedCommunicatingSamples { get; set; } 21 | 22 | [JsonProperty("detected_downloaded_samples")] 23 | public List DetectedDownloadedSamples { get; set; } 24 | 25 | [JsonProperty("detected_referrer_samples")] 26 | public List DetectedReferrerSamples { get; set; } 27 | 28 | [JsonProperty("detected_urls")] 29 | public List DetectedUrls { get; set; } 30 | 31 | public List Resolutions { get; set; } 32 | 33 | [JsonProperty("undetected_communicating_samples")] 34 | public List UndetectedCommunicatingSamples { get; set; } 35 | 36 | [JsonProperty("undetected_downloaded_samples")] 37 | public List UndetectedDownloadedSamples { get; set; } 38 | 39 | [JsonProperty("undetected_referrer_samples")] 40 | public List UndetectedReferrerSamples { get; set; } 41 | 42 | [JsonProperty("undetected_urls")] 43 | public List> UndetectedUrls { get; set; } 44 | 45 | /// 46 | /// The response code. Use this to determine the status of the report. 47 | /// 48 | [JsonProperty("response_code")] 49 | public IPReportResponseCode ResponseCode { get; set; } 50 | 51 | /// 52 | /// Contains the message that corresponds to the response code. 53 | /// 54 | [JsonProperty("verbose_msg")] 55 | public string VerboseMsg { get; set; } 56 | } -------------------------------------------------------------------------------- /src/VirusTotalNet/Results/RescanResult.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | using VirusTotalNet.ResponseCodes; 3 | 4 | namespace VirusTotalNet.Results; 5 | 6 | public class RescanResult 7 | { 8 | /// 9 | /// A unique link to this particular scan result. 10 | /// 11 | public string Permalink { get; set; } 12 | 13 | /// 14 | /// Id of the resource. 15 | /// 16 | public string Resource { get; set; } 17 | 18 | /// 19 | /// The unique scan id of the resource. 20 | /// 21 | [JsonProperty("scan_id")] 22 | public string ScanId { get; set; } 23 | 24 | /// 25 | /// SHA256 hash of the resource. 26 | /// 27 | public string SHA256 { get; set; } 28 | 29 | [JsonProperty("response_code")] 30 | public RescanResponseCode ResponseCode { get; set; } 31 | } -------------------------------------------------------------------------------- /src/VirusTotalNet/Results/ScanResult.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | using VirusTotalNet.ResponseCodes; 3 | 4 | namespace VirusTotalNet.Results; 5 | 6 | public class ScanResult 7 | { 8 | /// 9 | /// MD5 hash of the resource. 10 | /// 11 | public string MD5 { get; set; } 12 | 13 | /// 14 | /// A unique link to this particular scan result. 15 | /// 16 | public string Permalink { get; set; } 17 | 18 | /// 19 | /// Id of the resource. 20 | /// 21 | public string Resource { get; set; } 22 | 23 | /// 24 | /// The unique scan id of the resource. 25 | /// 26 | [JsonProperty("scan_id")] 27 | public string ScanId { get; set; } 28 | 29 | /// 30 | /// SHA256 hash of the resource. 31 | /// 32 | public string SHA1 { get; set; } 33 | 34 | /// 35 | /// SHA256 hash of the resource. 36 | /// 37 | public string SHA256 { get; set; } 38 | 39 | [JsonProperty("response_code")] 40 | public ScanFileResponseCode ResponseCode { get; set; } 41 | 42 | /// 43 | /// Contains the message that corresponds to the response code. 44 | /// 45 | [JsonProperty("verbose_msg")] 46 | public string VerboseMsg { get; set; } 47 | } -------------------------------------------------------------------------------- /src/VirusTotalNet/Results/UrlReport.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using Newtonsoft.Json; 4 | using VirusTotalNet.Objects; 5 | using VirusTotalNet.ResponseCodes; 6 | 7 | namespace VirusTotalNet.Results; 8 | 9 | public class UrlReport 10 | { 11 | /// 12 | /// File scan id of the resource. 13 | /// 14 | [JsonProperty("filescan_id")] 15 | public string FileScanId { get; set; } 16 | 17 | /// 18 | /// A permanent link that points to this specific scan. 19 | /// 20 | public string Permalink { get; set; } 21 | 22 | /// 23 | /// How many engines flagged this resource. 24 | /// 25 | public int Positives { get; set; } 26 | 27 | /// 28 | /// Contains the id of the resource. Can be a SHA256, MD5 or other hash type. 29 | /// 30 | public string Resource { get; set; } 31 | 32 | /// 33 | /// The date the resource was last scanned. 34 | /// 35 | [JsonProperty("scan_date")] 36 | public DateTime ScanDate { get; set; } 37 | 38 | /// 39 | /// Contains the scan id for this result. 40 | /// 41 | [JsonProperty("scan_id")] 42 | public string ScanId { get; set; } 43 | 44 | /// 45 | /// The scan results from each engine. 46 | /// 47 | public Dictionary Scans { get; set; } 48 | 49 | /// 50 | /// How many engines scanned this resource. 51 | /// 52 | public int Total { get; set; } 53 | 54 | public string URL { get; set; } 55 | 56 | /// 57 | /// The response code. Use this to determine the status of the report. 58 | /// 59 | [JsonProperty("response_code")] 60 | public UrlReportResponseCode ResponseCode { get; set; } 61 | 62 | /// 63 | /// Contains the message that corresponds to the response code. 64 | /// 65 | [JsonProperty("verbose_msg")] 66 | public string VerboseMsg { get; set; } 67 | } -------------------------------------------------------------------------------- /src/VirusTotalNet/Results/UrlScanResult.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Newtonsoft.Json; 3 | using VirusTotalNet.ResponseCodes; 4 | 5 | namespace VirusTotalNet.Results; 6 | 7 | public class UrlScanResult 8 | { 9 | public string Url { get; set; } 10 | 11 | [JsonProperty("scan_date")] 12 | public DateTime ScanDate { get; set; } 13 | 14 | /// 15 | /// A unique link to this particular scan result. 16 | /// 17 | public string Permalink { get; set; } 18 | 19 | /// 20 | /// The resource. 21 | /// 22 | public string Resource { get; set; } 23 | 24 | /// 25 | /// The unique scan id of the resource. 26 | /// 27 | [JsonProperty("scan_id")] 28 | public string ScanId { get; set; } 29 | 30 | [JsonProperty("response_code")] 31 | public UrlScanResponseCode ResponseCode { get; set; } 32 | 33 | /// 34 | /// Contains the message that corresponds to the response code. 35 | /// 36 | [JsonProperty("verbose_msg")] 37 | public string VerboseMsg { get; set; } 38 | } -------------------------------------------------------------------------------- /src/VirusTotalNet/VirusTotal.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Linq; 5 | using System.Net; 6 | using System.Net.Http; 7 | using System.Net.Http.Headers; 8 | using System.Text; 9 | using System.Threading.Tasks; 10 | using Newtonsoft.Json; 11 | using Newtonsoft.Json.Linq; 12 | using VirusTotalNet.Enums; 13 | using VirusTotalNet.Exceptions; 14 | using VirusTotalNet.Helpers; 15 | using VirusTotalNet.Internal.Objects; 16 | using VirusTotalNet.Internal.Other; 17 | using VirusTotalNet.Results; 18 | 19 | namespace VirusTotalNet; 20 | 21 | public class VirusTotal : IDisposable 22 | { 23 | private readonly HttpClient _client; 24 | private readonly HttpClientHandler _httpClientHandler; 25 | private readonly JsonSerializer _serializer; 26 | private readonly string _defaultApiUrl = "www.virustotal.com/vtapi/v2/"; 27 | private readonly string _apiUrl; 28 | private readonly string _apiKey; 29 | 30 | /// 31 | /// Create a new instance of VirusTotal client 32 | /// 33 | /// The API key you got from Virus Total 34 | /// An optional url for a different API endpoint 35 | /// 36 | public VirusTotal(string apiKey, string? apiUrl = null) 37 | { 38 | if (string.IsNullOrWhiteSpace(apiKey)) 39 | throw new ArgumentException("You have to set an API key.", nameof(apiKey)); 40 | 41 | if (apiKey.Length < 64) 42 | throw new ArgumentException("API key is too short.", nameof(apiKey)); 43 | 44 | _apiKey = apiKey; 45 | _apiUrl = apiUrl ?? _defaultApiUrl; 46 | 47 | _httpClientHandler = new HttpClientHandler(); 48 | _httpClientHandler.AllowAutoRedirect = true; 49 | _client = new HttpClient(_httpClientHandler); 50 | 51 | JsonSerializerSettings jsonSettings = new JsonSerializerSettings(); 52 | jsonSettings.NullValueHandling = NullValueHandling.Ignore; 53 | jsonSettings.Formatting = Formatting.None; 54 | 55 | _serializer = JsonSerializer.Create(jsonSettings); 56 | 57 | RestrictSizeLimits = true; 58 | RestrictNumberOfResources = true; 59 | } 60 | 61 | internal VirusTotal(string apiKey, JsonSerializerSettings settings) : this(apiKey) 62 | { 63 | _serializer = JsonSerializer.Create(settings); 64 | } 65 | 66 | /// 67 | /// Occurs when the raw JSON response is received from VirusTotal. 68 | /// 69 | public event Action? OnRawResponseReceived; 70 | 71 | /// 72 | /// Occurs just before we send a request to VirusTotal. 73 | /// 74 | public event Action? OnHTTPRequestSending; 75 | 76 | /// 77 | /// Occurs right after a response has been received from VirusTotal. 78 | /// 79 | public event Action? OnHTTPResponseReceived; 80 | 81 | /// 82 | /// When true, we check the file size before uploading it to Virus Total. The file size restrictions are based on the Virus Total public API 2.0 documentation. 83 | /// 84 | public bool RestrictSizeLimits { get; set; } 85 | 86 | /// 87 | /// When true, we check the number of resources that are submitted to Virus Total. The limits are according to Virus Total public API 2.0 documentation. 88 | /// 89 | public bool RestrictNumberOfResources { get; set; } 90 | 91 | /// 92 | /// The maximum size (in bytes) that the Virus Total public API 2.0 supports for file uploads. 93 | /// 94 | public int FileSizeLimit { get; set; } = 33553369; //32 MB - 1063 = 33553369 it is the effective limit by virus total as it measures file size limit on the TOTAL request size, and not just the file content. 95 | 96 | /// 97 | /// The maximum size when using the large file API functionality (part of private API) 98 | /// 99 | public long LargeFileSizeLimit { get; set; } = 1024 * 1024 * 200; //200 MB 100 | 101 | /// 102 | /// The maximum size (in bytes) of comments. 103 | /// 104 | public int CommentSizeRestriction { get; set; } = 4096; 105 | 106 | /// 107 | /// The maximum number of resources you can rescan in one request. 108 | /// 109 | public int RescanBatchSizeLimit { get; set; } = 25; 110 | 111 | /// 112 | /// The maximum number of resources you can get file reports for in one request. 113 | /// 114 | public int FileReportBatchSizeLimit { get; set; } = 4; 115 | 116 | /// 117 | /// The maximum number of URLs you can get reports for in one request. 118 | /// 119 | public int UrlReportBatchSizeLimit { get; set; } = 4; 120 | 121 | /// 122 | /// The maximum number of URLs you can scan in one request. 123 | /// 124 | public int UrlScanBatchSizeLimit { get; set; } = 25; 125 | 126 | /// 127 | /// Set to false to use HTTP instead of HTTPS. HTTPS is used by default. 128 | /// 129 | public bool UseTLS { get; set; } = true; 130 | 131 | /// 132 | /// The user-agent to use when doing queries 133 | /// 134 | public string UserAgent 135 | { 136 | get => _client.DefaultRequestHeaders.UserAgent.ToString(); 137 | set => _client.DefaultRequestHeaders.Add("User-Agent", value); 138 | } 139 | 140 | /// 141 | /// Get or set the proxy. 142 | /// 143 | public IWebProxy? Proxy 144 | { 145 | get => _httpClientHandler.Proxy; 146 | set 147 | { 148 | _httpClientHandler.UseProxy = value != null; 149 | _httpClientHandler.Proxy = value; 150 | } 151 | } 152 | 153 | /// 154 | /// Get or set the timeout. 155 | /// 156 | public TimeSpan Timeout 157 | { 158 | get => _client.Timeout; 159 | set => _client.Timeout = value; 160 | } 161 | 162 | /// 163 | /// Scan a file. 164 | /// Note: It is highly encouraged to get the report of the file before scanning, in case it has already been scanned before. 165 | /// 166 | /// The file to scan 167 | public async Task ScanFileAsync(string filePath) 168 | { 169 | if (!File.Exists(filePath)) 170 | throw new FileNotFoundException("The file was not found.", filePath); 171 | 172 | string filename = Path.GetFileName(filePath); 173 | 174 | using Stream fs = File.OpenRead(filePath); 175 | return await ScanFileAsync(fs, filename).ConfigureAwait(false); 176 | } 177 | 178 | /// 179 | /// Scan a file. 180 | /// Note: It is highly encouraged to get the report of the file before scanning, in case it has already been scanned before. 181 | /// 182 | /// The file to scan 183 | public async Task ScanFileAsync(FileInfo file) 184 | { 185 | if (!file.Exists) 186 | throw new FileNotFoundException("The file was not found.", file.Name); 187 | 188 | using Stream fs = file.OpenRead(); 189 | return await ScanFileAsync(fs, file.Name).ConfigureAwait(false); 190 | } 191 | 192 | /// 193 | /// Scan a file. 194 | /// Note: It is highly encouraged to get the report of the file before scanning, in case it has already been scanned before. 195 | /// Note: You are also strongly encouraged to provide the filename as it is rich metadata for the Virus Total database. 196 | /// 197 | /// The file to scan 198 | /// The filename of the file 199 | public async Task ScanFileAsync(byte[] file, string filename) 200 | { 201 | using MemoryStream ms = new MemoryStream(file); 202 | return await ScanFileAsync(ms, filename).ConfigureAwait(false); 203 | } 204 | 205 | /// 206 | /// Scan a file. 207 | /// Note: It is highly encouraged to get the report of the file before scanning, in case it has already been scanned before. 208 | /// Note: You are also strongly encouraged to provide the filename as it is rich metadata for the Virus Total database. 209 | /// 210 | /// The file to scan 211 | /// The filename of the file 212 | public async Task ScanFileAsync(Stream stream, string filename) 213 | { 214 | ValidateScanFileArguments(stream, FileSizeLimit, filename); 215 | 216 | using MultipartFormDataContent multi = new MultipartFormDataContent(); 217 | multi.Add(CreateApiPart()); 218 | multi.Add(CreateFileContent(stream, filename)); 219 | 220 | //https://www.virustotal.com/vtapi/v2/file/scan 221 | return await GetResponse("file/scan", HttpMethod.Post, multi).ConfigureAwait(false); 222 | } 223 | 224 | /// 225 | /// Scan a large file. The difference between and this method, is that this method sends 2 requests, and it is part of the private VT API, so you need an API key with large file upload support. 226 | /// Note: It is highly encouraged to get the report of the file before scanning, in case it has already been scanned before. 227 | /// 228 | /// The file to scan 229 | public async Task ScanLargeFileAsync(string filePath) 230 | { 231 | if (!File.Exists(filePath)) 232 | throw new FileNotFoundException("The file was not found.", filePath); 233 | 234 | string filename = Path.GetFileName(filePath); 235 | 236 | using Stream fs = File.OpenRead(filePath); 237 | return await ScanLargeFileAsync(fs, filename).ConfigureAwait(false); 238 | } 239 | 240 | /// 241 | /// Scan a large file. The difference between and this method, is that this method sends 2 requests, and it is part of the private VT API, so you need an API key with large file upload support. 242 | /// Note: It is highly encouraged to get the report of the file before scanning, in case it has already been scanned before. 243 | /// 244 | /// The file to scan 245 | public async Task ScanLargeFileAsync(FileInfo file) 246 | { 247 | if (!file.Exists) 248 | throw new FileNotFoundException("The file was not found.", file.Name); 249 | 250 | using Stream fs = file.OpenRead(); 251 | return await ScanLargeFileAsync(fs, file.Name).ConfigureAwait(false); 252 | } 253 | 254 | /// 255 | /// Scan a large file. The difference between and this method, is that this method sends 2 requests, and it is part of the private VT API, so you need an API key with large file upload support. 256 | /// Note: It is highly encouraged to get the report of the file before scanning, in case it has already been scanned before. 257 | /// Note: You are also strongly encouraged to provide the filename as it is rich metadata for the Virus Total database. 258 | /// 259 | /// The file to scan 260 | /// The filename of the file 261 | public async Task ScanLargeFileAsync(byte[] file, string filename) 262 | { 263 | using MemoryStream ms = new MemoryStream(file); 264 | return await ScanLargeFileAsync(ms, filename).ConfigureAwait(false); 265 | } 266 | 267 | /// 268 | /// Scan a large file. The difference between and this method, is that this method sends 2 requests, and it is part of the private VT API, so you need an API key with large file upload support. 269 | /// Note: It is highly encouraged to get the report of the file before scanning, in case it has already been scanned before. 270 | /// Note: You are also strongly encouraged to provide the filename as it is rich metadata for the Virus Total database. 271 | /// 272 | /// The file to scan 273 | /// The filename of the file 274 | public async Task ScanLargeFileAsync(Stream stream, string filename) 275 | { 276 | ValidateScanFileArguments(stream, LargeFileSizeLimit, filename); 277 | 278 | if (stream.Length <= FileSizeLimit) 279 | throw new ArgumentException($"Please use the ScanFileAsync() method for files smaller than {FileSizeLimit} bytes", nameof(stream)); 280 | 281 | //https://www.virustotal.com/vtapi/v2/file/scan/upload_url 282 | LargeFileUpload uploadUrlObj = await GetResponse("file/scan/upload_url?apikey=" + _apiKey, HttpMethod.Get, null).ConfigureAwait(false); 283 | 284 | if (string.IsNullOrEmpty(uploadUrlObj.UploadUrl)) 285 | throw new Exception("Something when wrong while getting the upload url. Are you using an API key with support for this request?"); 286 | 287 | using MultipartFormDataContent multi = new MultipartFormDataContent(); 288 | multi.Add(CreateFileContent(stream, filename, false)); //The big file upload API does not like it when multi-part uploads contain the size field 289 | 290 | return await GetResponse(uploadUrlObj.UploadUrl, HttpMethod.Post, multi).ConfigureAwait(false); 291 | } 292 | 293 | private void ValidateScanFileArguments(Stream stream, long fileSizeLimit, string filename) 294 | { 295 | if (stream == null) 296 | throw new ArgumentNullException(nameof(stream), "You must provide a stream that is not null"); 297 | 298 | if (stream.Length <= 0) 299 | throw new ArgumentException("You must provide a stream with content", nameof(stream)); 300 | 301 | if (RestrictSizeLimits && stream.Length > fileSizeLimit) 302 | throw new SizeLimitException(fileSizeLimit, stream.Length); 303 | 304 | if (string.IsNullOrWhiteSpace(filename)) 305 | throw new ArgumentException("You must provide a filename. Preferably the original filename.", nameof(filename)); 306 | } 307 | 308 | /// 309 | /// Tell VirusTotal to rescan a file. 310 | /// Note: This does not send the files to VirusTotal. It hashes the file and sends that instead. 311 | /// Note: Before requesting a rescan you should retrieve the latest report on the file. 312 | /// 313 | public async Task RescanFileAsync(FileInfo file) 314 | { 315 | return await RescanFileAsync(ResourcesHelper.GetResourceIdentifier(file)).ConfigureAwait(false); 316 | } 317 | 318 | /// 319 | /// Tell VirusTotal to rescan a file. 320 | /// Note: This does not send the files to VirusTotal. It hashes the file and sends that instead. 321 | /// Note: Before requesting a rescan you should retrieve the latest report on the file. 322 | /// 323 | public async Task RescanFileAsync(byte[] file) 324 | { 325 | return await RescanFileAsync(ResourcesHelper.GetResourceIdentifier(file)).ConfigureAwait(false); 326 | } 327 | 328 | /// 329 | /// Tell VirusTotal to rescan a file. 330 | /// Note: This does not send the files to VirusTotal. It hashes the file and sends that instead. 331 | /// Note: Before requesting a rescan you should retrieve the latest report on the file. 332 | /// 333 | public async Task RescanFileAsync(Stream stream) 334 | { 335 | return await RescanFileAsync(ResourcesHelper.GetResourceIdentifier(stream)).ConfigureAwait(false); 336 | } 337 | 338 | /// 339 | /// Tell VirusTotal to rescan a file without sending the actual file to VirusTotal. 340 | /// Note: Before requesting a rescan you should retrieve the latest report on the file. 341 | /// 342 | /// A hash of the file. It can be an MD5, SHA1 or SHA256 343 | public async Task RescanFileAsync(string resource) 344 | { 345 | resource = ResourcesHelper.ValidateResourcea(resource, ResourceType.AnyHash); 346 | 347 | //Required 348 | Dictionary values = new Dictionary(2, StringComparer.OrdinalIgnoreCase); 349 | values.Add("resource", resource); 350 | 351 | //https://www.virustotal.com/vtapi/v2/file/rescan 352 | return await GetResponse("file/rescan", HttpMethod.Post, CreateUrlEncodedContent(values)).ConfigureAwait(false); 353 | } 354 | 355 | /// 356 | /// Tell VirusTotal to rescan a file. 357 | /// Note: This does not send the files to VirusTotal. It hashes the file and sends that instead. 358 | /// Note: Before requesting a rescan you should retrieve the latest report on the files. 359 | /// 360 | public async Task> RescanFilesAsync(IEnumerable files) 361 | { 362 | return await RescanFilesAsync(ResourcesHelper.GetResourceIdentifier(files)).ConfigureAwait(false); 363 | } 364 | 365 | /// 366 | /// Tell VirusTotal to rescan a file. 367 | /// Note: This does not send the files to VirusTotal. It hashes the file and sends that instead. 368 | /// Note: Before requesting a rescan you should retrieve the latest report on the files. 369 | /// 370 | public async Task> RescanFilesAsync(IEnumerable files) 371 | { 372 | return await RescanFilesAsync(ResourcesHelper.GetResourceIdentifier(files)).ConfigureAwait(false); 373 | } 374 | 375 | /// 376 | /// Tell VirusTotal to rescan a file. 377 | /// Note: This does not send the content of the streams to VirusTotal. It hashes the content and sends that instead. 378 | /// Note: Before requesting a rescan you should retrieve the latest report on the files. 379 | /// 380 | public async Task> RescanFilesAsync(IEnumerable streams) 381 | { 382 | return await RescanFilesAsync(ResourcesHelper.GetResourceIdentifier(streams)).ConfigureAwait(false); 383 | } 384 | 385 | /// 386 | /// Tell VirusTotal to rescan a file. 387 | /// Note: This does not send the files to VirusTotal. It hashes the file and sends that instead. 388 | /// Note: Before requesting a rescan you should retrieve the latest report on the files. 389 | /// Note: You can use MD5, SHA1 or SHA256 and even mix them. 390 | /// Note: You can only request a maximum of 25 rescans at the time. 391 | /// 392 | /// a MD5, SHA1 or SHA256 of the files. You can also specify list made up of a combination of any of the three allowed hashes (up to 25 items), this allows you to perform a batch request with one single call. 393 | public async Task> RescanFilesAsync(IEnumerable resourceList) 394 | { 395 | resourceList = ResourcesHelper.ValidateResourcea(resourceList, ResourceType.AnyHash); 396 | 397 | string[] resources = resourceList as string[] ?? resourceList.ToArray(); 398 | 399 | if (RestrictNumberOfResources && resources.Length > RescanBatchSizeLimit) 400 | throw new ResourceLimitException($"Too many resources. There is a maximum of {RescanBatchSizeLimit} resources at the time."); 401 | 402 | //Required 403 | Dictionary values = new Dictionary(2, StringComparer.OrdinalIgnoreCase); 404 | values.Add("resource", string.Join(",", resources)); 405 | 406 | //https://www.virustotal.com/vtapi/v2/file/rescan 407 | return await GetResponses("file/rescan", HttpMethod.Post, CreateUrlEncodedContent(values)).ConfigureAwait(false); 408 | } 409 | 410 | /// 411 | /// Gets the report of the file. 412 | /// Note: This does not send the files to VirusTotal. It hashes the file and sends that instead. 413 | /// 414 | /// The file you want to get a report on. 415 | public async Task GetFileReportAsync(byte[] file) 416 | { 417 | return await GetFileReportAsync(ResourcesHelper.GetResourceIdentifier(file)).ConfigureAwait(false); 418 | } 419 | 420 | /// 421 | /// Gets the report of the file. 422 | /// Note: This does not send the files to VirusTotal. It hashes the file and sends that instead. 423 | /// 424 | /// The file you want to get a report on. 425 | public async Task GetFileReportAsync(FileInfo file) 426 | { 427 | return await GetFileReportAsync(ResourcesHelper.GetResourceIdentifier(file)).ConfigureAwait(false); 428 | } 429 | 430 | /// 431 | /// Gets the report of the file. 432 | /// Note: This does not send the files to VirusTotal. It hashes the file and sends that instead. 433 | /// 434 | /// The stream you want to get a report on. 435 | public async Task GetFileReportAsync(Stream stream) 436 | { 437 | return await GetFileReportAsync(ResourcesHelper.GetResourceIdentifier(stream)).ConfigureAwait(false); 438 | } 439 | 440 | /// 441 | /// Gets the report of the file. 442 | /// Note: This does not send the files to VirusTotal. It hashes the file and sends that instead. 443 | /// 444 | /// The resource (MD5, SHA1 or SHA256) you wish to get a report on. 445 | public async Task GetFileReportAsync(string resource) 446 | { 447 | resource = ResourcesHelper.ValidateResourcea(resource, ResourceType.AnyHash | ResourceType.ScanId); 448 | 449 | //Required 450 | Dictionary values = new Dictionary(2, StringComparer.OrdinalIgnoreCase); 451 | values.Add("resource", resource); 452 | 453 | //https://www.virustotal.com/vtapi/v2/file/report 454 | return await GetResponse("file/report", HttpMethod.Post, CreateUrlEncodedContent(values)).ConfigureAwait(false); 455 | } 456 | 457 | /// 458 | /// Gets a list of reports of the files. 459 | /// Note: This does not send the files to VirusTotal. It hashes the files and sends them instead. 460 | /// 461 | /// The files you want to get reports on. 462 | public async Task> GetFileReportsAsync(IEnumerable files) 463 | { 464 | return await GetFileReportsAsync(ResourcesHelper.GetResourceIdentifier(files)).ConfigureAwait(false); 465 | } 466 | 467 | /// 468 | /// Gets a list of reports of the files. 469 | /// Note: This does not send the files to VirusTotal. It hashes the files and sends them instead. 470 | /// 471 | /// The files you want to get reports on. 472 | public async Task> GetFileReportsAsync(IEnumerable files) 473 | { 474 | return await GetFileReportsAsync(ResourcesHelper.GetResourceIdentifier(files)).ConfigureAwait(false); 475 | } 476 | 477 | /// 478 | /// Gets a list of reports of the files. 479 | /// Note: This does not send the content of the streams to VirusTotal. It hashes the content of the stream and sends that instead. 480 | /// 481 | /// The streams you want to get reports on. 482 | public async Task> GetFileReportsAsync(IEnumerable streams) 483 | { 484 | return await GetFileReportsAsync(ResourcesHelper.GetResourceIdentifier(streams)).ConfigureAwait(false); 485 | } 486 | 487 | /// 488 | /// Gets the report of the file represented by its hash or scan ID. 489 | /// Keep in mind that URLs sent using the API have the lowest scanning priority, depending on VirusTotal's load, it may take several hours before the file is scanned, 490 | /// so query the report at regular intervals until the result shows up and do not keep submitting the file over and over again. 491 | /// 492 | /// SHA1, MD5 or SHA256 of the file. It can also be a scan ID of a previous scan. 493 | public async Task> GetFileReportsAsync(IEnumerable resourceList) 494 | { 495 | resourceList = ResourcesHelper.ValidateResourcea(resourceList, ResourceType.AnyHash | ResourceType.ScanId); 496 | 497 | string[] resources = resourceList as string[] ?? resourceList.ToArray(); 498 | 499 | if (RestrictNumberOfResources && resources.Length > FileReportBatchSizeLimit) 500 | throw new ResourceLimitException($"Too many hashes. There is a maximum of {FileReportBatchSizeLimit} resources at the same time."); 501 | 502 | //Required 503 | Dictionary values = new Dictionary(2, StringComparer.OrdinalIgnoreCase); 504 | values.Add("resource", string.Join(",", resources)); 505 | 506 | //https://www.virustotal.com/vtapi/v2/file/report 507 | return await GetResponses("file/report", HttpMethod.Post, CreateUrlEncodedContent(values)).ConfigureAwait(false); 508 | } 509 | 510 | /// 511 | /// Scan the given URL. The URL will be downloaded by VirusTotal and processed. 512 | /// Note: Before performing your submission, you should retrieve the latest report on the URL. 513 | /// 514 | /// The URL to process. 515 | public async Task ScanUrlAsync(string url) 516 | { 517 | url = ResourcesHelper.ValidateResourcea(url, ResourceType.URL); 518 | 519 | //Required 520 | Dictionary values = new Dictionary(2, StringComparer.OrdinalIgnoreCase); 521 | values.Add("url", url); 522 | 523 | //https://www.virustotal.com/vtapi/v2/url/scan 524 | return await GetResponse("url/scan", HttpMethod.Post, CreateUrlEncodedContent(values)).ConfigureAwait(false); 525 | } 526 | 527 | /// 528 | /// Scan the given URL. The URL will be downloaded by VirusTotal and processed. 529 | /// Note: Before performing your submission, you should retrieve the latest report on the URL. 530 | /// 531 | /// The URL to process. 532 | public async Task ScanUrlAsync(Uri url) 533 | { 534 | return await ScanUrlAsync(url.ToString()).ConfigureAwait(false); 535 | } 536 | 537 | /// 538 | /// Scan the given URLs. The URLs will be downloaded by VirusTotal and processed. 539 | /// Note: Before performing your submission, you should retrieve the latest reports on the URLs. 540 | /// 541 | /// The URLs to process. 542 | public async Task> ScanUrlsAsync(IEnumerable urls) 543 | { 544 | urls = ResourcesHelper.ValidateResourcea(urls, ResourceType.URL); 545 | 546 | string[] urlCast = urls as string[] ?? urls.ToArray(); 547 | 548 | if (RestrictNumberOfResources && urlCast.Length > UrlScanBatchSizeLimit) 549 | throw new ResourceLimitException($"Too many URLs. There is a maximum of {UrlScanBatchSizeLimit} URLs at the same time."); 550 | 551 | //Required 552 | Dictionary values = new Dictionary(2, StringComparer.OrdinalIgnoreCase); 553 | values.Add("url", string.Join(Environment.NewLine, urlCast)); 554 | 555 | //https://www.virustotal.com/vtapi/v2/url/scan 556 | return await GetResponses("url/scan", HttpMethod.Post, CreateUrlEncodedContent(values)).ConfigureAwait(false); 557 | } 558 | 559 | /// 560 | /// Scan the given URLs. The URLs will be downloaded by VirusTotal and processed. 561 | /// Note: Before performing your submission, you should retrieve the latest reports on the URLs. 562 | /// 563 | /// The URLs to process. 564 | public async Task> ScanUrlsAsync(IEnumerable urlList) 565 | { 566 | return await ScanUrlsAsync(urlList.Select(x => x.ToString())).ConfigureAwait(false); 567 | } 568 | 569 | /// 570 | /// Gets a scan report from an URL 571 | /// 572 | /// The URL you wish to get the report on. 573 | /// Set to true if you wish VirusTotal to scan the URL if it is not present in the database. 574 | public async Task GetUrlReportAsync(string url, bool scanIfNoReport = false) 575 | { 576 | url = ResourcesHelper.ValidateResourcea(url, ResourceType.URL | ResourceType.ScanId); 577 | 578 | //Required 579 | Dictionary values = new Dictionary(3, StringComparer.OrdinalIgnoreCase); 580 | values.Add("resource", url); 581 | 582 | //Optional 583 | if (scanIfNoReport) 584 | values.Add("scan", "1"); 585 | 586 | //Output 587 | return await GetResponse("url/report", HttpMethod.Post, CreateUrlEncodedContent(values)).ConfigureAwait(false); 588 | } 589 | 590 | /// 591 | /// Gets a scan report from an URL 592 | /// 593 | /// The URL you wish to get the report on. 594 | /// Set to true if you wish VirusTotal to scan the URL if it is not present in the database. 595 | public async Task GetUrlReportAsync(Uri url, bool scanIfNoReport = false) 596 | { 597 | return await GetUrlReportAsync(url.ToString(), scanIfNoReport).ConfigureAwait(false); 598 | } 599 | 600 | /// 601 | /// Gets a scan report from a list of URLs 602 | /// 603 | /// The URLs you wish to get the reports on. 604 | /// Set to true if you wish VirusTotal to scan the URLs if it is not present in the database. 605 | public async Task> GetUrlReportsAsync(IEnumerable urls, bool scanIfNoReport = false) 606 | { 607 | urls = ResourcesHelper.ValidateResourcea(urls, ResourceType.URL | ResourceType.ScanId); 608 | 609 | string[] urlCast = urls as string[] ?? urls.ToArray(); 610 | 611 | if (RestrictNumberOfResources && urlCast.Length > UrlReportBatchSizeLimit) 612 | throw new ResourceLimitException($"Too many URLs. There is a maximum of {UrlReportBatchSizeLimit} urls at the time."); 613 | 614 | //Required 615 | Dictionary values = new Dictionary(3, StringComparer.OrdinalIgnoreCase); 616 | values.Add("resource", string.Join(Environment.NewLine, urlCast)); 617 | 618 | //Optional 619 | if (scanIfNoReport) 620 | values.Add("scan", "1"); 621 | 622 | //Output 623 | return await GetResponses("url/report", HttpMethod.Post, CreateUrlEncodedContent(values)).ConfigureAwait(false); 624 | } 625 | 626 | /// 627 | /// Gets a scan report from a list of URLs 628 | /// 629 | /// The URLs you wish to get the reports on. 630 | /// Set to true if you wish VirusTotal to scan the URLs if it is not present in the database. 631 | public async Task> GetUrlReportsAsync(IEnumerable urlList, bool scanIfNoReport = false) 632 | { 633 | return await GetUrlReportsAsync(urlList.Select(x => x.ToString()), scanIfNoReport).ConfigureAwait(false); 634 | } 635 | 636 | /// 637 | /// Gets a scan report from an IP 638 | /// 639 | /// The IP you wish to get the report on. 640 | public async Task GetIPReportAsync(string ip) 641 | { 642 | ip = ResourcesHelper.ValidateResourcea(ip, ResourceType.IP); 643 | 644 | return await GetResponse($"ip-address/report?apikey={_apiKey}&ip={ip}", HttpMethod.Get, null).ConfigureAwait(false); 645 | } 646 | 647 | /// 648 | /// Gets a scan report from an IP 649 | /// 650 | /// The IP you wish to get the report on. 651 | public async Task GetIPReportAsync(IPAddress ip) 652 | { 653 | return await GetIPReportAsync(ip.ToString()).ConfigureAwait(false); 654 | } 655 | 656 | /// 657 | /// Gets a scan report from a domain 658 | /// 659 | /// The domain you wish to get the report on. 660 | public async Task GetDomainReportAsync(string domain) 661 | { 662 | domain = ResourcesHelper.ValidateResourcea(domain, ResourceType.Domain); 663 | 664 | //Hack because VT thought it was a good idea to have this API call as GET 665 | return await GetResponse($"domain/report?apikey={_apiKey}&domain={domain}", HttpMethod.Get, null).ConfigureAwait(false); 666 | } 667 | 668 | /// 669 | /// Gets a scan report from a domain 670 | /// 671 | /// The domain you wish to get the report on. 672 | public async Task GetDomainReportAsync(Uri domain) 673 | { 674 | return await GetDomainReportAsync(domain.Host).ConfigureAwait(false); 675 | } 676 | 677 | /// 678 | /// Retrieves a comment on a file. 679 | /// 680 | /// The file you wish to retrieve a comment from 681 | /// TODO 682 | public async Task GetCommentAsync(byte[] file, DateTime? before = null) 683 | { 684 | return await GetCommentAsync(ResourcesHelper.GetResourceIdentifier(file), before).ConfigureAwait(false); 685 | } 686 | 687 | /// 688 | /// Retrieves a comment on a file. 689 | /// 690 | /// The file you wish to retrieve a comment from 691 | /// Get the comments before this time 692 | public async Task GetCommentAsync(FileInfo file, DateTime? before = null) 693 | { 694 | return await GetCommentAsync(ResourcesHelper.GetResourceIdentifier(file), before).ConfigureAwait(false); 695 | } 696 | 697 | /// 698 | /// Retrieves a comment from an URL. 699 | /// 700 | /// The URL you wish to retrieve a comment from 701 | /// Get the comments before this time 702 | public async Task GetCommentAsync(Uri uri, DateTime? before = null) 703 | { 704 | return await GetCommentAsync(uri.ToString(), before).ConfigureAwait(false); 705 | } 706 | 707 | /// 708 | /// Retrieves a comment on a resource. 709 | /// 710 | /// The MD5/SHA1/SHA256 hash or URL. 711 | /// TODO 712 | public async Task GetCommentAsync(string resource, DateTime? before = null) 713 | { 714 | resource = ResourcesHelper.ValidateResourcea(resource, ResourceType.AnyHash | ResourceType.URL); 715 | 716 | string? beforeStr = null; 717 | 718 | if (before != null) 719 | beforeStr = "&before=" + before.Value.ToString("yyyyMMddHHmmss"); 720 | 721 | //https://developers.virustotal.com/v2.0/reference/comments-get 722 | return await GetResponse($"comments/get?apikey={_apiKey}&resource={resource}{beforeStr}", HttpMethod.Get, null).ConfigureAwait(false); 723 | } 724 | 725 | /// 726 | /// Creates a comment on a file 727 | /// 728 | /// The file you wish to create a comment on 729 | /// The comment you wish to add. 730 | public async Task CreateCommentAsync(byte[] file, string comment) 731 | { 732 | return await CreateCommentAsync(ResourcesHelper.GetResourceIdentifier(file), comment).ConfigureAwait(false); 733 | } 734 | 735 | /// 736 | /// Creates a comment on a file 737 | /// 738 | /// The file you wish to create a comment on 739 | /// The comment you wish to add. 740 | public async Task CreateCommentAsync(FileInfo file, string comment) 741 | { 742 | return await CreateCommentAsync(ResourcesHelper.GetResourceIdentifier(file), comment).ConfigureAwait(false); 743 | } 744 | 745 | /// 746 | /// Creates a comment on an URL 747 | /// 748 | /// The URL you wish to create a comment on 749 | /// The comment you wish to add. 750 | public async Task CreateCommentAsync(Uri url, string comment) 751 | { 752 | return await CreateCommentAsync(url.ToString(), comment).ConfigureAwait(false); 753 | } 754 | 755 | /// 756 | /// Creates a comment on a resource 757 | /// 758 | /// The MD5/SHA1/SHA256 hash or URL. 759 | /// The comment you wish to add. 760 | public async Task CreateCommentAsync(string resource, string comment) 761 | { 762 | resource = ResourcesHelper.ValidateResourcea(resource, ResourceType.AnyHash | ResourceType.URL); 763 | 764 | if (string.IsNullOrWhiteSpace(comment)) 765 | throw new ArgumentException("Comment must not be null or whitespace", nameof(comment)); 766 | 767 | if (RestrictSizeLimits && comment.Length > CommentSizeRestriction) 768 | throw new ArgumentOutOfRangeException(nameof(comment), $"Your comment is larger than the maximum size of {CommentSizeRestriction / 1024} KB"); 769 | 770 | //Required 771 | Dictionary values = new Dictionary(3, StringComparer.OrdinalIgnoreCase); 772 | values.Add("resource", resource); 773 | values.Add("comment", comment); 774 | 775 | //https://www.virustotal.com/vtapi/v2/comments/put 776 | return await GetResponse("comments/put", HttpMethod.Post, CreateUrlEncodedContent(values)).ConfigureAwait(false); 777 | } 778 | 779 | /// 780 | /// Gives you a link to a file analysis based on its hash. 781 | /// 782 | public string GetPublicFileScanLink(string resource) 783 | { 784 | resource = ResourcesHelper.ValidateResourcea(resource, ResourceType.AnyHash); 785 | 786 | return ResourcesHelper.NormalizeUrl($"www.virustotal.com/#/file/{resource}/detection", UseTLS); 787 | } 788 | 789 | /// 790 | /// Gives you a link to a file analysis based on its hash. 791 | /// Note: This actually hashes the file - if you have the hash already, use the overload that takes in a string. 792 | /// 793 | public string GetPublicFileScanLink(FileInfo file) 794 | { 795 | if (file == null) 796 | throw new ArgumentNullException(nameof(file)); 797 | 798 | if (!file.Exists) 799 | throw new FileNotFoundException("The file you provided does not exist.", file.FullName); 800 | 801 | return GetPublicFileScanLink(ResourcesHelper.GetResourceIdentifier(file)); 802 | } 803 | 804 | /// 805 | /// Gives you a link to a URL analysis. 806 | /// 807 | /// A link to VirusTotal that contains the report 808 | public string GetPublicUrlScanLink(string url) 809 | { 810 | url = ResourcesHelper.ValidateResourcea(url, ResourceType.URL); 811 | 812 | return ResourcesHelper.NormalizeUrl($"www.virustotal.com/#/url/{ResourcesHelper.GetResourceIdentifier(url)}/detection", UseTLS); 813 | } 814 | 815 | /// 816 | /// Gives you a link to a URL analysis. 817 | /// 818 | /// A link to VirusTotal that contains the report 819 | public string GetPublicUrlScanLink(Uri url) 820 | { 821 | return GetPublicUrlScanLink(url.ToString()); 822 | } 823 | 824 | private async Task> GetResponses(string url, HttpMethod method, HttpContent content) 825 | { 826 | using HttpResponseMessage response = await SendRequest(url, method, content).ConfigureAwait(false); 827 | 828 | using Stream responseStream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false); 829 | using StreamReader sr = new StreamReader(responseStream, Encoding.UTF8); 830 | using JsonTextReader jsonTextReader = new JsonTextReader(sr); 831 | jsonTextReader.CloseInput = false; 832 | 833 | SaveResponse(responseStream); 834 | 835 | JToken token = await JToken.LoadAsync(jsonTextReader).ConfigureAwait(false); 836 | 837 | if (token.Type == JTokenType.Array) 838 | { 839 | List? list = token.ToObject>(_serializer); 840 | 841 | if (list == null) 842 | throw new InvalidOperationException($"Unable to deserialize list response from {url}"); 843 | 844 | return list; 845 | } 846 | 847 | T? obj = token.ToObject(_serializer); 848 | 849 | if (obj == null) 850 | throw new InvalidOperationException($"Unable to deserialize object response from {url}"); 851 | 852 | return new List { obj }; 853 | } 854 | 855 | private async Task GetResponse(string url, HttpMethod method, HttpContent? content) 856 | { 857 | using HttpResponseMessage response = await SendRequest(url, method, content).ConfigureAwait(false); 858 | 859 | using Stream responseStream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false); 860 | using StreamReader sr = new StreamReader(responseStream, Encoding.UTF8); 861 | using JsonTextReader jsonTextReader = new JsonTextReader(sr); 862 | jsonTextReader.CloseInput = false; 863 | 864 | SaveResponse(responseStream); 865 | 866 | T? obj = _serializer.Deserialize(jsonTextReader); 867 | 868 | if (obj == null) 869 | throw new InvalidOperationException($"Unable to deserialize response from {url}"); 870 | 871 | return obj; 872 | } 873 | 874 | private async Task SendRequest(string url, HttpMethod method, HttpContent? content) 875 | { 876 | //We need this check because sometimes url is a full url and sometimes it is just an url segment 877 | if (!url.StartsWith("http", StringComparison.OrdinalIgnoreCase)) 878 | url = $"{(UseTLS ? "https://" : "http://")}{_apiUrl}{url}"; 879 | 880 | using HttpRequestMessage request = new HttpRequestMessage(method, url); 881 | request.Content = content; 882 | 883 | OnHTTPRequestSending?.Invoke(request); 884 | 885 | HttpResponseMessage response = await _client.SendAsync(request).ConfigureAwait(false); 886 | 887 | OnHTTPResponseReceived?.Invoke(response); 888 | 889 | if (response.StatusCode == HttpStatusCode.NoContent) 890 | throw new RateLimitException("You have reached the 4 requests pr. min. limit of VirusTotal"); 891 | 892 | if (response.StatusCode == HttpStatusCode.Forbidden) 893 | throw new AccessDeniedException($"You don't have access to this service at '{url}'. Make sure you have a working API key and the required access."); 894 | 895 | if (response.StatusCode == HttpStatusCode.RequestEntityTooLarge) 896 | throw new SizeLimitException(FileSizeLimit); 897 | 898 | if (response.StatusCode != HttpStatusCode.OK) 899 | throw new Exception($"API gave error code {response.StatusCode}"); 900 | 901 | return response; 902 | } 903 | 904 | private void SaveResponse(Stream stream) 905 | { 906 | if (OnRawResponseReceived == null) 907 | return; 908 | 909 | using (MemoryStream ms = new MemoryStream()) 910 | { 911 | stream.CopyTo(ms); 912 | OnRawResponseReceived(ms.ToArray()); 913 | } 914 | 915 | stream.Position = 0; 916 | } 917 | 918 | private HttpContent CreateApiPart() 919 | { 920 | HttpContent content = new StringContent(_apiKey); 921 | content.Headers.ContentDisposition = new ContentDispositionHeaderValue("form-data") 922 | { 923 | Name = "\"apikey\"" 924 | }; 925 | 926 | return content; 927 | } 928 | 929 | private HttpContent CreateFileContent(Stream stream, string fileName, bool includeSize = true) 930 | { 931 | StreamContent fileContent = new StreamContent(stream); 932 | 933 | ContentDispositionHeaderValue disposition = new ContentDispositionHeaderValue("form-data"); 934 | disposition.Name = "\"file\""; 935 | disposition.FileName = "\"" + fileName + "\""; 936 | 937 | if (includeSize) 938 | disposition.Size = stream.Length; 939 | 940 | fileContent.Headers.ContentDisposition = disposition; 941 | fileContent.Headers.ContentType = new MediaTypeHeaderValue("application/octet-stream"); 942 | return fileContent; 943 | } 944 | 945 | private HttpContent CreateUrlEncodedContent(Dictionary values) 946 | { 947 | values.Add("apikey", _apiKey); 948 | return new CustomURLEncodedContent(values); 949 | } 950 | 951 | protected virtual void Dispose(bool disposing) 952 | { 953 | if (disposing) 954 | { 955 | _client.Dispose(); 956 | _httpClientHandler.Dispose(); 957 | } 958 | } 959 | 960 | public void Dispose() 961 | { 962 | Dispose(true); 963 | GC.SuppressFinalize(this); 964 | } 965 | } -------------------------------------------------------------------------------- /src/VirusTotalNet/VirusTotalNet.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | netstandard2.0 6 | A full implementation of the VirusTotal 2.0 API 7 | VirusTotal 8 | enable 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | --------------------------------------------------------------------------------