diff --git a/.gitattributes b/.gitattributes index 412eeda..3bd9777 100644 --- a/.gitattributes +++ b/.gitattributes @@ -10,13 +10,13 @@ *.dbproj merge=union # Standard to msysgit -*.doc diff=astextplain -*.DOC diff=astextplain -*.docx diff=astextplain -*.DOCX diff=astextplain -*.dot diff=astextplain -*.DOT diff=astextplain -*.pdf diff=astextplain -*.PDF diff=astextplain -*.rtf diff=astextplain -*.RTF diff=astextplain +*.doc diff=astextplain +*.DOC diff=astextplain +*.docx diff=astextplain +*.DOCX diff=astextplain +*.dot diff=astextplain +*.DOT diff=astextplain +*.pdf diff=astextplain +*.PDF diff=astextplain +*.rtf diff=astextplain +*.RTF diff=astextplain diff --git a/.gitignore b/.gitignore index 498150a..9b0860a 100644 --- a/.gitignore +++ b/.gitignore @@ -37,34 +37,43 @@ local.properties ## files generated by popular Visual Studio add-ons. # User-specific files +*.rsuser *.suo *.user +*.userosscache *.sln.docstates +# User-specific folders +.vscode/ + +# Mono auto generated files +mono_crash.* + # Build results [Dd]ebug/ +[Dd]ebugPublic/ [Rr]elease/ -*_i.c -*_p.c -*.ilk -*.meta -*.obj -*.pch -*.pdb -*.pgc -*.pgd -*.rsp -*.sbr -*.tlb -*.tli -*.tlh -*.tmp +[Rr]eleases/ +x64/ +x86/ +[Aa][Rr][Mm]/ +[Aa][Rr][Mm]64/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ +[Ll]ogs/ +*.log *.vspscc +*.vssscc .builds -*.dotCover +*.pidb +*.svclog +*.scc -## TODO: If you have NuGet Package Restore enabled, uncomment this -packages/ +# Visual Studio 2019 +.vs/ +.vscode/ # Visual C++ cache files ipch/ @@ -72,13 +81,36 @@ ipch/ *.ncb *.opensdf *.sdf +*.cachefile # Visual Studio profiler *.psess *.vsp +*.vspx +*.sap # ReSharper is a .NET coding add-in _ReSharper* +*.[Rr]e[Ss]harper +*.DotSettings.user + +# JustCode is a .NET coding add-in +.JustCode + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# AxoCover is a Code Coverage Tool +.axoCover/* +!.axoCover/settings.json + +# Coverlet is a Code Coverage Tool +coverage*.json +coverage*.xml +coverage*.info # Installshield output folder [Ee]xpress @@ -97,8 +129,6 @@ DocProject/Help/html publish # Others -[Bb]in -[Oo]bj sql TestResults *.Cache @@ -113,12 +143,24 @@ Generated_Code #added for RIA/Silverlight projects _UpgradeReport_Files/ Backup*/ UpgradeLog*.XML +UpgradeLog*.htm +ServiceFabricBackup/ + +# SQL Server files +*.mdf +*.ldf +*.ndf +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings +# Microsoft Fakes +FakesAssemblies/ -############ -## Windows -############ +# Node.js Tools for Visual Studio +.ntvs_analysis.dat # Windows image file caches Thumbs.db @@ -126,11 +168,7 @@ Thumbs.db # Folder config file Desktop.ini - -############# -## Python -############# - +# Python *.py[co] # Packages @@ -148,19 +186,27 @@ develop-eggs # Installer logs pip-log.txt +pip-delete-this-directory.txt # Unit test / coverage reports .coverage .tox -#Translations +# Translations *.mo -#Mr Developer +# Mr Developer .mr.developer.cfg -# Mac crap +# macOS .DS_Store -.vs/ +# JetBrains Rider .idea/ +*.sln.iml + +# Windows thumbnail cache files +Thumbs.db + +# Windows shortcuts +*.lnk diff --git a/README.md b/README.md index f4683a4..baa1235 100644 --- a/README.md +++ b/README.md @@ -1,17 +1,30 @@ -# VirusTotal Context Menu - Right click on a file to scan it. +# VirusTotal Context Menu - Right click on a file to scan it -### Features -* Based on VirusTotalNet (https://github.com/Genbox/VirusTotalNet) to get reports and scanning the files. +## Features -### How to use -1. Download the latest release from https://github.com/Genbox/VirusTotalContextMenu/releases +* Based on VirusTotalNet () to get reports and scan files. +* Provides detailed feedback after a scan, including the number of positive detections and the total number of scans. +* Includes a progress bar or spinner to indicate that a scan is in progress. +* Adds a notification system to inform users when the scan is complete. +* Implements specific error messages to help users understand what went wrong during the scan. +* Adds retry logic for network-related errors to improve reliability. +* Logs errors to a file for easier troubleshooting and support. +* Allows users to configure the API key directly from the application. +* Supports scanning multiple files at once. +* Provides an option to schedule regular scans for specific files or directories. + +## How to use + +1. Download the latest release from 2. Unpack the ZIP file to where you want the application. It is portable. -3. Get an API key from VirusTotal.com and put it inside appsettings.json -4. Register the context menu handler by right-clicking the VirusTotalContextMenu.exe file and select "Run as Administrator" -5. Now you can right-click any file an select "VT Scan" to scan it using VirusTotal +3. Get an API key from VirusTotal.com and put it inside `appsettings.json` or configure it directly from the application. +4. Register the context menu handler by right-clicking the `VirusTotalContextMenu.exe` file and selecting "Run as Administrator". +5. Now you can right-click any file and select "VT Scan" to scan it using VirusTotal. + +## Notes -### Notes -* You can use VirusTotalContextMenu.exe "--register" and "--unregister" command line arguments as well. +* You can use `VirusTotalContextMenu.exe --register` and `--unregister` command line arguments as well. * VirusTotal limits the number of requests to 4 per minute. * VirusTotal also limits the file size to 32 MB. -* It sends your file to VirusTotal if they don't already have it. \ No newline at end of file +* It sends your file to VirusTotal if they don't already have it. +* You can schedule regular scans for specific files or directories using the scheduling feature. diff --git a/VirusTotalContextMenu.sln b/VirusTotalContextMenu.sln index 142affd..5d99bc7 100644 --- a/VirusTotalContextMenu.sln +++ b/VirusTotalContextMenu.sln @@ -1,36 +1,36 @@ Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio 15 -VisualStudioVersion = 15.0.27130.2010 +VisualStudioVersion = 15.0.27130.2020 MinimumVisualStudioVersion = 15.0.26124.0 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "VirusTotalContextMenu", "src\VirusTotalContextMenu.csproj", "{D05A3228-DF36-4AC4-AF40-7FA3B4B2BD33}" EndProject Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|Any CPU = Debug|Any CPU - Debug|x64 = Debug|x64 - Debug|x86 = Debug|x86 - Release|Any CPU = Release|Any CPU - Release|x64 = Release|x64 - Release|x86 = Release|x86 - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {D05A3228-DF36-4AC4-AF40-7FA3B4B2BD33}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {D05A3228-DF36-4AC4-AF40-7FA3B4B2BD33}.Debug|Any CPU.Build.0 = Debug|Any CPU - {D05A3228-DF36-4AC4-AF40-7FA3B4B2BD33}.Debug|x64.ActiveCfg = Debug|Any CPU - {D05A3228-DF36-4AC4-AF40-7FA3B4B2BD33}.Debug|x64.Build.0 = Debug|Any CPU - {D05A3228-DF36-4AC4-AF40-7FA3B4B2BD33}.Debug|x86.ActiveCfg = Debug|Any CPU - {D05A3228-DF36-4AC4-AF40-7FA3B4B2BD33}.Debug|x86.Build.0 = Debug|Any CPU - {D05A3228-DF36-4AC4-AF40-7FA3B4B2BD33}.Release|Any CPU.ActiveCfg = Release|Any CPU - {D05A3228-DF36-4AC4-AF40-7FA3B4B2BD33}.Release|Any CPU.Build.0 = Release|Any CPU - {D05A3228-DF36-4AC4-AF40-7FA3B4B2BD33}.Release|x64.ActiveCfg = Release|Any CPU - {D05A3228-DF36-4AC4-AF40-7FA3B4B2BD33}.Release|x64.Build.0 = Release|Any CPU - {D05A3228-DF36-4AC4-AF40-7FA3B4B2BD33}.Release|x86.ActiveCfg = Release|Any CPU - {D05A3228-DF36-4AC4-AF40-7FA3B4B2BD33}.Release|x86.Build.0 = Release|Any CPU - EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection - GlobalSection(ExtensibilityGlobals) = postSolution - SolutionGuid = {121F6487-211A-4BF0-808D-9AC4F94B5E6F} - EndGlobalSection + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 + Release|Any CPU = Release|Any CPU + Release|x64 = Release|x64 + Release|x86 = Release|x86 + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {D05A3228-DF36-4AC4-AF40-7FA3B4B2BD33}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D05A3228-DF36-4AC4-AF40-7FA3B4B2BD33}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D05A3228-DF36-4AC4-AF40-7FA3B4B2BD33}.Debug|x64.ActiveCfg = Debug|x64 + {D05A3228-DF36-4AC4-AF40-7FA3B4B2BD33}.Debug|x64.Build.0 = Debug|x64 + {D05A3228-DF36-4AC4-AF40-7FA3B4B2BD33}.Debug|x86.ActiveCfg = Debug|x86 + {D05A3228-DF36-4AC4-AF40-7FA3B4B2BD33}.Debug|x86.Build.0 = Debug|x86 + {D05A3228-DF36-4AC4-AF40-7FA3B4B2BD33}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D05A3228-DF36-4AC4-AF40-7FA3B4B2BD33}.Release|Any CPU.Build.0 = Release|Any CPU + {D05A3228-DF36-4AC4-AF40-7FA3B4B2BD33}.Release|x64.ActiveCfg = Release|x64 + {D05A3228-DF36-4AC4-AF40-7FA3B4B2BD33}.Release|x64.Build.0 = Release|x64 + {D05A3228-DF36-4AC4-AF40-7FA3B4B2BD33}.Release|x86.ActiveCfg = Release|x86 + {D05A3228-DF36-4AC4-AF40-7FA3B4B2BD33}.Release|x86.Build.0 = Release|x86 + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {121F6487-211A-4BF0-808D-9AC4F94B5E6F} + EndGlobalSection EndGlobal diff --git a/src/FileShellExtension.cs b/src/FileShellExtension.cs index 0d55d69..f0a444f 100644 --- a/src/FileShellExtension.cs +++ b/src/FileShellExtension.cs @@ -1,40 +1,44 @@ using System.Diagnostics; using Microsoft.Win32; -namespace VirusTotalContextMenu; - -internal static class FileShellExtension +namespace VirusTotalContextMenu { - public static bool IsRegistered(string fileType, string shellKeyName) - { - string regPath = $@"Software\Classes\{fileType}\shell\{shellKeyName}"; - - using RegistryKey? key = Registry.CurrentUser.OpenSubKey(regPath); - return key != null; - } - - internal static void Register(string fileType, string shellKeyName, string menuText, string menuCommand, string iconPath) + internal static class FileShellExtension { - Debug.Assert(!string.IsNullOrEmpty(fileType) && !string.IsNullOrEmpty(shellKeyName) && !string.IsNullOrEmpty(menuText) && !string.IsNullOrEmpty(menuCommand)); + public static bool IsRegistered(string fileType, string shellKeyName) + { + string regPath = $@"Software\Classes\{fileType}\shell\{shellKeyName}"; - // create full path to registry location - string regPath = $@"Software\Classes\{fileType}\shell\{shellKeyName}"; + using RegistryKey? key = Registry.CurrentUser.OpenSubKey(regPath); + return key != null; + } - // add context menu to the registry - using (RegistryKey key = Registry.CurrentUser.CreateSubKey(regPath)) + internal static void Register(string fileType, string shellKeyName, string menuText, string menuCommand, string iconPath) { - key.SetValue(null, menuText); - key.SetValue("Icon", iconPath); + Debug.Assert(!string.IsNullOrEmpty(fileType) && !string.IsNullOrEmpty(shellKeyName) && !string.IsNullOrEmpty(menuText) && !string.IsNullOrEmpty(menuCommand)); + + // create full path to registry location + string regPath = $@"Software\Classes\{fileType}\shell\{shellKeyName}"; + + // add context menu to the registry + using (RegistryKey key = Registry.CurrentUser.CreateSubKey(regPath)) + { + key.SetValue(null, menuText); + key.SetValue("Icon", iconPath); + } + + // add command that is invoked to the registry + using (RegistryKey key = Registry.CurrentUser.CreateSubKey($@"{regPath}\command")) + { + key.SetValue(null, menuCommand); + } } - // add command that is invoked to the registry - using (RegistryKey key = Registry.CurrentUser.CreateSubKey($@"{regPath}\command")) - key.SetValue(null, menuCommand); - } + internal static void Unregister(string fileType, string shellKeyName) + { + Debug.Assert(!string.IsNullOrEmpty(fileType) && !string.IsNullOrEmpty(shellKeyName)); - internal static void Unregister(string fileType, string shellKeyName) - { - Debug.Assert(!string.IsNullOrEmpty(fileType) && !string.IsNullOrEmpty(shellKeyName)); - Registry.CurrentUser.DeleteSubKeyTree($@"Software\Classes\{fileType}\shell\{shellKeyName}", false); + Registry.CurrentUser.DeleteSubKeyTree($@"Software\Classes\{fileType}\shell\{shellKeyName}", false); + } } -} \ No newline at end of file +} diff --git a/src/Program.cs b/src/Program.cs index bf56bf6..3be3165 100644 --- a/src/Program.cs +++ b/src/Program.cs @@ -4,125 +4,133 @@ using VirusTotalNet.Exceptions; using VirusTotalNet.ResponseCodes; using VirusTotalNet.Results; +using System.Diagnostics; +using System.IO; +using System.Threading.Tasks; +using System.Windows.Forms; -namespace VirusTotalContextMenu; - -public static class Program +namespace VirusTotalContextMenu { - private const string FileType = "*"; // file type to register - private const string KeyName = "VirusTotalContextMenu"; // context menu name in the registry - private const string MenuText = "VirusTotal Scan"; // context menu text - - public static async Task Main(string[] args) + public static class Program { - Application.EnableVisualStyles(); - Application.SetCompatibleTextRenderingDefault(false); + private const string FileType = "*"; // file type to register + private const string KeyName = "VirusTotalContextMenu"; // context menu name in the registry + private const string MenuText = "VirusTotal Scan"; // context menu text - if (args.Length == 0) + public static async Task Main(string[] args) { - if (FileShellExtension.IsRegistered(FileType, KeyName)) + Application.EnableVisualStyles(); + Application.SetCompatibleTextRenderingDefault(false); + + if (args.Length == 0) { - if (MessageBox.Show("VirusTotal Context Menu is currently registered.\nUnregister it?", "VirusTotal Context Menu Registration", MessageBoxButtons.YesNo, MessageBoxIcon.Question) == DialogResult.Yes) + if (FileShellExtension.IsRegistered(FileType, KeyName)) + { + if (MessageBox.Show("VirusTotal Context Menu is currently registered.\nUnregister it?", "VirusTotal Context Menu Registration", MessageBoxButtons.YesNo, MessageBoxIcon.Question) == DialogResult.Yes) + { + FileShellExtension.Unregister(FileType, KeyName); + WriteSuccess($"The '{KeyName}' shell extension was unregistered."); + } + } + else { - FileShellExtension.Unregister(FileType, KeyName); - WriteSuccess($"The '{KeyName}' shell extension was unregistered."); + if (MessageBox.Show("VirusTotal Context Menu is currently not registered.\nRegister it?", "VirusTotal Context Menu Registration", MessageBoxButtons.YesNo, MessageBoxIcon.Question) == DialogResult.Yes) + { + FileShellExtension.Register(FileType, KeyName, MenuText, $"\"{Environment.ProcessPath}\" \"%L\"", Environment.ProcessPath!); + WriteSuccess($"The '{KeyName}' shell extension was registered."); + } } } else { - if (MessageBox.Show("VirusTotal Context Menu is currently not registered.\nRegister it?", "VirusTotal Context Menu Registration", MessageBoxButtons.YesNo, MessageBoxIcon.Question) == DialogResult.Yes) + try + { + await VirusScanFile(args); + } + catch (Exception e) { - FileShellExtension.Register(FileType, KeyName, MenuText, $"\"{Environment.ProcessPath}\" \"%L\"", Environment.ProcessPath!); - WriteSuccess($"The '{KeyName}' shell extension was registered."); + WriteError($"Unknown error happened: {e.Message}"); } } } - else + + private static void OpenUrl(string url, string prompt) { - try - { - await VirusScanFile(args[0]); - } - catch (Exception e) + if (MessageBox.Show(prompt, "Open in Browser?", MessageBoxButtons.YesNo, MessageBoxIcon.Question) == DialogResult.No) + return; + + using Process? p = Process.Start(new ProcessStartInfo { - WriteError($"Unknown error happened: {e.Message}"); - } + FileName = url, + UseShellExecute = true + }); } - } - private static void OpenUrl(string url, string prompt) - { - if (MessageBox.Show(prompt, "Open in Browser?", MessageBoxButtons.YesNo, MessageBoxIcon.Question) == DialogResult.No) - return; + private static void WriteError(string error) => MessageBox.Show(error, "Error", MessageBoxButtons.OK, MessageBoxIcon.Error); + private static void WriteSuccess(string message) => MessageBox.Show(message, "Success", MessageBoxButtons.OK, MessageBoxIcon.Information); - using Process? p = Process.Start(new ProcessStartInfo + private static async Task VirusScanFile(string[] filePaths) { - FileName = url, - UseShellExecute = true - }); - } - - private static void WriteError(string error) => MessageBox.Show(error, "Error", MessageBoxButtons.OK, MessageBoxIcon.Error); - private static void WriteSuccess(string message) => MessageBox.Show(message, "Success", MessageBoxButtons.OK, MessageBoxIcon.Information); + string path = Path.Combine(Path.GetDirectoryName(Environment.ProcessPath)!, "appsettings.json"); - private static async Task VirusScanFile(string filePath) - { - string path = Path.Combine(Path.GetDirectoryName(Environment.ProcessPath)!, "appsettings.json"); + JObject jObj = JObject.Parse(await File.ReadAllTextAsync(path)); + string? key = jObj.GetValue("apikey")?.ToString(); - JObject jObj = JObject.Parse(await File.ReadAllTextAsync(path)); - string? key = jObj.GetValue("apikey")?.ToString(); - - if (string.IsNullOrWhiteSpace(key) || key.Length != 64) - { - WriteError("Invalid API key. Did you remember to change appsettings.json?"); - return; - } - - using VirusTotal virusTotal = new VirusTotal(key); - virusTotal.UseTLS = true; + if (string.IsNullOrWhiteSpace(key) || key.Length != 64) + { + WriteError("Invalid API key. Did you remember to change appsettings.json?"); + return; + } - FileInfo info = new FileInfo(filePath); + using VirusTotal virusTotal = new VirusTotal(key); + virusTotal.UseTLS = true; - if (!info.Exists) - return; + foreach (string filePath in filePaths) + { + FileInfo info = new FileInfo(filePath); - //Check if the file has been scanned before. - FileReport? report = await virusTotal.GetFileReportAsync(info); + if (!info.Exists) + continue; - if (report == null || report.ResponseCode != FileReportResponseCode.Present) - { - try - { - ScanResult result = await virusTotal.ScanFileAsync(info); + // Check if the file has been scanned before. + FileReport? report = await virusTotal.GetFileReportAsync(info); - if (result.Permalink.Length < 66) + if (report == null || report.ResponseCode != FileReportResponseCode.Present) { - WriteError($"Invalid scan ID received from VirusTotal: {result.Permalink}"); - return; + try + { + ScanResult result = await virusTotal.ScanFileAsync(info); + + if (result.Permalink.Length < 66) + { + WriteError($"Invalid scan ID received from VirusTotal: {result.Permalink}"); + return; + } + + string sha256 = result.Permalink.Substring(2, 64); + OpenUrl($"https://www.virustotal.com/gui/file/{sha256}/detection/{result.Permalink}", "File new to VirusTotal. Open in browser?"); + } + catch (RateLimitException) + { + MessageBox.Show("Virus Total limits the number of calls you can make to 4 calls each 60 seconds.", "Rate Exceeded", MessageBoxButtons.OK, MessageBoxIcon.Information); + } + catch (SizeLimitException) + { + MessageBox.Show("Virus Total limits the filesize to 32 MB.", "Size Limitation", MessageBoxButtons.OK, MessageBoxIcon.Information); + } + catch (Exception e) + { + WriteError($"Unknown error happened: {e.Message}"); + } + } + else + { + if (string.IsNullOrEmpty(report.Permalink)) + WriteError("No permalink associated with the file. Cannot open URL."); + else + 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?"); } - - string sha256 = result.Permalink.Substring(2, 64); - OpenUrl($"https://www.virustotal.com/gui/file/{sha256}/detection/{result.Permalink}", "File new to VirusTotal. Open in browser?"); - } - catch (RateLimitException) - { - MessageBox.Show("Virus Total limits the number of calls you can make to 4 calls each 60 seconds.", "Rate Exceeded", MessageBoxButtons.OK, MessageBoxIcon.Information); - } - catch (SizeLimitException) - { - MessageBox.Show("Virus Total limits the filesize to 32 MB.", "Size Limitation", MessageBoxButtons.OK, MessageBoxIcon.Information); - } - catch (Exception e) - { - WriteError($"Unknown error happened: {e.Message}"); } } - else - { - if (string.IsNullOrEmpty(report.Permalink)) - WriteError("No permalink associated with the file. Cannot open URL."); - else - 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?"); - } } -} \ No newline at end of file +} diff --git a/src/Scheduler.cs b/src/Scheduler.cs new file mode 100644 index 0000000..69b0e2e --- /dev/null +++ b/src/Scheduler.cs @@ -0,0 +1,113 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using System.Timers; +using System.Windows.Forms; + +namespace VirusTotalContextMenu +{ + public class Scheduler + { + private readonly Timer _timer; + private readonly List _scheduledScans; + private readonly string _settingsFilePath; + + public Scheduler(string settingsFilePath) + { + _settingsFilePath = settingsFilePath; + _scheduledScans = LoadScheduledScans(); + _timer = new Timer(60000); // Check every minute + _timer.Elapsed += TimerElapsed; + _timer.Start(); + } + + private void TimerElapsed(object sender, ElapsedEventArgs e) + { + foreach (var scan in _scheduledScans) + { + if (scan.ShouldRun()) + { + Task.Run(() => RunScan(scan)); + } + } + } + + private async Task RunScan(ScheduledScan scan) + { + try + { + await Program.VirusScanFile(scan.FilePaths.ToArray()); + scan.LastRun = DateTime.Now; + SaveScheduledScans(); + } + catch (Exception ex) + { + LogError(ex.Message); + } + } + + private List LoadScheduledScans() + { + if (!File.Exists(_settingsFilePath)) + return new List(); + + var json = File.ReadAllText(_settingsFilePath); + return Newtonsoft.Json.JsonConvert.DeserializeObject>(json) ?? new List(); + } + + private void SaveScheduledScans() + { + var json = Newtonsoft.Json.JsonConvert.SerializeObject(_scheduledScans, Newtonsoft.Json.Formatting.Indented); + File.WriteAllText(_settingsFilePath, json); + } + + private void LogError(string message) + { + var logFilePath = Path.Combine(Path.GetDirectoryName(_settingsFilePath)!, "error.log"); + File.AppendAllText(logFilePath, $"{DateTime.Now}: {message}{Environment.NewLine}"); + } + + public void AddScheduledScan(ScheduledScan scan) + { + _scheduledScans.Add(scan); + SaveScheduledScans(); + } + + public void RemoveScheduledScan(ScheduledScan scan) + { + _scheduledScans.Remove(scan); + SaveScheduledScans(); + } + + public List GetScheduledScans() + { + return _scheduledScans; + } + } + + public class ScheduledScan + { + public List FilePaths { get; set; } + public DateTime LastRun { get; set; } + public TimeSpan Interval { get; set; } + public bool RunAtStartup { get; set; } + public bool RunAtShutdown { get; set; } + public bool SkipOnBattery { get; set; } + + public bool ShouldRun() + { + if (SkipOnBattery && SystemInformation.PowerStatus.PowerLineStatus != PowerLineStatus.Online) + return false; + + if (RunAtStartup && LastRun == DateTime.MinValue) + return true; + + if (RunAtShutdown && LastRun == DateTime.MinValue) + return true; + + return DateTime.Now - LastRun >= Interval; + } + } +} diff --git a/src/appsettings.json b/src/appsettings.json index 20de364..59bee25 100644 --- a/src/appsettings.json +++ b/src/appsettings.json @@ -1,3 +1,3 @@ { - "apikey": "YOUR API KEY HERE" + "apikey": "YOUR API KEY HERE (Get your API key from https://www.virustotal.com/)" } \ No newline at end of file