├── src ├── appsettings.json ├── 3.ico ├── VirusTotalContextMenu.csproj ├── FileShellExtension.cs └── Program.cs ├── .gitattributes ├── README.md ├── VirusTotalContextMenu.sln └── .gitignore /src/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "apikey": "YOUR API KEY HERE" 3 | } -------------------------------------------------------------------------------- /src/3.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Genbox/VirusTotalContextMenu/HEAD/src/3.ico -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # VirusTotal Context Menu - Right click on a file to scan it. 2 | 3 | ### Features 4 | * Based on VirusTotalNet (https://github.com/Genbox/VirusTotalNet) to get reports and scanning the files. 5 | 6 | ### How to use 7 | 1. Download the latest release from https://github.com/Genbox/VirusTotalContextMenu/releases 8 | 2. Unpack the ZIP file to where you want the application. It is portable. 9 | 3. Get an API key from VirusTotal.com and put it inside appsettings.json 10 | 4. Register the context menu handler by right-clicking the VirusTotalContextMenu.exe file and select "Run as Administrator" 11 | 5. Now you can right-click any file an select "VT Scan" to scan it using VirusTotal 12 | 13 | ### Notes 14 | * You can use VirusTotalContextMenu.exe "--register" and "--unregister" command line arguments as well. 15 | * VirusTotal limits the number of requests to 4 per minute. 16 | * VirusTotal also limits the file size to 32 MB. 17 | * It sends your file to VirusTotal if they don't already have it. -------------------------------------------------------------------------------- /src/VirusTotalContextMenu.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | WinExe 5 | true 6 | net9.0-windows 7 | 2.0.0 8 | 9 | 10 | 11 | enable 12 | enable 13 | 14 | 15 | 16 | true 17 | true 18 | true 19 | <_SuppressWinFormsTrimError>true 20 | true 21 | true 22 | false 23 | false 24 | false 25 | false 26 | 3.ico 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | PreserveNewest 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /src/FileShellExtension.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics; 2 | using Microsoft.Win32; 3 | 4 | namespace VirusTotalContextMenu; 5 | 6 | internal static class FileShellExtension 7 | { 8 | public static bool IsRegistered(string fileType, string shellKeyName) 9 | { 10 | string regPath = $@"Software\Classes\{fileType}\shell\{shellKeyName}"; 11 | 12 | using RegistryKey? key = Registry.CurrentUser.OpenSubKey(regPath); 13 | return key != null; 14 | } 15 | 16 | internal static void Register(string fileType, string shellKeyName, string menuText, string menuCommand, string iconPath) 17 | { 18 | Debug.Assert(!string.IsNullOrEmpty(fileType) && !string.IsNullOrEmpty(shellKeyName) && !string.IsNullOrEmpty(menuText) && !string.IsNullOrEmpty(menuCommand)); 19 | 20 | // create full path to registry location 21 | string regPath = $@"Software\Classes\{fileType}\shell\{shellKeyName}"; 22 | 23 | // add context menu to the registry 24 | using (RegistryKey key = Registry.CurrentUser.CreateSubKey(regPath)) 25 | { 26 | key.SetValue(null, menuText); 27 | key.SetValue("Icon", iconPath); 28 | } 29 | 30 | // add command that is invoked to the registry 31 | using (RegistryKey key = Registry.CurrentUser.CreateSubKey($@"{regPath}\command")) 32 | key.SetValue(null, menuCommand); 33 | } 34 | 35 | internal static void Unregister(string fileType, string shellKeyName) 36 | { 37 | Debug.Assert(!string.IsNullOrEmpty(fileType) && !string.IsNullOrEmpty(shellKeyName)); 38 | Registry.CurrentUser.DeleteSubKeyTree($@"Software\Classes\{fileType}\shell\{shellKeyName}", false); 39 | } 40 | } -------------------------------------------------------------------------------- /VirusTotalContextMenu.sln: -------------------------------------------------------------------------------- 1 | Microsoft Visual Studio Solution File, Format Version 12.00 2 | # Visual Studio 15 3 | VisualStudioVersion = 15.0.27130.2010 4 | MinimumVisualStudioVersion = 15.0.26124.0 5 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "VirusTotalContextMenu", "src\VirusTotalContextMenu.csproj", "{D05A3228-DF36-4AC4-AF40-7FA3B4B2BD33}" 6 | EndProject 7 | Global 8 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 9 | Debug|Any CPU = Debug|Any CPU 10 | Debug|x64 = Debug|x64 11 | Debug|x86 = Debug|x86 12 | Release|Any CPU = Release|Any CPU 13 | Release|x64 = Release|x64 14 | Release|x86 = Release|x86 15 | EndGlobalSection 16 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 17 | {D05A3228-DF36-4AC4-AF40-7FA3B4B2BD33}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 18 | {D05A3228-DF36-4AC4-AF40-7FA3B4B2BD33}.Debug|Any CPU.Build.0 = Debug|Any CPU 19 | {D05A3228-DF36-4AC4-AF40-7FA3B4B2BD33}.Debug|x64.ActiveCfg = Debug|Any CPU 20 | {D05A3228-DF36-4AC4-AF40-7FA3B4B2BD33}.Debug|x64.Build.0 = Debug|Any CPU 21 | {D05A3228-DF36-4AC4-AF40-7FA3B4B2BD33}.Debug|x86.ActiveCfg = Debug|Any CPU 22 | {D05A3228-DF36-4AC4-AF40-7FA3B4B2BD33}.Debug|x86.Build.0 = Debug|Any CPU 23 | {D05A3228-DF36-4AC4-AF40-7FA3B4B2BD33}.Release|Any CPU.ActiveCfg = Release|Any CPU 24 | {D05A3228-DF36-4AC4-AF40-7FA3B4B2BD33}.Release|Any CPU.Build.0 = Release|Any CPU 25 | {D05A3228-DF36-4AC4-AF40-7FA3B4B2BD33}.Release|x64.ActiveCfg = Release|Any CPU 26 | {D05A3228-DF36-4AC4-AF40-7FA3B4B2BD33}.Release|x64.Build.0 = Release|Any CPU 27 | {D05A3228-DF36-4AC4-AF40-7FA3B4B2BD33}.Release|x86.ActiveCfg = Release|Any CPU 28 | {D05A3228-DF36-4AC4-AF40-7FA3B4B2BD33}.Release|x86.Build.0 = Release|Any CPU 29 | EndGlobalSection 30 | GlobalSection(SolutionProperties) = preSolution 31 | HideSolutionNode = FALSE 32 | EndGlobalSection 33 | GlobalSection(ExtensibilityGlobals) = postSolution 34 | SolutionGuid = {121F6487-211A-4BF0-808D-9AC4F94B5E6F} 35 | EndGlobalSection 36 | EndGlobal 37 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ################# 2 | ## Eclipse 3 | ################# 4 | 5 | *.pydevproject 6 | .project 7 | .metadata 8 | bin/ 9 | tmp/ 10 | *.tmp 11 | *.bak 12 | *.swp 13 | *~.nib 14 | local.properties 15 | .classpath 16 | .settings/ 17 | .loadpath 18 | 19 | # External tool builders 20 | .externalToolBuilders/ 21 | 22 | # Locally stored "Eclipse launch configurations" 23 | *.launch 24 | 25 | # CDT-specific 26 | .cproject 27 | 28 | # PDT-specific 29 | .buildpath 30 | 31 | 32 | ################# 33 | ## Visual Studio 34 | ################# 35 | 36 | ## Ignore Visual Studio temporary files, build results, and 37 | ## files generated by popular Visual Studio add-ons. 38 | 39 | # User-specific files 40 | *.suo 41 | *.user 42 | *.sln.docstates 43 | 44 | # Build results 45 | [Dd]ebug/ 46 | [Rr]elease/ 47 | *_i.c 48 | *_p.c 49 | *.ilk 50 | *.meta 51 | *.obj 52 | *.pch 53 | *.pdb 54 | *.pgc 55 | *.pgd 56 | *.rsp 57 | *.sbr 58 | *.tlb 59 | *.tli 60 | *.tlh 61 | *.tmp 62 | *.vspscc 63 | .builds 64 | *.dotCover 65 | 66 | ## TODO: If you have NuGet Package Restore enabled, uncomment this 67 | packages/ 68 | 69 | # Visual C++ cache files 70 | ipch/ 71 | *.aps 72 | *.ncb 73 | *.opensdf 74 | *.sdf 75 | 76 | # Visual Studio profiler 77 | *.psess 78 | *.vsp 79 | 80 | # ReSharper is a .NET coding add-in 81 | _ReSharper* 82 | 83 | # Installshield output folder 84 | [Ee]xpress 85 | 86 | # DocProject is a documentation generator add-in 87 | DocProject/buildhelp/ 88 | DocProject/Help/*.HxT 89 | DocProject/Help/*.HxC 90 | DocProject/Help/*.hhc 91 | DocProject/Help/*.hhk 92 | DocProject/Help/*.hhp 93 | DocProject/Help/Html2 94 | DocProject/Help/html 95 | 96 | # Click-Once directory 97 | publish 98 | 99 | # Others 100 | [Bb]in 101 | [Oo]bj 102 | sql 103 | TestResults 104 | *.Cache 105 | ClientBin 106 | stylecop.* 107 | ~$* 108 | *.dbmdl 109 | Generated_Code #added for RIA/Silverlight projects 110 | 111 | # Backup & report files from converting an old project file to a newer 112 | # Visual Studio version. Backup files are not needed, because we have git ;-) 113 | _UpgradeReport_Files/ 114 | Backup*/ 115 | UpgradeLog*.XML 116 | 117 | 118 | 119 | ############ 120 | ## Windows 121 | ############ 122 | 123 | # Windows image file caches 124 | Thumbs.db 125 | 126 | # Folder config file 127 | Desktop.ini 128 | 129 | 130 | ############# 131 | ## Python 132 | ############# 133 | 134 | *.py[co] 135 | 136 | # Packages 137 | *.egg 138 | *.egg-info 139 | dist 140 | build 141 | eggs 142 | parts 143 | bin 144 | var 145 | sdist 146 | develop-eggs 147 | .installed.cfg 148 | 149 | # Installer logs 150 | pip-log.txt 151 | 152 | # Unit test / coverage reports 153 | .coverage 154 | .tox 155 | 156 | #Translations 157 | *.mo 158 | 159 | #Mr Developer 160 | .mr.developer.cfg 161 | 162 | # Mac crap 163 | .DS_Store 164 | 165 | .vs/ 166 | .idea/ 167 | -------------------------------------------------------------------------------- /src/Program.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics; 2 | using Newtonsoft.Json.Linq; 3 | using VirusTotalNet; 4 | using VirusTotalNet.Exceptions; 5 | using VirusTotalNet.ResponseCodes; 6 | using VirusTotalNet.Results; 7 | 8 | namespace VirusTotalContextMenu; 9 | 10 | public static class Program 11 | { 12 | private const string FileType = "*"; // file type to register 13 | private const string KeyName = "VirusTotalContextMenu"; // context menu name in the registry 14 | private const string MenuText = "VirusTotal Scan"; // context menu text 15 | 16 | public static async Task Main(string[] args) 17 | { 18 | Application.EnableVisualStyles(); 19 | Application.SetCompatibleTextRenderingDefault(false); 20 | 21 | if (args.Length == 0) 22 | { 23 | if (FileShellExtension.IsRegistered(FileType, KeyName)) 24 | { 25 | if (MessageBox.Show("VirusTotal Context Menu is currently registered.\nUnregister it?", "VirusTotal Context Menu Registration", MessageBoxButtons.YesNo, MessageBoxIcon.Question) == DialogResult.Yes) 26 | { 27 | FileShellExtension.Unregister(FileType, KeyName); 28 | WriteSuccess($"The '{KeyName}' shell extension was unregistered."); 29 | } 30 | } 31 | else 32 | { 33 | if (MessageBox.Show("VirusTotal Context Menu is currently not registered.\nRegister it?", "VirusTotal Context Menu Registration", MessageBoxButtons.YesNo, MessageBoxIcon.Question) == DialogResult.Yes) 34 | { 35 | FileShellExtension.Register(FileType, KeyName, MenuText, $"\"{Environment.ProcessPath}\" \"%L\"", Environment.ProcessPath!); 36 | WriteSuccess($"The '{KeyName}' shell extension was registered."); 37 | } 38 | } 39 | } 40 | else 41 | { 42 | try 43 | { 44 | await VirusScanFile(args[0]); 45 | } 46 | catch (Exception e) 47 | { 48 | WriteError($"Unknown error happened: {e.Message}"); 49 | } 50 | } 51 | } 52 | 53 | private static void OpenUrl(string url, string prompt) 54 | { 55 | if (MessageBox.Show(prompt, "Open in Browser?", MessageBoxButtons.YesNo, MessageBoxIcon.Question) == DialogResult.No) 56 | return; 57 | 58 | using Process? p = Process.Start(new ProcessStartInfo 59 | { 60 | FileName = url, 61 | UseShellExecute = true 62 | }); 63 | } 64 | 65 | private static void WriteError(string error) => MessageBox.Show(error, "Error", MessageBoxButtons.OK, MessageBoxIcon.Error); 66 | private static void WriteSuccess(string message) => MessageBox.Show(message, "Success", MessageBoxButtons.OK, MessageBoxIcon.Information); 67 | 68 | private static async Task VirusScanFile(string filePath) 69 | { 70 | string path = Path.Combine(Path.GetDirectoryName(Environment.ProcessPath)!, "appsettings.json"); 71 | 72 | JObject jObj = JObject.Parse(await File.ReadAllTextAsync(path)); 73 | string? key = jObj.GetValue("apikey")?.ToString(); 74 | 75 | if (string.IsNullOrWhiteSpace(key) || key.Length != 64) 76 | { 77 | WriteError("Invalid API key. Did you remember to change appsettings.json?"); 78 | return; 79 | } 80 | 81 | using VirusTotal virusTotal = new VirusTotal(key); 82 | virusTotal.UseTLS = true; 83 | 84 | FileInfo info = new FileInfo(filePath); 85 | 86 | if (!info.Exists) 87 | return; 88 | 89 | //Check if the file has been scanned before. 90 | FileReport? report = await virusTotal.GetFileReportAsync(info); 91 | 92 | if (report == null || report.ResponseCode != FileReportResponseCode.Present) 93 | { 94 | try 95 | { 96 | ScanResult result = await virusTotal.ScanFileAsync(info); 97 | 98 | if (result.Permalink.Length < 66) 99 | { 100 | WriteError($"Invalid scan ID received from VirusTotal: {result.Permalink}"); 101 | return; 102 | } 103 | 104 | string sha256 = result.Permalink.Substring(2, 64); 105 | OpenUrl($"https://www.virustotal.com/gui/file/{sha256}/detection/{result.Permalink}", "File new to VirusTotal. Open in browser?"); 106 | } 107 | catch (RateLimitException) 108 | { 109 | MessageBox.Show("Virus Total limits the number of calls you can make to 4 calls each 60 seconds.", "Rate Exceeded", MessageBoxButtons.OK, MessageBoxIcon.Information); 110 | } 111 | catch (SizeLimitException) 112 | { 113 | MessageBox.Show("Virus Total limits the filesize to 32 MB.", "Size Limitation", MessageBoxButtons.OK, MessageBoxIcon.Information); 114 | } 115 | catch (Exception e) 116 | { 117 | WriteError($"Unknown error happened: {e.Message}"); 118 | } 119 | } 120 | else 121 | { 122 | if (string.IsNullOrEmpty(report.Permalink)) 123 | WriteError("No permalink associated with the file. Cannot open URL."); 124 | else 125 | OpenUrl(report.Permalink, $"Results: {report.Positives}/{report.Total} positive hits.\nLast scanned: {report.ScanDate:dd MMM yyyy} ({(DateTime.Now - report.ScanDate).Days} days ago).\nOpen in browser?"); 126 | } 127 | } 128 | } --------------------------------------------------------------------------------