azure/Detonate-VM.psm1
|
# Taken and modified from https://github.com/stevenjudd/safelydetonate/tree/main # Requires -Modules Az.Accounts, Az.Compute, Az.Resources, Az.Network # try to import required modules and if not found, install them, but ask user first $requiredModules = @('Az.Accounts', 'Az.Compute', 'Az.Resources', 'Az.Network') $missingModules = @() foreach ($module in $requiredModules) { if (-not (Get-Module -ListAvailable -Name $module)) { $missingModules += $module } } if ($missingModules) { $installModules = Read-Host -Prompt "The following modules are missing: $($missingModules -join ', '). Do you want to install them? (Y/N)" if ($installModules -eq 'Y') { Install-Module -Name $missingModules -AllowClobber -Force } else { Write-Host "Required modules not found. Exiting." -ForegroundColor Red return } } Write-Host "`n=== Azure Detonate VM Module Loaded ===" -ForegroundColor Cyan Write-Host "`nExample: Create a new VM" -ForegroundColor Yellow Write-Host 'JRE-NewAzureDetonateVm -EmailRecipient "me@jranck.com"' Write-Host "`nExample: Remove a VM" -ForegroundColor Yellow Write-Host "JRE-RemoveAzureDetonateVm" Write-Host "" function JRE-NewAzureDetonateVm { param( [parameter(Mandatory)] [ValidatePattern('^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$')] [string]$EmailRecipient, [securestring]$VMLocalAdminSecurePassword, [string]$Subscription = "29e1c74a-acb0-4547-b7e2-e5077025d6ed", [string]$VMLocalAdminUser = 'vmAdmin', [string]$LocationName = "northcentralus", [ValidateLength(1, 10)] [string]$UserName, [ValidateLength(1, 5)] [string]$VmRootName = 'DetVM', [ValidateRange(10, 11)] [string]$OsVersion = 11, [string]$VmSize = 'Standard_D4as_v6', [hashtable]$ResourceGroupTag = @{ 'Supervisor' = 'Da Boss' 'Manager' = 'Big Boss' 'Support Group' = 'Digital Security' 'Application Name' = 'Digital Security Detonate OS' }, [switch]$WaitDebugger ) # Function to validate password complexity and clear memory # DOES NOT WORK ON LINUX, THUS IS COMMENTED OUT UNTIL IT GETS FIXED # function Test-PasswordComplexity { # param ( # [securestring]$SecurePassword # ) # # Convert the SecureString to plain-text # $passwordPointer = [Runtime.InteropServices.Marshal]::SecureStringToBSTR($SecurePassword) # $plainTextPassword = [Runtime.InteropServices.Marshal]::PtrToStringAuto($passwordPointer) # try { # # Validate the password complexity # $complexityRequirementsMet = $( # $plainTextPassword -match '\d' -and # Contains a digit # $plainTextPassword -match '[A-Z]' -and # Contains an uppercase letter # $plainTextPassword -match '[a-z]' -and # Contains a lowercase letter # $plainTextPassword -match '[!@#$%^&*(),.?":{}|<>[\]]' -and # Contains a special character # ($plainTextPassword.Length -ge 14) # Minimum length of 8 characters # ) # if ($complexityRequirementsMet) { # $true # } else { # $false # } # } finally { # # Clear the password from memory # [Runtime.InteropServices.Marshal]::ZeroFreeBSTR($passwordPointer) # } # } # end function Test-PasswordComplexity # if (-not (Test-PasswordComplexity -SecurePassword $VMLocalAdminSecurePassword)) { # throw 'The provided password does not meet the complexity requirements.' # } Connect-AzAccount -Subscription $Subscription -ErrorAction Stop if (-not $VMLocalAdminSecurePassword) { do { $firstPassword = Read-Host -Prompt "Enter VM Admin Password" -AsSecureString $confirmPassword = Read-Host -Prompt "Confirm VM Admin Password" -AsSecureString $firstPtr = [Runtime.InteropServices.Marshal]::SecureStringToBSTR($firstPassword) $confirmPtr = [Runtime.InteropServices.Marshal]::SecureStringToBSTR($confirmPassword) try { $firstPlain = [Runtime.InteropServices.Marshal]::PtrToStringAuto($firstPtr) $confirmPlain = [Runtime.InteropServices.Marshal]::PtrToStringAuto($confirmPtr) $passwordsMatch = $firstPlain -eq $confirmPlain } finally { [Runtime.InteropServices.Marshal]::ZeroFreeBSTR($firstPtr) [Runtime.InteropServices.Marshal]::ZeroFreeBSTR($confirmPtr) } if (-not $passwordsMatch) { Write-Warning "Passwords do not match. Please try again." } } while (-not $passwordsMatch) $VMLocalAdminSecurePassword = $firstPassword } # check VvSize value if ($VmSize -notin (Get-AzComputeResourceSku -Location $LocationName).Name) { throw "Unable to find VmSize $VmSize in Datacenter $LocationName" } # $ErrorActionPreference = 'Stop' if ($UserName) { $userNameTrim = $UserName } else { $userNameTrim = switch ($true) { $IsLinux { $env:USER } $IsMacOS { $env:USER } $IsWindows { $env:USERNAME } default { $env:USERNAME } } } if ($userNameTrim -match '[^a-z0-9]') { Write-Verbose "Remove non-alphanumeric characters from username: $userNameValue" $userNameTrim = $userNameTrim.ToLower() -replace '[^a-z0-9]', '' } if ($userNameTrim.Length -gt 10) { Write-Verbose "Trim username '$userNameAlphaNum' to 10 characters" $userNameTrim = $userNameTrim.Substring(0, 10) } $detName = $VmRootName + $userNameTrim Write-Verbose "Use '$detName' as the RG and VM name" $resourceGroupName = "$detName" $vmName = "$detName" $vmPublisherName = 'MicrosoftWindowsDesktop' $vmOffer = "Windows-$OsVersion" $getAzVMImageSkuParam = @{ Location = "$LocationName" PublisherName = 'MicrosoftWindowsDesktop' Offer = $vmOffer } if ($WaitDebugger) { Wait-Debugger } $vmSkus = Get-AzVMImageSku @getAzVMImageSkuParam | Where-Object 'Skus' -Match 'win.*-pro$' | Sort-Object 'Skus' | Select-Object -Last 1 -ExpandProperty 'Skus' $vmVersion = 'latest' $networkName = "$detName-vnet" $nicName = "$detName-nic" $subnetName = "$detName-subnet" $subnetAddressPrefix = '10.0.0.0/24' $vnetAddressPrefix = '10.0.0.0/24' $publicIpAddress = "$detName-publicip" $networkSecurityGroupName = "$detName-nsg" if (-not (Get-AzSubscription -ErrorAction SilentlyContinue)) { throw 'Unable to get Azure Subscription. Please connect using Add-AzAccount.' } Write-Verbose "Setting the AzContext to the '$Subscription' subscription" try { Set-AzContext -Subscription $Subscription -ErrorAction Stop } catch { throw "Unable to set the AzContext to $Subscription" } $getAzVmParam = @{ 'ResourceGroupName' = $resourceGroupName 'Name' = $vmName 'ErrorAction' = 'SilentlyContinue' } if (Get-AzVM @getAzVmParam) { Write-Warning 'VM already exists' return } $getAzReourceGroupParam = @{ 'Name' = $resourceGroupName 'ErrorAction' = 'SilentlyContinue' } if (Get-AzResourceGroup @GetAzReourceGroupParam) { Write-Warning 'ResourceGroup already exists' } else { $newAzResourceGroupParam = @{ Name = $resourceGroupName Location = $LocationName Tag = $ResourceGroupTag } New-AzResourceGroup @newAzResourceGroupParam } #region Create security rules # $securityRules = @() $priority = 100 # base params $newAzNetworkSecurityRuleConfigParam = @{ 'Access' = 'Allow' 'Protocol' = 'Tcp' 'Direction' = 'Inbound' 'SourceAddressPrefix' = 'Internet' 'SourcePortRange' = '*' 'DestinationAddressPrefix' = '*' } # Enable to allow RDP traffic $newAzNetworkSecurityRuleConfigParam.Name = 'rdp-rule' $newAzNetworkSecurityRuleConfigParam.Description = 'Allow RDP' $newAzNetworkSecurityRuleConfigParam.DestinationPortRange = 3389 $newAzNetworkSecurityRuleConfigParam.Priority = $priority $securityRules += New-AzNetworkSecurityRuleConfig @newAzNetworkSecurityRuleConfigParam $priority++ <# # Enable to allow http traffic $newAzNetworkSecurityRuleConfigParam.Name = 'http-rule' $newAzNetworkSecurityRuleConfigParam.Description = 'Allow HTTP' $newAzNetworkSecurityRuleConfigParam.Priority = $priority $newAzNetworkSecurityRuleConfigParam.DestinationPortRange = 80 $securityRules += New-AzNetworkSecurityRuleConfig @newAzNetworkSecurityRuleConfigParam $priority++ # Enable to allow https traffic $newAzNetworkSecurityRuleConfigParam.Name = 'https-rule' $newAzNetworkSecurityRuleConfigParam.Description = 'Allow HTTPS' $newAzNetworkSecurityRuleConfigParam.Priority = $priority $newAzNetworkSecurityRuleConfigParam.DestinationPortRange = 443 $securityRules += New-AzNetworkSecurityRuleConfig @newAzNetworkSecurityRuleConfigParam $priority++ #> #endregion Create security rules # Apply security rules $newAzNetworkSecurityGroupParam = @{ 'Name' = $networkSecurityGroupName 'ResourceGroupName' = $resourceGroupName 'Location' = $LocationName 'SecurityRules' = $securityRules } $nsg = New-AzNetworkSecurityGroup @newAzNetworkSecurityGroupParam $newAzVirtualNetworkSubnetConfigParam = @{ 'Name' = $subnetName 'AddressPrefix' = $subnetAddressPrefix } $singleSubnet = New-AzVirtualNetworkSubnetConfig @newAzVirtualNetworkSubnetConfigParam $newAzVirtualNetworkParam = @{ 'Name' = $networkName 'ResourceGroupName' = $resourceGroupName 'Location' = $LocationName 'AddressPrefix' = $vnetAddressPrefix 'Subnet' = $singleSubnet } $vnet = New-AzVirtualNetwork @newAzVirtualNetworkParam $newAzPublicIpAddressParam = @{ 'Name' = $publicIpAddress 'ResourceGroupName' = $resourceGroupName 'AllocationMethod' = 'Static' 'Location' = $LocationName } $publicIp = New-AzPublicIpAddress @newAzPublicIpAddressParam $newAzNetworkInterfaceParam = @{ 'Name' = $nicName 'ResourceGroupName' = $resourceGroupName 'Location' = $LocationName 'SubnetId' = $vnet.Subnets[0].Id 'PublicIpAddressId' = $publicIp.Id 'NetworkSecurityGroupId' = $nsg.Id } $nic = New-AzNetworkInterface @newAzNetworkInterfaceParam $credential = New-Object System.Management.Automation.PSCredential ( $VMLocalAdminUser, $VMLocalAdminSecurePassword ) $newAzVMConfigParam = @{ 'VMName' = $vmName 'VMSize' = $VMSize } $virtualMachine = New-AzVMConfig @newAzVMConfigParam $setAzVMOperatingSystemParam = @{ 'VM' = $virtualMachine 'Windows' = $true 'ComputerName' = $vmName 'Credential' = $credential 'ProvisionVMAgent' = $true 'EnableAutoUpdate' = $true } $virtualMachine = Set-AzVMOperatingSystem @setAzVMOperatingSystemParam $addAzVMNetworkInterfaceParam = @{ 'VM' = $virtualMachine 'Id' = $nic.Id } $virtualMachine = Add-AzVMNetworkInterface @addAzVMNetworkInterfaceParam $setAzVMSourceImageParam = @{ 'VM' = $virtualMachine 'PublisherName' = $vmPublisherName 'Offer' = $vmOffer 'Skus' = $vmSkus 'Version' = $vmVersion } $virtualMachine = Set-AzVMSourceImage @setAzVMSourceImageParam # Disable boot diagnostics $setAzVMBootDiagnostic = @{ 'VM' = $virtualMachine 'Disable' = $true } $virtualMachine = Set-AzVMBootDiagnostic @setAzVMBootDiagnostic Write-Host '===========================' -ForegroundColor Green Write-Host 'Creating VM. Please wait...' Write-Host '===========================' -ForegroundColor Green $newAzVMParam = @{ 'ResourceGroupName' = $resourceGroupName 'Location' = $LocationName 'VM' = $virtualMachine 'Verbose' = $true } if ($WaitDebugger) { Wait-Debugger } New-AzVM @newAzVMParam $newVm = Get-AzVM -ResourceGroupName $resourceGroupName -Name $vmName Write-Host '============================' -ForegroundColor Green Write-Host 'Creating autoshutdown object' Write-Host '============================' -ForegroundColor Green # create autoshutdown object $search = 'Microsoft\.Compute\/virtualMachines\/' $replace = 'microsoft.devtestlab/schedules/shutdown-computevm-' $shutDownResourceId = $newVm.Id -replace $search, $replace $twoHoursLater = (Get-Date).ToUniversalTime().AddHours(2) | Get-Date -Format 'HHmm' $shutDownResourceProperties = @{ 'Status' = 'Enabled' 'TaskType' = 'ComputeVmShutdownTask' 'DailyRecurrence' = @{'time' = "$twoHoursLater" } 'TimeZoneId' = 'UTC' 'NotificationSettings' = @{ 'Status' = 'Enabled' 'TimeInMinutes' = 30 'EmailRecipient' = "$EmailRecipient" 'NotificationLocale' = 'en' } 'TargetResourceId' = $newVm.Id } $newAzResourceParams = @{ 'ResourceId' = $shutDownResourceId 'Location' = $LocationName 'Properties' = $shutDownResourceProperties 'Force' = $true } New-AzResource @newAzResourceParams Write-Host '=================================' -ForegroundColor Green Write-Host 'Setting Edge First Run Experience' Write-Host '=================================' -ForegroundColor Green # # Setting Edge First Run Experience # $vmSetupScript = { # #New-Item -Path HKLM:\SOFTWARE\Microsoft\Edge -ItemType Directory # $newItemPropertyParams1 = @{ # 'Path' = 'HKLM:\SOFTWARE\Microsoft\Edge' # 'Name' = 'HideFirstRunExperience' # 'Value' = 1 # 'PropertyType' = 'DWORD' # } # New-ItemProperty @newItemPropertyParams1 # $newItemPropertyParams2 = @{ # 'Path' = 'HKLM:\SOFTWARE\Microsoft\Edge' # 'Name' = 'HomepageLocation' # 'Value' = 'about:blank' # 'PropertyType' = 'String' # } # New-ItemProperty @newItemPropertyParams2 # } # $invokeAzVMRunCommandParams = @{ # 'ResourceGroupName' = $resourceGroupName # 'Name' = $vmName # 'CommandId' = 'RunPowerShellScript' # 'ScriptString' = $vmSetupScript # } # Invoke-AzVMRunCommand @invokeAzVMRunCommandParams Invoke-AzVMRunCommand -ResourceGroupName $resourceGroupName -VMName $vmName -CommandId 'RunPowerShellScript' -ScriptString "if (-not (Test-Path 'HKLM:\Software\Policies\Microsoft\Edge')) {New-Item -Path 'HKLM:\Software\Policies\Microsoft\Edge' -Force}" Invoke-AzVMRunCommand -ResourceGroupName $resourceGroupName -VMName $vmName -CommandId 'RunPowerShellScript' -ScriptString "if (-not (Test-Path 'HKLM:\Software\Policies\Microsoft\Windows\OOBE')) {New-Item -Path 'HKLM:\Software\Policies\Microsoft\Windows\OOBE' -Force}" Invoke-AzVMRunCommand -ResourceGroupName $resourceGroupName -VMName $vmName -CommandId 'RunPowerShellScript' -ScriptString "New-ItemProperty -Path 'HKLM:\Software\Policies\Microsoft\Edge' -Name 'HideFirstRunExperience' -Value '1' -PropertyType DWord -Force" Invoke-AzVMRunCommand -ResourceGroupName $resourceGroupName -VMName $vmName -CommandId 'RunPowerShellScript' -ScriptString "New-ItemProperty -Path 'HKLM:\Software\Policies\Microsoft\Windows\OOBE' -Name 'DisablePrivacyExperience' -Value '1' -PropertyType DWord -Force" # connect via Remote Desktop $vmIpAddress = $((Get-AzPublicIpAddress -ResourceName $publicIpAddress).IpAddress) switch ($true) { $IsLinux { $doneMessage = "Run mstsc and connect as .\'$VMLocalAdminUser' to $vmIpAddress" } $IsMacOS { $doneMessage = "Run mstsc and connect as .\'$VMLocalAdminUser' to $vmIpAddress" } $IsWindows { $doneMessage = "Connect as .\'$VMLocalAdminUser' to $vmIpAddress" mstsc /v:$vmIpAddress /prompt } default { $doneMessage = "Connect as .\'$VMLocalAdminUser' to $vmIpAddress" mstsc /v:$vmIpAddress /prompt } } Write-Host $('↓' * $($doneMessage.length)) -ForegroundColor Green Write-Host $doneMessage Write-Host $('↑' * $($doneMessage.length)) -ForegroundColor Green Write-Host '' Write-Host 'Remember to run JRE-RemoveAzureDetonateVm when done to remove the VM' -ForegroundColor Magenta } # $NewAzureWin10VmParam = @{ # 'EmailRecipient' = (Read-Host -Prompt 'Enter email to notify about shutdown') # 'VMLocalAdminSecurePassword' = (Read-Host -Prompt 'Enter password for the Admin account' -AsSecureString) # 'Subscription' = 'NotFree' # 'VMLocalAdminUser' = 'vmAdmin' # 'LocationName' = 'eastus' # } # New-AzureWin10Vm @NewAzureWin10VmParam function JRE-RemoveAzureDetonateVm { param( [string]$UserName, [ValidateLength(1, 5)] [string]$VmRootName = 'DetVM' ) if ($UserName) { $userNameTrim = $UserName } else { $userNameTrim = switch ($true) { $IsLinux { $env:USER } $IsMacOS { $env:USER } $IsWindows { $env:USERNAME } default { $env:USERNAME } } } if ($userNameTrim -match '[^a-z0-9]') { Write-Verbose "Remove non-alphanumeric characters from username: $userNameValue" $userNameTrim = $userNameTrim.ToLower() -replace '[^a-z0-9]', '' } if ($userNameTrim.Length -gt 10) { Write-Verbose "Trim username '$userNameAlphaNum' to 10 characters" $userNameTrim = $userNameTrim.Substring(0, 10) } $nameRoot = $VmRootName + $userNameTrim try { if ( Get-AzResourceGroup -Name $nameRoot -ErrorAction Stop | Remove-AzResourceGroup -ErrorAction Stop -Verbose ) { Write-Host "Successfully removed VM: $nameRoot" -ForegroundColor Green } } catch { # Wait-Debugger # Get-AzVM | Select-Object -Property ResourceGroupName,Name # throw $_ Write-Error "Cannot find and/or remove ResourceGroup: $nameRoot" return } } #end function JRE-RemoveAzureDetonateVm |