diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d755937 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +Ignore/* +.vs/* +.vscode/* +Artefacts/* \ No newline at end of file diff --git a/Build/Manage-VirusTotal.ps1 b/Build/Manage-VirusTotal.ps1 index cfafee1..221771f 100644 --- a/Build/Manage-VirusTotal.ps1 +++ b/Build/Manage-VirusTotal.ps1 @@ -1,165 +1,107 @@ Clear-Host -Import-Module "C:\Support\GitHub\PSPublishModule\PSPublishModule.psd1" -Force - -$Configuration = @{ - Information = @{ - ModuleName = 'VirusTotalAnalyzer' - DirectoryProjects = 'C:\Support\GitHub' - - Manifest = @{ - # Version number of this module. - ModuleVersion = '0.0.X' - # Supported PSEditions - CompatiblePSEditions = @('Desktop', 'Core') - # ID used to uniquely identify this module - GUID = '2e82faa1-d870-42b2-b5aa-4a63bf02f43e' - # Author of this module - Author = 'Przemyslaw Klys' - # Company or vendor of this module - CompanyName = 'Evotec' - # Copyright statement for this module - Copyright = "(c) 2011 - $((Get-Date).Year) Przemyslaw Klys @ Evotec. All rights reserved." - # Description of the functionality provided by this module - Description = 'PowerShell module that intearacts with the VirusTotal service using a VirusTotal API (free)' - # Minimum version of the Windows PowerShell engine required by this module - PowerShellVersion = '5.1' - # 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. - # 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. - Tags = @('Windows', 'Linux', 'macOs', 'VirusTotal', 'virus', 'threat', 'analyzer') - - ProjectUri = 'https://github.com/EvotecIT/VirusTotalAnalyzer' - - RequiredModules = @( - #@{ ModuleName = 'PSEventViewer'; ModuleVersion = 'Latest'; Guid = '5df72a79-cdf6-4add-b38d-bcacf26fb7bc' } - #@{ ModuleName = 'PSSharedGoods'; ModuleVersion = 'Latest'; Guid = 'ee272aa8-baaa-4edf-9f45-b6d6f7d844fe' } - #@{ ModuleName = 'PSWriteHTML'; ModuleVersion = 'Latest'; Guid = 'a7bdf640-f5cb-4acf-9de0-365b322d245c' } - ) - } + +Build-Module -ModuleName 'VirusTotalAnalyzer' { + # Usual defaults as per standard module + $Manifest = [ordered] @{ + # Version number of this module. + ModuleVersion = '0.0.X' + # Supported PSEditions + CompatiblePSEditions = @('Desktop', 'Core') + # ID used to uniquely identify this module + GUID = '2e82faa1-d870-42b2-b5aa-4a63bf02f43e' + # Author of this module + Author = 'Przemyslaw Klys' + # Company or vendor of this module + CompanyName = 'Evotec' + # Copyright statement for this module + Copyright = "(c) 2011 - $((Get-Date).Year) Przemyslaw Klys @ Evotec. All rights reserved." + # Description of the functionality provided by this module + Description = 'PowerShell module that intearacts with the VirusTotal service using a VirusTotal API (free)' + # Minimum version of the Windows PowerShell engine required by this module + PowerShellVersion = '5.1' + # 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. + # 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. + Tags = @('Windows', 'Linux', 'macOs', 'VirusTotal', 'virus', 'threat', 'analyzer') + + ProjectUri = 'https://github.com/EvotecIT/VirusTotalAnalyzer' } - Options = @{ - Merge = @{ - Sort = 'None' - FormatCodePSM1 = @{ - Enabled = $true - RemoveComments = $false - FormatterSettings = @{ - IncludeRules = @( - 'PSPlaceOpenBrace', - 'PSPlaceCloseBrace', - 'PSUseConsistentWhitespace', - 'PSUseConsistentIndentation', - 'PSAlignAssignmentStatement', - 'PSUseCorrectCasing' - ) - - Rules = @{ - PSPlaceOpenBrace = @{ - Enable = $true - OnSameLine = $true - NewLineAfter = $true - IgnoreOneLineBlock = $true - } - - PSPlaceCloseBrace = @{ - Enable = $true - NewLineAfter = $false - IgnoreOneLineBlock = $true - NoEmptyLineBefore = $false - } - - PSUseConsistentIndentation = @{ - Enable = $true - Kind = 'space' - PipelineIndentation = 'IncreaseIndentationAfterEveryPipeline' - IndentationSize = 4 - } - - PSUseConsistentWhitespace = @{ - Enable = $true - CheckInnerBrace = $true - CheckOpenBrace = $true - CheckOpenParen = $true - CheckOperator = $true - CheckPipe = $true - CheckSeparator = $true - } - - PSAlignAssignmentStatement = @{ - Enable = $true - CheckHashtable = $true - } - - PSUseCorrectCasing = @{ - Enable = $true - } - } - } - } - FormatCodePSD1 = @{ - Enabled = $true - RemoveComments = $false - } - Integrate = @{ - ApprovedModules = @('PSSharedGoods', 'PSWriteColor', 'Connectimo', 'PSUnifi', 'PSWebToolbox', 'PSMyPassword') - } - } - Standard = @{ - FormatCodePSM1 = @{ - - } - FormatCodePSD1 = @{ - Enabled = $true - #RemoveComments = $true - } - } - ImportModules = @{ - Self = $true - RequiredModules = $false - Verbose = $false - } - PowerShellGallery = @{ - ApiKey = 'C:\Support\Important\PowerShellGalleryAPI.txt' - FromFile = $true - } - GitHub = @{ - ApiKey = 'C:\Support\Important\GithubAPI.txt' - FromFile = $true - UserName = 'EvotecIT' - #RepositoryName = 'PSWriteHTML' - } - Documentation = @{ - Path = 'Docs' - PathReadme = 'Docs\Readme.md' - } + New-ConfigurationManifest @Manifest + + # Add standard module dependencies (directly, but can be used with loop as well) + New-ConfigurationModule -Type RequiredModule -Name 'PSSharedGoods' -Guid 'Auto' -Version 'Latest' + New-ConfigurationModule -Type ExternalModule -Name 'Microsoft.PowerShell.Management', 'Microsoft.PowerShell.Utility' + + # Add approved modules, that can be used as a dependency, but only when specific function from those modules is used + # And on that time only that function and dependant functions will be copied over + # Keep in mind it has it's limits when "copying" functions such as it should not depend on DLLs or other external files + New-ConfigurationModule -Type ApprovedModule -Name 'PSSharedGoods', 'PSWriteColor', 'Connectimo', 'PSUnifi', 'PSWebToolbox', 'PSMyPassword' + + $ConfigurationFormat = [ordered] @{ + RemoveComments = $false + + PlaceOpenBraceEnable = $true + PlaceOpenBraceOnSameLine = $true + PlaceOpenBraceNewLineAfter = $true + PlaceOpenBraceIgnoreOneLineBlock = $false + + PlaceCloseBraceEnable = $true + PlaceCloseBraceNewLineAfter = $true + PlaceCloseBraceIgnoreOneLineBlock = $false + PlaceCloseBraceNoEmptyLineBefore = $true + + UseConsistentIndentationEnable = $true + UseConsistentIndentationKind = 'space' + UseConsistentIndentationPipelineIndentation = 'IncreaseIndentationAfterEveryPipeline' + UseConsistentIndentationIndentationSize = 4 + + UseConsistentWhitespaceEnable = $true + UseConsistentWhitespaceCheckInnerBrace = $true + UseConsistentWhitespaceCheckOpenBrace = $true + UseConsistentWhitespaceCheckOpenParen = $true + UseConsistentWhitespaceCheckOperator = $true + UseConsistentWhitespaceCheckPipe = $true + UseConsistentWhitespaceCheckSeparator = $true + + AlignAssignmentStatementEnable = $true + AlignAssignmentStatementCheckHashtable = $true + + UseCorrectCasingEnable = $true } - Steps = @{ - BuildModule = @{ # requires Enable to be on to process all of that - Enable = $true - DeleteBefore = $false - Merge = $true - MergeMissing = $true - SignMerged = $true - Releases = $true - ReleasesUnpacked = $false - RefreshPSD1Only = $false - } - BuildDocumentation = @{ - Enable = $false # enables documentation processing - StartClean = $true # always starts clean - UpdateWhenNew = $true # always updates right after new - } - ImportModules = @{ - Self = $true - RequiredModules = $false - Verbose = $false - } - PublishModule = @{ # requires Enable to be on to process all of that - Enabled = $false - Prerelease = '' - RequireForce = $false - GitHub = $false - } + # format PSD1 and PSM1 files when merging into a single file + # enable formatting is not required as Configuration is provided + New-ConfigurationFormat -ApplyTo 'OnMergePSM1', 'OnMergePSD1' -Sort None @ConfigurationFormat + # format PSD1 and PSM1 files within the module + # enable formatting is required to make sure that formatting is applied (with default settings) + New-ConfigurationFormat -ApplyTo 'DefaultPSD1', 'DefaultPSM1' -EnableFormatting -Sort None + # when creating PSD1 use special style without comments and with only required parameters + New-ConfigurationFormat -ApplyTo 'DefaultPSD1', 'OnMergePSD1' -PSD1Style 'Minimal' + + # configuration for documentation, at the same time it enables documentation processing + New-ConfigurationDocumentation -Enable:$false -StartClean -UpdateWhenNew -PathReadme 'Docs\Readme.md' -Path 'Docs' + + New-ConfigurationImportModule -ImportSelf -ImportRequiredModules + + $newConfigurationBuildSplat = @{ + Enable = $true + SignModule = $true + CertificateThumbprint = '483292C9E317AA13B07BB7A96AE9D1A5ED9E7703' + DeleteTargetModuleBeforeBuild = $true + MergeModuleOnBuild = $true + MergeFunctionsFromApprovedModules = $true + DoNotAttemptToFixRelativePaths = $true } -} -New-PrepareModule -Configuration $Configuration \ No newline at end of file + New-ConfigurationBuild @newConfigurationBuildSplat + + New-ConfigurationArtefact -Type Unpacked -Enable -Path "$PSScriptRoot\..\Artefacts\Unpacked" -AddRequiredModules -RequiredModulesPath "$PSScriptRoot\..\Artefacts\Unpacked\Modules" -CopyFiles @{ + + } -CopyFilesRelative + + New-ConfigurationArtefact -Type Packed -Enable -Path "$PSScriptRoot\..\Artefacts\Packed" -AddRequiredModules -RequiredModulesPath "$PSScriptRoot\..\Artefacts\Packed\Modules" -CopyFiles @{ + + } -CopyFilesRelative -IncludeTagName + + # global options for publishing to github/psgallery + #New-ConfigurationPublish -Type PowerShellGallery -FilePath 'C:\Support\Important\PowerShellGalleryAPI.txt' -Enabled:$false + #New-ConfigurationPublish -Type GitHub -FilePath 'C:\Support\Important\GitHubAPI.txt' -UserName 'EvotecIT' -Enabled:$false +} diff --git a/CHANGELOG.MD b/CHANGELOG.MD index 2e930cc..a5f1bac 100644 --- a/CHANGELOG.MD +++ b/CHANGELOG.MD @@ -1,4 +1,7 @@ -#### 0.0.4 - 2023.03.15 +#### 0.0.5 - 2025.01.07 +- Improve `New-VirusScan` to allow upload files larger than 32MB + +#### 0.0.4 - 2023.03.15 - 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) #### 0.0.3 - 2022.08.10 diff --git a/Examples/Example-SendToVirusTotalLarge.ps1 b/Examples/Example-SendToVirusTotalLarge.ps1 new file mode 100644 index 0000000..60e29d5 --- /dev/null +++ b/Examples/Example-SendToVirusTotalLarge.ps1 @@ -0,0 +1,17 @@ +Import-Module .\VirusTotalAnalyzer.psd1 -Force + +$VTApi = Get-Content -LiteralPath "C:\Support\Important\VirusTotalApi.txt" + +$Items = "C:\Users\przemyslaw.klys\Downloads\amd-software-adrenalin-edition-24.10.1-minimalsetup-241017_web.exe" + +# Submit file to scan +$Output = New-VirusScan -ApiKey $VTApi -Verbose -File $Items +$Output | Format-List + +Start-Sleep -Seconds 120 + +# Since the output will return scan ID we can use it to get the report +$OutputScan = Get-VirusReport -ApiKey $VTApi -AnalysisId $Output.data.id +$OutputScan | Format-List +$OutputScan.Meta | Format-List +$OutputScan.Data | Format-List \ No newline at end of file diff --git a/Private/ConvertTo-VTBody.ps1 b/Private/ConvertTo-VTBody.ps1 index a77cf5f..38e56c8 100644 --- a/Private/ConvertTo-VTBody.ps1 +++ b/Private/ConvertTo-VTBody.ps1 @@ -26,32 +26,35 @@ [string] $Boundary ) [byte[]] $CRLF = 13, 10 # ASCII code for CRLF - $MemoryStream = [System.IO.MemoryStream]::new() + # Write boundary $BoundaryInformation = [System.Text.Encoding]::ASCII.GetBytes("--$Boundary") $MemoryStream.Write($BoundaryInformation, 0, $BoundaryInformation.Length) $MemoryStream.Write($CRLF, 0, $CRLF.Length) - $FileData = [System.Text.Encoding]::ASCII.GetBytes("Content-Disposition: form-data; name=`"file`"; filename=$($FileInformation.Name);") + # Content-Disposition (wrap filename in quotes) + $FileData = [System.Text.Encoding]::ASCII.GetBytes("Content-Disposition: form-data; name=`"file`"; filename=`"$($FileInformation.Name)`"") $MemoryStream.Write($FileData, 0, $FileData.Length) $MemoryStream.Write($CRLF, 0, $CRLF.Length) - $ContentType = [System.Text.Encoding]::ASCII.GetBytes('Content-Type:application/octet-stream') + # Content-Type + $ContentType = [System.Text.Encoding]::ASCII.GetBytes('Content-Type: application/octet-stream') $MemoryStream.Write($ContentType, 0, $ContentType.Length) - $MemoryStream.Write($CRLF, 0, $CRLF.Length) $MemoryStream.Write($CRLF, 0, $CRLF.Length) + # File content $FileContent = [System.IO.File]::ReadAllBytes($FileInformation.FullName) $MemoryStream.Write($FileContent, 0, $FileContent.Length) - $MemoryStream.Write($CRLF, 0, $CRLF.Length) - $MemoryStream.Write($BoundaryInformation, 0, $BoundaryInformation.Length) + # End boundary + $MemoryStream.Write($BoundaryInformation, 0, $BoundaryInformation.Length) $Closure = [System.Text.Encoding]::ASCII.GetBytes('--') $MemoryStream.Write($Closure, 0, $Closure.Length) $MemoryStream.Write($CRLF, 0, $CRLF.Length) + # Return raw byte array , $MemoryStream.ToArray() } \ No newline at end of file diff --git a/Public/New-VirusScan.ps1 b/Public/New-VirusScan.ps1 index 9b9dfb8..2874598 100644 --- a/Public/New-VirusScan.ps1 +++ b/Public/New-VirusScan.ps1 @@ -20,7 +20,10 @@ Provide a file path for a file to sendto Virus Total. .PARAMETER Url - Parameter description + Provide a URL to send to Virus Total. + + .PARAMETER Password + Password to use for the file. This is used for password protected files. .EXAMPLE $VTApi = 'YourApiCode' @@ -43,6 +46,7 @@ .NOTES API Reference: https://developers.virustotal.com/reference/files-scan + This function now supports large files (> 32MB) by requesting an upload_url. #> [CmdletBinding()] @@ -55,21 +59,56 @@ [Parameter(ParameterSetName = "FileInformation", ValueFromPipeline, ValueFromPipelineByPropertyName)] [System.IO.FileInfo] $File, [alias('Uri')][Parameter(ParameterSetName = "Url", ValueFromPipeline, ValueFromPipelineByPropertyName)] - [Uri] $Url + [Uri] $Url, + [string] $Password ) process { $RestMethod = @{} if ($PSCmdlet.ParameterSetName -eq 'FileInformation') { - $Boundary = [Guid]::NewGuid().ToString().Replace('-', '') - $RestMethod = @{ - Method = 'POST' - Uri = 'https://www.virustotal.com/api/v3/files' - Headers = @{ - "Accept" = "application/json" - 'x-apikey' = $ApiKey + if ($File.Length -gt 33554432) { + # Request large file upload URL + try { + $UploadUrlResponse = Invoke-RestMethod -Method 'GET' -Uri 'https://www.virustotal.com/api/v3/files/upload_url' -Headers @{ + "Accept" = "application/json" + 'x-apikey' = $ApiKey + } -ErrorAction Stop + } catch { + if ($PSBoundParameters.ErrorAction -eq 'Stop') { + throw + } else { + Write-Warning -Message "New-VirusScan - Using $($PSCmdlet.ParameterSetName) task failed with error: $($_.Exception.Message)" + } + } + $Boundary = [Guid]::NewGuid().ToString().Replace('-', '') + + Write-Verbose -Message "New-VirusScan - Uploading large file $($File.FullName) to VirusTotal using $($UploadUrlResponse.data)" + + $RestMethod = @{ + Method = 'POST' + Uri = $UploadUrlResponse.data + Headers = @{ + "accept" = "application/json" + 'x-apikey' = $ApiKey + 'password' = $Password + } + Body = ConvertTo-VTBody -File $File -Boundary $Boundary + ContentType = 'multipart/form-data; boundary=' + $Boundary + } + Remove-EmptyValue -Hashtable $RestMethod.Headers + } else { + $Boundary = [Guid]::NewGuid().ToString().Replace('-', '') + $RestMethod = @{ + Method = 'POST' + Uri = 'https://www.virustotal.com/api/v3/files' + Headers = @{ + "Accept" = "application/json" + 'x-apikey' = $ApiKey + 'password' = $Password + } + Body = ConvertTo-VTBody -File $File -Boundary $Boundary + ContentType = 'multipart/form-data; boundary=' + $boundary } - Body = ConvertTo-VTBody -File $File -Boundary $Boundary - ContentType = 'multipart/form-data; boundary=' + $boundary + Remove-EmptyValue -Hashtable $RestMethod.Headers } } elseif ($PSCmdlet.ParameterSetName -eq "Hash") { $RestMethod = @{ @@ -111,10 +150,22 @@ $InvokeApiOutput = Invoke-RestMethod @RestMethod -ErrorAction Stop $InvokeApiOutput } catch { - if ($PSBoundParameters.ErrorAction -eq 'Stop') { - throw + if ($_.ErrorDetails.Message) { + if ($PSBoundParameters.ErrorAction -eq 'Stop') { + throw + } else { + if ($_.ErrorDetails.RecommendedAction) { + Write-Warning -Message "New-VirusScan - Using $($PSCmdlet.ParameterSetName) task failed with error: $($_.Exception.Message) and full message: $($_.ErrorDetails.Message) and recommended action: $($_.ErrorDetails.RecommendedAction)" + } else { + Write-Warning -Message "New-VirusScan - Using $($PSCmdlet.ParameterSetName) task failed with error: $($_.Exception.Message) and full message: $($_.ErrorDetails.Message)" + } + } } else { - Write-Warning -Message "New-VirusScan - Using $($PSCmdlet.ParameterSetName) task failed with error: $($_.Exception.Message)" + if ($PSBoundParameters.ErrorAction -eq 'Stop') { + throw + } else { + Write-Warning -Message "New-VirusScan - Using $($PSCmdlet.ParameterSetName) task failed with error: $($_.Exception.Message)" + } } } } diff --git a/VirusTotalAnalyzer.psd1 b/VirusTotalAnalyzer.psd1 index b745364..7e1831f 100644 --- a/VirusTotalAnalyzer.psd1 +++ b/VirusTotalAnalyzer.psd1 @@ -1,20 +1,26 @@ @{ - AliasesToExport = 'Get-VirusScan' + AliasesToExport = @('Get-VirusScan') Author = 'Przemyslaw Klys' CmdletsToExport = @() CompanyName = 'Evotec' CompatiblePSEditions = @('Desktop', 'Core') - Copyright = '(c) 2011 - 2023 Przemyslaw Klys @ Evotec. All rights reserved.' + Copyright = '(c) 2011 - 2025 Przemyslaw Klys @ Evotec. All rights reserved.' Description = 'PowerShell module that intearacts with the VirusTotal service using a VirusTotal API (free)' FunctionsToExport = @('Get-VirusReport', 'New-VirusScan') GUID = '2e82faa1-d870-42b2-b5aa-4a63bf02f43e' - ModuleVersion = '0.0.4' + ModuleVersion = '0.0.5' PowerShellVersion = '5.1' PrivateData = @{ PSData = @{ - Tags = @('Windows', 'Linux', 'macOs', 'VirusTotal', 'virus', 'threat', 'analyzer') - ProjectUri = 'https://github.com/EvotecIT/VirusTotalAnalyzer' + ExternalModuleDependencies = @('Microsoft.PowerShell.Management', 'Microsoft.PowerShell.Utility') + ProjectUri = 'https://github.com/EvotecIT/VirusTotalAnalyzer' + Tags = @('Windows', 'Linux', 'macOs', 'VirusTotal', 'virus', 'threat', 'analyzer') } } + RequiredModules = @(@{ + Guid = 'ee272aa8-baaa-4edf-9f45-b6d6f7d844fe' + ModuleName = 'PSSharedGoods' + ModuleVersion = '0.0.303' + }, 'Microsoft.PowerShell.Management', 'Microsoft.PowerShell.Utility') RootModule = 'VirusTotalAnalyzer.psm1' } \ No newline at end of file diff --git a/VirusTotalAnalyzer.psm1 b/VirusTotalAnalyzer.psm1 index 9b431a4..98d24f0 100644 --- a/VirusTotalAnalyzer.psm1 +++ b/VirusTotalAnalyzer.psm1 @@ -1,4 +1,4 @@ -#Get public and private function definition files. +#Get public and private function definition files. $Public = @( Get-ChildItem -Path $PSScriptRoot\Public\*.ps1 -ErrorAction SilentlyContinue -Recurse ) $Private = @( Get-ChildItem -Path $PSScriptRoot\Private\*.ps1 -ErrorAction SilentlyContinue -Recurse ) $Classes = @( Get-ChildItem -Path $PSScriptRoot\Classes\*.ps1 -ErrorAction SilentlyContinue -Recurse )