├── .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 | [](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 |
--------------------------------------------------------------------------------