├── .github └── FUNDING.yml ├── .gitignore ├── Build └── Manage-VirusTotal.ps1 ├── CHANGELOG.MD ├── Examples ├── Example-GetVirusTotalReport.ps1 ├── Example-SendToVirusTotalFile.ps1 ├── Example-SendToVirusTotalLarge.ps1 ├── Example-SendToVirusTotalRescan.ps1 ├── Example-SendToVirusTotalUrl.ps1 └── Submisions │ └── TestFile.txt ├── License ├── Private └── ConvertTo-VTBody.ps1 ├── Public ├── Get-VirusReport.ps1 └── New-VirusScan.ps1 ├── README.MD ├── VirusTotalAnalyzer.psd1 └── VirusTotalAnalyzer.psm1 /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: PrzemyslawKlys 4 | custom: https://paypal.me/PrzemyslawKlys -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | Ignore/* 2 | .vs/* 3 | .vscode/* 4 | Artefacts/* -------------------------------------------------------------------------------- /Build/Manage-VirusTotal.ps1: -------------------------------------------------------------------------------- 1 | Clear-Host 2 | 3 | Build-Module -ModuleName 'VirusTotalAnalyzer' { 4 | # Usual defaults as per standard module 5 | $Manifest = [ordered] @{ 6 | # Version number of this module. 7 | ModuleVersion = '0.0.X' 8 | # Supported PSEditions 9 | CompatiblePSEditions = @('Desktop', 'Core') 10 | # ID used to uniquely identify this module 11 | GUID = '2e82faa1-d870-42b2-b5aa-4a63bf02f43e' 12 | # Author of this module 13 | Author = 'Przemyslaw Klys' 14 | # Company or vendor of this module 15 | CompanyName = 'Evotec' 16 | # Copyright statement for this module 17 | Copyright = "(c) 2011 - $((Get-Date).Year) Przemyslaw Klys @ Evotec. All rights reserved." 18 | # Description of the functionality provided by this module 19 | Description = 'PowerShell module that intearacts with the VirusTotal service using a VirusTotal API (free)' 20 | # Minimum version of the Windows PowerShell engine required by this module 21 | PowerShellVersion = '5.1' 22 | # Functions to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no functions to export. 23 | # Private data to pass to the module specified in RootModule/ModuleToProcess. This may also contain a PSData hashtable with additional module metadata used by PowerShell. 24 | Tags = @('Windows', 'Linux', 'macOs', 'VirusTotal', 'virus', 'threat', 'analyzer') 25 | 26 | ProjectUri = 'https://github.com/EvotecIT/VirusTotalAnalyzer' 27 | } 28 | New-ConfigurationManifest @Manifest 29 | 30 | # Add standard module dependencies (directly, but can be used with loop as well) 31 | New-ConfigurationModule -Type RequiredModule -Name 'PSSharedGoods' -Guid 'Auto' -Version 'Latest' 32 | New-ConfigurationModule -Type ExternalModule -Name 'Microsoft.PowerShell.Management', 'Microsoft.PowerShell.Utility' 33 | 34 | # Add approved modules, that can be used as a dependency, but only when specific function from those modules is used 35 | # And on that time only that function and dependant functions will be copied over 36 | # Keep in mind it has it's limits when "copying" functions such as it should not depend on DLLs or other external files 37 | New-ConfigurationModule -Type ApprovedModule -Name 'PSSharedGoods', 'PSWriteColor', 'Connectimo', 'PSUnifi', 'PSWebToolbox', 'PSMyPassword' 38 | 39 | $ConfigurationFormat = [ordered] @{ 40 | RemoveComments = $false 41 | 42 | PlaceOpenBraceEnable = $true 43 | PlaceOpenBraceOnSameLine = $true 44 | PlaceOpenBraceNewLineAfter = $true 45 | PlaceOpenBraceIgnoreOneLineBlock = $false 46 | 47 | PlaceCloseBraceEnable = $true 48 | PlaceCloseBraceNewLineAfter = $true 49 | PlaceCloseBraceIgnoreOneLineBlock = $false 50 | PlaceCloseBraceNoEmptyLineBefore = $true 51 | 52 | UseConsistentIndentationEnable = $true 53 | UseConsistentIndentationKind = 'space' 54 | UseConsistentIndentationPipelineIndentation = 'IncreaseIndentationAfterEveryPipeline' 55 | UseConsistentIndentationIndentationSize = 4 56 | 57 | UseConsistentWhitespaceEnable = $true 58 | UseConsistentWhitespaceCheckInnerBrace = $true 59 | UseConsistentWhitespaceCheckOpenBrace = $true 60 | UseConsistentWhitespaceCheckOpenParen = $true 61 | UseConsistentWhitespaceCheckOperator = $true 62 | UseConsistentWhitespaceCheckPipe = $true 63 | UseConsistentWhitespaceCheckSeparator = $true 64 | 65 | AlignAssignmentStatementEnable = $true 66 | AlignAssignmentStatementCheckHashtable = $true 67 | 68 | UseCorrectCasingEnable = $true 69 | } 70 | # format PSD1 and PSM1 files when merging into a single file 71 | # enable formatting is not required as Configuration is provided 72 | New-ConfigurationFormat -ApplyTo 'OnMergePSM1', 'OnMergePSD1' -Sort None @ConfigurationFormat 73 | # format PSD1 and PSM1 files within the module 74 | # enable formatting is required to make sure that formatting is applied (with default settings) 75 | New-ConfigurationFormat -ApplyTo 'DefaultPSD1', 'DefaultPSM1' -EnableFormatting -Sort None 76 | # when creating PSD1 use special style without comments and with only required parameters 77 | New-ConfigurationFormat -ApplyTo 'DefaultPSD1', 'OnMergePSD1' -PSD1Style 'Minimal' 78 | 79 | # configuration for documentation, at the same time it enables documentation processing 80 | New-ConfigurationDocumentation -Enable:$false -StartClean -UpdateWhenNew -PathReadme 'Docs\Readme.md' -Path 'Docs' 81 | 82 | New-ConfigurationImportModule -ImportSelf -ImportRequiredModules 83 | 84 | $newConfigurationBuildSplat = @{ 85 | Enable = $true 86 | SignModule = $true 87 | CertificateThumbprint = '483292C9E317AA13B07BB7A96AE9D1A5ED9E7703' 88 | DeleteTargetModuleBeforeBuild = $true 89 | MergeModuleOnBuild = $true 90 | MergeFunctionsFromApprovedModules = $true 91 | DoNotAttemptToFixRelativePaths = $true 92 | } 93 | 94 | New-ConfigurationBuild @newConfigurationBuildSplat 95 | 96 | New-ConfigurationArtefact -Type Unpacked -Enable -Path "$PSScriptRoot\..\Artefacts\Unpacked" -AddRequiredModules -RequiredModulesPath "$PSScriptRoot\..\Artefacts\Unpacked\Modules" -CopyFiles @{ 97 | 98 | } -CopyFilesRelative 99 | 100 | New-ConfigurationArtefact -Type Packed -Enable -Path "$PSScriptRoot\..\Artefacts\Packed" -AddRequiredModules -RequiredModulesPath "$PSScriptRoot\..\Artefacts\Packed\Modules" -CopyFiles @{ 101 | 102 | } -CopyFilesRelative -IncludeTagName 103 | 104 | # global options for publishing to github/psgallery 105 | #New-ConfigurationPublish -Type PowerShellGallery -FilePath 'C:\Support\Important\PowerShellGalleryAPI.txt' -Enabled:$true 106 | #New-ConfigurationPublish -Type GitHub -FilePath 'C:\Support\Important\GitHubAPI.txt' -UserName 'EvotecIT' -Enabled:$true 107 | } 108 | -------------------------------------------------------------------------------- /CHANGELOG.MD: -------------------------------------------------------------------------------- 1 | #### 0.0.5 - 2025.01.07 2 | - Improve `New-VirusScan` to allow upload files larger than 32MB 3 | 4 | #### 0.0.4 - 2023.03.15 5 | - Fix wrong return by `Get-VirusReport` of a string instead of an object when key in json is empty [#1](https://github.com/EvotecIT/VirusTotalAnalyzer/issues/1) 6 | 7 | #### 0.0.3 - 2022.08.10 8 | - Fixed small error in `Get-VirusReport` 9 | - Added `Get-VirusScan` alias to `Get-VirusReport` 10 | - Small documentation updates 11 | 12 | #### 0.0.2 - 2022.08.10 13 | - Renamed `Invoke-VirusScan` to `New-VirusScan` to better reflect what will happen 14 | 15 | #### 0.0.1 - 2022.08.09 16 | - First version -------------------------------------------------------------------------------- /Examples/Example-GetVirusTotalReport.ps1: -------------------------------------------------------------------------------- 1 | Import-Module .\VirusTotalAnalyzer.psd1 -Force 2 | 3 | $VTApi = Get-Content -LiteralPath "C:\Support\Important\VirusTotalApi.txt" 4 | 5 | $T1 = Get-VirusReport -ApiKey $VTApi -Hash 'BFF77EECBB2F7DA25ECBC9D9673E5DC1DB68DCC68FD76D006E836F9AC61C547E' 6 | $T1 7 | $T2 = Get-VirusReport -ApiKey $VTApi -Hash '44676ad570f565608ddd6759532c3ae7b1e1a97d' 8 | $T2 9 | $T3 = Get-VirusReport -ApiKey $VTApi -File "$PSScriptRoot\Submisions\TestFile.txt" 10 | $T3 11 | $T4 = Get-VirusReport -ApiKey $VTApi -DomainName 'evotec.xyz' 12 | $T4 13 | $T5 = Get-VirusReport -ApiKey $VTApi -IPAddress '1.1.1.1' 14 | $T5 15 | $T6 = Get-VirusReport -ApiKey $VTApi -Search "https://evotec.xyz" 16 | $T6 -------------------------------------------------------------------------------- /Examples/Example-SendToVirusTotalFile.ps1: -------------------------------------------------------------------------------- 1 | Import-Module .\VirusTotalAnalyzer.psd1 -Force 2 | 3 | $VTApi = Get-Content -LiteralPath "C:\Support\Important\VirusTotalApi.txt" 4 | 5 | $Items = Get-ChildItem -LiteralPath "C:\Users\przemyslaw.klys\Documents\WindowsPowerShell\Modules\PSWriteHTML\Resources\CSS" -Include "*.css" -File -Recurse 6 | 7 | # Submit file to scan 8 | $Output = $Items | New-VirusScan -ApiKey $VTApi -Verbose 9 | $Output | Format-List 10 | 11 | Start-Sleep -Seconds 120 12 | 13 | # Since the output will return scan ID we can use it to get the report 14 | $OutputScan = Get-VirusReport -ApiKey $VTApi -AnalysisId $Output.data.id 15 | $OutputScan | Format-List 16 | $OutputScan.Meta | Format-List 17 | $OutputScan.Data | Format-List -------------------------------------------------------------------------------- /Examples/Example-SendToVirusTotalLarge.ps1: -------------------------------------------------------------------------------- 1 | Import-Module .\VirusTotalAnalyzer.psd1 -Force 2 | 3 | $VTApi = Get-Content -LiteralPath "C:\Support\Important\VirusTotalApi.txt" 4 | 5 | $Items = "C:\Users\przemyslaw.klys\Downloads\amd-software-adrenalin-edition-24.10.1-minimalsetup-241017_web.exe" 6 | 7 | # Submit file to scan 8 | $Output = New-VirusScan -ApiKey $VTApi -Verbose -File $Items 9 | $Output | Format-List 10 | 11 | Start-Sleep -Seconds 120 12 | 13 | # Since the output will return scan ID we can use it to get the report 14 | $OutputScan = Get-VirusReport -ApiKey $VTApi -AnalysisId $Output.data.id 15 | $OutputScan | Format-List 16 | $OutputScan.Meta | Format-List 17 | $OutputScan.Data | Format-List -------------------------------------------------------------------------------- /Examples/Example-SendToVirusTotalRescan.ps1: -------------------------------------------------------------------------------- 1 | Import-Module .\VirusTotalAnalyzer.psd1 -Force 2 | 3 | $VTApi = Get-Content -LiteralPath "C:\Support\Important\VirusTotalApi.txt" 4 | 5 | # Submit file hash to rescan from existing file (doesn't sends the file) 6 | $Output = New-VirusScan -ApiKey $VTApi -FileHash "$PSScriptRoot\Submisions\TestFile.txt" 7 | $Output | Format-List 8 | 9 | # Submit hash to rescan 10 | $Output = New-VirusScan -ApiKey $VTApi -Hash "ThisHashHasToExistsOnVirusTotal" 11 | $Output | Format-List -------------------------------------------------------------------------------- /Examples/Example-SendToVirusTotalUrl.ps1: -------------------------------------------------------------------------------- 1 | Import-Module .\VirusTotalAnalyzer.psd1 -Force 2 | 3 | $VTApi = Get-Content -LiteralPath "C:\Support\Important\VirusTotalApi.txt" 4 | 5 | New-VirusScan -ApiKey $VTApi -Url 'evotec.pl' 6 | New-VirusScan -ApiKey $VTApi -Url 'https://evotec.pl' -------------------------------------------------------------------------------- /Examples/Submisions/TestFile.txt: -------------------------------------------------------------------------------- 1 | MyTestFile -------------------------------------------------------------------------------- /License: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Evotec 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 NON-INFRINGEMENT. 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 | -------------------------------------------------------------------------------- /Private/ConvertTo-VTBody.ps1: -------------------------------------------------------------------------------- 1 | function ConvertTo-VTBody { 2 | <# 3 | .SYNOPSIS 4 | Converts file to memory stream to create body for Invoke-RestMethod and send it to Virus Total. 5 | 6 | .DESCRIPTION 7 | Converts file to memory stream to create body for Invoke-RestMethod and send it to Virus Total. 8 | 9 | .PARAMETER FileInformation 10 | Path to a file to send to Virus Total 11 | 12 | .PARAMETER Boundary 13 | Boundary information to say where the file starts and ends. 14 | 15 | .EXAMPLE 16 | $Boundary = [Guid]::NewGuid().ToString().Replace('-', '') 17 | ConvertTo-VTBody -File $File -Boundary $Boundary 18 | 19 | .NOTES 20 | Notes 21 | 22 | #> 23 | [cmdletBinding()] 24 | param( 25 | [parameter(Mandatory)][System.IO.FileInfo] $FileInformation, 26 | [string] $Boundary 27 | ) 28 | [byte[]] $CRLF = 13, 10 # ASCII code for CRLF 29 | $MemoryStream = [System.IO.MemoryStream]::new() 30 | 31 | # Write boundary 32 | $BoundaryInformation = [System.Text.Encoding]::ASCII.GetBytes("--$Boundary") 33 | $MemoryStream.Write($BoundaryInformation, 0, $BoundaryInformation.Length) 34 | $MemoryStream.Write($CRLF, 0, $CRLF.Length) 35 | 36 | # Content-Disposition (wrap filename in quotes) 37 | $FileData = [System.Text.Encoding]::ASCII.GetBytes("Content-Disposition: form-data; name=`"file`"; filename=`"$($FileInformation.Name)`"") 38 | $MemoryStream.Write($FileData, 0, $FileData.Length) 39 | $MemoryStream.Write($CRLF, 0, $CRLF.Length) 40 | 41 | # Content-Type 42 | $ContentType = [System.Text.Encoding]::ASCII.GetBytes('Content-Type: application/octet-stream') 43 | $MemoryStream.Write($ContentType, 0, $ContentType.Length) 44 | $MemoryStream.Write($CRLF, 0, $CRLF.Length) 45 | $MemoryStream.Write($CRLF, 0, $CRLF.Length) 46 | 47 | # File content 48 | $FileContent = [System.IO.File]::ReadAllBytes($FileInformation.FullName) 49 | $MemoryStream.Write($FileContent, 0, $FileContent.Length) 50 | $MemoryStream.Write($CRLF, 0, $CRLF.Length) 51 | 52 | # End boundary 53 | $MemoryStream.Write($BoundaryInformation, 0, $BoundaryInformation.Length) 54 | $Closure = [System.Text.Encoding]::ASCII.GetBytes('--') 55 | $MemoryStream.Write($Closure, 0, $Closure.Length) 56 | $MemoryStream.Write($CRLF, 0, $CRLF.Length) 57 | 58 | # Return raw byte array 59 | , $MemoryStream.ToArray() 60 | } -------------------------------------------------------------------------------- /Public/Get-VirusReport.ps1: -------------------------------------------------------------------------------- 1 | function Get-VirusReport { 2 | <# 3 | .SYNOPSIS 4 | Get the report from Virus Total about file, hash, url, ip address or domain. 5 | 6 | .DESCRIPTION 7 | Get the report from Virus Total about file, hash, url, ip address or domain. 8 | 9 | .PARAMETER ApiKey 10 | Provide ApiKey from Virus Total. 11 | 12 | .PARAMETER FileHash 13 | Provide FileHash to check. You can do this with Get-FileHash. 14 | 15 | .PARAMETER File 16 | Provide FilePath to a file to check. 17 | 18 | .PARAMETER Url 19 | Provide Url to check on Virus Total 20 | 21 | .PARAMETER IPAddress 22 | Provide IPAddress to check on Virus Total 23 | 24 | .PARAMETER DomainName 25 | Provide DomainName to check on Virus Total 26 | 27 | .PARAMETER Search 28 | Search for file hash, URL, domain, IP address or Tag comments. 29 | 30 | .EXAMPLE 31 | $VTApi = 'ApiKey from VirusTotal' 32 | 33 | Get-VirusReport -ApiKey $VTApi -FileHash 'BFF77EECBB2F7DA25ECBC9D9673E5DC1DB68DCC68FD76D006E836F9AC61C547E' 34 | Get-VirusReport -ApiKey $VTApi -File 'C:\Support\GitHub\PSPublishModule\Releases\v0.9.47\PSPublishModule.psm1' 35 | Get-VirusReport -ApiKey $VTApi -DomainName 'evotec.xyz' 36 | Get-VirusReport -ApiKey $VTApi -IPAddress '1.1.1.1' 37 | 38 | .NOTES 39 | General notes 40 | #> 41 | [alias('Get-VirusScan')] 42 | [CmdletBinding(DefaultParameterSetName = 'FileInformation')] 43 | Param( 44 | [Parameter(Mandatory)][string] $ApiKey, 45 | [Parameter(ParameterSetName = "Analysis", ValueFromPipeline, ValueFromPipelineByPropertyName)] 46 | [string] $AnalysisId, 47 | [Parameter(ParameterSetName = "Hash", ValueFromPipeline, ValueFromPipelineByPropertyName)] 48 | [string] $Hash, 49 | [alias('FileHash')][Parameter(ParameterSetName = "FileInformation", ValueFromPipeline, ValueFromPipelineByPropertyName)] 50 | [System.IO.FileInfo] $File, 51 | [alias('Uri')][Parameter(ParameterSetName = "Url", ValueFromPipeline, ValueFromPipelineByPropertyName)] 52 | [Uri] $Url, 53 | [Parameter(ParameterSetName = "IPAddress", ValueFromPipeline , ValueFromPipelineByPropertyName)] 54 | [string] $IPAddress, 55 | [Parameter(ParameterSetName = "DomainName", ValueFromPipeline, ValueFromPipelineByPropertyName)] 56 | [string] $DomainName, 57 | [Parameter(ParameterSetName = "Search", ValueFromPipeline, ValueFromPipelineByPropertyName)] 58 | [string] $Search 59 | ) 60 | Process { 61 | $RestMethod = @{} 62 | if ($PSCmdlet.ParameterSetName -eq 'FileInformation') { 63 | if (Test-Path -LiteralPath $File) { 64 | $VTFileHash = Get-FileHash -LiteralPath $File -Algorithm SHA256 65 | $RestMethod = @{ 66 | Method = 'GET' 67 | Uri = "https://www.virustotal.com/api/v3/files/$($VTFileHash.Hash)" 68 | Headers = @{ 69 | "Accept" = "application/json" 70 | 'X-Apikey' = $ApiKey 71 | } 72 | } 73 | } else { 74 | if ($PSBoundParameters.ErrorAction -eq 'Stop') { 75 | throw "Failed because the file $File does not exist." 76 | } else { 77 | Write-Warning -Message "Get-VirusReport - Using $($PSCmdlet.ParameterSetName) task failed because the file $File does not exist." 78 | } 79 | } 80 | } elseif ($PSCmdlet.ParameterSetName -eq "Analysis") { 81 | $SearchQueryEscaped = [uri]::EscapeUriString($AnalysisId) 82 | $RestMethod = @{ 83 | Method = 'GET' 84 | Uri = "https://www.virustotal.com/api/v3/analyses/$SearchQueryEscaped" 85 | Headers = @{ 86 | "Accept" = "application/json" 87 | 'X-Apikey' = $ApiKey 88 | } 89 | } 90 | 91 | } elseif ($PSCmdlet.ParameterSetName -eq "Hash") { 92 | $RestMethod = @{ 93 | Method = 'GET' 94 | Uri = "https://www.virustotal.com/api/v3/files/$Hash" 95 | Headers = @{ 96 | "Accept" = "application/json" 97 | 'X-Apikey' = $ApiKey 98 | } 99 | } 100 | } elseif ($PSCmdlet.ParameterSetName -eq "Url") { 101 | $RestMethod = @{ 102 | Method = 'POST' 103 | Uri = "https://www.virustotal.com/api/v3/urls/$Url" 104 | Headers = @{ 105 | "Accept" = "application/json" 106 | 'X-Apikey' = $ApiKey 107 | } 108 | } 109 | } elseif ($PSCmdlet.ParameterSetName -eq "IPAddress") { 110 | $RestMethod = @{ 111 | Method = 'GET' 112 | Uri = "http://www.virustotal.com/api/v3/ip_addresses/$IPAddress" 113 | Headers = @{ 114 | "Accept" = "application/json" 115 | 'X-Apikey' = $ApiKey 116 | } 117 | } 118 | } elseif ($PSCmdlet.ParameterSetName -eq "DomainName") { 119 | $RestMethod = @{ 120 | Method = 'GET' 121 | Uri = "http://www.virustotal.com/api/v3/domains/$DomainName" 122 | Headers = @{ 123 | "Accept" = "application/json" 124 | 'X-Apikey' = $ApiKey 125 | } 126 | } 127 | } elseif ($PSCmdlet.ParameterSetName -eq 'Search') { 128 | #$SearchQueryEscaped = [System.Web.HttpUtility]::UrlEncode(($Search) 129 | #$SearchQueryEscaped = [uri]::EscapeDataString($Search) 130 | $SearchQueryEscaped = [uri]::EscapeUriString($Search) 131 | $RestMethod = @{ 132 | Method = 'GET' 133 | Uri = "http://www.virustotal.com/api/v3/search?query=$SearchQueryEscaped" 134 | Headers = @{ 135 | "Accept" = "application/json" 136 | 'X-Apikey' = $ApiKey 137 | } 138 | } 139 | } 140 | Try { 141 | $InvokeApiOutput = Invoke-RestMethod @RestMethod -ErrorAction Stop 142 | if ($InvokeApiOutput -is [string]) { 143 | $InvokeApiOutput = $InvokeApiOutput.Replace("`"`": {", "`"external_assemblies`": {") 144 | try { 145 | $InvokeApiOutput | ConvertFrom-Json -ErrorAction Stop 146 | } catch { 147 | Write-Warning -Message "Get-VirusReport - Using $($PSCmdlet.ParameterSetName) task failed with error: $($_.Exception.Message)" 148 | } 149 | } else { 150 | $InvokeApiOutput 151 | } 152 | } catch { 153 | if ($PSBoundParameters.ErrorAction -eq 'Stop') { 154 | throw 155 | } else { 156 | Write-Warning -Message "Get-VirusReport - Using $($PSCmdlet.ParameterSetName) task failed with error: $($_.Exception.Message)" 157 | } 158 | } 159 | } 160 | } 161 | 162 | -------------------------------------------------------------------------------- /Public/New-VirusScan.ps1: -------------------------------------------------------------------------------- 1 | function New-VirusScan { 2 | <# 3 | .SYNOPSIS 4 | Send a file, file hash or url to VirusTotal for a scan. 5 | 6 | .DESCRIPTION 7 | Send a file, file hash or url to VirusTotal for a scan using Virus Total v3 Api. 8 | If file hash is provided then we tell VirusTotal to reanalyze the file it has rather than sending a new file. 9 | 10 | .PARAMETER ApiKey 11 | ApiKey to use for the scan. This key is available only for registred users (free). 12 | 13 | .PARAMETER Hash 14 | Provide a file hash to scan on VirusTotal (file itself is not sent) 15 | 16 | .PARAMETER FileHash 17 | Porvide a file which hash will be used to send to Virus Total (file itself is not sent) 18 | 19 | .PARAMETER File 20 | Provide a file path for a file to sendto Virus Total. 21 | 22 | .PARAMETER Url 23 | Provide a URL to send to Virus Total. 24 | 25 | .PARAMETER Password 26 | Password to use for the file. This is used for password protected files. 27 | 28 | .EXAMPLE 29 | $VTApi = 'YourApiCode' 30 | 31 | New-VirusScan -ApiKey $VTApi -Url 'evotec.pl' 32 | New-VirusScan -ApiKey $VTApi -Url 'https://evotec.pl' 33 | 34 | .EXAMPLE 35 | $VTApi = 'YourApiCode 36 | 37 | # Submit file to scan 38 | $Output = New-VirusScan -ApiKey $VTApi -File "C:\Users\przemyslaw.klys\Documents\WindowsPowerShell\Modules\AuditPolicy\AuditPolicy.psd1" 39 | $Output | Format-List 40 | 41 | # Since the output will return scan ID we can use it to get the report 42 | $OutputScan = Get-VirusReport -ApiKey $VTApi -AnalysisId $Output.data.id 43 | $OutputScan | Format-List 44 | $OutputScan.Meta | Format-List 45 | $OutputScan.Data | Format-List 46 | 47 | .NOTES 48 | API Reference: https://developers.virustotal.com/reference/files-scan 49 | This function now supports large files (> 32MB) by requesting an upload_url. 50 | 51 | #> 52 | [CmdletBinding()] 53 | param( 54 | [Parameter(Mandatory)][string] $ApiKey, 55 | [Parameter(ParameterSetName = "Hash", ValueFromPipeline, ValueFromPipelineByPropertyName)] 56 | [string] $Hash, 57 | [Parameter(ParameterSetName = "FileHash", ValueFromPipeline, ValueFromPipelineByPropertyName)] 58 | [string] $FileHash, 59 | [Parameter(ParameterSetName = "FileInformation", ValueFromPipeline, ValueFromPipelineByPropertyName)] 60 | [System.IO.FileInfo] $File, 61 | [alias('Uri')][Parameter(ParameterSetName = "Url", ValueFromPipeline, ValueFromPipelineByPropertyName)] 62 | [Uri] $Url, 63 | [string] $Password 64 | ) 65 | process { 66 | $RestMethod = @{} 67 | if ($PSCmdlet.ParameterSetName -eq 'FileInformation') { 68 | if ($File.Length -gt 33554432) { 69 | # Request large file upload URL 70 | try { 71 | $UploadUrlResponse = Invoke-RestMethod -Method 'GET' -Uri 'https://www.virustotal.com/api/v3/files/upload_url' -Headers @{ 72 | "Accept" = "application/json" 73 | 'x-apikey' = $ApiKey 74 | } -ErrorAction Stop 75 | } catch { 76 | if ($PSBoundParameters.ErrorAction -eq 'Stop') { 77 | throw 78 | } else { 79 | Write-Warning -Message "New-VirusScan - Using $($PSCmdlet.ParameterSetName) task failed with error: $($_.Exception.Message)" 80 | } 81 | } 82 | $Boundary = [Guid]::NewGuid().ToString().Replace('-', '') 83 | 84 | Write-Verbose -Message "New-VirusScan - Uploading large file $($File.FullName) to VirusTotal using $($UploadUrlResponse.data)" 85 | 86 | $RestMethod = @{ 87 | Method = 'POST' 88 | Uri = $UploadUrlResponse.data 89 | Headers = @{ 90 | "accept" = "application/json" 91 | 'x-apikey' = $ApiKey 92 | 'password' = $Password 93 | } 94 | Body = ConvertTo-VTBody -File $File -Boundary $Boundary 95 | ContentType = 'multipart/form-data; boundary=' + $Boundary 96 | } 97 | Remove-EmptyValue -Hashtable $RestMethod.Headers 98 | } else { 99 | $Boundary = [Guid]::NewGuid().ToString().Replace('-', '') 100 | $RestMethod = @{ 101 | Method = 'POST' 102 | Uri = 'https://www.virustotal.com/api/v3/files' 103 | Headers = @{ 104 | "Accept" = "application/json" 105 | 'x-apikey' = $ApiKey 106 | 'password' = $Password 107 | } 108 | Body = ConvertTo-VTBody -File $File -Boundary $Boundary 109 | ContentType = 'multipart/form-data; boundary=' + $boundary 110 | } 111 | Remove-EmptyValue -Hashtable $RestMethod.Headers 112 | } 113 | } elseif ($PSCmdlet.ParameterSetName -eq "Hash") { 114 | $RestMethod = @{ 115 | Method = 'POST' 116 | Uri = "https://www.virustotal.com/api/v3/files/$Hash/analyse" 117 | Headers = @{ 118 | "Accept" = "application / json" 119 | 'X-Apikey' = $ApiKey 120 | } 121 | } 122 | } elseif ($PSCmdlet.ParameterSetName -eq "FileHash") { 123 | if (Test-Path -LiteralPath $FileHash) { 124 | $VTFileHash = Get-FileHash -LiteralPath $FileHash -Algorithm SHA256 125 | $RestMethod = @{ 126 | Method = 'POST' 127 | Uri = "https://www.virustotal.com/api/v3/files/$($VTFileHash.Hash)/analyse" 128 | Headers = @{ 129 | "Accept" = "application/json" 130 | 'X-Apikey' = $ApiKey 131 | } 132 | } 133 | } else { 134 | Write-Warning -Message "New-VirusScan - File $FileHash doesn't exists. Skipping..." 135 | } 136 | } elseif ($PSCmdlet.ParameterSetName -eq "Url") { 137 | $RestMethod = @{ 138 | Method = 'POST' 139 | Uri = 'https://www.virustotal.com/api/v3/urls' 140 | Headers = @{ 141 | "Accept" = "application/json" 142 | 'X-Apikey' = $ApiKey 143 | "Content-Type" = "application/x-www-form-urlencoded" 144 | } 145 | Body = @{ 'url' = [uri]::EscapeUriString($Url) } 146 | } 147 | } 148 | if ($RestMethod.Count -gt 0) { 149 | try { 150 | $InvokeApiOutput = Invoke-RestMethod @RestMethod -ErrorAction Stop 151 | $InvokeApiOutput 152 | } catch { 153 | if ($_.ErrorDetails.Message) { 154 | if ($PSBoundParameters.ErrorAction -eq 'Stop') { 155 | throw 156 | } else { 157 | if ($_.ErrorDetails.RecommendedAction) { 158 | Write-Warning -Message "New-VirusScan - Using $($PSCmdlet.ParameterSetName) task failed with error: $($_.Exception.Message) and full message: $($_.ErrorDetails.Message) and recommended action: $($_.ErrorDetails.RecommendedAction)" 159 | } else { 160 | Write-Warning -Message "New-VirusScan - Using $($PSCmdlet.ParameterSetName) task failed with error: $($_.Exception.Message) and full message: $($_.ErrorDetails.Message)" 161 | } 162 | } 163 | } else { 164 | if ($PSBoundParameters.ErrorAction -eq 'Stop') { 165 | throw 166 | } else { 167 | Write-Warning -Message "New-VirusScan - Using $($PSCmdlet.ParameterSetName) task failed with error: $($_.Exception.Message)" 168 | } 169 | } 170 | } 171 | } 172 | } 173 | } -------------------------------------------------------------------------------- /README.MD: -------------------------------------------------------------------------------- 1 | 

2 | 3 | 4 | 5 |

6 | 7 |

8 | 9 | 10 | 11 | 12 |

13 | 14 |

15 | 16 | 17 | 18 |

19 | 20 | # VirusTotalAnalyzer PowerShell Module 21 | 22 | **VirusTotalAnalyzer** is very small PowerShell module that helps with submiting files to VirusTotal service and getting results. 23 | It allowws to check if file is infected or not and also to get information about file. 24 | You can also request information about URL, Domain or IPAddress. 25 | 26 | 27 | You can read about it on my blog: 28 | 29 | - [Working with Virus Total from PowerShell](https://evotec.xyz/working-with-virustotal-from-powershell/) 30 | 31 | ### Getting information from VirusTotal 32 | After installation of module you can use it like this: 33 | 34 | ```powershell 35 | Import-Module VirusTotalAnalyzer -Force 36 | 37 | # API KEY can be found once you register to Virus Total service (it's free) 38 | $VTApi = 'APIKEY' 39 | 40 | $T1 = Get-VirusReport -ApiKey $VTApi -Hash 'BFF77EECBB2F7DA25ECBC9D9673E5DC1DB68DCC68FD76D006E836F9AC61C547E' 41 | $T2 = Get-VirusReport -ApiKey $VTApi -File "$PSScriptRoot\Submisions\TestFile.txt" 42 | $T3 = Get-VirusReport -ApiKey $VTApi -DomainName 'evotec.xyz' 43 | $T4 = Get-VirusReport -ApiKey $VTApi -IPAddress '1.1.1.1' 44 | $T5 = Get-VirusReport -ApiKey $VTApi -Search "https://evotec.xyz" 45 | ``` 46 | 47 | Each variable from above delivers additional information about given request. 48 | 49 | Output first level 50 | ``` 51 | data 52 | ---- 53 | @{attributes=; type=file; id=bff77eecbb2f7da25ecbc9d9673e5dc1db68dcc68fd76d006e836f9ac61c547e; links=} 54 | ``` 55 | 56 | 57 | Output second level 58 | ``` 59 | attributes 60 | ---------- 61 | @{type_description=Powershell; tlsh=T10404B65A7D05522320B36B76E8A78008FF77423B4254111978ECD6C87F75928D3BAFEA; vhash=029198501f8f46256cb0cf2e4fbb8ce7; trid=System.Object[]; crowdsourced_yara_results=System.Object[]; names=System.Object[]; last_modification_date=1659953097; type_tag=powers... 62 | ``` 63 | 64 | Output third level 65 | ``` 66 | attributes : @{type_description=Powershell; tlsh=T10404B65A7D05522320B36B76E8A78008FF77423B4254111978ECD6C87F75928D3BAFEA; vhash=029198501f8f46256cb0cf2e4fbb8ce7; trid=System.Object[]; crowdsourced_yara_results=System.Object[]; names=System.Object[]; last_modification_date=1659953097; 67 | type_tag=powershell; times_submitted=2; total_votes=; size=184182; type_extension=ps1; last_submission_date=1659903352; last_analysis_results=; sandbox_verdicts=; sha256=bff77eecbb2f7da25ecbc9d9673e5dc1db68dcc68fd76d006e836f9ac61c547e; tags=System.Object[]; 68 | last_analysis_date=1659903352; unique_sources=2; first_submission_date=1659862256; ssdeep=3072:wMxUx42PfUYYxlQ7uZtAcI5GCy23KV9syb0wqV:wa2G923K6V; md5=e3c925286ccafd07fb61bd6a12a2ee94; sha1=79fc6a99468f83c7f98e58fdbb811cd95a153567; magic=UTF-8 Unicode (with BOM) English text, 69 | with very long lines, with CRLF line terminators; powershell_info=; last_analysis_stats=; meaningful_name=PSPublishModule.psm1; reputation=0} 70 | type : file 71 | id : bff77eecbb2f7da25ecbc9d9673e5dc1db68dcc68fd76d006e836f9ac61c547e 72 | links : @{self=https://www.virustotal.com/api/v3/files/bff77eecbb2f7da25ecbc9d9673e5dc1db68dcc68fd76d006e836f9ac61c547e} 73 | ``` 74 | 75 | Output fourth level 76 | 77 | ``` 78 | type_description : Powershell 79 | tlsh : T10404B65A7D05522320B36B76E8A78008FF77423B4254111978ECD6C87F75928D3BAFEA 80 | vhash : 029198501f8f46256cb0cf2e4fbb8ce7 81 | trid : {@{file_type=Text - UTF-8 encoded; probability=100.0}} 82 | crowdsourced_yara_results : {@{description=This signature fires on the presence of Base64 encoded URI prefixes (http:// and https://) across any file. The simple presence of such strings is not inherently an indicator of malicious content, but is worth further investigation.; 83 | source=https://github.com/InQuest/yara-rules-vt; author=InQuest Labs; ruleset_name=Base64_Encoded_URL; rule_name=Base64_Encoded_URL; ruleset_id=0122bae1e9}, @{description=This signature detects the presence of a number of Windows API functionality often seen 84 | within embedded executables. When this signature alerts on an executable, it is not an indication of malicious behavior. However, if seen firing in other file types, deeper investigation may be warranted.; source=https://github.com/InQuest/yara-rules-vt; 85 | author=InQuest Labs; ruleset_name=Windows_API_Function; rule_name=Windows_API_Function; ruleset_id=0122a7f913}} 86 | names : {PSPublishModule.psm1} 87 | last_modification_date : 1659953097 88 | type_tag : powershell 89 | times_submitted : 2 90 | total_votes : @{harmless=0; malicious=0} 91 | size : 184182 92 | type_extension : ps1 93 | last_submission_date : 1659903352 94 | last_analysis_results : @{Bkav=; Lionic=; tehtris=; DrWeb=; MicroWorld-eScan=; FireEye=; CAT-QuickHeal=; ALYac=; Malwarebytes=; VIPRE=; Paloalto=; Sangfor=; K7AntiVirus=; Alibaba=; K7GW=; Trustlook=; BitDefenderTheta=; VirIT=; Cyren=; SymantecMobileInsight=; Symantec=; Elastic=; 95 | ESET-NOD32=; APEX=; TrendMicro-HouseCall=; Avast=; ClamAV=; Kaspersky=; BitDefender=; NANO-Antivirus=; SUPERAntiSpyware=; Tencent=; Ad-Aware=; Emsisoft=; Comodo=; F-Secure=; Baidu=; Zillya=; TrendMicro=; McAfee-GW-Edition=; SentinelOne=; Trapmine=; CMC=; 96 | Sophos=; Ikarus=; GData=; Jiangmin=; Webroot=; Avira=; Antiy-AVL=; Kingsoft=; Gridinsoft=; Arcabit=; ViRobot=; ZoneAlarm=; Avast-Mobile=; Microsoft=; Cynet=; BitDefenderFalx=; AhnLab-V3=; Acronis=; McAfee=; MAX=; VBA32=; Cylance=; Zoner=; Rising=; Yandex=; 97 | TACHYON=; MaxSecure=; Fortinet=; Cybereason=; Panda=; CrowdStrike=} 98 | sandbox_verdicts : @{C2AE=} 99 | sha256 : bff77eecbb2f7da25ecbc9d9673e5dc1db68dcc68fd76d006e836f9ac61c547e 100 | tags : {powershell} 101 | last_analysis_date : 1659903352 102 | unique_sources : 2 103 | first_submission_date : 1659862256 104 | ssdeep : 3072:wMxUx42PfUYYxlQ7uZtAcI5GCy23KV9syb0wqV:wa2G923K6V 105 | md5 : e3c925286ccafd07fb61bd6a12a2ee94 106 | sha1 : 79fc6a99468f83c7f98e58fdbb811cd95a153567 107 | magic : UTF-8 Unicode (with BOM) English text, with very long lines, with CRLF line terminators 108 | powershell_info : @{dotnet_calls=System.Object[]; cmdlets=System.Object[]; functions=System.Object[]; cmdlets_alias=System.Object[]; ps_variables=System.Object[]} 109 | last_analysis_stats : @{harmless=0; type-unsupported=15; suspicious=0; confirmed-timeout=0; timeout=0; failure=0; malicious=0; undetected=59} 110 | meaningful_name : PSPublishModule.psm1 111 | reputation : 0 112 | ``` 113 | 114 | Depending on which type of object we're working with the results may be diferrent. 115 | 116 | ### Sending a file or url to Virus Total 117 | 118 | To send Url to Virus Total 119 | 120 | ```powershell 121 | Import-Module VirusTotalAnalyzer -Force 122 | 123 | $VTApi = 'APIKEY' 124 | 125 | New-VirusScan -ApiKey $VTApi -Url 'evotec.pl' 126 | New-VirusScan -ApiKey $VTApi -Url 'https://evotec.pl' 127 | ``` 128 | 129 | To send file to Virus Total 130 | 131 | ```powershell 132 | Import-Module VirusTotalAnalyzer -Force 133 | 134 | $VTApi = 'APIKEY' 135 | 136 | # Submit file to scan 137 | $Output = New-VirusScan -ApiKey $VTApi -File "$PSScriptRoot\Submisions\TestFile.txt" 138 | $Output | Format-List 139 | 140 | Start-Sleep -Seconds 60 141 | 142 | # Since the output will return scan ID we can use it to get the report 143 | $OutputScan = Get-VirusReport -ApiKey $VTApi -AnalysisId $Output.data.id 144 | $OutputScan | Format-List 145 | $OutputScan.Meta | Format-List 146 | $OutputScan.Data | Format-List 147 | ``` 148 | 149 | `New-VirusScan` will return an object which then can be verified via `Get-VirusReport`. 150 | Give it some time before checking for results, as it takes time to scan the file. 151 | `New-VirusScan` also provides a way to rescan a file that was already submitted. 152 | You can do so using `Hash` or `FileHash` paramater. 153 | 154 | Once file is finally scanned it will be available using `Get-VirusTotal` with one of the available options. 155 | 156 | ## To install 157 | 158 | ```powershell 159 | Install-Module -Name VirusTotalAnalyzer -AllowClobber -Force 160 | ``` 161 | 162 | Force and AllowClobber aren't necessary, but they do skip errors in case some appear. 163 | 164 | ## And to update 165 | 166 | ```powershell 167 | Update-Module -Name VirusTotalAnalyzer 168 | ``` 169 | 170 | That's it. Whenever there's a new version, you run the command, and you can enjoy it. Remember that you may need to close, reopen PowerShell session if you have already used module before updating it. 171 | 172 | **The essential thing** is if something works for you on production, keep using it till you test the new version on a test computer. I do changes that may not be big, but big enough that auto-update may break your code. For example, small rename to a parameter and your code stops working! Be responsible! -------------------------------------------------------------------------------- /VirusTotalAnalyzer.psd1: -------------------------------------------------------------------------------- 1 | @{ 2 | AliasesToExport = @('Get-VirusScan') 3 | Author = 'Przemyslaw Klys' 4 | CmdletsToExport = @() 5 | CompanyName = 'Evotec' 6 | CompatiblePSEditions = @('Desktop', 'Core') 7 | Copyright = '(c) 2011 - 2025 Przemyslaw Klys @ Evotec. All rights reserved.' 8 | Description = 'PowerShell module that intearacts with the VirusTotal service using a VirusTotal API (free)' 9 | FunctionsToExport = @('Get-VirusReport', 'New-VirusScan') 10 | GUID = '2e82faa1-d870-42b2-b5aa-4a63bf02f43e' 11 | ModuleVersion = '0.0.5' 12 | PowerShellVersion = '5.1' 13 | PrivateData = @{ 14 | PSData = @{ 15 | ExternalModuleDependencies = @('Microsoft.PowerShell.Management', 'Microsoft.PowerShell.Utility') 16 | ProjectUri = 'https://github.com/EvotecIT/VirusTotalAnalyzer' 17 | Tags = @('Windows', 'Linux', 'macOs', 'VirusTotal', 'virus', 'threat', 'analyzer') 18 | } 19 | } 20 | RequiredModules = @(@{ 21 | Guid = 'ee272aa8-baaa-4edf-9f45-b6d6f7d844fe' 22 | ModuleName = 'PSSharedGoods' 23 | ModuleVersion = '0.0.303' 24 | }, 'Microsoft.PowerShell.Management', 'Microsoft.PowerShell.Utility') 25 | RootModule = 'VirusTotalAnalyzer.psm1' 26 | } -------------------------------------------------------------------------------- /VirusTotalAnalyzer.psm1: -------------------------------------------------------------------------------- 1 | #Get public and private function definition files. 2 | $Public = @( Get-ChildItem -Path $PSScriptRoot\Public\*.ps1 -ErrorAction SilentlyContinue -Recurse ) 3 | $Private = @( Get-ChildItem -Path $PSScriptRoot\Private\*.ps1 -ErrorAction SilentlyContinue -Recurse ) 4 | $Classes = @( Get-ChildItem -Path $PSScriptRoot\Classes\*.ps1 -ErrorAction SilentlyContinue -Recurse ) 5 | $Enums = @( Get-ChildItem -Path $PSScriptRoot\Enums\*.ps1 -ErrorAction SilentlyContinue -Recurse ) 6 | 7 | $AssemblyFolders = Get-ChildItem -Path $PSScriptRoot\Lib -Directory -ErrorAction SilentlyContinue 8 | $Assembly = @( 9 | if ($AssemblyFolders.BaseName -contains 'Standard') { 10 | @( Get-ChildItem -Path $PSScriptRoot\Lib\Standard\*.dll -ErrorAction SilentlyContinue -Recurse) 11 | } 12 | if ($PSEdition -eq 'Core') { 13 | @( Get-ChildItem -Path $PSScriptRoot\Lib\Core\*.dll -ErrorAction SilentlyContinue -Recurse ) 14 | } else { 15 | @( Get-ChildItem -Path $PSScriptRoot\Lib\Default\*.dll -ErrorAction SilentlyContinue -Recurse ) 16 | } 17 | ) 18 | $FoundErrors = @( 19 | Foreach ($Import in @($Assembly)) { 20 | try { 21 | Write-Verbose -Message $Import.FullName 22 | Add-Type -Path $Import.Fullname -ErrorAction Stop 23 | # } 24 | } catch [System.Reflection.ReflectionTypeLoadException] { 25 | Write-Warning "Processing $($Import.Name) Exception: $($_.Exception.Message)" 26 | $LoaderExceptions = $($_.Exception.LoaderExceptions) | Sort-Object -Unique 27 | foreach ($E in $LoaderExceptions) { 28 | Write-Warning "Processing $($Import.Name) LoaderExceptions: $($E.Message)" 29 | } 30 | $true 31 | #Write-Error -Message "StackTrace: $($_.Exception.StackTrace)" 32 | } catch { 33 | Write-Warning "Processing $($Import.Name) Exception: $($_.Exception.Message)" 34 | $LoaderExceptions = $($_.Exception.LoaderExceptions) | Sort-Object -Unique 35 | foreach ($E in $LoaderExceptions) { 36 | Write-Warning "Processing $($Import.Name) LoaderExceptions: $($E.Message)" 37 | } 38 | $true 39 | #Write-Error -Message "StackTrace: $($_.Exception.StackTrace)" 40 | } 41 | } 42 | #Dot source the files 43 | Foreach ($Import in @($Private + $Public + $Classes + $Enums)) { 44 | Try { 45 | . $Import.Fullname 46 | } Catch { 47 | Write-Error -Message "Failed to import functions from $($import.Fullname): $_" 48 | $true 49 | } 50 | } 51 | ) 52 | 53 | if ($FoundErrors.Count -gt 0) { 54 | $ModuleName = (Get-ChildItem $PSScriptRoot\*.psd1).BaseName 55 | Write-Warning "Importing module $ModuleName failed. Fix errors before continuing." 56 | break 57 | } 58 | 59 | Export-ModuleMember -Function '*' -Alias '*' --------------------------------------------------------------------------------