├── Get-OwlHMirrorStats-OneSwitch.ps1 ├── Get-OwlHMirrorStats.ps1 ├── New-LXCFromWebTemplate.ps1 ├── New-VMFromFile.ps1 └── New-VulnhubVM.ps1 /Get-OwlHMirrorStats-OneSwitch.ps1: -------------------------------------------------------------------------------- 1 | # Silence verbosity 2 | $InformationPreference = 'SilentlyContinue' 3 | $VerbosePreference = 'SilentlyContinue' 4 | $ProgressPreference = 'SilentlyContinue' 5 | 6 | # Log directory and file names for script output 7 | $mirrorLogDir = '/var/log/OwlH/' 8 | $mirrorLog = $mirrorLogDir + 'OwlHlog.txt' 9 | $statCache = $mirrorLogDir + 'cache.clixml' 10 | $guestCache = $mirrorLogDir + 'guest-cache.txt' 11 | 12 | # Lab switch 13 | # Update according to your environment 14 | $labSwitch = 'vmbr0' 15 | 16 | # Sniff interface 1 17 | # Modify as needed based on VM ID 18 | $tap1Name = 'veth208i1' 19 | 20 | # Span port names for Open vSwitch 21 | $span0Name = 'owlhLab' 22 | 23 | # Get the current I/O stats for the port mirrors 24 | # Use this as a baseline to check for ongoing I/O 25 | # If the next I/O check is higher, the mirror is working 26 | $mirrorStats = ovs-vsctl --format=csv list mirror | ConvertFrom-Csv | Where-Object name -like 'owlh*' 27 | 28 | # Get the current running guests 29 | # Use this as a baseline to check if any guests have started or stopped 30 | # Mirrors need to be reconfigured if any hosts are started or stopped 31 | $cachedGuestIDs = Get-Content $guestCache 32 | $vms = /usr/sbin/qm list | grep running 33 | $vms = $vms -replace '^\s{1,}', '' # Get rid of whitespace at the star of the string 34 | $containers = /usr/sbin/pct list | grep running 35 | $guests = $vms + $containers 36 | $guestIDs = $guests.ForEach({$_.Split(' ')[0]}) 37 | $guestIDs > $guestCache 38 | 39 | # Create log dirs/files as needed 40 | if (-not (Test-Path $mirrorLogDir)) { 41 | New-Item -ItemType Directory -Path $mirrorLogDir -Force | Out-Null 42 | New-Item -ItemType File -Path $mirrorLog -Force | Out-Null 43 | } 44 | # Create the cache file for consecutive script runs to compare I/O 45 | if (-not (Test-Path $statCache)) { 46 | New-Item -ItemType File -Path $statCache -Force | Out-Null 47 | $mirrorStats | Export-Clixml $statCache -Force 48 | } 49 | # Create the cache file to check if any guests have started or stopped between mirror configuration checks 50 | if (-not (Test-Path $guestCache)) { 51 | New-Item -ItemType -Path $guestCache -Force | Out-Null 52 | $guestIDs > $guestCache 53 | break # First run, first cache. Stop execution. 54 | } 55 | 56 | if ($mirrorStats.count -lt 1) { 57 | 58 | # Recreate the mirrors and refresh data since there should always be a minimum of two 59 | # This is based on my lab environment, where I have two switches 60 | # https://benheater.com/proxmox-lab-wazuh-siem-and-nids/ 61 | ovs-vsctl clear Brige $labSwitch mirrors 2>&1 > /dev/null 62 | ovs-vsctl -- --id=@p get port $tap1Name -- --id=@m create mirror name=$span0Name select-all=true output-port=@p -- set bridge $labSwitch mirrors=@m | Out-Null 63 | Start-Sleep -Seconds 5 64 | $mirrorStats = ovs-vsctl --format=csv list mirror | ConvertFrom-Csv | Where-Object name -like 'owlh*' 65 | if ($mirrorStats.Count -lt 1) { 66 | ovs-vsctl clear Brige $labSwitch mirrors 2>&1 > /dev/null 67 | Write-Output 'Stopping script, as the mirror count remains less than 1 after initial attempt to restart.' > $mirrorLog 68 | } 69 | 70 | } 71 | else { 72 | 73 | # Import the cache file to compare I/O on this run 74 | $cacheStats = Import-Clixml $statCache 75 | $cacheStats = $cacheStats | Sort-Object name 76 | $mirrorStats = $mirrorStats | Sort-Object name 77 | 78 | # $mirrorStats will contain the CSV output from ovs-vsctl converted to object notation 79 | # Check the number of objects in the array 80 | # I'm pretty certain I added this logic in at one point cause I was testing mirroring on a third switch and didn't want the script to destroy break it 81 | if ($mirrorStats.count -gt $cacheStats.count) { 82 | Write-Output 'No action taken, as the number of current mirror ports exceeds that in the cache.' > $mirrorLog 83 | $mirrorStats | Export-Clixml $statCache -Force 84 | } 85 | else { 86 | 87 | # Take both SPAN port objects and compare them individually against the current and cached I/O 88 | $currentSpan0 = $mirrorStats | Where-Object {$_.name -match $span0Name} 89 | $cacheSpan0 = $cacheStats | Where-Object {$_.name -match $span0Name} 90 | $currentSpan0txData = $currentSpan0.statistics -replace '{' -replace '}' -split ', ' | ConvertFrom-StringData 91 | $cacheSpan0txData = $cacheSpan0.statistics -replace '{' -replace '}' -split ', ' | ConvertFrom-StringData 92 | 93 | if ($guestIDs.Count -ne $cachedGuestIDs.Count) { 94 | # Reconfigure port mirrors because the number of guests is greater or less than the cached amount 95 | Write-Output 'A guest or guests have either been added/removed/started/stopped between checks. Mirrors will be reconfigured.' > $mirrorLog 96 | ovs-vsctl clear Brige $labSwitch mirrors 2>&1 > /dev/null 97 | ovs-vsctl -- --id=@p get port $tap1Name -- --id=@m create mirror name=$span0Name select-all=true output-port=@p -- set bridge $labSwitch mirrors=@m | Out-Null 98 | Start-Sleep -Seconds 5 99 | $mirrorStats = ovs-vsctl --format=csv list mirror | ConvertFrom-Csv | Where-Object name -like 'owlh*' 100 | $mirrorStats | Export-Clixml $statCache -Force 101 | } 102 | else { 103 | 104 | if ($currentSpan0txData.tx_bytes -gt $cacheSpan0txData.tx_bytes) { 105 | # No problems, as tx_bytes property is larger than that in the cache 106 | Write-Output 'No action taken as current span TX data is greater than that in the cache.' > $mirrorLog 107 | $mirrorStats | Export-Clixml $statCache -Force 108 | } 109 | else { 110 | # The cached bytes and the current span bytes are either non-existent or equal to the cached bytes 111 | # Recreate the mirror 112 | Write-Output 'Recreated mirrors as current span TX data was equal to or older than that in the cache.' > $mirrorLog 113 | ovs-vsctl clear Brige $labSwitch mirrors 2>&1 > /dev/null 114 | ovs-vsctl -- --id=@p get port $tap1Name -- --id=@m create mirror name=$span0Name select-all=true output-port=@p -- set bridge $labSwitch mirrors=@m | Out-Null 115 | Start-Sleep -Seconds 5 116 | $mirrorStats = ovs-vsctl --format=csv list mirror | ConvertFrom-Csv | Where-Object name -like 'owlh*' 117 | $mirrorStats | Export-Clixml $statCache -Force 118 | } 119 | 120 | } 121 | 122 | } 123 | 124 | } 125 | -------------------------------------------------------------------------------- /Get-OwlHMirrorStats.ps1: -------------------------------------------------------------------------------- 1 | # Silence verbosity 2 | $InformationPreference = 'SilentlyContinue' 3 | $VerbosePreference = 'SilentlyContinue' 4 | $ProgressPreference = 'SilentlyContinue' 5 | 6 | # Log directory and file names for script output 7 | $mirrorLogDir = '/var/log/OwlH/' 8 | $mirrorLog = $mirrorLogDir + 'OwlHlog.txt' 9 | $statCache = $mirrorLogDir + 'cache.clixml' 10 | $guestCache = $mirrorLogDir + 'guest-cache.txt' 11 | 12 | # Prod switch 13 | # Update according to your environment 14 | $prodSwitch = 'vmbr0' 15 | # Vuln switch 16 | # Update according to your environment 17 | $vulnSwitch = 'vmbr1' 18 | 19 | # Sniff interface 1 20 | # Modify as needed based on VM ID 21 | $tap1Name = 'veth208i1' 22 | # Sniff interface 2 23 | # Modify as needed based on VM ID 24 | $tap2Name = 'veth208i2' 25 | 26 | # Span port names for Open vSwitch 27 | $span0Name = 'owlhProd' 28 | $span1Name = 'owlhSec' 29 | 30 | # Get the current I/O stats for the port mirrors 31 | # Use this as a baseline to check for ongoing I/O 32 | # If the next I/O check is higher, the mirror is working 33 | $mirrorStats = ovs-vsctl --format=csv list mirror | ConvertFrom-Csv | Where-Object name -like 'owlh*' 34 | 35 | # Get the current running guests 36 | # Use this as a baseline to check if any guests have started or stopped 37 | # Mirrors need to be reconfigured if any hosts are started or stopped 38 | $cachedGuestIDs = Get-Content $guestCache 39 | $vms = /usr/sbin/qm list | grep running 40 | $vms = $vms -replace '^\s{1,}', '' # Get rid of whitespace at the star of the string 41 | $containers = /usr/sbin/pct list | grep running 42 | $guests = $vms + $containers 43 | $guestIDs = $guests.ForEach({$_.Split(' ')[0]}) 44 | $guestIDs > $guestCache 45 | 46 | # Create log dirs/files as needed 47 | if (-not (Test-Path $mirrorLogDir)) { 48 | New-Item -ItemType Directory -Path $mirrorLogDir -Force | Out-Null 49 | New-Item -ItemType File -Path $mirrorLog -Force | Out-Null 50 | } 51 | # Create the cache file for consecutive script runs to compare I/O 52 | if (-not (Test-Path $statCache)) { 53 | New-Item -ItemType File -Path $statCache -Force | Out-Null 54 | $mirrorStats | Export-Clixml $statCache -Force 55 | } 56 | # Create the cache file to check if any guests have started or stopped between mirror configuration checks 57 | if (-not (Test-Path $guestCache)) { 58 | New-Item -ItemType -Path $guestCache -Force | Out-Null 59 | $guestIDs > $guestCache 60 | break # First run, first cache. Stop execution. 61 | } 62 | 63 | if ($mirrorStats.count -lt 2) { 64 | 65 | # Recreate the mirrors and refresh data since there should always be a minimum of two 66 | # This is based on my lab environment, where I have two switches 67 | # https://benheater.com/proxmox-lab-wazuh-siem-and-nids/ 68 | ovs-vsctl clear Brige $prodSwitch mirrors 2>&1 > /dev/null 69 | ovs-vsctl clear Brige $vulnSwitch mirrors 2>&1 > /dev/null 70 | ovs-vsctl -- --id=@p get port $tap1Name -- --id=@m create mirror name=$span0Name select-all=true output-port=@p -- set bridge $prodSwitch mirrors=@m | Out-Null 71 | ovs-vsctl -- --id=@p get port $tap2Name -- --id=@m create mirror name=$span1Name select-all=true output-port=@p -- set bridge $vulnSwitch mirrors=@m | Out-Null 72 | Start-Sleep -Seconds 5 73 | $mirrorStats = ovs-vsctl --format=csv list mirror | ConvertFrom-Csv | Where-Object name -like 'owlh*' 74 | if ($mirrorStats.Count -lt 2) { 75 | ovs-vsctl clear Brige $prodSwitch mirrors 2>&1 > /dev/null 76 | ovs-vsctl clear Brige $vulnSwitch mirrors 2>&1 > /dev/null 77 | Write-Output 'Stopping script, as the mirror count remains less than 2 after initial attempt to restart.' > $mirrorLog 78 | } 79 | 80 | } 81 | else { 82 | 83 | # Import the cache file to compare I/O on this run 84 | $cacheStats = Import-Clixml $statCache 85 | $cacheStats = $cacheStats | Sort-Object name 86 | $mirrorStats = $mirrorStats | Sort-Object name 87 | 88 | # $mirrorStats will contain the CSV output from ovs-vsctl converted to object notation 89 | # Check the number of objects in the array 90 | # I'm pretty certain I added this logic in at one point cause I was testing mirroring on a third switch and didn't want the script to destroy break it 91 | if ($mirrorStats.count -gt $cacheStats.count) { 92 | Write-Output 'No action taken, as the number of current mirror ports exceeds that in the cache.' > $mirrorLog 93 | $mirrorStats | Export-Clixml $statCache -Force 94 | } 95 | else { 96 | 97 | # Take both SPAN port objects and compare them individually against the current and cached I/O 98 | $currentSpan0 = $mirrorStats | Where-Object {$_.name -match $span0Name} 99 | $currentSpan1 = $mirrorStats | Where-Object {$_.name -match $span1Name} 100 | $cacheSpan0 = $cacheStats | Where-Object {$_.name -match $span0Name} 101 | $cacheSpan1 = $cacheStats | Where-Object {$_.name -match $span1Name} 102 | $currentSpan0txData = $currentSpan0.statistics -replace '{' -replace '}' -split ', ' | ConvertFrom-StringData 103 | $currentSpan1txData = $currentSpan1.statistics -replace '{' -replace '}' -split ', ' | ConvertFrom-StringData 104 | $cacheSpan0txData = $cacheSpan0.statistics -replace '{' -replace '}' -split ', ' | ConvertFrom-StringData 105 | $cacheSpan1txData = $cacheSpan1.statistics -replace '{' -replace '}' -split ', ' | ConvertFrom-StringData 106 | 107 | if ($guestIDs.Count -ne $cachedGuestIDs.Count) { 108 | # Reconfigure port mirrors because the number of guests is greater or less than the cached amount 109 | Write-Output 'A guest or guests have either been added/removed/started/stopped between checks. Mirrors will be reconfigured.' > $mirrorLog 110 | ovs-vsctl clear Brige $prodSwitch mirrors 2>&1 > /dev/null 111 | ovs-vsctl clear Brige $vulnSwitch mirrors 2>&1 > /dev/null 112 | ovs-vsctl -- --id=@p get port $tap1Name -- --id=@m create mirror name=$span0Name select-all=true output-port=@p -- set bridge $prodSwitch mirrors=@m | Out-Null 113 | ovs-vsctl -- --id=@p get port $tap2Name -- --id=@m create mirror name=$span1Name select-all=true output-port=@p -- set bridge $vulnSwitch mirrors=@m | Out-Null 114 | Start-Sleep -Seconds 5 115 | $mirrorStats = ovs-vsctl --format=csv list mirror | ConvertFrom-Csv | Where-Object name -like 'owlh*' 116 | $mirrorStats | Export-Clixml $statCache -Force 117 | } 118 | else { 119 | 120 | if (($currentSpan0txData.tx_bytes -gt $cacheSpan0txData.tx_bytes) -or ($currentSpan1txData.tx_bytes -gt $cacheSpan1txData.tx_bytes)) { 121 | # No problems, as tx_bytes property is larger than that in the cache 122 | Write-Output 'No action taken as current span TX data is greater than that in the cache.' > $mirrorLog 123 | $mirrorStats | Export-Clixml $statCache -Force 124 | } 125 | else { 126 | # The cached bytes and the current span bytes are either non-existent or equal to the cached bytes 127 | # Recreate the mirror 128 | Write-Output 'Recreated mirrors as current span TX data was equal to or older than that in the cache.' > $mirrorLog 129 | ovs-vsctl clear Brige $prodSwitch mirrors 2>&1 > /dev/null 130 | ovs-vsctl clear Brige $vulnSwitch mirrors 2>&1 > /dev/null 131 | ovs-vsctl -- --id=@p get port $tap1Name -- --id=@m create mirror name=$span0Name select-all=true output-port=@p -- set bridge $prodSwitch mirrors=@m | Out-Null 132 | ovs-vsctl -- --id=@p get port $tap2Name -- --id=@m create mirror name=$span1Name select-all=true output-port=@p -- set bridge $vulnSwitch mirrors=@m | Out-Null 133 | Start-Sleep -Seconds 5 134 | $mirrorStats = ovs-vsctl --format=csv list mirror | ConvertFrom-Csv | Where-Object name -like 'owlh*' 135 | $mirrorStats | Export-Clixml $statCache -Force 136 | } 137 | 138 | } 139 | 140 | } 141 | 142 | } 143 | -------------------------------------------------------------------------------- /New-LXCFromWebTemplate.ps1: -------------------------------------------------------------------------------- 1 | [CmdletBinding(DefaultParameterSetName = '__AllParameterSets')] 2 | Param ( 3 | [Parameter(Mandatory = $true)] 4 | [ValidateScript({ 5 | if ($_.host -notlike '*.images.linuxcontainers.org' -or $_.Segments[-1] -ne 'rootfs.tar.xz') { 6 | throw "Please provide a link to a valid rootfs.tar.xz file from images.linuxcontainers.org" 7 | } 8 | else { 9 | return $true 10 | } 11 | })] 12 | [System.Uri] 13 | $ImageURI, 14 | 15 | [Parameter(Mandatory = $true)] 16 | [ValidateScript({ 17 | if (-not (Test-Path $_)) { 18 | throw "Path does not exist: $_" 19 | } 20 | else { 21 | return $true 22 | } 23 | })] 24 | [String] 25 | $DownloadDirectory, 26 | 27 | [Parameter(Mandatory = $true)] 28 | [ValidateScript({ 29 | $id = $_ 30 | $id -ge 0 31 | if (Invoke-Command -ScriptBlock {qm status $id 2>/dev/null}) { 32 | throw "VM with ID: $id already exists." 33 | } 34 | elseif (Invoke-Command -ScriptBlock {pct status $id 2>/dev/null}) { 35 | throw "Container with ID: $id already exists." 36 | } 37 | else { 38 | return $true 39 | } 40 | })] 41 | [Int] 42 | $ContainerID, 43 | 44 | [Parameter(ParameterSetName = 'Password', Mandatory = $true)] 45 | [Parameter()] 46 | [Switch] 47 | $SetRootPassword, 48 | 49 | [Parameter()] 50 | [ValidateSet('console', 'shell', 'tty')] 51 | [String] 52 | $DefaultConsole = 'tty', 53 | 54 | [Parameter(ParameterSetName = 'Password', Mandatory = $true)] 55 | [Parameter()] 56 | [ValidateNotNullOrEmpty()] 57 | [String] 58 | $SSHPublicKey, 59 | 60 | [Parameter(HelpMessage = 'This is used to setup configuration inside the container, and corresponds to lxc setup scripts in /usr/share/lxc/config/.common.conf. Value unmanaged can be used to skip and OS specific setup.' )] 61 | [ValidateSet('alpine', 'archlinux', 'centos', 'debian', 'fedora', 'gentoo', 'opensuse', 'ubuntu', 'unmanaged')] 62 | [String] 63 | $ContainerOSType, 64 | 65 | [Parameter( 66 | Mandatory = $true, 67 | HelpMessage = 'Example: local-lvm. This is where the guest boot disk will be stored on the Proxmox node.' 68 | )] 69 | [ValidateScript({ 70 | if (-not (pvesm list $_ 2>/dev/null)) { 71 | throw "Storage volume: $_ does not exist." 72 | } 73 | else { 74 | return $true 75 | } 76 | })] 77 | [String] 78 | $ContainerDiskStorageVolume, 79 | 80 | [Parameter( 81 | Mandatory = $true, 82 | HelpMessage = 'Example: vmbr0' 83 | )] 84 | [ValidateNotNullOrEmpty()] 85 | [ValidateScript({ 86 | if (-not (ip link show $_)) { 87 | throw "Network interface not found." 88 | } 89 | else { 90 | return $true 91 | } 92 | })] 93 | [String] 94 | $NetworkBridge, 95 | 96 | [Parameter( 97 | Mandatory = $true, 98 | HelpMessage = 'Useful for name resolution.' 99 | )] 100 | [ValidateNotNullOrEmpty()] 101 | [String] 102 | $Hostname, 103 | 104 | [Parameter(HelpMessage = 'Example: 2048')] 105 | [ValidateScript({$_ -ge 16})] 106 | [Int] 107 | $MemoryMiB, 108 | 109 | [Parameter()] 110 | [Bool] 111 | $StartOnCreate = $false 112 | ) 113 | begin { 114 | 115 | if (-not [System.IO.Path]::EndsInDirectorySeparator($DownloadDirectory)) { $DownloadDirectory = $DownloadDirectory + '/' } 116 | $fileName = $ImageURI.Segments[-1] -replace 'rootfs', $Hostname 117 | $downloadPath = $DownloadDirectory + $fileName 118 | $parameterCollection = @() 119 | $parameterCollection += "--storage $ContainerDiskStorageVolume" 120 | $parameterCollection += "--net0 name=eth0,bridge=$NetworkBridge" 121 | $parameterCollection += "--hostname $Hostname" 122 | if ($SetRootPassword.IsPresent) { $parameterCollection += '--password' } 123 | if ($PSBoundParameters['DefaultConsole']) { $parameterCollection += "--cmode $DefaultConsole" } 124 | if ($PSBoundParameters['SSHPublicKey']) { 125 | $sshPublicKeysFile = "/tmp/$fileName-pubkeys" 126 | touch $sshPublicKeysFile 127 | $SSHPublicKey | Out-File -FilePath $sshPublicKeysFile -Encoding utf8 128 | $parameterCollection += "--ssh-public-keys $sshPublicKeysFile" 129 | } 130 | if ($PSBoundParameters['ContainerOSType']) { $parameterCollection += "--ostype $ContainerOSType" } 131 | if ($PSBoundParameters['MemoryMiB']) { $parameterCollection += "--memory $MemoryMiB" } 132 | if ($PSBoundParameters['StartOnCreate']) { $parameterCollection += "--start 1" } 133 | $parameterString = $parameterCollection -join ' ' 134 | 135 | } 136 | process { 137 | 138 | Write-Host "Downloading container template from $ImageURI. Please be patient..." -ForegroundColor Green 139 | wget $ImageURI.ToString() -q --show-progress -O $downloadPath 140 | Start-Sleep -Seconds 2 141 | 142 | Write-Host "Attempting to create the container with the following command: pct create $ContainerID $downloadPath $parameterString" -ForegroundColor Green 143 | try { 144 | if ($SetRootPassword.IsPresent) { # Don't redirect stdout. User should be prompted to set root user password 145 | Start-Process pct -ArgumentList "create $ContainerID $downloadPath $parameterString" -Wait 146 | } 147 | else { # Silence stdout 148 | Start-Process pct -ArgumentList "create $ContainerID $downloadPath $parameterString" -Wait -RedirectStandardOut /dev/null 149 | } 150 | Write-Host "Command executed successfully." -ForegroundColor Green 151 | } 152 | catch { 153 | throw "pct create failed:`n$_" 154 | } 155 | 156 | } 157 | end { 158 | 159 | if (Test-Path $downloadPath -ErrorAction SilentlyContinue) { Remove-Item $downloadPath -Force | Out-Null } 160 | if (Test-Path $sshPublicKeysFile -ErrorAction SilentlyContinue) { Remove-Item $sshPublicKeysFile -Force | Out-Null } 161 | 162 | } 163 | -------------------------------------------------------------------------------- /New-VMFromFile.ps1: -------------------------------------------------------------------------------- 1 | [CmdletBinding()] 2 | Param ( 3 | [Parameter(Mandatory = $true)] 4 | [ValidateScript({Test-Path $_})] 5 | [String] 6 | $FilePath, 7 | 8 | [Parameter(Mandatory = $true)] 9 | [ValidateScript({ 10 | $id = $_ 11 | $id -ge 0 12 | if (Invoke-Command -ScriptBlock {qm status $id 2>/dev/null}) { 13 | throw "VM with ID: $id already exists." 14 | } 15 | elseif (Invoke-Command -ScriptBlock {pct status $id 2>/dev/null}) { 16 | throw "Container with ID: $id already exists." 17 | } 18 | else { 19 | return $true 20 | } 21 | })] 22 | [Int] 23 | $VMID, 24 | 25 | [Parameter( 26 | Mandatory = $true, 27 | HelpMessage = 'other: unspecified OS; wxp: Windows XP; w2k: Windows 2000; w2k3: Windows 2003; w2k8: Windows 2008; wvista: Windows Vista; win7: Windows 7; win8: Windows 8; win10: Windows 10; l24: Linux Kernel 2.4; l26: Linux Kernel 2.6; solaris: Solaris/OpenSolaris/OpenIndiana kernel' 28 | )] 29 | [ValidateSet('other', 'wxp', 'w2k', 'w2k3', 'w2k8', 'wvista', 'win7','win8', 'win10', 'l24', 'l26', 'solaris')] 30 | [String] 31 | $GuestOSType, 32 | 33 | [Parameter( 34 | Mandatory = $true, 35 | HelpMessage = 'Example: local-lvm. This is where the guest boot disk will be stored on the Proxmox node.' 36 | )] 37 | [ValidateScript({ 38 | if (-not (pvesm list $_ 2>/dev/null)) { 39 | throw "Storage volume: $_ does not exist." 40 | } 41 | else { 42 | return $true 43 | } 44 | })] 45 | [String] 46 | $VMDiskStorageVolume, 47 | 48 | [Parameter(HelpMessage = 'Example: vmbr0')] 49 | [ValidateNotNullOrEmpty()] 50 | [ValidateScript({ 51 | if (-not (ip link show $_)) { 52 | throw "Network interface not found." 53 | } 54 | else { 55 | return $true 56 | } 57 | })] 58 | [String] 59 | $NetworkBridge, 60 | 61 | [Parameter(HelpMessage = 'Enter an integer between 0 and 4094')] 62 | [ValidateRange(0,4094)] 63 | [Int] 64 | $VlanTag, 65 | 66 | [Parameter(HelpMessage = 'Strictly for administrative purposes only.')] 67 | [ValidateNotNullOrEmpty()] 68 | [String] 69 | $VMName, 70 | 71 | [Parameter(HelpMessage = 'Example: 2048')] 72 | [Int] 73 | $MemoryMiB 74 | ) 75 | begin { 76 | 77 | if (-not (which unar)) { 78 | throw "This script requires the program, unar, which is a single archive decompression tool that works on a variety or archive types.`nPlease install that program and re-run the script." 79 | } 80 | 81 | function Find-VMDK ($Directory) { 82 | 83 | $files = Get-ChildItem $Directory -Recurse 84 | $vmdk = $files | Where-Object {$_.Extension -eq '.vmdk'} 85 | if (-not $vmdk) { 86 | $files | ForEach-Object { 87 | $file = $_ 88 | $type = file $file.FullName 89 | $isArchive = $type -like '*archive*' -or $type -like '*compressed*' 90 | if ($isArchive) { 91 | Write-Host $isArchive.FullName -ForegroundColor Green 92 | $archiveFileFound = $file 93 | } 94 | } 95 | if ($archiveFileFound) { 96 | $subdirectory = "$Directory/temp$(Get-Random)" 97 | unar $archiveFileFound.FullName -o $subdirectory 98 | Find-VMDK -Directory $subdirectory 99 | } 100 | else { 101 | throw "No .vmdk file found and finished recursively checking for archives without results." 102 | } 103 | } 104 | else { 105 | return $vmdk 106 | } 107 | 108 | } 109 | 110 | $slashType = [System.IO.Path]::DirectorySeparatorChar 111 | $getFullPath = Resolve-Path $FilePath # In case a relative path is specified 112 | if ($getFullPath -like '*.iso') { throw "Creating VMs from ISO files not yet implemented." } 113 | elseif ($getFullPath -like '*.vmdk') { $gotVmdk = $true } 114 | else { 115 | $targetFile = Get-ChildItem $getFullPath.Path 116 | $archiveOutputDirectory = $targetFile.Directory.FullName + $slashType + $targetFile.BaseName + "-temp$(Get-Random)" 117 | $archiveOutputDirectory = $archiveOutputDirectory -replace ' ', '_' 118 | } 119 | $parameterCollection = @() 120 | $parameterCollection += "--ostype $GuestOSType" 121 | $parameterCollection += "--storage $VMDiskStorageVolume" 122 | if ($PSBoundParameters['NetworkBridge']) { 123 | if ($PSBoundParameters['VlanTag']) { 124 | $parameterCollection += "--net0 model=virtio,bridge=$NetworkBridge,firewall=0,tag=$VlanTag" 125 | } 126 | else { 127 | $parameterCollection += "--net0 model=virtio,bridge=$NetworkBridge,firewall=0" 128 | } 129 | } 130 | if ($PSBoundParameters['VMName']) { $parameterCollection += "--name $VMName" } 131 | if ($PSBoundParameters['MemoryMiB']) { $parameterCollection += "--memory $MemoryMiB" } 132 | $parameterString = $parameterCollection -join ' ' 133 | 134 | } 135 | process { 136 | 137 | if (-not $gotVmdk) { 138 | 139 | try { 140 | Write-Host "Extracting files to $archiveOutputDirectory" -ForegroundColor Green 141 | unar $targetFile.FullName -o $archiveOutputDirectory 142 | } 143 | catch { 144 | throw "Error expanding archive:`n$_" 145 | } 146 | 147 | try { 148 | Write-Host "Replacing any whitespace from file paths for compatibility." -ForegroundColor Green 149 | Get-ChildItem $archiveOutputDirectory -Recurse | 150 | ForEach-Object {# Arbitrarily try to remove any whitespace in file path, as this has been an issue before 151 | $removeWhiteSpace = $_.FullName -replace ' ', '_' 152 | if ($removeWhiteSpace -ne $_.FullName) { 153 | Move-Item $_.FullName $removeWhiteSpace 154 | } 155 | } 156 | $vmDisk = Find-VMDK -Directory $archiveOutputDirectory 157 | $vmDisk = Find-VMDK -Directory $archiveOutputDirectory # Rediscover the renamed disks 158 | $vmDisk = $vmDisk | Sort-Object Name -Descending 159 | } 160 | catch { 161 | Get-Item $archiveOutputDirectory | Remove-Item -Recurse -Force # Clean up any artifacts after error. 162 | throw $_ 163 | } 164 | } 165 | else { 166 | # User provided path to the VMDK file explicitly 167 | $vmDisk = Get-ChildItem $getFullPath 168 | } 169 | 170 | try { 171 | Write-Host "Attempting to create the VM with the following command: qm create $VMID $parameterString." -ForegroundColor Green 172 | Start-Process qm -ArgumentList "create $VMID $parameterString" -Wait -RedirectStandardOutput /dev/null 173 | 174 | Write-Host "Attempting to convert the VMDK file(s) to QCOW2 to support snapshots." -ForegroundColor Green 175 | Write-Warning "If the disk files are encrypted or there are formatting issues, this will fail. Debug accordingly." 176 | $qcow2Disks = @() 177 | $vmDisk | ForEach-Object { 178 | $vmdkfile = $_ 179 | $qcow2file = $vmdkfile.FullName -replace 'vmdk', 'qcow2' 180 | $qcow2Disks += $qcow2file 181 | Write-Host "Attempting command: " -NoNewLine -ForegroundColor Magenta 182 | Write-Host "qemu-img convert -f vmdk -O qcow2 $($vmdkfile.FullName) $qcow2file" -ForegroundColor Green 183 | Start-Process qemu-img -ArgumentList "convert -f vmdk -O qcow2 $($vmdkfile.FullName) $qcow2file" -Wait 184 | } 185 | 186 | Write-Host "Attempting to import the QCOW2 file(s) as a disk." -ForegroundColor Green 187 | Write-Warning "If the disk files are encrypted or there are formatting issues, this will fail. Debug accordingly." 188 | $qcow2Disks | ForEach-Object { 189 | $disk = $_ 190 | Write-Host "Running command: " -NoNewLine -ForegroundColor Magenta 191 | Write-Host "qm importdisk $VMID $disk $VMDiskStorageVolume --format qcow2" -ForegroundColor Green 192 | Start-Process qm -ArgumentList "importdisk $VMID $disk $VMDiskStorageVolume --format qcow2" -Wait -RedirectStandardOutput /dev/null 193 | } 194 | 195 | $iteration = 0 196 | $qcowDisks | ForEach-Object { 197 | Write-Host "Attempting to attach the disk to the VM's SATA controller." -ForegroundColor Green 198 | Write-Host "Running command: " -NoNewLine -ForegroundColor Magenta 199 | Write-Host "qm set $VMID --sata$iteration $($VMDiskStorageVolume):vm-$VMID-disk-$iteration" -ForegroundColor Green 200 | Start-Process qm -ArgumentList "set $VMID --sata$iteration $($VMDiskStorageVolume):vm-$VMID-disk-$iteration" -Wait -RedirectStandardOutput /dev/null 201 | $iteration++ 202 | } 203 | 204 | Write-Host "Setting sata0 as the boot device." -ForegroundColor Green 205 | Start-Process qm -ArgumentList "set $VMID --boot=`"order=sata0`"" -Wait -RedirectStandardOutput /dev/null 206 | 207 | Write-Host "All commands completed successfully" -ForegroundColor Green 208 | } 209 | catch { 210 | throw "Command failed with the following error:`n$_" 211 | } 212 | 213 | } 214 | end { 215 | 216 | if (Test-Path $archiveOutputDirectory -ErrorAction SilentlyContinue) { 217 | Write-Host "Removing any files created by the script." -ForegroundColor Green 218 | Remove-Item $archiveOutputDirectory -Recurse -Force -ErrorAction SilentlyContinue | Out-Null 219 | } 220 | 221 | } 222 | 223 | -------------------------------------------------------------------------------- /New-VulnhubVM.ps1: -------------------------------------------------------------------------------- 1 | [CmdletBinding()] 2 | Param ( 3 | [Parameter(Mandatory = $true)] 4 | [ValidateScript({$_.Host -eq 'download.vulnhub.com'})] 5 | [System.Uri] 6 | $VulnhubURI, 7 | 8 | [Parameter(Mandatory = $true)] 9 | [ValidateScript({ 10 | if (-not (Test-Path $_)) { 11 | throw "Specified path does not exist:`n$_" 12 | } 13 | else { 14 | return $true 15 | } 16 | })] 17 | [String] 18 | $DownloadDirectory, 19 | 20 | [Parameter(Mandatory = $true)] 21 | [ValidateScript({ 22 | $id = $_ 23 | $id -ge 0 24 | if (Invoke-Command -ScriptBlock {qm status $id 2>/dev/null}) { 25 | throw "VM with ID: $id already exists." 26 | } 27 | elseif (Invoke-Command -ScriptBlock {pct status $id 2>/dev/null}) { 28 | throw "Container with ID: $id already exists." 29 | } 30 | else { 31 | return $true 32 | } 33 | })] 34 | [Int] 35 | $VMID, 36 | 37 | [Parameter( 38 | Mandatory = $true, 39 | HelpMessage = 'other: unspecified OS; wxp: Windows XP; w2k: Windows 2000; w2k3: Windows 2003; w2k8: Windows 2008; wvista: Windows Vista; win7: Windows 7; win8: Windows 8; win10: Windows 10; l24: Linux Kernel 2.4; l26: Linux Kernel 2.6; solaris: Solaris/OpenSolaris/OpenIndiana kernel' 40 | )] 41 | [ValidateSet('other', 'wxp', 'w2k', 'w2k3', 'w2k8', 'wvista', 'win7','win8', 'win10', 'l24', 'l26', 'solaris')] 42 | [String] 43 | $GuestOSType, 44 | 45 | [Parameter( 46 | Mandatory = $true, 47 | HelpMessage = 'Example: local-lvm. This is where the guest boot disk will be stored on the Proxmox node.' 48 | )] 49 | [ValidateScript({ 50 | if (-not (pvesm list $_ 2>/dev/null)) { 51 | throw "Storage volume: $_ does not exist." 52 | } 53 | else { 54 | return $true 55 | } 56 | })] 57 | [String] 58 | $VMDiskStorageVolume, 59 | 60 | [Parameter(HelpMessage = 'Example: vmbr0')] 61 | [ValidateNotNullOrEmpty()] 62 | [ValidateScript({ 63 | if (-not (ip link show $_)) { 64 | throw "Network interface not found." 65 | } 66 | else { 67 | return $true 68 | } 69 | })] 70 | [String] 71 | $NetworkBridge, 72 | 73 | [Parameter(HelpMessage = 'Enter an integer between 0 and 4094')] 74 | [ValidateRange(0,4094)] 75 | [Int] 76 | $VlanTag, 77 | 78 | [Parameter(HelpMessage = 'Strictly for administrative purposes only.')] 79 | [ValidateNotNullOrEmpty()] 80 | [String] 81 | $VMName, 82 | 83 | [Parameter(HelpMessage = 'Example: 2048')] 84 | [Int] 85 | $MemoryMiB 86 | ) 87 | begin { 88 | 89 | if (-not (which unar)) { 90 | throw "This script requires the program, unar, which is a single archive decompression tool that works on a variety or archive types.`nPlease install that program and re-run the script." 91 | } 92 | 93 | function Find-VMDK ($Directory) { 94 | 95 | $files = Get-ChildItem $Directory -Recurse 96 | $vmdk = $files | Where-Object {$_.Extension -eq '.vmdk'} 97 | if (-not $vmdk) { 98 | $files | ForEach-Object { 99 | $file = $_ 100 | $type = file $file.FullName 101 | $isArchive = $type -like '*archive*' -or $type -like '*compressed*' 102 | if ($isArchive) { 103 | Write-Host "Nested archive found: " -NoNewLine 104 | Write-Host $file.FullName -ForegroundColor Green 105 | $archiveFileFound = $file 106 | } 107 | } 108 | if ($archiveFileFound) { 109 | $subdirectory = "$Directory/temp$(Get-Random)" 110 | unar $archiveFileFound.FullName -o $subdirectory 111 | Find-VMDK -Directory $subdirectory 112 | } 113 | else { 114 | throw "No .vmdk file found and finished recursively checking for archives without results." 115 | } 116 | } 117 | else { 118 | return $vmdk 119 | } 120 | 121 | } 122 | 123 | if (-not [System.IO.Path]::EndsInDirectorySeparator($DownloadDirectory)) { $DownloadDirectory = $DownloadDirectory + '/' } 124 | $fileName = $VulnhubURI.Segments[-1] # Define the download file name based on the URI provided. 125 | if ($fileName -like '*.iso') { throw "Creating VMs from ISO files not yet implemented." } 126 | $downloadPath = $DownloadDirectory + $fileName 127 | $archiveOutputDirectory = $DownloadDirectory + "temp$(Get-Random)" 128 | $parameterCollection = @() 129 | $parameterCollection += "--ostype $GuestOSType" 130 | $parameterCollection += "--storage $VMDiskStorageVolume" 131 | if ($PSBoundParameters['NetworkBridge']) { 132 | if ($PSBoundParameters['VlanTag']) { 133 | $parameterCollection += "--net0 model=virtio,bridge=$NetworkBridge,firewall=0,tag=$VlanTag" 134 | } 135 | else { 136 | $parameterCollection += "--net0 model=virtio,bridge=$NetworkBridge,firewall=0" 137 | } 138 | } 139 | if ($PSBoundParameters['VMName']) { $parameterCollection += "--name $VMName" } 140 | if ($PSBoundParameters['MemoryMiB']) { $parameterCollection += "--memory $MemoryMiB" } 141 | $parameterString = $parameterCollection -join ' ' 142 | 143 | } 144 | process { 145 | 146 | Write-Host "[+] Downloading VM from Vulnhub. Please be patient...`n" -ForegroundColor Green 147 | wget $VulnhubURI.ToString() -q --show-progress -O $downloadPath 148 | $downloadedVM = Get-ChildItem $downloadPath 149 | try { 150 | Write-Host "[+] Decompressing archive: $downloadedVM to $archiveOutputDirectory`n" -ForegroundColor Green 151 | Write-Host "[!] This may take a while depending on the size of the archive.`n" -ForegroundColor Yellow 152 | unar $downloadedVM.FullName -o $archiveOutputDirectory 153 | } 154 | catch { 155 | throw "Error expanding archive:`n$_" 156 | } 157 | 158 | try { 159 | Write-Host "[+] Replacing any whitespace from file paths for compatibility.`n" -ForegroundColor Green 160 | Get-ChildItem $archiveOutputDirectory -Recurse | ForEach-Object { # Arbitrarily try to remove any whitespace in file path, as this has been an issue before 161 | $removeWhiteSpace = $_.FullName -replace ' ', '_' 162 | if ($removeWhiteSpace -ne $_.FullName) { 163 | Move-Item $_.FullName $removeWhiteSpace 164 | } 165 | } 166 | Write-Host "[+] Searching for the .vmdk disk file(s) in $archiveOutputDirectory`n" -ForegroundColor Green 167 | $vmDisk = Find-VMDK -Directory $archiveOutputDirectory 168 | $vmDisk = Find-VMDK -Directory $archiveOutputDirectory # Rediscover the renamed disks 169 | 170 | Write-Host "[+] Attempting to convert the VMDK file(s) to QCOW2 to support snapshots.`n" -ForegroundColor Green 171 | $qcow2Disks = @() 172 | $vmDisk | ForEach-Object { 173 | $vmdkfile = $_ 174 | $qcow2file = $vmdkfile.FullName -replace 'vmdk', 'qcow2' 175 | $qcow2Disks += $qcow2file 176 | Start-Process qemu-img -ArgumentList "convert -f vmdk -O qcow2 $($vmdkfile.FullName) $qcow2file" -Wait 177 | } 178 | } 179 | catch { 180 | Get-Item $downloadPath, $archiveOutputDirectory | Remove-Item -Recurse -Force # Clean up any artifacts after error. 181 | throw $_ 182 | } 183 | 184 | try { 185 | Write-Host "[+] Attempting to create the VM with the following command: qm create $VMID $parameterString.`n" -ForegroundColor Green 186 | Start-Process qm -ArgumentList "create $VMID $parameterString" -Wait -RedirectStandardOutput /dev/null 187 | 188 | Write-Host "[+] Attempting to import the QCOW2 file(s) as a disk.`n" -ForegroundColor Green 189 | $qcow2Disks | ForEach-Object { 190 | $disk = $_ 191 | Write-Host "[+] Running command: qm importdisk $VMID $disk $VMDiskStorageVolume --format qcow2`n" -ForegroundColor Green 192 | Start-Process qm -ArgumentList "importdisk $VMID $disk $VMDiskStorageVolume --format qcow2" -Wait -RedirectStandardOutput /dev/null 193 | } 194 | 195 | $iteration = 0 196 | $qcow2Disks | ForEach-Object { 197 | Write-Host "[+] Attempting to attach the disk to the VM's SATA controller.`n" -ForegroundColor Green 198 | Write-Host "[+] Running command: qm set $VMID --sata$iteration $($VMDiskStorageVolume):vm-$VMID-disk-$iteration`n" -ForegroundColor Green 199 | Start-Process qm -ArgumentList "set $VMID --sata$iteration $($VMDiskStorageVolume):vm-$VMID-disk-$iteration" -Wait -RedirectStandardOutput /dev/null 200 | $iteration++ 201 | } 202 | 203 | Write-Host "[+] Setting sata0 as the boot device.`n" -ForegroundColor Green 204 | Start-Process qm -ArgumentList "set $VMID --boot=`"order=sata0`"" -Wait -RedirectStandardOutput /dev/null 205 | 206 | Write-Host "[+] All commands completed successfully`n" -ForegroundColor Green 207 | } 208 | catch { 209 | throw "Command failed with the following error:`n$_" 210 | } 211 | 212 | } 213 | end { 214 | 215 | if ((Test-Path $downloadPath -ErrorAction SilentlyContinue) -or (Test-Path $archiveOutputDirectory -ErrorAction SilentlyContinue)) { 216 | Write-Host "[+] Removing any files created by the script.`n" -ForegroundColor Green 217 | Remove-Item $downloadPath -Recurse -Force -ErrorAction SilentlyContinue | Out-Null 218 | Remove-Item $archiveOutputDirectory -Recurse -Force -ErrorAction SilentlyContinue | Out-Null 219 | } 220 | 221 | } 222 | 223 | --------------------------------------------------------------------------------