AZSBTools.psm1
#region Variables $EventKeyWords = [System.Diagnostics.Eventing.Reader.StandardEventKeywords] | Get-Member -Static -MemberType Property | foreach { [PSCustomObject][Ordered]@{ Name = $_.Name Number = ([System.Diagnostics.Eventing.Reader.StandardEventKeywords]::$($_.Name)).Value__ } } <# $WellKnwonSids = [PSCustomObject][Ordered]@{ Sid = Name = Description = } #> #region AD $thisComputersystem = Get-WmiObject -Class Win32_ComputerSystem $IsDomainMember = $thisComputersystem.PartOfDomain $thisForest = try { [system.directoryservices.activedirectory.Forest]::GetCurrentForest() } catch { 'Not domain joined' } $thisDomainName = if ($IsDomainMember) { $thisComputersystem.Domain } else { $False } $thisDomainDCList = foreach ($Domain in $thisForest.Domains) { if ($Domain.Name -eq $thisDomainName) { $Domain.DomainControllers | foreach { $_.Name } } } $EncType = @( New-Object -TypeName PSObject -Property @{ Id = 1 ; Name = 'DES-CBC-CRC' } New-Object -TypeName PSObject -Property @{ Id = 3 ; Name = 'DES-CBC-MD5' } New-Object -TypeName PSObject -Property @{ Id = 17; Name = 'AES128-CTS-HMAC-SHA-1' } New-Object -TypeName PSObject -Property @{ Id = 18; Name = 'AES256-CTS-HMAC-SHA-1' } New-Object -TypeName PSObject -Property @{ Id = 23; Name = 'RC4-HMAC' } ) # https://datatracker.ietf.org/doc/html/rfc3961 #endregion $WinDrive = ($env:windir -split ':')[0] $thisOS = Get-CimInstance -Class Win32_OperatingSystem $thisWindowsIdentity = [Security.Principal.WindowsIdentity]::GetCurrent() $thisWindowsPrincipal = New-Object Security.Principal.WindowsPrincipal($thisWindowsIdentity) $IsElevated = $thisWindowsPrincipal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator) $AESKey1 = '4e 99 06 e8 fc b6 6c c9 fa f4 93 10 62 0f fe e8 f4 96 e8 06 cc 05 79 90 20 9b 09 a4 33 b6 6c 1b' # https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-gppref/2c15cbf0-f086-4c74-8b70-1f2fa45dd4be?redirectedfrom=MSDN #region Shodan # https://developer.shodan.io/api $ShodanAPIBaseURL = 'https://api.shodan.io' $ShodanAPIMethodList = @( 'api-info' 'account/profile' 'tools/httpheaders' 'dns/reverse' 'dns/resolve' 'dns/domain' 'org' 'shodan/query' 'shodan/query/search' 'shodan/query/tags' 'shodan/ports' 'shodan/protocols' 'shodan/scans' 'shodan/host' ) $ShodanPortList = @( # 27 August 2021 7, 11, 13, 15, 17, 19, 20, 21, 22, 23, 24, 25, 26, 37, 38, 43, 49, 51, 53, 69, 70, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 95, 96, 97, 98, 99, 100, 102, 104, 106, 110, 111, 113, 119, 121, 123, 129, 131, 135, 137, 139, 143, 154, 161, 175, 179, 180, 195, 199, 211, 221, 222, 225, 263, 264, 311, 340, 389, 443, 444, 445, 447, 448, 449, 450, 465, 491, 500, 502, 503, 515, 520, 522, 523, 541, 548, 554, 555, 587, 593, 623, 626, 631, 636, 646, 666, 675, 685, 771, 772, 777, 789, 800, 801, 805, 806, 808, 830, 843, 873, 880, 888, 902, 943, 990, 992, 993, 994, 995, 999, 1000, 1010, 1012, 1022, 1023, 1024, 1025, 1026, 1027, 1028, 1029, 1050, 1063, 1080, 1099, 1110, 1111, 1119, 1167, 1177, 1194, 1200, 1234, 1250, 1290, 1311, 1344, 1355, 1366, 1388, 1400, 1433, 1434, 1471, 1494, 1500, 1515, 1521, 1554, 1588, 1599, 1604, 1650, 1660, 1723, 1741, 1777, 1800, 1820, 1830, 1833, 1883, 1900, 1901, 1911, 1935, 1947, 1950, 1951, 1962, 1981, 1990, 1991, 2000, 2001, 2002, 2003, 2006, 2008, 2010, 2012, 2018, 2020, 2021, 2022, 2030, 2048, 2049, 2050, 2051, 2052, 2053, 2054, 2055, 2056, 2057, 2058, 2059, 2060, 2061, 2062, 2063, 2064, 2065, 2066, 2067, 2068, 2069, 2070, 2077, 2079, 2080, 2081, 2082, 2083, 2086, 2087, 2095, 2096, 2100, 2111, 2121, 2122, 2123, 2126, 2150, 2152, 2181, 2200, 2201, 2202, 2211, 2220, 2221, 2222, 2223, 2225, 2232, 2233, 2250, 2259, 2266, 2320, 2323, 2332, 2345, 2351, 2352, 2375, 2376, 2379, 2382, 2404, 2443, 2455, 2480, 2506, 2525, 2548, 2549, 2550, 2551, 2552, 2553, 2554, 2555, 2556, 2557, 2558, 2559, 2560, 2561, 2562, 2563, 2566, 2567, 2568, 2569, 2570, 2572, 2598, 2601, 2602, 2626, 2628, 2650, 2701, 2709, 2761, 2762, 2806, 2985, 3000, 3001, 3002, 3005, 3048, 3049, 3050, 3051, 3052, 3053, 3054, 3055, 3056, 3057, 3058, 3059, 3060, 3061, 3062, 3063, 3066, 3067, 3068, 3069, 3070, 3071, 3072, 3073, 3074, 3075, 3076, 3077, 3078, 3079, 3080, 3081, 3082, 3083, 3084, 3085, 3086, 3087, 3088, 3089, 3090, 3091, 3092, 3093, 3094, 3095, 3096, 3097, 3098, 3099, 3100, 3101, 3102, 3103, 3104, 3105, 3106, 3107, 3108, 3109, 3110, 3111, 3112, 3113, 3114, 3115, 3116, 3117, 3118, 3119, 3120, 3121, 3128, 3129, 3200, 3211, 3221, 3260, 3270, 3283, 3299, 3306, 3307, 3310, 3311, 3333, 3337, 3352, 3386, 3388, 3389, 3391, 3400, 3401, 3402, 3403, 3404, 3405, 3406, 3407, 3408, 3409, 3410, 3412, 3443, 3460, 3479, 3498, 3503, 3521, 3522, 3523, 3524, 3541, 3542, 3548, 3549, 3550, 3551, 3552, 3554, 3555, 3556, 3557, 3558, 3559, 3560, 3561, 3562, 3563, 3566, 3567, 3568, 3569, 3570, 3671, 3689, 3690, 3702, 3749, 3780, 3784, 3790, 3791, 3792, 3793, 3794, 3838, 3910, 3922, 3950, 3951, 3952, 3953, 3954, 4000, 4001, 4002, 4010, 4022, 4040, 4042, 4043, 4063, 4064, 4070, 4100, 4117, 4118, 4157, 4190, 4200, 4242, 4243, 4282, 4321, 4369, 4430, 4433, 4443, 4444, 4445, 4482, 4500, 4505, 4506, 4523, 4524, 4545, 4550, 4567, 4643, 4646, 4664, 4700, 4730, 4734, 4747, 4782, 4786, 4800, 4808, 4840, 4848, 4911, 4949, 4999, 5000, 5001, 5002, 5003, 5004, 5005, 5006, 5007, 5008, 5009, 5010, 5025, 5050, 5060, 5070, 5080, 5090, 5094, 5122, 5150, 5172, 5190, 5201, 5209, 5222, 5269, 5280, 5321, 5353, 5357, 5400, 5431, 5432, 5443, 5446, 5454, 5494, 5500, 5542, 5552, 5555, 5560, 5567, 5568, 5569, 5577, 5590, 5591, 5592, 5593, 5594, 5595, 5596, 5597, 5598, 5599, 5600, 5601, 5602, 5603, 5604, 5605, 5606, 5607, 5608, 5609, 5632, 5672, 5673, 5683, 5684, 5800, 5801, 5822, 5853, 5858, 5900, 5901, 5906, 5907, 5908, 5909, 5910, 5938, 5984, 5985, 5986, 6000, 6001, 6002, 6003, 6004, 6005, 6006, 6007, 6008, 6009, 6010, 6036, 6080, 6102, 6161, 6262, 6264, 6308, 6352, 6363, 6379, 6443, 6464, 6503, 6510, 6511, 6512, 6543, 6550, 6560, 6561, 6565, 6580, 6581, 6588, 6590, 6600, 6601, 6602, 6603, 6605, 6622, 6650, 6662, 6664, 6666, 6667, 6668, 6697, 6748, 6789, 6881, 6887, 6955, 6969, 6998, 7000, 7001, 7002, 7003, 7004, 7005, 7010, 7014, 7070, 7071, 7080, 7081, 7090, 7170, 7171, 7218, 7401, 7415, 7433, 7443, 7444, 7445, 7465, 7474, 7493, 7500, 7510, 7535, 7537, 7547, 7548, 7634, 7654, 7657, 7676, 7700, 7776, 7777, 7778, 7779, 7788, 7887, 7979, 7998, 7999, 8000, 8001, 8002, 8003, 8004, 8005, 8006, 8007, 8008, 8009, 8010, 8011, 8012, 8013, 8014, 8015, 8016, 8017, 8018, 8019, 8020, 8021, 8022, 8023, 8024, 8025, 8026, 8027, 8028, 8029, 8030, 8031, 8032, 8033, 8034, 8035, 8036, 8037, 8038, 8039, 8040, 8041, 8042, 8043, 8044, 8045, 8046, 8047, 8048, 8049, 8050, 8051, 8052, 8053, 8054, 8055, 8056, 8057, 8058, 8060, 8064, 8066, 8069, 8071, 8072, 8080, 8081, 8082, 8083, 8084, 8085, 8086, 8087, 8088, 8089, 8090, 8091, 8092, 8093, 8094, 8095, 8096, 8097, 8098, 8099, 8100, 8101, 8102, 8103, 8104, 8105, 8106, 8107, 8108, 8109, 8110, 8111, 8112, 8118, 8123, 8126, 8139, 8140, 8143, 8159, 8180, 8181, 8182, 8184, 8190, 8200, 8222, 8236, 8237, 8238, 8239, 8241, 8243, 8248, 8249, 8251, 8252, 8282, 8291, 8333, 8334, 8383, 8401, 8402, 8403, 8404, 8405, 8406, 8407, 8408, 8409, 8410, 8411, 8412, 8413, 8414, 8415, 8416, 8417, 8418, 8419, 8420, 8421, 8422, 8423, 8424, 8425, 8426, 8427, 8428, 8429, 8430, 8431, 8432, 8433, 8442, 8443, 8444, 8445, 8446, 8447, 8448, 8500, 8513, 8545, 8553, 8554, 8585, 8586, 8590, 8602, 8621, 8622, 8623, 8637, 8649, 8663, 8666, 8686, 8688, 8700, 8733, 8765, 8766, 8767, 8779, 8782, 8784, 8787, 8788, 8789, 8790, 8791, 8800, 8801, 8802, 8803, 8804, 8805, 8806, 8807, 8808, 8809, 8810, 8811, 8812, 8813, 8814, 8815, 8816, 8817, 8818, 8819, 8820, 8821, 8822, 8823, 8824, 8825, 8826, 8827, 8828, 8829, 8830, 8831, 8832, 8833, 8834, 8835, 8836, 8837, 8838, 8839, 8840, 8841, 8842, 8843, 8844, 8845, 8846, 8847, 8848, 8849, 8850, 8851, 8852, 8853, 8854, 8855, 8856, 8857, 8858, 8859, 8860, 8861, 8862, 8863, 8864, 8865, 8866, 8867, 8868, 8869, 8870, 8871, 8872, 8873, 8874, 8875, 8876, 8877, 8878, 8879, 8880, 8881, 8885, 8887, 8888, 8889, 8890, 8891, 8899, 8935, 8969, 8988, 8989, 8990, 8991, 8993, 8999, 9000, 9001, 9002, 9003, 9004, 9005, 9006, 9007, 9008, 9009, 9010, 9011, 9012, 9013, 9014, 9015, 9016, 9017, 9018, 9019, 9020, 9021, 9022, 9023, 9024, 9025, 9026, 9027, 9028, 9029, 9030, 9031, 9032, 9033, 9034, 9035, 9036, 9037, 9038, 9039, 9040, 9041, 9042, 9043, 9044, 9045, 9046, 9047, 9048, 9049, 9050, 9051, 9070, 9080, 9082, 9084, 9088, 9089, 9090, 9091, 9092, 9093, 9094, 9095, 9096, 9097, 9098, 9099, 9100, 9101, 9102, 9103, 9104, 9105, 9106, 9107, 9108, 9109, 9110, 9111, 9119, 9136, 9151, 9160, 9189, 9191, 9199, 9200, 9201, 9202, 9203, 9204, 9205, 9206, 9207, 9208, 9209, 9210, 9211, 9212, 9213, 9214, 9215, 9216, 9217, 9218, 9219, 9220, 9221, 9222, 9251, 9295, 9299, 9300, 9301, 9302, 9303, 9304, 9305, 9306, 9307, 9308, 9309, 9310, 9311, 9389, 9418, 9433, 9443, 9444, 9445, 9500, 9527, 9530, 9550, 9595, 9600, 9606, 9633, 9663, 9682, 9690, 9704, 9743, 9761, 9765, 9861, 9869, 9876, 9898, 9899, 9943, 9944, 9950, 9955, 9966, 9981, 9988, 9990, 9991, 9992, 9993, 9994, 9997, 9998, 9999, 10000, 10001, 10134, 10243, 10250, 10443, 10554, 11112, 11211, 11300, 12000, 12345, 13579, 14147, 14265, 14344, 16010, 16464, 16992, 16993, 17000, 18081, 18245, 20000, 20087, 20256, 20547, 21025, 21379, 22222, 23023, 23424, 25105, 25565, 27015, 27016, 27017, 27036, 28015, 28017, 30718, 32400, 32764, 33060, 33338, 37215, 37777, 41794, 44818, 47808, 48899, 49152, 49153, 50000, 50050, 50070, 50100, 51106, 51235, 52869, 53413, 54138, 54984, 55442, 55443, 55553, 55554, 60001, 60129, 62078, 64738 ) #endregion #endregion #region Aliases @( @{ Name = 'Log' ; Value = 'Write-Log' } @{ Name = 'Get-FileShares' ; Value = 'Get-FileShareInfo' } @{ Name = 'New-SBAZServicePrincipal' ; Value = 'New-AzureServicePrincipal' } ) | foreach { Remove-Item -Path "Alias:$($_.Name)" -EA 0 try { New-Alias -Name $_.Name -Value $_.Value -EA 1 } catch { Write-Log $_.Exception.Message Yellow } } #endregion #region Azure Functions #region Azure Storage function Login-AZSubscription { [CmdletBinding(ConfirmImpact='Low')] Param( [Parameter(Mandatory=$true)][String]$LoginName, [Parameter(Mandatory=$true)][String]$SubscriptionName, [Parameter(Mandatory=$true)][String]$LogFile ) Begin { } Process { $LoggedIn = $false if ($Login = Get-AzContext) { if ($Login.Account.Id -eq $LoginName -and $Login.Name -match $SubscriptionName) { # Write-Log 'Already connected to Azure subscription',$SubscriptionName,'as',$LoginName Green,Cyan,Green,Cyan $LogFile } elseif (-not $LoggedIn) { Connect-AzAccount -Credential (Get-SBCredential $LoginName) | Out-Null # -Environment AzureCloud Write-Log 'Connected to Azure subscription',$SubscriptionName,'as',$LoginName Green,Cyan,Green,Cyan $LogFile try { Get-AzSubscription -SubscriptionName $SubscriptionName -WA 0 -EA 1 | Set-AzContext | Out-Null Write-Log ' Set Azure subscription context to',$SubscriptionName Green,Cyan $LogFile } catch { Write-Log $PSItem.Exception.Message Magenta $LogFile break } } } } End { Get-AzContext } } function Retry-OnRequest { # Requires -Modules Azure, Azure.Storage # Requires -Version 5 <# .SYNOPSIS Function to retry storage requests when encountering temporary/transient errors .DESCRIPTION Function to retry storage requests when encountering temporary/transient errors, like network errors, or storage server busy errors .PARAMETER Action This is a script block to get the block list of a given BLOB This is invoked by this function Example: $action = { param ($requestOption) return $Blob.ICloudBlob.DownloadBlockList([Microsoft.WindowsAzure.Storage.Blob.BlockListingFilter]::All, $null, $requestOption) } where $Blob is a Microsoft.WindowsAzure.Commands.Common.Storage.ResourceModel.AzureStorageBlob object that can be obtained from the Get-AzureStorageBlob cmdlet for example .PARAMETER TimeOutInMinutes This is the time span in minutes on which the Microsoft.WindowsAzure.Storage.RetryPolicies.ExponentialRetry object is configured This is an optional parameter. Default is (New-TimeSpan -Minutes 15) .PARAMETER maxRetryCountOnException This is the maximum number of times the function will retry the call. This is an optional parameter. Default is 3 times .EXAMPLE $action = { param ($requestOption) return $Blob.ICloudBlob.DownloadBlockList([Microsoft.WindowsAzure.Storage.Blob.BlockListingFilter]::All, $null, $requestOption) } $blocks = Retry-OnRequest $action where $Blob is a Microsoft.WindowsAzure.Commands.Common.Storage.ResourceModel.AzureStorageBlob object that can be obtained from the Get-AzureStorageBlob cmdlet for example .LINK https://superwidgets.wordpress.com/category/powershell/ .NOTES Function by Sam Boutros, based on script by Emma Zhu - Microsoft/ShangHai - emmazhu@microsoft.com v0.1 - 5 December 2018 #> param( [Parameter(Mandatory=$true)]$Action, [Parameter(Mandatory=$false)][System.TimeSpan]$TimeOutInMinutes = (New-TimeSpan -Minutes 15), [Parameter(Mandatory=$false)][Int16]$maxRetryCountOnException = 3 ) Begin { } Process { $requestOption = @{ RetryPolicy = (New-Object -TypeName Microsoft.WindowsAzure.Storage.RetryPolicies.ExponentialRetry -ArgumentList @($TimeOutInMinutes, 10)) } $shouldRetryOnException = $false $retryCount = 0 do { try { return $Action.Invoke($requestOption) } catch { if ($_.Exception.InnerException -ne $null -And $_.Exception.InnerException.GetType() -Eq [System.TimeoutException] -And $maxRetryCountOnException -gt 0) { $shouldRetryOnException = $true $maxRetryCountOnException -- $retryCount ++ Write-Log 'retrying request.. #',$retryCount Yellow,Cyan } else { $shouldRetryOnException = $false throw } } } while ($shouldRetryOnException) } End { } } function Get-BlobBytes { # Requires -Modules Azure, Azure.Storage # Requires -Version 5 <# .SYNOPSIS Function to calculate the amount of storage used by a BLOB .DESCRIPTION Function to calculate the amount of storage used by a BLOB .PARAMETER Blob This is a Microsoft.WindowsAzure.Commands.Common.Storage.ResourceModel.AzureStorageBlob object that can be obtained from the Get-AzureStorageBlob cmdlet for example .PARAMETER IsPremiumAccount An optional Boolean (True/False) parameter that defaults to False .EXAMPLE $LoginName = 'samb@mydomain.com' $SubscriptionName = 'my azure subscription name' $StorageAccountName = 'mystorageacct' # Import-Module Azure, Azure.Storage, AZSBTools -DisableNameChecking Login-AzureRmAccount -Credential (Get-SBCredential $LoginName) | Out-Null # -Environment AzureCloud $Subsciption = Get-AzureRmSubscription -SubscriptionName $SubscriptionName -WA 0 $Subsciption | Set-AzureRmContext | Out-Null Write-Log 'Connected to',$Subsciption.Name,'as',$LoginName Green,Cyan,Green,Cyan $StorageAccount = Get-AzureRmStorageAccount | where StorageAccountName -eq $StorageAccountName $IsPremiumAccount = ($StorageAccount.Sku.Tier -eq "Premium") Write-Log 'Processing storage account',$StorageAccount.StorageAccountName,'in RG',$StorageAccount.ResourceGroupName Green,Cyan,Green,Cyan $ContainerList = Get-AzureStorageContainer -Context $StorageAccount.Context $Container = $ContainerList | select -First 1 Write-Log ' Processing container',$Container.Name Green,Cyan $BlobList = Get-AzureStorageBlob -Context $StorageAccount.Context -Container $Container.Name $Blob = $BlobList | select -First 1 Write-Log ' Processing blob',$Blob.Name Green,Cyan -NoNewLine $SizeInBytes = Get-BlobBytes $Blob $IsPremiumAccount $myOutput = [PSCustomObject][Ordered]@{ Name = $Blob.Name StorageAccount = $storageAccount.StorageAccountName Container = $Container.Name Type = $Blob.BlobType SizeInBytes = $SizeInBytes LastModified = $Blob.LastModified } Write-log $SizeInBytes,'bytes' Yellow,Cyan $myOutput | select Name,Type,StorageAccount,Container, @{n='SizeInGB';e={[Math]::Round($_.SizeInBytes/1GB,1)}},LastModified | sort SizeInGB -Descending | FL This example calculates the size of the first Blob in the first container of the provided storage account .EXAMPLE $LoginName = 'samb@mydomain.com' $SubscriptionName = 'my azure subscription name' $StorageAccountName = 'mystorageacct' # Import-Module Azure, Azure.Storage, AZSBTools -DisableNameChecking Login-AzureRmAccount -Credential (Get-SBCredential $LoginName) | Out-Null # -Environment AzureCloud $Subsciption = Get-AzureRmSubscription -SubscriptionName $SubscriptionName -WA 0 $Subsciption | Set-AzureRmContext | Out-Null Write-Log 'Connected to',$Subsciption.Name,'as',$LoginName Green,Cyan,Green,Cyan $StorageAccount = Get-AzureRmStorageAccount | where StorageAccountName -eq $StorageAccountName $IsPremiumAccount = ($StorageAccount.Sku.Tier -eq "Premium") Write-Log 'Processing storage account',$StorageAccount.StorageAccountName,'in RG',$StorageAccount.ResourceGroupName Green,Cyan,Green,Cyan $BlobList = foreach ($Container in (Get-AzureStorageContainer -Context $StorageAccount.Context)) { Write-Log ' Processing container',$Container.Name Green,Cyan $Token = $Null do { $Blobs = Get-AzureStorageBlob -Context $StorageAccount.Context -Container $Container.Name -ContinuationToken $Token if ($Blobs -eq $Null) { break } if ($Blobs.GetType().Name -eq 'AzureStorageBlob') { $Token = $Null } else { $Token = $Blobs[-1].ContinuationToken } $Blobs | ForEach { Write-Log ' Processing blob',$_.Name Green,Cyan -NoNewLine $SizeInBytes = Get-BlobBytes $_ $IsPremiumAccount [PSCustomObject][Ordered]@{ Name = $_.Name StorageAccount = $storageAccount.StorageAccountName Container = $Container.Name Type = $_.BlobType SizeInBytes = $SizeInBytes LastModified = $_.LastModified } Write-log $SizeInBytes,'bytes' Yellow,Cyan } } While ($Token -ne $Null) } $BlobList | select Name,Type,StorageAccount,Container, @{n='SizeInGB';e={[Math]::Round($_.SizeInBytes/1GB,1)}},LastModified | sort SizeInGB -Descending | FT -a This example calculates blob sizes for all blobs in all containers of the provided storage account .LINK https://superwidgets.wordpress.com/category/powershell/ .NOTES Function by Sam Boutros, based on script by Emma Zhu - Microsoft/ShangHai - emmazhu@microsoft.com v0.1 - 5 December 2018 #> param( [Parameter(Mandatory=$true)]$Blob, [Parameter(Mandatory=$false)][bool]$IsPremiumAccount = $false ) Begin { if (-not ([System.Management.Automation.PSTypeName]'PageRange').Type) { Add-Type -TypeDefinition " public class PageRange { public long StartOffset; public long EndOffset; } " } } Process { # Base + blobname $blobSizeInBytes = 124 + $Blob.Name.Length * 2 # Size of metadata $metadataEnumerator = $Blob.ICloudBlob.Metadata.GetEnumerator() while($metadataEnumerator.MoveNext()) { $blobSizeInBytes += 3 + $metadataEnumerator.Current.Key.Length + $metadataEnumerator.Current.Value.Length } if (-not $IsPremiumAccount) { if ($Blob.BlobType -eq [Microsoft.WindowsAzure.Storage.Blob.BlobType]::BlockBlob) { $blobSizeInBytes += 8 $action = { # Default is Microsoft.WindowsAzure.Storage.Blob.BlockListingFilter.Committed. Need All param ($requestOption) return $Blob.ICloudBlob.DownloadBlockList([Microsoft.WindowsAzure.Storage.Blob.BlockListingFilter]::All, $null, $requestOption) } $blocks = Retry-OnRequest $action if ($blocks -eq $null) { $blobSizeInBytes += $Blob.ICloudBlob.Properties.Length } else { $blocks | ForEach { $blobSizeInBytes += $_.Length + $_.Name.Length } } } elseif ($Blob.BlobType -eq [Microsoft.WindowsAzure.Storage.Blob.BlobType]::PageBlob) { # It could cause server time out issue when trying to get page ranges of highly fragmented page blob # Get page ranges in segment can mitigate chance of meeting such kind of server time out issue # See https://blogs.msdn.microsoft.com/windowsazurestorage/2012/03/26/getting-the-page-ranges-of-a-large-page-blob-in-segments/ for details. $pageRangesSegSize = 148 * 1024 * 1024L $totalSize = $Blob.ICloudBlob.Properties.Length $pageRangeSegOffset = 0 $pageRangesTemp = New-Object System.Collections.ArrayList while ($pageRangeSegOffset -lt $totalSize) { $action = { param($requestOption) return $Blob.ICloudBlob.GetPageRanges($pageRangeSegOffset, $pageRangesSegSize, $null, $requestOption) } Retry-OnRequest $action | ForEach { $pageRangesTemp.Add($_) } | Out-Null $pageRangeSegOffset += $pageRangesSegSize } $pageRanges = New-Object System.Collections.ArrayList foreach ($pageRange in $pageRangesTemp) { if($lastRange -eq $Null) { $lastRange = New-Object PageRange $lastRange.StartOffset = $pageRange.StartOffset $lastRange.EndOffset = $pageRange.EndOffset } else { if (($lastRange.EndOffset + 1) -eq $pageRange.StartOffset) { $lastRange.EndOffset = $pageRange.EndOffset } else { $pageRanges.Add($lastRange) | Out-Null $lastRange = New-Object PageRange $lastRange.StartOffset = $pageRange.StartOffset $lastRange.EndOffset = $pageRange.EndOffset } } } $pageRanges.Add($lastRange) | Out-Null $pageRanges | ForEach { $blobSizeInBytes += 12 + $_.EndOffset - $_.StartOffset } } else { $blobSizeInBytes += $Blob.ICloudBlob.Properties.Length } } else { $blobSizeInBytes += $Blob.ICloudBlob.Properties.Length } } End { $blobSizeInBytes } } function Get-ContainerBytes { # Requires -Modules Azure, Azure.Storage # Requires -Version 5 <# .SYNOPSIS Function to calculate container overhead storage size .DESCRIPTION Function to calculate container overhead storage size .PARAMETER Container This is an object of type Microsoft.WindowsAzure.Storage.Blob.CloudBlobContainer that can be obtained from the CloudBlobContainer property of the output object of the Get-AzureStorageContainer cmdlet - see example below .EXAMPLE $LoginName = 'samb@mydomain.com' $SubscriptionName = 'my subscription name' $StorageAccountName = 'mystorageacct' # Import-Module Azure, Azure.Storage, AZSBTools -DisableNameChecking Login-AzureRmAccount -Credential (Get-SBCredential $LoginName) | Out-Null # -Environment AzureCloud $Subsciption = Get-AzureRmSubscription -SubscriptionName $SubscriptionName -WA 0 $Subsciption | Set-AzureRmContext | Out-Null Write-Log 'Connected to',$Subsciption.Name,'as',$LoginName Green,Cyan,Green,Cyan $StorageAccount = Get-AzureRmStorageAccount | where StorageAccountName -eq $StorageAccountName $IsPremiumAccount = ($StorageAccount.Sku.Tier -eq "Premium") Write-Log 'Processing storage account',$StorageAccount.StorageAccountName,'in RG',$StorageAccount.ResourceGroupName Green,Cyan,Green,Cyan Get-AzureStorageContainer -Context $StorageAccount.Context | foreach { Write-Log ' Calculating overhead bytes for container',$_.Name Green,Cyan -NoNewLine $ContainerOverheadBytes = Get-ContainerBytes -Container $_.CloudBlobContainer Write-Log $ContainerOverheadBytes,'bytes' Yellow,Cyan } This example calculate overhead bytes for all containers in the provided storage account .LINK https://superwidgets.wordpress.com/category/powershell/ .NOTES Function by Sam Boutros, based on script by Emma Zhu - Microsoft/ShangHai - emmazhu@microsoft.com v0.1 - 5 December 2018 #> param( [Parameter(Mandatory=$true)][Microsoft.WindowsAzure.Storage.Blob.CloudBlobContainer]$Container ) Begin { } Process { # Base + name of container $ContainerOverheadBytes = 48 + $Container.Name.Length * 2 # Get size of metadata $metadataEnumerator = $Container.Metadata.GetEnumerator() while($metadataEnumerator.MoveNext()) { $ContainerOverheadBytes += 3 + $metadataEnumerator.Current.Key.Length + $metadataEnumerator.Current.Value.Length } # Get size for SharedAccessPolicies $ContainerOverheadBytes += $Container.GetPermissions().SharedAccessPolicies.Count * 512 } End { $ContainerOverheadBytes } } function Get-AzureRMDiskSpace { <# .SYNOPSIS Function to obtain used disk space of one or more Azure VMs .DESCRIPTION Function to obtain used disk space of one or more Azure VMs This function calculates disk space of unmanaged disks only Microsoft charges for the entire allocated space of a managed disk regardless of how much is used, so finding the actual used size is irrelevent .PARAMETER AzureVM One or more of type Microsoft.Azure.Commands.Compute.Models.PSVirtualMachineList which can be obtained from the output of the AzureRM cmdlet Get-AzureRmVM .PARAMETER RetryCount This is an optional number between 0 and 99 The cmdlet will retry the disks that fail to get used disk space amount that many times .EXAMPLE Login-AzureRmAccount -Credential (Get-SBCredential 'nam@domain.com') | Out-Null # -Environment AzureCloud Get-AzureRmSubscription -SubscriptionName 'my subscription anme' -WA 0 | Set-AzureRmContext | Out-Null $VMList = (Get-AzureRmVM -WA 0)[0..2] $DiskSpaceUsage = Get-AzureRMDiskSpace -AzureVM $VMList -RetryCount 1 -Verbose $DiskSpaceUsage | FT -a . OUTPUTS PSCustom object (one for each disk) containing the following properties/example: VMName DiskName StorageAccount BlobName TotalSizeGB UsedSizeGB Source DateReported RetryCount ------ -------- -------------- -------- ----------- ---------- ------ ------------ ---------- MigrationAdmin1 MigrationAdmin1 devgdisks756 MigrationAdmin104435.vhd 127 ? AzureStorage 8/8/2018 11:04 AM 5 DEBCSV01 DEBCSV01 debcssa DEBCSV0120180802110039.vhd 32 3.96 AzureStorage 8/8/2018 10:49 AM 0 DECEX16VO1 DECEX16VO1 decsa DECEX16VO120180403203752.vhd 127 30.33 AzureStorage 8/8/2018 10:50 AM 0 DECEX16VO1 DECEX16VO1-DD1 decsa DECEX16VO1-DD1.vhd 40 ? AzureStorage 8/8/2018 11:06 AM 5 .LINK https://superwidgets.wordpress.com/ .NOTES Function by Sam Boutros v0.1 - 20 July 2018 - Known issue: not able to get used space of some disks, getting: $Blob.ICloudBlob.GetPageRanges(): Exception calling "GetPageRanges" with "0" argument(s): "Unable to read data from the transport connection: An existing connection was forcibly closed by the remote host." #> [CmdletBinding(ConfirmImpact='Low')] Param( [Parameter(Mandatory=$true,ValueFromPipeLine=$true,ValueFromPipeLineByPropertyName=$true)] [Microsoft.Azure.Commands.Compute.Models.PSVirtualMachineList[]]$AzureVM, [Parameter(Mandatory=$false)][ValidateRange(0,99)][Int16]$RetryCount = 0 ) Begin { $myOutput = @() function Get-DiskBlobSize { [CmdletBinding(ConfirmImpact='Low')] Param( [Parameter(Mandatory=$true)][PSCustomObject]$Disk, [Parameter(Mandatory=$true)][Microsoft.Azure.Commands.Compute.Models.PSVirtualMachineList]$VM ) Write-Log 'Processing disk:' Green Write-Log ($Disk | Out-String).Trim() Cyan $StorageAccount = Get-AzureRmStorageAccount -ResourceGroupName $VM.ResourceGroupName -Name $Disk.StorageAccount $Blob = Get-AzureStorageBlob -Container vhds -Context $StorageAccount.Context -Blob $Disk.BlobName $blobSize = 124 + $Blob.Name.Length * 2 $blobSize += ($Blob.ICloudBlob.Metadata.Keys.Length | measure -Sum).Sum $blobSize += ($Blob.ICloudBlob.Metadata.Values.Length | measure -Sum).Sum $PageRanges = $Blob.ICloudBlob.GetPageRangesAsync() # $Blob.ICloudBlob.GetPageRanges() Write-Verbose ($PageRanges | Out-String) if ($PageRanges.Result) { $PageRanges.Result | foreach { $blobSize += 12 + $_.EndOffset - $_.StartOffset } [Math]::Round($blobSize/1GB,2) } else { '?' } } } Process { #region First run foreach ($VM in $AzureVM) { #region Get list of unmanaged VM disks $DiskList = @() if ($VM.StorageProfile.OsDisk.ManagedDisk) { Write-Log 'Disk',$VM.StorageProfile.OsDisk.Name,'is a managed disk, skipping..' Yellow,Cyan,Yellow } else { $DiskList += @($VM.StorageProfile.OsDisk | select Name,DiskSizeGB, @{n='StorageAccount';e={$_.Vhd.Uri.Split('.')[0].Split('/')[2]}}, @{n='BlobName';e={$_.Vhd.Uri.Split('/')[-1]}}) } foreach ($VMDisk in $VM.StorageProfile.DataDisks) { if ($VMDisk.ManagedDisk) { Write-Log 'Disk',$VMDisk.Name,'is a managed disk, skipping..' Yellow,Cyan,Yellow } else { $DiskList += $VMDisk | select Name,DiskSizeGB, @{n='StorageAccount';e={$_.Vhd.Uri.Split('.')[0].Split('/')[2]}}, @{n='BlobName';e={$_.Vhd.Uri.Split('/')[-1]}} } } #endregion if ($DiskList) { Write-Log 'Calculating used disk space for',$DiskList.Count,'disk(s) of VM',$VM.Name Green,Cyan,Green,Cyan foreach ($Disk in $DiskList) { $myOutput += [PSCustomObject][Ordered]@{ VMName = $VM.Name DiskName = $Disk.Name StorageAccount = $Disk.StorageAccount BlobName = $Disk.BlobName TotalSizeGB = $Disk.DiskSizeGB UsedSizeGB = Get-DiskBlobSize -Disk $Disk -VM $VM Source = 'AzureStorage' DateReported = Get-Date -Format g RetryCount = 0 } } } } #endregion #region Retries if ($RetryCount -gt 0) { foreach ($Retry in 1..$RetryCount) { Write-Log 'Retry #',$Retry Cyan,Yellow foreach ($Disk in ($myOutput | where { $PSItem.UsedSizeGB -eq '?' })) { $Disk.UsedSizeGB = Get-DiskBlobSize -Disk $Disk -VM ($AzureVM | where { $Disk.VMName -eq $PSItem.Name }) $Disk.DateReported = Get-Date -Format g $Disk.RetryCount = $Retry } } } #endregion } End { $myOutput } } function Get-AzureStorageAccountList { # Requires -Modules Az # Requires -Version 5 <# .SYNOPSIS Function to get Azure storage accounts in a given subscription .DESCRIPTION Function to get Azure storage accounts in a given subscription .PARAMETER LoginName The username required to authenticate to Azure Example: samb@mydomain.com .PARAMETER SubscriptionName The Azure subscription name such as 'My Dev EA subscription' .EXAMPLE Get-AzureStorageAccountList -LoginName 'sam.boutros@mydomain.com' -SubscriptionName 'my subscription name' .OUTPUTS This function returns a PS object for each Stprage Account containing the following properties/example: Name : maybcstorage Type : ARM-GPv1 # This is either ASM, ARM-GPv1, ARM-GPv2, or ARM-BlobOnly GeoReplication : Standard_RAGRS # This is either Standard_LRS, Standard_GRS, Standard_RAGRS, Standard_ZRS Tier : Standard # This is either Standard (HDD) or Enhanced (SSD) ResourceGroup : myrs1 Location : uksouth .LINK https://superwidgets.wordpress.com/category/powershell/ https://superwidgets.wordpress.com/2018/07/02/azure-storage-features-and-pricing-june-2018/ .NOTES Function by Sam Boutros v0.1 - 24 October 2018 v0.2 - 24 May 2019 - Updated to use AZ module instead of AzureRM module #> [CmdletBinding(ConfirmImpact='Low')] Param( [Parameter(Mandatory=$true)][String]$LoginName, [Parameter(Mandatory=$true)][String]$SubscriptionName ) Begin { if (-not ($Login = Login-AzSubscription -LoginName $LoginName -SubscriptionName $SubscriptionName -LogFile $LogFile)) { break } } Process { Get-AzResource | where ResourceType -Match 'storage' | foreach { [PSCustomObject][Ordered]@{ Name = $_.Name Type = $( if ($_.ResourceType -eq 'Microsoft.ClassicStorage/storageAccounts') { 'ASM' } elseif ($_.ResourceType -eq 'Microsoft.Storage/storageAccounts') { if ($_.Kind -eq 'StorageV2') { 'ARM-GPv2' } elseif ($_.Kind -eq 'BlobStorage') { 'ARM-BlobOnly' } else { 'ARM-GPv1' } } else { '???' } ) GeoReplication = $_.sku.name Tier = $_.sku.tier ResourceGroup = $_.ResourceGroupName Location = $_.Location } } } End {} } Function Delete-AzureRMUnattachedManagedDisks { # Requires -Modules AzureRM,ImportExcel # Requires -Version 5 <# .SYNOPSIS Function to delete Azure unused/unattached managed disks .DESCRIPTION Function to delete Azure unused/unattached managed disks This applies to ARM disks only not classic ASM disks This function depends on AzureRM and ImportExcel PowerShell modules available in the PowerShell Gallery To install: Install-Module AzureRM,ImportExcel This function has been tested to work with PowerShell version 5 .PARAMETER LoginName The username required to authenticate to Azure Example: samb@mydomain.com .PARAMETER SubscriptionName The Azure subscription name such as 'My Dev EA subscription' .PARAMETER OutputFile This is an optional parameter that specifies the path to output Excel file This defaults to a file in the current folder where the script is running .PARAMETER LogFile This is an optional parameter that specifies the path to the log file where the script logs its progress This defaults to a file in the current folder where the script is running .EXAMPLE Delete-AzureRMUnattachedManagedDisks -LoginName 'samb@mydomain.com' -SubscriptionName 'my Azure subscription name here' .LINK https://superwidgets.wordpress.com/category/powershell/ .NOTES Function by Sam Boutros v0.1 - 20 December 2018 #> [CmdletBinding(ConfirmImpact='High')] Param( [Parameter(Mandatory=$true)][String]$LoginName, [Parameter(Mandatory=$true)][String]$SubscriptionName, [Parameter(Mandatory=$false)][String]$OutputFile = ".\Unattached Managed Disk List - $SubscriptionName - $(Get-Date -Format 'ddMMMMyyyy_hh-mm-ss_tt').xlsx", [Parameter(Mandatory=$false)][String]$LogFile = ".\Delete-AzureRMUnattachedManagedDisks - $SubscriptionName - $(Get-Date -Format 'ddMMMMyyyy_hh-mm-ss_tt').txt" ) Begin { Login-AzureRmAccount -Credential (Get-SBCredential $LoginName) | Out-Null # -Environment AzureCloud try { Get-AzureRmSubscription -SubscriptionName $SubscriptionName -WA 0 -EA 1 | Set-AzureRmContext | Out-Null Write-Log 'Connected to Azure subscription',$SubscriptionName,'as',$LoginName Green,Cyan,Green,Cyan $LogFile } catch { Write-Log $PSItem.Exception.Message Yellow $LogFile break } } Process{ $ManagedDisks = Get-AzureRmDisk if ($ManagedDisks) { Write-Log 'Identified',$ManagedDisks.Count,'managed disks' Green,Yellow,Green $LogFile $MDList = $ManagedDisks | foreach { [PSCustomObject][Ordered]@{ DiskName = $_.Name SizeGB = $_.DiskSizeGB ResourceGroup = $_.ResourceGroupName AttachedTo = $_.ManagedBy } } Write-Log ($MDList | FT -a | Out-String).Trim() Cyan $LogFile $UnattachedMDList = $MDList | where {-not $_.AttachedTo } if ($UnattachedMDList) { Write-Log ' of which',$UnattachedMDList.Count,'disks are not attached to or used by any VM' Green,Yellow,Green $LogFile Write-Log ($UnattachedMDList | FT -a | Out-String).Trim() Yellow $LogFile Write-Log 'Exporting list of unattached managed disks to file',$OutputFile Green,Cyan $LogFile $UnattachedMDList | Export-Excel -Path $OutputFile -ConditionalText $( ($UnattachedMDList | Get-Member -MemberType NoteProperty).Name | foreach { New-ConditionalText $_ White SteelBlue } ) -AutoSize -FreezeTopRowFirstColumn Write-Log 'Deleting',$UnattachedMDList.Count,'unattached managed disks' Green,Cyan,Green $LogFile -NoNewLine $Result = $UnattachedMDList | foreach { Remove-AzureRmDisk -ResourceGroupName $_.ResourceGroup -DiskName $_.DiskName -Force } Write-Log 'done, task details:' Cyan $LogFile Write-Log ($Result | FT -a | Out-String).Trim() Green $LogFile } else { Write-Log ' all of which are attached/used by VMs' Green $LogFile } } else { Write-Log 'No managed disks found' Green $LogFile } } End { } } Function Remove-AzureUnmanagedDiskSnapshot { # Requires -Modules Az # Requires -Version 5 <# .SYNOPSIS Function to delete Azure disk snapshot(s) for unmanaged disks .DESCRIPTION Function to delete disk snapshot(s) for a given unmanaged disk This applies to unmanaged ARM disk snapshots only not classic ASM disks or managed ARM disks This function depends on Az PowerShell module available in the PowerShell Gallery To install required module: Install-Module Az This function has been tested to work with PowerShell version 5 .PARAMETER LoginName The username required to authenticate to Azure Example: samb@mydomain.com .PARAMETER StorageAccountName The Azure storage account name such as 'storfluxwidget3vm' .PARAMETER ContainerName The Container name such as 'Vhds' .PARAMETER BlobName The disk name such as 'Widget3VM-20181226-093810.vhd' .PARAMETER FromDate Snapshots with datetime stamp after this point and before the ToDate will be deleted Example: 1/1/2018, or 12/11/2018 11:00 AM If either ToDate or FromDate is not provided, all snapshots of the provided page blob will be deleted .PARAMETER ToDate Snapshots with datetime stamp before this point and after the FromDate will be deleted Example: 1/10/2018, or 12/12/2018 12:00 AM If either ToDate or FromDate is not provided, all snapshots of the provided page blob will be deleted .PARAMETER LogFile This is an optional parameter that specifies the path to the log file where the script logs its progress This defaults to a file in the current folder where the script is running .EXAMPLE $ParameterList = @{ LoginName = 'sam@dmain.com' SubscriptionName = 'my subscription name' StorageAccountName = 'storfluxwidget3vm' ContainerName = 'vhds' BlobName = 'Widget3VM-20181226-093810.vhd' } Remove-AzureUnmanagedDiskSnapshot @ParameterList This example deletes all snapshots of the provided disk .EXAMPLE $ParameterList = @{ LoginName = 'sam@dmain.com' SubscriptionName = 'my subscription name' StorageAccountName = 'storfluxwidget3vm' ContainerName = 'vhds' FromDate = '1/1/2019' ToDate = Get-Date BlobName = 'Widget3VM-20181226-093810.vhd' } Remove-AzureUnmanagedDiskSnapshot @ParameterList This example deletes all snapshots of the provided disk from 1/1/2019 to now .EXAMPLE $LoginName = 'sam@dmain.com' $SubscriptionName = 'my subscription name' $DiskList = Get-AzureVMUnmanagedDisk -LoginName $LoginName -SubscriptionName $SubscriptionName -VMName (Get-AzVM).Name # By defining the $LogFile variable before the loop, we get to put all the logs in one file $LogFile = ".\Remove-AzureUnmanagedDiskSnapshot - $SubscriptionName - $(Get-Date -Format 'ddMMMMyyyy_hh-mm-ss_tt').txt" $SnapShotList = foreach ($Disk in $DiskList) { $ParameterList = @{ LoginName = $LoginName SubscriptionName = $SubscriptionName StorageAccountName = $Disk.StorageAccountName ContainerName = $Disk.ContainerName BlobName = $Disk.BlobName LogFile = $LogFile } Remove-AzureUnmanagedDiskSnapshot @ParameterList } This example lists all unmanaged disks of all ARM VMs in the given subscription, then deletes all their snapshots .LINK https://superwidgets.wordpress.com/category/powershell/ .NOTES Function by Sam Boutros v0.1 - 20 December 2018 v0.2 - 1 January 2019 - Rewrite based on Logan Zhao (zhezhao@microsoft.com) input regarding $storageContainer.CloudBlobContainer interface, and .CloudBlobContainer.ListBlobs() method v0.3 - 24 May 2019 - Updated to use AZ module instead of AzureRM module #> [CmdletBinding(ConfirmImpact='High')] Param( [Parameter(Mandatory=$true)][String]$LoginName, [Parameter(Mandatory=$true)][String]$SubscriptionName, [Parameter(Mandatory=$true)][String]$StorageAccountName, [Parameter(Mandatory=$true)][String]$ContainerName, [Parameter(Mandatory=$true)][String]$BlobName, [Parameter(Mandatory=$false)][String]$FromDate, [Parameter(Mandatory=$false)][String]$ToDate, [Parameter(Mandatory=$false)][String]$LogFile = ".\Remove-AzureUnmanagedDiskSnapshot - $SubscriptionName - $(Get-Date -Format 'ddMMMMyyyy_hh-mm-ss_tt').txt" ) Begin { if (-not ($Login = Login-AzSubscription -LoginName $LoginName -SubscriptionName $SubscriptionName -LogFile $LogFile)) { break } } Process{ #region Validate Input if ($StorageAccount = Get-AzStorageAccount | where StorageAccountName -EQ $StorageAccountName) { Write-Log 'Validated Storage Account',$StorageAccountName Green,Cyan $LogFile } else { Write-Log 'Unable to find Storage Account',$StorageAccountName,'in subscription',$SubscriptionName Magenta,Yellow,Magenta,Yellow $LogFile break } if ($StorageKey = (Get-AzStorageAccountKey -ResourceGroupName $StorageAccount.ResourceGroupName -Name $StorageAccount.StorageAccountName)[0].Value) { Write-Log 'Acquired access key for Storage Account',$StorageAccountName Green,Cyan $LogFile } else { Write-Log 'Unable to acquire access key for Storage Account',$StorageAccountName,'in subscription',$SubscriptionName Magenta,Yellow,Magenta,Yellow $LogFile Write-Log $Error[0].Exception.Message Yellow $LogFile break } if ($Context = New-AzStorageContext -StorageAccountName $StorageAccount.StorageAccountName -StorageAccountKey $StorageKey) { Write-Log 'Acquired context for Storage Account',$StorageAccountName Green,Cyan $LogFile } else { Write-Log 'Unable to acquire context for Storage Account',$StorageAccountName,'in subscription',$SubscriptionName Magenta,Yellow,Magenta,Yellow $LogFile Write-Log $Error[0].Exception.Message Yellow $LogFile break } if ($Container = Get-AzStorageContainer -Context $Context -Name $ContainerName) { Write-Log 'Read Storage Container',$ContainerName,'under',$StorageAccountName Green,Cyan,Green,Cyan $LogFile } else { Write-Log 'Unable to read Storage Container',$ContainerName,'under',$StorageAccountName Magenta,Yellow,Magenta,Yellow $LogFile break } if ($FromDate) { if ($FromDate -as [DateTime]) { $FromDate = [DateTime]$FromDate Write-Log 'From Date received:',$FromDate Green,Cyan $LogFile } else { Write-Log 'Bad Date/Time format received as -FromDate:',$FromDate,'stopping' Magenta,Yellow,Magenta $LogFile break } } else { Write-Log 'No From Date received, deleting all snapshots named',$BlobName,'in',"$StorageAccountName\$ContainerName" Yellow,Cyan,Green,Cyan $LogFile $FromDate = [DateTime]'1/1/1900' } if ($ToDate) { if ($ToDate -as [DateTime]) { $ToDate = [DateTime]$ToDate Write-Log 'To Date received:',$ToDate Green,Cyan $LogFile } else { Write-Log 'Bad Date/Time format received as -ToDate:',$ToDate,'stopping' Magenta,Yellow,Magenta $LogFile break } } else { Write-Log 'No To Date received, deleting all snapshots named',$BlobName,'in',"$StorageAccountName\$ContainerName" Yellow,Cyan,Green,Cyan $LogFile $ToDate = Get-Date } if ( $SnapshotList = $Container.CloudBlobContainer.ListBlobs($BlobName, $true,'Snapshot') | where { $_.IsSnapShot } ) { Write-Log 'Identified',$SnapshotList.Count,'disk snapshots for the disk/page Blob',$BlobName Green,yellow,Green,Cyan $LogFile Write-Log ' dated',($SnapshotList.SnapShotTime -join ', ') Green,Cyan $LogFile } else { Write-Log 'No disk snapshots found for the disk/page Blob',$BlobName Magenta,Yellow $LogFile } #endregion #region Delete snapshots foreach ($Snapshot in $SnapshotList) { if ( ($Snapshot.SnapshotTime -le $ToDate -and $Snapshot.SnapshotTime -ge $FromDate) -or $DeleteAll ) { Write-Log 'Deleting Snapshot',$Snapshot.SnapshotTime Green,Cyan $LogFile -NoNewLine $Snapshot.Delete() $Container = Get-AzStorageContainer -Context $Context -Name $ContainerName if ($Container.CloudBlobContainer.ListBlobs($BlobName, $true,'Snapshot') | where { $_.SnapshotTime -eq $Snapshot.SnapshotTime }) { Write-Log 'failed' Yellow $LogFile } else { Write-Log 'done' DarkYellow $LogFile } } } #endregion } End { } } Function Get-AzureUnmanagedDiskSnapshot { # Requires -Modules Az # Requires -Version 5 <# .SYNOPSIS Function to get Azure disk snapshot for unmanaged disks .DESCRIPTION Function to get disk snapshots for a given unmanaged disk This applies to unmanaged ARM disk snapshots only not classic ASM disks or managed ARM disks This function depends on Az PowerShell module available in the PowerShell Gallery To install required module: Install-Module Az This function has been tested to work with PowerShell version 5 .PARAMETER LoginName The username required to authenticate to Azure Example: samb@mydomain.com .PARAMETER StorageAccountName The Azure storage account name such as 'storfluxwidget3vm' .PARAMETER ContainerName The Container name such as 'Vhds' .PARAMETER BlobName The disk name such as 'Widget3VM-20181226-093810.vhd' .PARAMETER LogFile This is an optional parameter that specifies the path to the log file where the script logs its progress This defaults to a file in the current folder where the script is running .EXAMPLE $ParameterList = @{ LoginName = 'sam@dmain.com' SubscriptionName = 'my subscription name' StorageAccountName = 'storfluxwidget3vm' ContainerName = 'vhds' BlobName = 'Widget3VM-20181226-093810.vhd' } Get-AzureUnmanagedDiskSnapshot @ParameterList This example lists all snapshots of the provided disk .EXAMPLE $LoginName = 'sam@dmain.com' $SubscriptionName = 'my subscription name' $DiskList = Get-AzureVMUnmanagedDisk -LoginName $LoginName -SubscriptionName $SubscriptionName -VMName (Get-AzVM).Name # By defining the $LogFile variable before the loop, we get to put all the logs in one file $LogFile = ".\Get-AzureUnmanagedDiskSnapshot - $SubscriptionName - $(Get-Date -Format 'ddMMMMyyyy_hh-mm-ss_tt').txt" $SnapShotList = foreach ($Disk in $DiskList) { $ParameterList = @{ LoginName = $LoginName SubscriptionName = $SubscriptionName StorageAccountName = $Disk.StorageAccountName ContainerName = $Disk.ContainerName BlobName = $Disk.BlobName LogFile = $LogFile } Get-AzureUnmanagedDiskSnapshot @ParameterList } This example lists all unmanaged disks of all ARM VMs in the given subscription, then lists all their snapshots .OUTPUTS This function returns objects of type Microsoft.WindowsAzure.Storage.Blob.CloudPageBlob for each snapshot found that matches the provided storageaccount/container/blob parameters .LINK https://superwidgets.wordpress.com/category/powershell/ .NOTES Function by Sam Boutros v0.1 - 2 January 2019 v0.2 - 24 May 2019 - Updated to use AZ module instead of AzureRM module #> [CmdletBinding(ConfirmImpact='Low')] Param( [Parameter(Mandatory=$true)][String]$LoginName, [Parameter(Mandatory=$true)][String]$SubscriptionName, [Parameter(Mandatory=$true)][String]$StorageAccountName, [Parameter(Mandatory=$true)][String]$ContainerName, [Parameter(Mandatory=$true)][String]$BlobName, [Parameter(Mandatory=$false)][String]$LogFile = ".\Get-AzureUnmanagedDiskSnapshot - $SubscriptionName - $(Get-Date -Format 'ddMMMMyyyy_hh-mm-ss_tt').txt" ) Begin { if (-not ($Login = Login-AzSubscription -LoginName $LoginName -SubscriptionName $SubscriptionName -LogFile $LogFile)) { break } } Process{ #region Validate Input if ($StorageAccount = Get-AzStorageAccount | where StorageAccountName -EQ $StorageAccountName) { Write-Log 'Validated Storage Account',$StorageAccountName Green,Cyan $LogFile } else { Write-Log 'Unable to find Storage Account',$StorageAccountName,'in subscription',$SubscriptionName Magenta,Yellow,Magenta,Yellow $LogFile break } if ($StorageKey = (Get-AzStorageAccountKey -ResourceGroupName $StorageAccount.ResourceGroupName -Name $StorageAccount.StorageAccountName)[0].Value) { Write-Log 'Acquired access key for Storage Account',$StorageAccountName Green,Cyan $LogFile } else { Write-Log 'Unable to acquire access key for Storage Account',$StorageAccountName,'in subscription',$SubscriptionName Magenta,Yellow,Magenta,Yellow $LogFile Write-Log $Error[0].Exception.Message Yellow $LogFile break } if ($Context = New-AzStorageContext -StorageAccountName $StorageAccount.StorageAccountName -StorageAccountKey $StorageKey) { Write-Log 'Acquired context for Storage Account',$StorageAccountName Green,Cyan $LogFile } else { Write-Log 'Unable to acquire context for Storage Account',$StorageAccountName,'in subscription',$SubscriptionName Magenta,Yellow,Magenta,Yellow $LogFile Write-Log $Error[0].Exception.Message Yellow $LogFile break } if ($Container = Get-AzStorageContainer -Context $Context -Name $ContainerName) { Write-Log 'Read Storage Container',$ContainerName,'under',$StorageAccountName Green,Cyan,Green,Cyan $LogFile } else { Write-Log 'Unable to read Storage Container',$ContainerName,'under',$StorageAccountName Magenta,Yellow,Magenta,Yellow $LogFile break } #endregion #region Get snapshots if ($SnapshotList = $Container.CloudBlobContainer.ListBlobs($BlobName, $true,'Snapshot') | where { $_.IsSnapShot } ) { Write-Log 'Identified',$SnapshotList.Count,'disk snapshots for the disk/page Blob',$BlobName Green,yellow,Green,Cyan $LogFile Write-Log ' dated',($SnapshotList.SnapShotTime -join ', ') Green,Cyan $LogFile } else { Write-Log 'No disk snapshots found for the disk/page Blob',$BlobName Magenta,Yellow $LogFile } #endregion } End { $SnapshotList } } Function New-AzureUnmanagedDiskSnapshot { # Requires -Modules Az # Requires -Version 5 <# .SYNOPSIS Function to create Azure disk snapshot for unmanaged disks .DESCRIPTION Function to create disk snapshots for a given unmanaged disk This applies to unmanaged ARM disk snapshots only not classic ASM disks or managed ARM disks This function depends on Az PowerShell modules available in the PowerShell Gallery To install required module: Install-Module Az This function has been tested to work with PowerShell version 5 .PARAMETER LoginName The username required to authenticate to Azure Example: samb@mydomain.com .PARAMETER StorageAccountName The Azure storage account name such as 'storfluxwidget3vm' .PARAMETER ContainerName The Container name such as 'Vhds' .PARAMETER BlobName The disk name such as 'Widget3VM-20181226-093810.vhd' .PARAMETER LogFile This is an optional parameter that specifies the path to the log file where the script logs its progress This defaults to a file in the current folder where the script is running .EXAMPLE $ParameterList = @{ LoginName = 'sam@domain.com' SubscriptionName = 'my subscription name' StorageAccountName = 'storfluxwidget4vm' ContainerName = 'vhds' BlobName = 'Widget4VM-20181226-093810.vhd' } New-AzureRMUnmanagedDiskSnapshot @ParameterList This example creates a new snapshot of the provided disk .OUTPUTS This function returns object of type Microsoft.WindowsAzure.Storage.Blob.CloudPageBlob for the snapshot created .LINK https://superwidgets.wordpress.com/category/powershell/ .NOTES Function by Sam Boutros v0.1 - 2 January 2019 v0.2 - 24 May 2019 - Updated to use AZ module instead of AzureRM module #> [CmdletBinding(ConfirmImpact='High')] Param( [Parameter(Mandatory=$true)][String]$LoginName, [Parameter(Mandatory=$true)][String]$SubscriptionName, [Parameter(Mandatory=$true)][String]$StorageAccountName, [Parameter(Mandatory=$true)][String]$ContainerName, [Parameter(Mandatory=$true)][String]$BlobName, [Parameter(Mandatory=$false)][String]$LogFile = ".\New-AzureUnmanagedDiskSnapshot - $SubscriptionName - $(Get-Date -Format 'ddMMMMyyyy_hh-mm-ss_tt').txt" ) Begin { if (-not ($Login = Login-AzSubscription -LoginName $LoginName -SubscriptionName $SubscriptionName -LogFile $LogFile)) { break } } Process{ #region Validate Input if ($StorageAccount = Get-AzStorageAccount | where StorageAccountName -EQ $StorageAccountName) { Write-Log 'Validated Storage Account',$StorageAccountName Green,Cyan $LogFile } else { Write-Log 'Unable to find Storage Account',$StorageAccountName,'in subscription',$SubscriptionName Magenta,Yellow,Magenta,Yellow $LogFile break } if ($StorageKey = (Get-AzStorageAccountKey -ResourceGroupName $StorageAccount.ResourceGroupName -Name $StorageAccount.StorageAccountName)[0].Value) { Write-Log 'Acquired access key for Storage Account',$StorageAccountName Green,Cyan $LogFile } else { Write-Log 'Unable to acquire access key for Storage Account',$StorageAccountName,'in subscription',$SubscriptionName Magenta,Yellow,Magenta,Yellow $LogFile Write-Log $Error[0].Exception.Message Yellow $LogFile break } if ($Context = New-AzStorageContext -StorageAccountName $StorageAccount.StorageAccountName -StorageAccountKey $StorageKey) { Write-Log 'Acquired context for Storage Account',$StorageAccountName Green,Cyan $LogFile } else { Write-Log 'Unable to acquire context for Storage Account',$StorageAccountName,'in subscription',$SubscriptionName Magenta,Yellow,Magenta,Yellow $LogFile Write-Log $Error[0].Exception.Message Yellow $LogFile break } if ($Blob = Get-AzStorageBlob -Container $ContainerName -Context $Context | Where { $_.Name -eq $BlobName -and (-not $_.ICloudBlob.IsSnapshot)}) { Write-Log 'Validated page blob/disk',$BlobName,'under',"$StorageAccountName\$ContainerName" Green,Cyan,Green,Cyan $LogFile } else { Write-Log 'Page blob/disk',$BlobName,'not found under',"$StorageAccountName\$ContainerName" Magenta,Yellow,Magenta,Yellow $LogFile break } #endregion #region New snapshot $SnapShot = $Blob.ICloudBlob.CreateSnapshot() #endregion } End { $SnapShot } } function Get-AzureVMUnmanagedDisk { # Requires -Modules Az # Requires -Version 5 <# .SYNOPSIS Function to return unmanaged disk information of a given Azure VM .DESCRIPTION Function to return unmanaged disk information of a given Azure VM This function is intended for ARM disks and VMs not ASM This function is intended for unmanaged disks only It returns information on OS disk and data disks if any .PARAMETER LoginName The username required to authenticate to Azure Example: samb@mydomain.com .PARAMETER SubscriptionName The Azure subscription name such as 'My Dev EA subscription' .PARAMETER VMName The name of the Virtual Machine .PARAMETER LogFile This is an optional parameter that specifies the path to the log file where the script logs its progress This defaults to a file in the current folder where the script is running .EXAMPLE Get-AzureRMVMUnmanagedDisk -LoginName 'samb@mydomain.com' -SubscriptionName 'my azure subscription name here' -VMName 'Widget3VM' This example lists the unmanaged disks of a given VM .EXAMPLE Get-AzureRMVMUnmanagedDisk -LoginName 'samb@mydomain.com' -SubscriptionName 'my azure subscription name here' -VMName (Get-AzureRMVM).Name | FT -a This example lists all unmanaged disks in the given subscription .OUTPUTS Array of PS Custom objects, one for each disk found with the following properties: BlobName ContainerName StorageAccountName VMName ResourceGroup ==> this is the Resource Group Name IsOSDisk ==> True/False .LINK https://superwidgets.wordpress.com/category/powershell/ .NOTES Function by Sam Boutros v0.1 - 2 January, 2019 - original release and minor updates v0.2 - 24 May 2019 - Updated to use AZ module instead of AzureRM module #> [CmdletBinding(ConfirmImpact='Low')] Param( [Parameter(Mandatory=$true)][String]$LoginName, [Parameter(Mandatory=$true)][String]$SubscriptionName, [Parameter(Mandatory=$true)][String[]]$VMName, [Parameter(Mandatory=$false)][String]$LogFile = ".\Get-AzureVMUnmanagedDisk - $SubscriptionName - $(Get-Date -Format 'ddMMMMyyyy_hh-mm-ss_tt').txt" ) Begin { if (-not ($Login = Login-AzSubscription -LoginName $LoginName -SubscriptionName $SubscriptionName -LogFile $LogFile)) { break } } Process { #region Get VM List $AllVMs = Get-AzVM -WA 0 $VMList = @() foreach ($VMItem in $VMName) { if ($MatchingVMs = $AllVMs | where Name -EQ $VMItem) { $VMList += $MatchingVMs Write-Log 'Validated VM',$VMItem Green,Cyan $LogFile } else { Write-Log 'Unable to find VM',$VMItem,'in subscription',$SubscriptionName Magenta,Yellow,Magenta,Yellow $LogFile } } #endregion #region Get VM disks $DiskList = @() foreach ($VM in $VMList) { if ($VM.StorageProfile.OsDisk.Vhd.Uri) { $DiskName = Split-Path $VM.StorageProfile.OsDisk.Vhd.Uri -Leaf $DiskList += [PSCustomObject][Ordered]@{ BlobName = $DiskName ContainerName = (Split-Path $VM.StorageProfile.OsDisk.Vhd.Uri).Split('\')[3] StorageAccountName = (Split-Path $VM.StorageProfile.OsDisk.Vhd.Uri).Split('\')[2].Split('.')[0] VMName = $VM.Name ResourceGroup = $VM.ResourceGroupName IsOSDisk = $true } Write-Log 'Identified VM',$VM.Name,'OS disk',$DiskName Green,Cyan,Green,Cyan $LogFile } else { Write-Log 'VM',$VM.Name,'OS disk is a Managed disk, skipping..' Magenta,Yellow,Magenta $LogFile } if ($VM.StorageProfile.DataDisks) { foreach ($Disk in $VM.StorageProfile.DataDisks) { if ($Disk.Vhd.Uri) { $DiskName = Split-Path $Disk.Vhd.Uri -Leaf $DiskList += [PSCustomObject][Ordered]@{ BlobName = $DiskName ContainerName = (Split-Path $Disk.Vhd.Uri).Split('\')[3] StorageAccountName = (Split-Path $Disk.Vhd.Uri).Split('\')[2].Split('.')[0] VMName = $VM.Name ResourceGroup = $VM.ResourceGroupName IsOSDisk = $false } Write-Log 'Identified VM',$VM.Name,'data disk',$DiskName Green,Cyan,Green,Cyan $LogFile } else { Write-Log 'VM',$VM.Name,'data disk',$DiskName,'is a Managed disk, skipping..' Magenta,Yellow,Magenta,Yellow,Magenta $LogFile } } } else { Write-Log 'VM',$VM.Name,'has no data disks, skipping..' Magenta,Yellow,Magenta $LogFile } } #endregion } End { $DiskList } } function Delete-AzBlobAndContainerAndAccount { # Requires -Modules Az # Requires -Version 5 <# .SYNOPSIS Function to delete an Azure Blob, its container if empty, and its storage account if empty .DESCRIPTION Function to delete an Azure Blob, its container if empty, and its storage account if empty .LINK https://superwidgets.wordpress.com/category/powershell/ .NOTES Function by Sam Boutros v0.1 - 14 January 2019 v0.2 - 24 May 2019 - Updated to use AZ module instead of AzureRM module #> [CmdletBinding(ConfirmImpact='High')] Param( [Parameter(Mandatory=$true)][String]$LoginName, [Parameter(Mandatory=$true)][String]$SubscriptionName, [Parameter(Mandatory=$true)][String]$StorageAccountName, [Parameter(Mandatory=$true)][String]$ContainerName, [Parameter(Mandatory=$true)][String]$BlobName, [Parameter(Mandatory=$false)][String]$LogFile = ".\Delete-AzBlobAndContainerAndAccount - $BlobName - $(Get-Date -Format 'ddMMMMyyyy_hh-mm-ss_tt').txt" ) Begin { # Validate Azure access if (-not ($Login = Login-AzSubscription -LoginName $LoginName -SubscriptionName $SubscriptionName -LogFile $LogFile)) { break } } Process { $Go =$true if ($StorageAccount = Get-AzStorageAccount | where StorageAccountName -EQ $StorageAccountName) { Write-Log ' Identified Storage Account',$StorageAccountName Green,Cyan $LogFile } else { Write-Log ' Unable to find Storage Account',$StorageAccountName,'in subscription',$SubscriptionName Magenta,Yellow,Magenta,Yellow $LogFile $Go = $false } if ($StorageKey = (Get-AzStorageAccountKey -ResourceGroupName $StorageAccount.ResourceGroupName -Name $StorageAccount.StorageAccountName)[0].Value) { Write-Log ' Acquired access key for Storage Account',$StorageAccountName Green,Cyan $LogFile } else { Write-Log ' Unable to acquire access key for Storage Account',$StorageAccountName,'in subscription',$SubscriptionName Magenta,Yellow,Magenta,Yellow $LogFile Write-Log $Error[0].Exception.Message Yellow $LogFile $Go = $false } if ($Context = New-AzStorageContext -StorageAccountName $StorageAccount.StorageAccountName -StorageAccountKey $StorageKey) { Write-Log ' Acquired context for Storage Account',$StorageAccountName Green,Cyan $LogFile } else { Write-Log ' Unable to acquire context for Storage Account',$StorageAccountName,'in subscription',$SubscriptionName Magenta,Yellow,Magenta,Yellow $LogFile Write-Log $Error[0].Exception.Message Yellow $LogFile $Go = $false } try { $Container = Get-AzStorageContainer -Context $Context -Name $ContainerName -EA 1 Write-Log ' Read Storage Container',$ContainerName,'under',$StorageAccountName Green,Cyan,Green,Cyan $LogFile #region Delete Blob(s) if ($BlobList = $Container.CloudBlobContainer.ListBlobs() | where Name -Match $BlobName) { foreach ($Blob in $BlobList) { Write-Log ' Deleting Blob',$Blob.Name Green,Yellow $LogFile $Blob.Delete() } $Container = Get-AzStorageContainer -Context $Context -Name $ContainerName -EA 1 if ($BlobList = $Container.CloudBlobContainer.ListBlobs() | where Name -Match $BlobName) { Write-Log ' Failed to delete 1 or more blobs' Magenta $LogFile } else { Write-Log ' Blob deletion successful' Cyan $LogFile } } else { Write-Log ' Blob',$BlobName,'not found in',"$StorageAccountName/$ContainerName" Magenta,Yellow,Magenta,Yellow $LogFile } #endregion #region Delete container if empty if ($BlobList = $Container.CloudBlobContainer.ListBlobs()) { Write-Log ' Container',$ContainerName,'is not empty - skipping, it has the following blobs:' Green,Yellow,Green $LogFile $BlobList | foreach { Write-Log " $($_.Name)" Cyan $LogFile } } else { Write-Log ' Deleting empty container',$ContainerName Green,Yellow $LogFile -NoNewLine try { $Result = $Container | Remove-AzureStorageContainer -PassThru -Force -EA 1 Write-Log 'done' DarkYellow $LogFile } catch { Write-Log 'failed' Magenta $LogFile } } #endregion } catch { Write-Log ' Unable to read Storage Container',$ContainerName,'under',$StorageAccountName Magenta,Yellow,Magenta,Yellow $LogFile } #region Delete Storage Account if empty if ($Go) { if ($ContainerList = Get-AzStorageContainer -Context $Context) { Write-Log ' Storage account',$StorageAccountName,'is not empty - skipping, currently has the following container(s)' Cyan,Yellow,Cyan $LogFile $ContainerList.Name | foreach { Write-Log " $_" Green $LogFile } } else { Write-Log 'Deleting empty Storage Account',$StorageAccountName Green,Cyan $LogFile -NoNewLine $StorageAccount | Remove-AzStorageAccount -Force Write-Log 'done' Green $LogFile } } #endregion } End { } } function Delete-AzVM { # Requires -Modules Az # Requires -Version 5 <# .SYNOPSIS Function to delete an Azure ARM VM and all its objects .DESCRIPTION Function to delete an Azure ARM VM and all its objects including: - Boot Diagnostics blob(s), storage container, storage account if empty - VM object - OS disk, storage container if ampty, storage account if ampty - Data disk(s) if any, storage container(s) if ampty, storage account(s) if ampty - VM NIC(s) - VM public IP objects if any NSG's are not deleted by this function since they may be linked to many NICs This function will not delete a running VM by design .PARAMETER LoginName The username required to authenticate to Azure Example: samb@mydomain.com .PARAMETER SubscriptionName The Azure subscription name such as 'My Dev EA subscription' .PARAMETER VMName The name of one or more ARM Virtual Machines .PARAMETER LogFile This is an optional parameter that specifies the path to the log file where the script logs its progress This defaults to a file in the current folder where the script is running .EXAMPLE Delete-AzVM -LoginName 'samb@mydomain.com' -SubscriptionName 'my azure subscription name here' -ARMVMName 'Widget3VM' .LINK https://superwidgets.wordpress.com/ .NOTES Function by Sam Boutros v0.1 - 14 January 2019 v0.2 - 24 May 2019 - Updated to use AZ module instead of AzureRM module #> [CmdletBinding(ConfirmImpact='Low')] Param( [Parameter(Mandatory=$true)][String]$LoginName, [Parameter(Mandatory=$true)][String]$SubscriptionName, [Parameter(Mandatory=$true)][String]$VMName, [Parameter(Mandatory=$false)][String]$ResourceGroupName, [Parameter(Mandatory=$false)][String]$LogFile = ".\Delete-AzVM - $VMName - $(Get-Date -Format 'ddMMMMyyyy_hh-mm-ss_tt').txt" ) Begin { # Validate Azure access, Input if (-not ($Login = Login-AzSubscription -LoginName $LoginName -SubscriptionName $SubscriptionName -LogFile $LogFile)) { break } Try { $StorageAccountList = Get-AzStorageAccount -EA 1 if (-not $StorageAccountList) { Write-Log 'No storage accounts found' Magenta $LogFile; Break } } catch { Write-Log 'Unable to list Storage Accounts in Subscription',$SubscriptionName Magenta,Yellow $LogFile; Break } Try { $RawVMList = Get-AzVM -EA 1 if (-not $RawVMList) { Write-Log 'No VMs found' Magenta $LogFile; Break } } catch { Write-Log 'Unable to list VMs in Subscription',$SubscriptionName Magenta,Yellow $LogFile; Break } } Process { if ($VM = $RawVMList | where Name -EQ $VMName) { if ($VM.Count -gt 1) { if ($ResourceGroupName) { $VM = Get-AzVM -Name $VMName -ResourceGroupName $ResourceGroupName } else { Write-Log 'Delete-AzVM input error:','Found more than 1 VM named',$VMName Magenta,Yellow,Magenta $LogFile Write-Log ($VM|Out-String).Trim() Yellow $LogFile Write-Log 'If more than 1 VM exist in the same subscription with the same name, you must specify the ResourceGroupName' Magenta $LogFile break } } } else { Write-Log 'VM',$VMName,'not found in subscription',$SubscriptionName Magenta,Yellow,Magenta,Yellow $LogFile break } if ($VM) { Write-Log 'Processing VM' Green $LogFile Write-Log ($VM|Out-String).Trim() Cyan $LogFile $VMStatus = (Get-AzVM -ResourceGroupName $VM.ResourceGroupName -Name $VMName -Status).Statuses[1].DisplayStatus if ($VMStatus -eq 'VM deallocated') { #region Delete Boot Diagnostics blob(s) if configured, container, storage account if empty if ($VM.DiagnosticsProfile.bootDiagnostics.storageUri) { $StorageAccountName = ($VM.DiagnosticsProfile.bootDiagnostics.storageUri).Split('/')[2].Split('.')[0] $ContainerName = "bootdiagnostics-$($vm.Name.ToLower().Substring(0, 9))-$($VM.vmId)" $Go =$true if ($StorageAccount = Get-AzStorageAccount | where StorageAccountName -EQ $StorageAccountName) { Write-Log ' Identified Diagnostics Storage Account',$StorageAccountName Green,Cyan $LogFile } else { Write-Log ' Unable to find Diagnostics Storage Account',$StorageAccountName,'in subscription',$SubscriptionName Magenta,Yellow,Magenta,Yellow $LogFile $Go = $false } if ($StorageKey = (Get-AzStorageAccountKey -ResourceGroupName $StorageAccount.ResourceGroupName -Name $StorageAccount.StorageAccountName)[0].Value) { Write-Log ' Acquired access key for Storage Account',$StorageAccountName Green,Cyan $LogFile } else { Write-Log ' Unable to acquire access key for Storage Account',$StorageAccountName,'in subscription',$SubscriptionName Magenta,Yellow,Magenta,Yellow $LogFile Write-Log $Error[0].Exception.Message Yellow $LogFile $Go = $false } if ($Context = New-AzStorageContext -StorageAccountName $StorageAccount.StorageAccountName -StorageAccountKey $StorageKey) { Write-Log ' Acquired context for Storage Account',$StorageAccountName Green,Cyan $LogFile } else { Write-Log ' Unable to acquire context for Storage Account',$StorageAccountName,'in subscription',$SubscriptionName Magenta,Yellow,Magenta,Yellow $LogFile Write-Log $Error[0].Exception.Message Yellow $LogFile $Go = $false } try { $Container = Get-AzStorageContainer -Context $Context -Name $ContainerName -EA 1 Write-Log ' Read Storage Container',$ContainerName,'under',$StorageAccountName Green,Cyan,Green,Cyan $LogFile if ($BlobList = $Container.CloudBlobContainer.ListBlobs() ) { Write-Log ' Found the following blobs in',"$StorageAccountName/$ContainerName" Green,Cyan $LogFile $BlobList.Name | foreach { Write-Log " $_" Cyan $LogFile } } else { Write-Log ' No Blobs found in',"$StorageAccountName/$ContainerName" Magenta,Yellow $LogFile } Write-Log ' Deleting the container',$ContainerName,'and all its Blobs..' Green,Yellow,Green $LogFile -NoNewLine try { $Result = $Container | Remove-AzStorageContainer -PassThru -Force -EA 1 Write-Log 'done' Cyan $LogFile } catch { Write-Log 'failed' Magenta $LogFile } } catch { Write-Log ' Unable to read Storage Container',$ContainerName,'under',$StorageAccountName Magenta,Yellow,Magenta,Yellow $LogFile } # Delete Container if ($Go) { if ($ContainerList = Get-AzStorageContainer -Context $Context) { Write-Log ' Storage account',$StorageAccountName,'is not empty - skipping, currently has the following containers' Cyan,Yellow,Cyan $LogFile $ContainerList.Name | foreach { Write-Log " $_" Green $LogFile } } else { Write-Log 'Deleting empty Storage Account',$StorageAccountName Green,Cyan $LogFile -NoNewLine $StorageAccount | Remove-AzStorageAccount -Force Write-Log 'done' Green $LogFile } } # Delete Storage Account if empty } else { Write-Log ' Boot diagnostics not configured for VM',$VM.Name Green,Yellow $LogFile } #endregion #region Delete VM Write-Log ' Deleting VM',$VMName Green,Cyan $LogFile -NoNewLine $Result = $VM | Remove-AzVM –Force Write-Log 'done' DarkYellow $LogFile #endregion #region Delete OS disk, status blob if($VM.StorageProfile.OsDisk.ManagedDisk) { Write-Log ' Deleting managed OS disk',$VM.StorageProfile.OSDisk.Name,'for VM',$VM.Name Green,Cyan,Green,Cyan $LogFile -NoNewLine Get-AzDisk -ResourceGroupName $VM.ResourceGroupName -DiskName $VM.StorageProfile.OSDisk.Name | Remove-AzDisk -Force Write-Log 'done' DarkYellow $LogFile } else { $StorageAccountName = ($VM.StorageProfile.OSDisk.Vhd.Uri).Split('/')[2].Split('.')[0] $ContainerName = ($VM.StorageProfile.OSDisk.Vhd.Uri).Split('/')[3] $BlobName = ($VM.StorageProfile.OSDisk.Vhd.Uri).Split('/')[4] Write-Log 'Identified OS disk',$BlobName,'in Storage Account/Container',"$StorageAccountName/$ContainerName" Green,Cyan,Green,Cyan $LogFile $ParameterList = @{ LoginName = $LoginName SubscriptionName = $SubscriptionName StorageAccountName = $StorageAccountName ContainerName = $ContainerName BlobName = $BlobName LogFile = $LogFile } Delete-AzBlobAndContainerAndAccount @ParameterList } #endregion #region Delete data disks foreach ($DataDisk in $VM.StorageProfile.DataDisks) { if($DataDisk.ManagedDisk) { Write-Log ' Deleting managed data disk',$DataDisk.Name,'for VM',$VM.Name Green,Cyan,Green,Cyan $LogFile -NoNewLine Get-AzDisk -ResourceGroupName $VM.ResourceGroupName -DiskName $DataDisk.Name | Remove-AzDisk -Force Write-Log 'done' DarkYellow $LogFile } else { $StorageAccountName = ($DataDisk.Vhd.Uri).Split('/')[2].Split('.')[0] $ContainerName = ($DataDisk.Vhd.Uri).Split('/')[3] $BlobName = ($DataDisk.Vhd.Uri).Split('/')[4] Write-Log 'Identified data disk',$BlobName,'in Storage Account/Container',"$StorageAccountName/$ContainerName" Green,Cyan,Green,Cyan $LogFile $ParameterList = @{ LoginName = $LoginName SubscriptionName = $SubscriptionName StorageAccountName = $StorageAccountName ContainerName = $ContainerName BlobName = $BlobName LogFile = $LogFile } Delete-AzBlobAndContainerAndAccount @ParameterList } } #endregion #region delete vNIC(s) foreach ($VMNIC in ($VM.NetworkProfile.NetworkInterfaces | where {$_.ID})) { $NICName = Split-Path -Path $VMNIC.ID -leaf Write-Log ' Deleting VM NIC',$NICName Green,Cyan $LogFile -NoNewLine Get-AzNetworkInterface -ResourceGroupName $VM.ResourceGroupName -Name $NICName | Remove-AzNetworkInterface -Force Write-Log 'done' DarkYellow $LogFile } #endregion #region delete public IP if any Remove-Variable FoundPublicIP -EA 0 foreach ($VMNIC in $VM.NetworkProfile.NetworkInterfaces.Id) { foreach ($PublicIP in (Get-AzPublicIpAddress -ResourceGroupName $VM.ResourceGroupName | Where { $_.IpConfiguration.Id })) { if (($PublicIP.IpConfiguration.Id).Split('/')[8] -eq $VMNIC.Split('/')[8]) { Write-Log 'Identified Public IP object',$PublicIP.Name,'associated with VM NIC',($VMNIC.Split('/')[8]),'of VM',$VM.Name Green,Cyan,Green,Cyan ,Green,Cyan $FoundPublicIP = $PublicIP } } } if ($FoundPublicIP) { Write-Log ' Deleting VM public IP object',$PublicIP.Name Green,Cyan $LogFile -NoNewLine Get-AzPublicIpAddress -ResourceGroupName $VM.ResourceGroupName -Name $PublicIP.Name | Remove-AzPublicIpAddress -Force Write-Log 'done' DarkYellow $LogFile } else { Write-Log ' No public IP object found for VM',$VM.Name Green,Cyan $LogFile } #endregion # Not deleting NSG's here, since they may apply to several NICs that belong to several VMs # Will have a separate function to delete unused NSG's (not linked to any NICs) } else { Write-Log 'VM',$VMName,'is not powered off. Current status is:',$VMStatus,'skipping..' Magenta,Yellow,Magenta,Yellow,Magenta $LogFile } } } End { } } function Report-AzureRMSubscriptionVMBackup { <# .SYNOPSIS Function to list backup recovery points of Azure VMs in one or more subscriptions .DESCRIPTION Function to list backup recovery points of Azure VMs in one or more subscriptions The script provides interim output to the console indicating its progress through the hierarchy of: Subscriptions Recovery Services Vaults Registered AzureVM Backup containers Backup Items Recovery points .PARAMETER SubscriptionName Name of Azure subscription If not provided it will default to all accessible Azure subscriptions .EXAMPLE Login-AzureRmAccount -Credential (Get-SBCredential 'name@domain.com') | Out-Null # -Environment AzureCloud Report-AzureRMSubscriptionVMBackup .EXAMPLE $VMBackupList = Report-AzureRMSubscriptionVMBackup -SubscriptionName 'my subscription name' $VMBackupList | Format-Table -Auto # to display to the console $VMBackupList | Out-GridView # to display to ISE GridView $VMBackupList | Export-Csv .\VMBackupList1.csv -NoType # to export to CSV . OUTPUTS PSCustom object (one for each recovery point) containing the following properties/example: VMName VaultName ResourceGroup SubscriptionName RecoveryPointType RecoveryPointTime EncryptionEnabled ------ ------------ ------------- ---------------- ----------------- ----------------- ----------------- ab123xyzw01 xyz abc my subscription name CrashConsistent 8/9/2018 6:01:25 AM False ab123xyzw01 xyz abc my subscription name CrashConsistent 8/8/2018 6:08:09 AM False ab123xyzw01 xyz abc my subscription name CrashConsistent 8/7/2018 6:11:49 AM False .LINK https://superwidgets.wordpress.com/ .NOTES Function by Sam Boutros v0.1 - 9 August 2018 v0.2 - 24 September 2018 - Fixed bug with Get-AzureRmResource line v0.3 - 25 September 2018 - Added Vault Name in output #> [CmdletBinding(ConfirmImpact='Low')] Param( [Parameter(Mandatory=$false,ValueFromPipeLine=$true,ValueFromPipeLineByPropertyName=$true)] [String[]]$SubscriptionName ) Begin { $myOutput = @() # Validate AzureRM PowerShell module is available if(-not (Get-Module -ListAvailable AzureRM)) { Write-Log 'Required AzureRM PowerShell module not found. You can install it from the PowerShell Gallery by running:' Magenta Write-Log 'Install-Module AzureRM' Yellow break } # Validate that we're logged in to Azure try { Get-AzureRmSubscription -EA 1 -WA 0 | Out-Null } catch { Write-Log $_.exception.message Yellow; break } } Process { if (-not $SubscriptionName) { $SubscriptionName = (Get-AzureRmSubscription -WA 0).Name } foreach ($Subscription in $SubscriptionName) { Write-Log 'Processing subscription',$Subscription Green,Cyan try { Get-AzureRmSubscription -SubscriptionName $Subscription -EA 1 -WA 0 | Set-AzureRmContext | Out-Null $VaultList = Get-AzureRmResource | where ResourceType -EQ Microsoft.RecoveryServices/vaults | select Name,ResourceGroupName,Location if ($VaultList) { Write-Log ' Identified',$VaultList.Count,'Recovery Services Vaults;',($VaultList.Name -join ', ') Green,Cyan,Green,Cyan foreach ($Vault in $VaultList) { Write-Log ' Processing Recovery Services Vault',$Vault.Name Green,Cyan Set-AzureRmRecoveryServicesVaultContext -Vault $Vault $ContainerList = Get-AzureRmRecoveryServicesBackupContainer -ContainerType 'AzureVM' -Status 'Registered' if ($ContainerList) { Write-Log ' Identified',$ContainerList.Count,'Azure VM backup sets/containers;',($ContainerList.FriendlyName -join ', ') Green,Cyan,Green,Cyan foreach ($Container in $ContainerList) { $backupitem = Get-AzureRmRecoveryServicesBackupItem -Container $Container -WorkloadType 'AzureVM' if ($backupitem) { $RecoveryPointList = Get-AzureRmRecoveryServicesBackupRecoveryPoint -Item $backupitem if ($RecoveryPointList) { Write-Log ' Identified',$RecoveryPointList.Count,'recovery points for VM',$Container.FriendlyName Green,Cyan,Green,Cyan foreach ($RecoveryPoint in $RecoveryPointList) { $myOutput += [PSCustomObject][Ordered]@{ VMName = $RecoveryPoint.ItemName.Split(';')[2] ResourceGroup = $RecoveryPoint.ItemName.Split(';')[1] VaultName = $Vault.Name SubscriptionName = $Subscription RecoveryPointType = $RecoveryPoint.RecoveryPointType RecoveryPointTime = $RecoveryPoint.RecoveryPointTime EncryptionEnabled = $RecoveryPoint.EncryptionEnabled } } } else { Write-Log ' No recovery points found for VM',$Container.FriendlyName Green,yellow } } } } else { Write-Log ' No registered VM backup containers found in Recovery Services Vault',$Vault.Name Green,Yellow } } } else { Write-Log ' No Recovery Services Vaults found in subscription',$Subscription Green,Yellow } } catch { Write-Log $_.exception.message Yellow } } } End { $myOutput } } function Remove-AzureRMVMBackup { <# .SYNOPSIS Function to disable backup of a given VM and delete existing backups (recovery points) .DESCRIPTION Function to disable backup of a given VM and delete existing backups (recovery points) If there are multiple VMs with the same name (under different Resource Groups) in the same subscription, this function will not delete the backups (cannot tell which VM the backups belong to) This function will work on both ARM and ASM VM backups .PARAMETER LoginName The username required to authenticate to Azure Example: samb@mydomain.com .PARAMETER SubscriptionName The Azure subscription name such as 'My Dev EA subscription' .PARAMETER VMName The name of a given Virtual Machine .PARAMETER LogFile This is an optional parameter that specifies the path to the log file where the script logs its progress This defaults to a file in the current folder where the script is running .EXAMPLE Remove-AzureRMVMBackup -LoginName 'samb@mydomain.com' -SubscriptionName 'my azure subscription name here' -VMName 'Widget3VM' .LINK https://superwidgets.wordpress.com/2019/01/16/remove-azurermvmbackup-function-added-to-azsbtools-powershell-module/ .NOTES Function by Sam Boutros v0.1 - 16 January 2019 #> [CmdletBinding(ConfirmImpact='High')] Param( [Parameter(Mandatory=$true)][String]$LoginName, [Parameter(Mandatory=$true)][String]$SubscriptionName, [Parameter(Mandatory=$true)][String]$VMName, [Parameter(Mandatory=$false)][String]$LogFile = ".\Remove-AzureRMVMBackup - $VMName - $(Get-Date -Format 'ddMMMMyyyy_hh-mm-ss_tt').txt" ) Begin { if (-not ($Login = Login-AzureRMSubscription -LoginName $LoginName -SubscriptionName $SubscriptionName -LogFile $LogFile)) { break } } Process { # List backup containers because ASM VM backups may not show in which Vault their container is located $BackupContainerList = foreach ($RSVault in Get-AzureRmRecoveryServicesVault) { Get-AzureRmRecoveryServicesBackupContainer -ContainerType AzureVM -Status Registered -VaultId $RSVault.ID | select FriendlyName, @{n='VaultId' ;e={$RSVault.ID}}, @{n='Vault' ;e={Split-Path $RSVault.ID -Leaf}}, @{n='Container';e={$_.FriendlyName}} } Write-Verbose 'Identified list of backup vaults and containers:' Write-Verbose ($BackupContainerList | FT Vault,Container -a | Out-String).Trim() if ($FoundContainer = $BackupContainerList | where FriendlyName -EQ $VMName) { $BackupContainer = Get-AzureRmRecoveryServicesBackupContainer -ContainerType AzureVM -Status Registered -VaultId $FoundContainer.VaultId -FriendlyName $FoundContainer.FriendlyName Write-Log 'Identified VM Backup Container',$BackupContainer.FriendlyName,'for VM',$VMName Green,Cyan,Green,Cyan $LogFile Write-Log ($BackupContainer | FL | Out-String).Trim() Cyan if ($BackupContainer.Count -gt 1) { Write-Log 'Remove-AzureRMVMBackup: Found more than 1 backup container for VM',$VMName,'skipping..' Magenta,Yellow,Magenta $LogFile } else { if ($BackupItem = Get-AzureRmRecoveryServicesBackupItem -Container $BackupContainer -WorkloadType AzureVM -VaultId $FoundContainer.VaultId) { Write-Log ' Identified',($BackupItem.Name.Split(';')[2]),'VM Backup Item' Green,Cyan,Green Write-Log ($BackupItem | FL | Out-String).Trim() Cyan if ($BackupItem.Count -gt 1) { Write-Log 'Remove-AzureRMVMBackup: Found more than 1 Backup item for VM',$VMName,'skipping..' Magenta,Yellow,Magenta $LogFile } else { Write-Log ' Disabling backup for VM',$VMName,'and deleting existing backups' Green,Cyan,Green $LogFile -NoNewLine $Result = Disable-AzureRmRecoveryServicesBackupProtection -Item $BackupItem -RemoveRecoveryPoints -Force -VaultId $FoundContainer.VaultId Write-Log 'done' DarkYellow $LogFile } } else { Write-Log ' No Backup Item found for VM',$VMName Green,Yellow } } } else { Write-Log ' No Backup Container found for VM',$VMName Green,Yellow } } End { } } function Get-AzureBlob { <# .SYNOPSIS Function to return an Azure blob object if it exists based on a blob URL .DESCRIPTION Function to return an Azure blob object if it exists Function returns False if blob does not exist in the given URL .PARAMETER LoginName The username required to authenticate to Azure Example: samb@mydomain.com .PARAMETER SubscriptionName The Azure subscription name such as 'My Dev EA subscription' .PARAMETER URL This is the Blob URL like https://paklfjlkdjalsdkfjalk5.blob.core.windows.net/vhds/AdfsdfsdI-2015-09-14.vhd This can be obtained from the Get-AzureVM and Get-AzureRMVM cmdlets For example, ASM VM OS disk: $VM.vm.OSVirtualHardDisk.MediaLink.AbsoluteUri ASM VM data disk URLs: $VM.VM.DataVirtualHardDisks.medialink.AbsoluteUri .PARAMETER LogFile This is an optional parameter that specifies the path to the log file where the script logs its progress This defaults to a file in the current folder where the script is running .EXAMPLE Get-AzureBlob -LoginName 'samb@mydomain.com' -SubscriptionName 'my azure subscription name here' -URL 'https://paklfjlkdjalsdkfjalk5.blob.core.windows.net/vhds/AdfsdfsdI-2015-09-14.vhd' .LINK https://superwidgets.wordpress.com/ .NOTES Function by Sam Boutros v0.1 - 17 January 2019 #> [CmdletBinding(ConfirmImpact='Low')] Param( [Parameter(Mandatory=$true)][String]$LoginName, [Parameter(Mandatory=$true)][String]$SubscriptionName, [Parameter(Mandatory=$true)][String[]]$URL, [Parameter(Mandatory=$false)][String]$LogFile = ".\Get-AzureBlob - $URL - $(Get-Date -Format 'ddMMMMyyyy_hh-mm-ss_tt').txt" ) Begin { if (-not ($Login = Login-AzureRMSubscription -LoginName $LoginName -SubscriptionName $SubscriptionName -LogFile $LogFile)) { break } } Process { foreach ($URI in $URL) { $Go = $true try { $StorageAccountName = $URL.Split('/')[2].Split('.')[0] } catch { $Go = $false Write-Log 'Unable to get Storage Account name from provided URL',$URI Magenta,Yellow $LogFile Write-Log 'Expecting URL in the format','https://paklfjlkdjalsdkfjalk5.blob.core.windows.net/vhds/AdfsdfsdI-2015-09-14.vhd' Cyan,Yellow $LogFile } try { $ContainerName = $URL.Split('/')[3] } catch { $Go = $false Write-Log 'Unable to get Container name from provided URL',$URI Magenta,Yellow $LogFile Write-Log 'Expecting URL in the format','https://paklfjlkdjalsdkfjalk5.blob.core.windows.net/vhds/AdfsdfsdI-2015-09-14.vhd' Cyan,Yellow $LogFile } try { $VHDName = $URL.Split('/')[4] } catch { $Go = $false Write-Log 'Unable to get VHD/Blob name from provided URL',$URI Magenta,Yellow $LogFile Write-Log 'Expecting URL in the format','https://paklfjlkdjalsdkfjalk5.blob.core.windows.net/vhds/AdfsdfsdI-2015-09-14.vhd' Cyan,Yellow $LogFile } if ($Go) { $Context = (Get-AzureStorageAccount -StorageAccountName $StorageAccountName).Context try { Get-AzureStorageBlob -Container $ContainerName -Blob $VHDName -Context $Context -EA 1 } catch { $false } } } } End { } } function Clone-AzureRMUnmanagedDisk { # Requires -Modules AzureRM # Requires -Version 5 <# .SYNOPSIS Function to copy Azure ARM VM unmanaged disk from one storage account to another .DESCRIPTION Function to copy Azure ARM VM unmanaged disk from one storage account to another or from one container to another in the same storage account This can be useful in migrating VMs from managed to unmanaged disks, VM backup that does not depend on VM OS or bakup agent in the VM, VM cloning scenarios, VM migration from one subscription to another, VM migration from one Azure region to another, VM migration from one storage account type to another (ASM/ARM, Standard/Premium) especially where not supported by the Microsoft provided tools Disk copy is validated by comparing the count of used bytes of the source disk snapshot and the destination disk .PARAMETER LoginName The username required to authenticate to Azure Example: samb@mydomain.com .PARAMETER StorageAccountName The Azure storage account name such as 'storfluxwidget3vm' .PARAMETER DiskName This is the source disk name .PARAMETER SourceStorageAccount This is the name of the source Storage Account .PARAMETER SourceContainer This is the name of the source Container .PARAMETER DestinationStorageAccount This is the name of the destination Storage Account .PARAMETER DestinationContainer This is the name of the destination container If not present, the function will create it .PARAMETER OverWriteDest This is an optional parameter set to False by default When set to True, it causes the function to over-write the destination disk/page blob if it exists If set to False, the function will not over-write desination disk/page blob if it already exists .PARAMETER DeleteSource This is an optional parameter set to False by default When set to True, it causes the function to delete the source disk after a validated copy If set to False, the source disk must will be left behind to be deleted manually thereafter .PARAMETER LogFile This is an optional parameter that specifies the path to the log file where the script logs its progress This defaults to a file in the current folder where the script is running .EXAMPLE $ParameterSet = @{ LoginName = 'sam@mydomain.com' SubscriptionName = 'my subscription name' DiskName = 'mydiskname.vhd' SourceStorageAccount = 'mysourcesa' SourceContainer = 'vhds' DestinationStorageAccount = 'mydestsa' } Clone-AzureRMUnmanagedDisk @ParameterSet This will copy the provided disk and not delete the source .LINK https://superwidgets.wordpress.com/ .NOTES Function by Sam Boutros v0.1 - 13 February 2019 #> [CmdletBinding(ConfirmImpact='High')] Param( [Parameter(Mandatory=$true)][String]$LoginName, [Parameter(Mandatory=$true)][String]$SubscriptionName, [Parameter(Mandatory=$true)][String]$DiskName, # Example 'Widget1VM-20181218-123351' [Parameter(Mandatory=$true)][String]$SourceStorageAccount, # Example 'storfluxwidget1vm' [Parameter(Mandatory=$true)][String]$SourceContainer, # Example 'vhds' [Parameter(Mandatory=$true)][String]$DestinationStorageAccount, # Example 'storfluxwidget2vm' [Parameter(Mandatory=$false)][String]$DestinationContainer = $SourceContainer, [Parameter(Mandatory=$false)][Switch]$DeleteSource = $false, [Parameter(Mandatory=$false)][Switch]$OverWriteDest = $false, [Parameter(Mandatory=$false)][String]$LogFile = ".\Clone-AzureRMUnmanagedDisk - $DiskName - $(Get-Date -Format 'ddMMMMyyyy_hh-mm-ss_tt').txt" ) Begin { # Validate Azure access, Input if (-not ($Login = Login-AzureRMSubscription -LoginName $LoginName -SubscriptionName $SubscriptionName -LogFile $LogFile)) { break } Try { $StorageAccountList = Get-AzureRmStorageAccount -EA 1 if (-not $StorageAccountList) { Write-Log 'No storage accounts found in subscription',$SubscriptionName Magenta,Yellow $LogFile; Break } } catch { Write-Log 'No storage accounts found in subscription',$SubscriptionName Magenta,Yellow $LogFile; Break } @($SourceStorageAccount,$DestinationStorageAccount) | foreach { if (-not ($StorageAccountList | where StorageAccountName -EQ $_)) { Write-Log 'Storage Account',$_,'not found in subscription', $SubscriptionName Magenta,Yellow,Magenta,Yellow $LogFile Break } else { Write-Log 'Validated Storage Account',$_,'in subscription', $SubscriptionName Green,Cyan,Green,Cyan $LogFile } } $StorageAccount = Get-AzureRmStorageAccount | where StorageAccountName -EQ $DestinationStorageAccount $StorageKey = (Get-AzureRmStorageAccountKey -ResourceGroupName $StorageAccount.ResourceGroupName -Name $StorageAccount.StorageAccountName)[0].Value $DestContext = New-AzureStorageContext -StorageAccountName $StorageAccount.StorageAccountName -StorageAccountKey $StorageKey $ContainerList = Get-AzureRmStorageContainer -ResourceGroupName $StorageAccount.ResourceGroupName -StorageAccountName $StorageAccount.StorageAccountName if ($DestinationContainer -in $ContainerList.Name) { Write-Log 'Validated destination container',$DestinationContainer,'in destination Storage Account',$DestinationStorageAccount Green,Cyan,Green,Cyan $LogFile } else { Write-Log 'Destination container',$DestinationContainer,'not found in destination Storage Account',$DestinationStorageAccount,'creating..' Cyan,Yellow,Cyan,Yellow,Cyan -NoNewLine $LogFile New-AzureRmStorageContainer -ResourceGroupName $StorageAccount.ResourceGroupName -StorageAccountName $StorageAccount.StorageAccountName -Name $DestinationContainer | Out-Null Write-Log 'done' Green $LogFile } $StorageAccount = Get-AzureRmStorageAccount | where StorageAccountName -EQ $SourceStorageAccount $StorageKey = (Get-AzureRmStorageAccountKey -ResourceGroupName $StorageAccount.ResourceGroupName -Name $StorageAccount.StorageAccountName)[0].Value $SrcContext = New-AzureStorageContext -StorageAccountName $StorageAccount.StorageAccountName -StorageAccountKey $StorageKey $ContainerList = Get-AzureRmStorageContainer -ResourceGroupName $StorageAccount.ResourceGroupName -StorageAccountName $StorageAccount.StorageAccountName if ($SourceContainer -in $ContainerList.Name) { Write-Log 'Validated source container',$SourceContainer,'in source Storage Account',$SourceStorageAccount Green,Cyan,Green,Cyan $LogFile } else { Write-Log 'Source container',$SourceContainer,'not found in source Storage Account',$SourceStorageAccount Magenta,Yellow,Magenta,Yellow $LogFile break } $DiskName = $DiskName.ToLower() # if (-not ($DiskName.EndsWith('.vhd'))) { $DiskName = "$DiskName.vhd" } if ($PageBlob = Get-AzureStorageBlob -Container $SourceContainer -Context $SrcContext | where { $_.Name -EQ $DiskName -and -not $_.ICloudBlob.IsSnapshot} ) { Write-Log 'Validated unmanaged disk (page blob)',$DiskName,'in container',$SourceContainer Green,Cyan,Green,Cyan } else { Write-Log 'Unmanaged disk (page blob)',$DiskName,'not found in container',$SourceContainer Magenta,Yellow,Magenta,Yellow break } } Process { #region Snapshot, copy source disk to destination, monitor and wait for copy $Go = $true if ($DestBlob = Get-AzureStorageBlob -Container $DestinationContainer -Context $DestContext | where Name -EQ $DiskName) { Write-Log 'Page blob already exists in the destination',"$DestinationStorageAccount/$DestinationContainer/$DiskName" Green,Cyan $LogFile if ($OverWriteDest) { Write-Log ' and ''OverWriteDest'' switch is set to',$OverWriteDest,'- over-writing destination page blob..' Green,Cyan,Green -NoNewLine $LogFile } else { Write-Log ' and ''OverWriteDest'' switch is set to',$OverWriteDest,'- aborting..' Yellow,Magenta,Yellow $LogFile $Go = $false } } if ($Go) { Write-Log 'Creating a snapshot of the source disk/page blob',"$SourceStorageAccount/$SourceContainer/$DiskName" Green,Cyan -NoNewLine $LogFile $Snapshot = $PageBlob.ICloudBlob.CreateSnapshot() $SnapshotBlob = Get-AzureStorageBlob -Container $SourceContainer -Context $SrcContext | where SnapshotTime -EQ $Snapshot.SnapshotTime $SourceBlobSizeInBytes = Get-BlobBytes -Blob $SnapshotBlob -IsPremiumAccount ($SourceStorageAccount.Sku.Tier -eq 'Premium') if ($Snapshot.Name -eq $PageBlob.Name) { Write-Log 'done, time stamp',$Snapshot.SnapshotTime DarkYellow,Cyan $LogFile Write-Log 'Copying snapshot of source disk/page blob to destination',"$DestinationStorageAccount/$DestinationContainer/$DiskName" Green,Cyan $LogFile Write-Log ' Allocated size',"$([Math]::Round($SnapshotBlob.Length/1GB,1))GB ($('{0:n0}' -f $SnapshotBlob.Length) bytes)",'used size',"$([Math]::Round($SourceBlobSizeInBytes/1GB,1))GB ($('{0:n0}' -f $SourceBlobSizeInBytes) bytes)" Green,Cyan,Green,Cyan $LogFile $Duration = Measure-Command { Start-AzureStorageBlobCopy -CloudBlob $SnapshotBlob.ICloudBlob -Context $SrcContext -DestContainer $DestinationContainer -DestContext $DestContext -Force | Out-Null $DestBlob = Get-AzureStorageBlob -Container $DestinationContainer -Context $DestContext | where Name -EQ $DiskName $Result = Get-AzureStorageBlobCopyState -CloudBlob $DestBlob.ICloudBlob -Context $DestContext -WaitForComplete } if ($Result.Status -eq 'Failed') { Write-Log 'Failed:' Magenta $LogFile Write-Log " $($Result.StatusDescription)" Yellow $LogFile } else { Write-Log 'done in',"$($Duration.Hours):$($Duration.Minutes):$($Duration.Seconds) hh:mm:ss" Green,Cyan $LogFile } $Snapshot.Delete() } else { Write-Log 'failed' Magenta $LogFile } #region Validate copy success $DestBlob = Get-AzureStorageBlob -Container $DestinationContainer -Context $DestContext | where Name -EQ $DiskName $DestBlobSizeInBytes = Get-BlobBytes -Blob $DestBlob -IsPremiumAccount ($DestinationStorageAccount.Sku.Tier -eq 'Premium') if ($SourceBlobSizeInBytes -eq $DestBlobSizeInBytes) { Write-Log 'Validated successful disk/page blob copy' Green } else { Write-Log 'Destination blob/disk size is',$DestBlobSizeInBytes,'bytes which is different from the source blob/disk size of',$SourceBlobSizeInBytes,'bytes' Magenta,Yellow,Magenta,Yellow,Magenta break } #endregion #region Delete source if ($DeleteSource) { Write-Log 'Deleting source disk/page blob',"$SourceStorageAccount/$SourceContainer/$DiskName" Green,Cyan -NoNewLine $LogFile $PageBlob.ICloudBlob.Delete() if ($PageBlob = Get-AzureStorageBlob -Container $SourceContainer -Context $SrcContext | where { $_.Name -EQ $DiskName -and -not $_.ICloudBlob.IsSnapshot} ) { Write-Log 'failed to delete source disk/page blob' Magenta $LogFile } else { Write-Log 'done' Green $LogFile } } #endregion } #endregion } End { } } #endregion #region Graph API function Get-GraphAPIToken { <# .SYNOPSIS Function to get a Graph API token .DESCRIPTION Function to get a Graph API token Depends on Get-PSCredential function of the AZSBTools PS module .PARAMETER TenantId Your Azure Tenant Id such as mydomain.com .PARAMETER AppId App ID from Azure App registration at https://portal.azure.com/#blade/Microsoft_AAD_RegisteredApps/ApplicationsListBlade Example: '12abcabc-2233-4444-999-c3333f75555' .PARAMETER AppName This is the App Name from Azure App registration On first time use, the operator will prompted for the password, which is a secret obtained from the App 'Certificates and Secredts' link at https://portal.azure.com/#blade/Microsoft_AAD_RegisteredApps/ApplicationsListBlade To replace the save encrypted credential, use: Get-SBCredential 'myAppNameHere' -Refresh The saved encrypted credential is specific to the operator and computer where this function is invoked For more details see Get-SBCredential help .PARAMETER APIVersion API version such as v1.0 or beta https://docs.microsoft.com/en-us/graph/use-the-api#version .EXAMPLE Get-GraphAPIToken -TenantId 'mydomain.com' -AppId '12abcabc-2233-4444-999-c3333f75555' -AppName 'PowerShellAutomation99' .LINK https://superwidgets.wordpress.com/category/powershell/ .NOTES Function by Sam Boutros v0.1 - 17 September 2019 #> param( [Parameter(Mandatory=$true)][String]$TenantId, [Parameter(Mandatory=$true)][String]$AppId, [Parameter(Mandatory=$true)][String]$AppName, [Parameter(Mandatory=$false)][ValidateSet('v1.0','beta')][String]$APIVersion = 'v1.0' ) Begin { } Process { $Secret = Get-SBCredential $AppName $APIBaseUri = "https://graph.microsoft.com/$APIVersion" $secretEncoded = [System.Uri]::EscapeDataString($Secret.GetNetworkCredential().Password) $ParameterList = @{ Uri = "https://login.microsoftonline.com/$TenantId/oauth2/v2.0/token" Method = 'Post' ContentType = 'application/x-www-form-urlencoded' Body = "client_id=$AppId&scope=https%3A%2F%2Fgraph.microsoft.com%2F.default&client_secret=$secretEncoded&grant_type=client_credentials" } } End { (Invoke-RestMethod @ParameterList).access_token } } #endregion function Deploy-AzureARMVM { <# .SYNOPSIS Function to automate provisioning of Azure ARM VM(s) .DESCRIPTION Function to automate provisioning of Azure ARM VM(s) .PARAMETER SubscriptionName Name of existing Azure subscription .PARAMETER Location Name of Azure Data center/Location Example: 'eastus' To see location list use: Get-AzureRmLocation | sort Location | Select Location .PARAMETER ResourceGroup Name of Resource Group. Example: 'VMGroup17' The script will create it if it does not exist .PARAMETER AvailabilitySetName Example: 'Availability17' The script will create it if it does not exist .PARAMETER ConfirmShutdown This switch accepts $true or $False, and defaaults to $False If adding existing VMs to Availaibility set, the script must shut down the VMs .PARAMETER StorageAccountPrefix Only lower case letters and numbers, must be Azure (globally) unique .PARAMETER AdminName Example: 'myAdmin17' This will be the new VM local administrator .PARAMETER VMName Example: ('vm01','vm02') Name(s) of VM(s) to be created. Each is 15 characters maximum. If VMs exist, they will be added to Availability Set .PARAMETER VMSize Example: 'Standard_A1_v2' To see available sizes in this Azure location use: (Get-AzureRoleSize).RoleSizeLabel .PARAMETER WinOSImage This defaults to '2012-R2-Datacenter' Available options: '2008-R2-SP1','2012-Datacenter','2012-R2-Datacenter','2016-Datacenter','2016-Datacenter-Server-Core','2016-Datacenter-with-Containers','2016-Nano-Server' To see current options in a given Azure Location use: (Get-AzureRMVMImageSku -Location usgovvirginia -Publisher MicrosoftWindowsServer -Offer WindowsServer).Skus For more information see https://docs.microsoft.com/en-us/azure/virtual-machines/windows/cli-ps-findimage .PARAMETER vNetName Example: 'Seventeen' This will be the name of the virtual network to be created/updated if exist .PARAMETER vNetPrefix Example: '10.17.0.0/16' To be created/updated .PARAMETER SubnetName Example: 'vmSubnet' This will be the name of the subnet to be created/updated .PARAMETER SubnetPrefix Example: '10.17.0.0/24' Must be subset of vNetPrefix above - to be created/updated .PARAMETER LogFile' Path to log file where this scrit will log its commands and output Default is ".\Logs\Deploy-AzureARMVM-$($VMName -join '_')-$(Get-Date -Format 'ddMMMMyyyy_hh-mm-ss_tt').txt" .EXAMPLE Connect-AzureRmAccount -Environment AzureUSGovernment $myParamters = @{ SubscriptionName = 'Azure Government T1' Location = 'usgovvirginia' ResourceGroup = 'EncryptionTest01' AvailabilitySetName = 'AvailabilityTest01' ConfirmShutdown = $false StorageAccountPrefix = 'sam150318a' AdminName = 'myAdmin150318a' VMName = @('vm01','vm02','vm03') VMSize = 'Standard_A0' WinOSImage = '2016-Datacenter' vNetName = 'EncryptionTest01VNet' vNetPrefix = '10.3.0.0/16' SubnetName = 'vmSubnet' SubnetPrefix = '10.3.15.0/24' } Deploy-AzureARMVM @myParamters .LINK http://www.exigent.net/blog/microsoft-azure/provisioning-and-tearing-down-azure-virtual-machines/ .NOTES Function by Sam Boutros 3 January 2017 - v0.1 - Initial release 19 January 2017 - v0.2 Updated parameters - set to mandatory Updated Storage Account creation region, create a separate storage account for each VM Updated Initialize region; removing subscription login, adding input echo, adding error handling Added functionality to configure VMs in availability set 5 March 2018 - v0.3 Cosmetic updates #> [CmdletBinding(ConfirmImpact='Low')] Param( [Parameter(Mandatory=$true)][String]$SubscriptionName , # Example: 'Sam Test 1' # Name of existing Azure subscription [Parameter(Mandatory=$true)][String]$Location , # Example: 'eastus' # Get-AzureRmLocation | sort Location | Select Location [Parameter(Mandatory=$true)][String]$ResourceGroup , # Example: 'VMGroup17' # To be created if not exist [Parameter(Mandatory=$false)][String]$AvailabilitySetName , # Example: 'Availability17' # To be created if not exist [Parameter(Mandatory=$false)][Switch]$ConfirmShutdown = $false, # If adding existing VMs to Availaibility set, the script must shut down the VMs [Parameter(Mandatory=$false)][String]$StorageAccountPrefix , # To be created if not exist, only lower case letters and numbers, must be Azure unique [Parameter(Mandatory=$true)][String]$AdminName , # Example: 'myAdmin17' # This will be the new VM local administrator [Parameter(Mandatory=$true)][String[]]$VMName , # Example: ('vm01','vm02') # Name(s) of VM(s) to be created. Each is 15 characters maximum. If VMs exist, they will be added to Availability Set [Parameter(Mandatory=$true)][String]$VMSize , # Example: 'Standard_A1_v2' # (Get-AzureRoleSize).RoleSizeLabel to see available sizes in this Azure location [Parameter(Mandatory=$false)][ValidateSet('2008-R2-SP1','2012-Datacenter','2012-R2-Datacenter','2016-Datacenter','2016-Datacenter-Server-Core','2016-Datacenter-with-Containers','2016-Nano-Server')] [String]$WinOSImage = '2012-R2-Datacenter' , # https://docs.microsoft.com/en-us/azure/virtual-machines/windows/cli-ps-findimage [Parameter(Mandatory=$true)][String]$vNetName , # Example: 'Seventeen' # This will be the name of the virtual network to be created/updated if exist [Parameter(Mandatory=$true)][String]$vNetPrefix , # Example: '10.17.0.0/16' # To be created/updated [Parameter(Mandatory=$true)][String]$SubnetName , # Example: 'vmSubnet' # This will be the name of the subnet to be created/updated [Parameter(Mandatory=$true)][String]$SubnetPrefix , # Example: '10.17.0.0/24' # Must be subset of vNetPrefix above - to be created/updated [Parameter(Mandatory=$false)][String]$LogFile = ".\Logs\Deploy-AzureARMVM-$($VMName -join '_')-$(Get-Date -Format 'ddMMMMyyyy_hh-mm-ss_tt').txt" ) Begin { #region Initialize if (!(Test-Path (Split-Path $LogFile))) { New-Item -Path (Split-Path $LogFile) -ItemType directory -Force | Out-Null } Write-Log 'Input received:' Green $LogFile write-log " SubscriptionName: $SubscriptionName" Cyan $LogFile write-log " Location: $Location" Cyan $LogFile write-log " ResourceGroup: $ResourceGroup" Cyan $LogFile write-log " AvailabilitySetName: $AvailabilitySetName" Cyan $LogFile write-log " ConfirmShutdown: $ConfirmShutdown" Cyan $LogFile write-log " StorageAccountPrefix: $StorageAccountPrefix" Cyan $LogFile write-log " AdminName: $AdminName" Cyan $LogFile write-log " VMName(s): $($VMName -join ', ')" Cyan $LogFile write-log " VMSize: $VMSize" Cyan $LogFile write-log " vNetName: $vNetName" Cyan $LogFile write-log " vNetPrefix: $vNetPrefix" Cyan $LogFile write-log " SubnetName: $SubnetName" Cyan $LogFile write-log " SubnetPrefix: $SubnetPrefix" Cyan $LogFile $Cred = Get-SBCredential -UserName $AdminName #endregion #region Connect to Azure subscription Write-Log 'Connecting to Azure subscription',$SubscriptionName Green,Cyan $LogFile -NoNewLine try { $Result = Get-AzureRmSubscription –SubscriptionName $SubscriptionName -ErrorAction Stop | Select-AzureRmSubscription Write-Log 'done' Green $LogFile Write-Log ($Result | Out-String).Trim() Cyan $LogFile } catch { throw "unable to get Azure Subscription '$SubscriptionName'" } #endregion #region Create/Update Resource group Write-Log 'Create/Update Resource group',$ResourceGroup Green,Cyan $LogFile -NoNewLine try { $Result = New-AzureRmResourceGroup -Name $ResourceGroup -Location $Location -Force -ErrorAction Stop Write-Log 'done' Green $LogFile Write-Log ($Result | Out-String).Trim() Cyan $LogFile } catch { throw "Failed to create Resource Group '$ResourceGroup'" } #endregion #region Create/Update Subnet and vNet Write-Log 'Creating/updating vNet',$vNetName,$vNetPrefix,'and subnet',$SubnetName,$SubnetPrefix Cyan,Green,DarkYellow,Cyan,Green,DarkYellow $LogFile -NoNewLine $Subnet = New-AzureRmVirtualNetworkSubnetConfig -Name $SubnetName -AddressPrefix $SubnetPrefix $vNet = New-AzureRmVirtualNetwork -Name $vNetName -ResourceGroupName $ResourceGroup -Location $Location -AddressPrefix $vNetPrefix -Subnet $Subnet -Force Write-Log 'done' Green #endregion } Process { foreach ($Name in $VMName) { # Provision Azure VM(s) #region Create Storage Account if it does not exist $StorageAccountName = "stor$($StorageAccountPrefix.ToLower())$($Name.ToLower())" if ($StorageAccountName.Length -gt 20) { Write-Log 'Storage account name',$StorageAccountName,'is too long, using first 20 characters only..' Green,Yellow,Green $LogFile $StorageAccountName = $StorageAccountName.Substring(0,19) } Write-Log 'Creating Storage Account',$StorageAccountName Green,Cyan $LogFile try { $StorageAccount = Get-AzureRmStorageAccount -Name $StorageAccountName -ResourceGroupName $ResourceGroup -ErrorAction Stop Write-Log 'Using existing storage account',$StorageAccountName Green,Cyan $LogFile } catch { $i=0 $DesiredStorageAccountName = $StorageAccountName while (!(Get-AzureRmStorageAccountNameAvailability $StorageAccountName).NameAvailable) { $i++ $StorageAccountName = "$StorageAccountName$i" } if ($DesiredStorageAccountName -ne $StorageAccountName ) { Write-Log 'Storage account',$DesiredStorageAccountName,'is taken, using',$StorageAccountName,'instead (available)' Greem,Yellow,Green,Cyan,Green $LogFile } try { $Splatt = @{ ResourceGroupName = $ResourceGroup Name = $StorageAccountName SkuName = 'Standard_LRS' Kind = 'Storage' Location = $Location ErrorAction = 'Stop' } $StorageAccount = New-AzureRmStorageAccount @Splatt Write-Log 'Created storage account',$StorageAccountName Green,Cyan $LogFile } catch { Write-Log 'Failed to create storage account',$StorageAccountName Magenta,Yellow $LogFile throw $PSItem.exception.message } } #endregion #region Create/validate Availability Set if ($AvailabilitySetName) { Write-Log 'Creating/verifying Availability Set',$AvailabilitySetName Green,Cyan $LogFile try { $AvailabilitySet = Get-AzureRmAvailabilitySet -ResourceGroupName $ResourceGroup -Name $AvailabilitySetName -ErrorAction Stop Write-Log 'Availability Set',$AvailabilitySetName,'already exists' Green,Yellow,Green $LogFile Write-Log ($AvailabilitySet | Out-String).Trim() Cyan $LogFile } catch { try { $AvailabilitySet = New-AzureRmAvailabilitySet -ResourceGroupName $ResourceGroup -Name $AvailabilitySetName -Location $Location -ErrorAction Stop Write-Log 'Created Availability Set',$AvailabilitySetName Green,Cyan $LogFile } catch { Write-Log 'Failed to create Availability Set',$AvailabilitySetName Magenta,Yellow $LogFile throw $PSItem.exception.message } } if ($AvailabilitySet.Location -ne $Location) { Write-Log 'Unable to proceed, Availability set must be in the same location',$AvailabilitySet.Location,'as the desired VM location',$Location Magenta,Yellow,Magenta,Yellow $LogFile break } } #endregion try { $ExistingVM = Get-AzureRmVM -ResourceGroupName $ResourceGroup -Name $Name -ErrorAction Stop Write-Log 'VM',$ExistingVM.Name,'already exists' Green,Yellow,Gree $LogFile if ($AvailabilitySetName) { if ($ConfirmShutdown) { Write-Log 'Shutting down VM',$Name,'to add it to Availability set',$AvailabilitySetName Green,Cayn,Green,Cyan $LogFile Stop-AzureRmVM -Name $Name -Force -StayProvisioned -ResourceGroupName $ResourceGroup -Confirm:$false # Remove current VM Remove-AzureRmVM -ResourceGroupName $ResourceGroup -Name $Name -Force -Confirm:$false # Prepare to recreate VM $VM = New-AzureRmVMConfig -VMName $ExistingVM.Name -VMSize $ExistingVM.HardwareProfile.VmSize -AvailabilitySetId $AvailabilitySet.Id Set-AzureRmVMOSDisk -VM $VM -VhdUri $ExistingVM.StorageProfile.OsDisk.Vhd.Uri -Name $ExistingVM.Name -CreateOption Attach -Windows #Add Data Disks foreach ($Disk in $ExistingVM.StorageProfile.DataDisks) { Add-AzureRmVMDataDisk -VM $VM -Name $Disk.Name -VhdUri $Disk.Vhd.Uri -Caching $Disk.Caching -Lun $Disk.Lun -CreateOption Attach -DiskSizeInGB $Disk.DiskSizeGB } #Add NIC(s) foreach ($NIC in $ExistingVM.NetworkInterfaceIDs) { Add-AzureRmVMNetworkInterface -VM $VM -Id $NIC } # Recreate the VM as part of the Availability Set New-AzureRmVM -ResourceGroupName $ResourceGroup -Location $ExistingVM.Location -VM $VM -DisableBginfoExtension } else { Write-Log 'To add existing VM(s) to availability set, the VM(s) must be shut down. Use the','-ConfirmShutdown:$true','switch' Yellow,Cyan,Yellow $LogFile break } } } catch { Write-Log 'Preparing to create new VM',$Name Green,Cyan $LogFile Write-Log 'Requesting/updating public IP address assignment',"$Name-PublicIP" Green,Cyan $LogFile $PublicIp = New-AzureRmPublicIpAddress -Name "$Name-PublicIP" -ResourceGroupName $ResourceGroup -Location $Location -AllocationMethod Dynamic -Force Write-Log 'Provisining/updating vNIC',"$Name-vNIC" Green,Cyan $LogFile $vNIC = New-AzureRmNetworkInterface -Name "$Name-vNIC" -ResourceGroupName $ResourceGroup -Location $Location -SubnetId $vNet.Subnets[0].Id -PublicIpAddressId $PublicIp.Id -Force Write-Log 'Provisioning VM configuration object for VM',$Name Green,Cyan $LogFile if ($AvailabilitySetName) { $VM = New-AzureRmVMConfig -VMName $Name -VMSize $VMSize -AvailabilitySetId $AvailabilitySet.Id } else { $VM = New-AzureRmVMConfig -VMName $Name -VMSize $VMSize } Write-Log 'Configuring VM OS (Windows),',$Cred.UserName,'local admin' Green,Cyan,Green $LogFile $VM = Set-AzureRmVMOperatingSystem -VM $VM -Windows -ComputerName $Name -Credential $Cred -ProvisionVMAgent -EnableAutoUpdate Write-Log 'Selecting VM image - Latest',$WinOSImage Green,Cyan $LogFile $VM = Set-AzureRmVMSourceImage -VM $VM -PublisherName "MicrosoftWindowsServer" -Offer "WindowsServer" -Skus $WinOSImage -Version "latest" Write-Log 'Adding vNIC' Green $LogFile $VM = Add-AzureRmVMNetworkInterface -VM $VM -Id $vNIC.Id $VhdUri = "$($StorageAccount.PrimaryEndpoints.Blob.ToString())vhds/$($Name)-OsDisk1.vhd" Write-Log 'Configuring OS Disk',$VhdUri Green,Cyan $LogFile $VM = Set-AzureRmVMOSDisk -VM $VM -Name 'OSDisk' -VhdUri $VhdUri -CreateOption FromImage Write-Log 'Creating VM..' Green -NoNewLine New-AzureRmVM -ResourceGroupName $ResourceGroup -Location $Location -VM $VM Write-Log 'done' Green $LogFile $DoneVM = Get-AzureRmVM | where { $_.Name -eq $Name } | FT -a Write-Log ($DoneVM | Out-String).Trim() cyan $LogFile } } } End { if ($AvailabilitySetName) { $AvailabilitySet = Get-AzureRmAvailabilitySet -ResourceGroupName $ResourceGroup -Name $AvailabilitySetName $VMDomains = $AvailabilitySet.VirtualMachinesReferences | foreach { $VM = Get-AzureRMVM -Name (Get-AzureRmResource -Id $_.id).Name -ResourceGroup $ResourceGroup -Status [PSCustomObject][Ordered]@{ Name = $VM.Name FaultDomain = $VM.PlatformFaultDomain UpdateDomain = $VM.PlatformUpdateDomain } } Write-Log ($VMDomains | sort Name | FT -a | Out-String).Trim() Cyan $LogFile } } } function Tag-AzResource { [CmdletBinding(ConfirmImpact='Low')] Param( [Parameter(Mandatory=$true)][String]$ResourceId, [Parameter(Mandatory=$true)][HashTable]$TagList, [Parameter(Mandatory=$false)][String]$LogFile = ".\Tag-AzResource-$(Get-Date -Format 'ddMMMMyyyy_hh-mm-ss_tt').txt" ) Begin { } Process { try { $Resource = Get-AzResource -ResourceId $ResourceId -EA 1 } catch { Write-Log '[Tag-AzResource] Error:' Magenta $LogFile Write-Log $PSItem.Exception.Message Yellow $LogFile return } $OK2Save = $false if ($Resource.Tags) { [HashTable]$CurrentTags = $Resource.Tags foreach ($key in $TagList.Keys) { if (-not($CurrentTags.keys -icontains $key)) { Write-Log ' Tag',$key,'is not set for resource',$Resource.Name,'setting as',$TagList.$key Green,Cyan,Yellow,Cyan,Green,Cyan $LogFile $UpdatedTagList = $CurrentTags + @{ $key = $TagList.$key } $OK2Save = $true } elseif ($CurrentTags[$key] -eq $TagList[$key]) { Write-Log ' Tag',$key,'is already set for resource',$Resource.Name,'value:',$CurrentTags[$key],'skipping..' Green,Cyan,Green,Cyan,Green,Cyan,Green $LogFile } else { Write-Log ' Tag',$key,'is already set for resource',$Resource.Name,'value:',$CurrentTags[$key],'updating to',$TagList.$key Green,Cyan,Green,Cyan,Green,Yellow,Green,Cyan $LogFile $Resource.Tags.$key = $TagList.$key [HashTable]$UpdatedTagList = $Resource.Tags $OK2Save = $true } } } else { $UpdatedTagList = $TagList Write-Log ' No tags configured for resource',$Resource.Name,'adding tag(s)',($UpdatedTagList.Keys -join ','),'value(s)',($UpdatedTagList.Values -join ',') Green,Cyan,Green,Cyan,Green,Cyan $LogFile $OK2Save = $true } if ($OK2Save) { try { Set-AzResource -Tag $UpdatedTagList -ResourceId $ResourceId -Force -EA 1 | Out-Null Write-Log 'done' Green $LogFile } catch { Write-Log 'failed' Magenta $LogFile Write-Log $PSItem.Exception.Message Yellow $LogFile } } } End { } } function Tag-AzVM { <# .SYNOPSIS Function to apply one or more Azure resource tags to one or more VMs and its related objects .DESCRIPTION Function to apply one or more Azure resource tags to one or more VMs and its related objects. Curently this function supports the following related VM objects: NICs Managed OS Disks Managed Data Disks This function is intended for Azure ARM VMs not ASM VMs. .PARAMETER $VMObj This is an objct of Type Microsoft.Azure.Commands.Compute.Models.PSVirtualMachine that can be obtained via the Get-AzVM cmdlet of the Az.Compute PS module .PARAMETER TagList This is a HashTable of desired tags. Example: @{ COMPANY = 'my company' OWNER = 'Sam.Boutros' } .PARAMETER LogFile Path to the file where this function will save time-stamped entries of its console output .EXAMPLE Tag-AzVM -VMObj (Get-AzVM -Name myVMName -ResourceGroupName myResourceGroup) -TagList @{ CostCenter = 'myCostCenter'; COMPANY = 'myCompany' } .OUTPUTS None .LINK https://superwidgets.wordpress.com/category/powershell/ .NOTES Function by Sam Boutros v0.1 - 4 June 2018 - Initial release v0.2 - 14 June 2018 - Parameterized, added error handling and documentation v0.3 - 9 April 2020 - Rewrite to work with Az PS module instead of AzureRM, update logic #> [CmdletBinding(ConfirmImpact='Low')] Param( [Parameter(Mandatory=$true)][Microsoft.Azure.Commands.Compute.Models.PSVirtualMachine]$VMObj, [Parameter(Mandatory=$true)][HashTable]$TagList, [Parameter(Mandatory=$false)][String]$LogFile = ".\Tag-AzVM-$(Get-Date -Format 'ddMMMMyyyy_hh-mm-ss_tt').txt" ) Begin { } Process { Write-Log 'Processing VM',$VMObj.Name,'in resource group',$VMObj.ResourceGroupName Green,Cyan,Green,Cyan $LogFile # Tag VM Tag-AzResource -ResourceId $VMObj.Id -TagList $TagList -LogFile $LogFile # Tag managed OS disk if ($OSDiskId = $VMObj.StorageProfile.OsDisk.ManagedDisk.Id) { Tag-AzResource -ResourceId $OSDiskId -TagList $TagList -LogFile $LogFile } # Tag managed Data disks if ($DataDiskName = $VMObj.StorageProfile.DataDisks.ManagedDisk.Name) { foreach ($Name in $DataDiskName) { $Id = (Get-AzDisk -ResourceGroupName $VMObj.ResourceGroupName -DiskName $Name).Id Tag-AzResource -ResourceId $Id -TagList $TagList -LogFile $LogFile } } # Tag NICs if ($NICId = $VMobj.NetworkProfile.NetworkInterfaces.Id) { foreach ($Id in $NICId) { Tag-AzResource -ResourceId $Id -TagList $TagList -LogFile $LogFile } } } End { } } function Expand-Json { <# .SYNOPSIS Function to expand a custom PowerShell object in a more readable format .DESCRIPTION Function to expand a custom PowerShell object in a more readable format The ConvertFrom-Json cmdlet of the Microsoft.PowerShell.Utility module outputs a PS Custom Object that often contains sub objects and so on. This function expands all objects and displays the key/value pairs in a more humanly readable format - see the example .PARAMETER Json PS Custom Object, typically the output of ConvertFrom-Json cmdlet - see the example .PARAMETER Parent This is optional parameter used to show sub-objects when using the function recursively .PARAMETER LogFile This is an optional parameter that specifies the path to the log file where the script logs its progress This defaults to a file in the current folder where the script is running .EXAMPLE Get-Content E:\Scripts\ARMTemplates\Storage1.json | ConvertFrom-Json | Expand-Json where the contents of Storage1.json file are: { "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#", "contentVersion": "1.0.0.0", "parameters": { "storageAccountType": { "type": "string", "defaultValue": "Standard_LRS", "allowedValues": [ "Standard_LRS", "Standard_GRS", "Standard_ZRS", "Premium_LRS" ], "metadata": { "description": "Storage Account type" } } }, "variables": { "storageAccountName": "[concat(uniquestring(resourceGroup().id), 'standardsa')]" }, "resources": [ { "type": "Microsoft.Storage/storageAccounts", "name": "[variables('storageAccountName')]", "apiVersion": "2016-01-01", "location": "[resourceGroup().location]", "sku": { "name": "[parameters('storageAccountType')]" }, "kind": "Storage", "properties": { } } ], "outputs": { "storageAccountName": { "type": "string", "value": "[variables('storageAccountName')]" } } } The output of Get-Content E:\Scripts\ARMTemplates\Storage1.json | ConvertFrom-Json would look like: $schema : https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json# contentVersion : 1.0.0.0 parameters : @{storageAccountType=} variables : @{storageAccountName=[concat(uniquestring(resourceGroup().id), 'standardsa')]} resources : {@{type=Microsoft.Storage/storageAccounts; name=[variables('storageAccountName')]; apiVersion=2016-01-01; location=[resourceGroup().location]; sku=; kind=Storage; properties=}} outputs : @{storageAccountName=} which does not show sub-objects such as parameters.storageAccountType.allowedValues, parameters.storageAccountType.defaultValue, ... However, the output of Get-Content E:\Scripts\ARMTemplates\Storage1.json | ConvertFrom-Json | Expand-Json shows all objects, sub-objects, and their key/pair values: $schema: https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json# contentVersion: 1.0.0.0 outputs.storageAccountName.type: string outputs.storageAccountName.value: [variables('storageAccountName')] parameters.storageAccountType.allowedValues: Standard_LRS, Standard_GRS, Standard_ZRS, Premium_LRS parameters.storageAccountType.defaultValue: Standard_LRS parameters.storageAccountType.metadata.description: Storage Account type parameters.storageAccountType.type: string resources.apiVersion: 2016-01-01 resources.kind: Storage resources.location: [resourceGroup().location] resources.name: [variables('storageAccountName')] resources.sku.name: [parameters('storageAccountType')] resources.type: Microsoft.Storage/storageAccounts variables.storageAccountName: [concat(uniquestring(resourceGroup().id), 'standardsa')] .LINK https://superwidgets.wordpress.com/ .NOTES Function by Sam Boutros v0.1 - 28 March 2018 v0.2 - 20 June 2019 - Added Log feature to allow logging output to file #> [CmdletBinding(ConfirmImpact='Low')] Param( [Parameter(Mandatory=$true,ValueFromPipeLine=$true,ValueFromPipeLineByPropertyName=$true)][PSCustomObject]$JSON, [Parameter(Mandatory=$false)][String[]]$Parent, [Parameter(Mandatory=$false)][String]$LogFile = ".\Expand-Json - $(Get-Date -Format 'ddMMMMyyyy_hh-mm-ss_tt').txt" ) Begin { Write-Verbose "JSON: $($JSON | Out-String)" Write-Verbose "Parent: $($Parent -join '.')" } Process { foreach ($NoteProperty in ($JSON | Get-Member -MemberType NoteProperty)) { if ($NoteProperty.Definition -match 'PSCustomObject') { Expand-Json -JSON $JSON.($NoteProperty.Name) -Parent ($Parent + $NoteProperty.Name) } else { if (($JSON.($NoteProperty.Name) -join '').Trim()) { Write-Log "$(($Parent + $NoteProperty.Name) -join '.'):",($JSON.($NoteProperty.Name) -join ', ') Green,Cyan $LogFile } else { Expand-Json -JSON $JSON.($NoteProperty.Name) -Parent ($Parent + $NoteProperty.Name) -EA 0 } } } } End { } } function Report-AzureRMVM { # Requires -Modules Az, ImportExcel # Requires -Version 5 <# .SYNOPSIS Function to report on Azure VM population in a given Azure subscription .DESCRIPTION Function to report on Azure VM population in a given Azure subscription The report is saved to xlsx file This function uses ImportExcel PowerShell module available in the PowerShell gallery This function reports on Azure ARM VMs only (not classic ASM VMs) .PARAMETER LoginName The username required to authenticate to Azure Example: samb@mydomain.com .PARAMETER SubscriptionName The Azure subscription name such as 'My Dev EA subscription' .PARAMETER OutputFile Path to xlsx file, where the function will write its output .PARAMETER LogFile This is an optional parameter that specifies the path to the log file where the script logs its progress This defaults to a file in the current folder where the script is running .EXAMPLE Report-AzureRMVM -LoginName 'samb@mydomain.com' -SubscriptionName 'my azure subscription name here' -Verbose .EXAMPLE $SubscriptionList = Get-AzSubscription | where Name -Match Citrix $myVMList = foreach ($SubscriptionName in $SubscriptionList.Name) { Report-AzureRMVM -LoginName 'does not matter' -SubscriptionName $SubscriptionName -Verbose } $OutputFile = ".\Report-AzureRMVM - $(Get-Date -Format 'ddMMMMyyyy_hh-mm-ss_tt').xlsx" $myVMList | Export-Excel -Path $OutputFile -AutoSize -FreezeTopRowFirstColumn -ConditionalText $( ($myVMList | Get-Member -MemberType NoteProperty).Name | foreach { New-ConditionalText $_ White SteelBlue } ) This example will create a single report for VMs from several subscriptions .OUTPUTS Array of PS Custom objects, one for each ARM VM found with the following properties/example: VMName : AZ-abcBDEV-01 ResourceGroup : AZ-abcEV-RG Status : VM running Subscription : abc Enterprise Dev/Test Size : Standard_D2s_v3 Cores : 2 RAM(GB) : 8 HybridLicense : False Location : eastus MACAddress : 00-0D-3A-1C-87-11 IPv4Address : 172.129.132.112 AdminName : cdabcadmin OperatingSystem : Windows OSDiskSize(GB) : 127 DataDisks : (AZ-abcBDEV-01_SQLDATA, 1028 GB, LUN 0), (AZ-abcBDEV-01_SQLLOG, 1028 GB, LUN 1) .LINK https://superwidgets.wordpress.com/category/powershell/ .NOTES Function by Sam Boutros v0.1 - 4 June 2018 - Initial release v0.2 - 14 June 2018 - Parameterized, added error handling and documentation v0.3 - 23 January 2019 - Added logfile parameter, updated subscription login section, added HybridLicense property to output v0.4 - 28 February 2019 - Added Status (running/deallocated) v0.5 - 24 May 2019 - Update to use AZ module instead of AzureRM v0.6 - 7 April 2020 - Added AzLogon Switch to bypass Azure Logon check (for use with Azure Cloud Shell) Added auto-install of ImportExcel PS module #> [CmdletBinding(ConfirmImpact='Low')] Param( [Parameter(Mandatory=$true)][String]$LoginName, [Parameter(Mandatory=$true)][String]$SubscriptionName, [Parameter(Mandatory=$false)][Switch]$AzLogon, [Parameter(Mandatory=$false)][String]$OutputFile = ".\Report-AzureRMVM - $SubscriptionName - $(Get-Date -Format 'ddMMMMyyyy_hh-mm-ss_tt').xlsx", [Parameter(Mandatory=$false)][String]$LogFile = ".\Report-AzureRMVM - $SubscriptionName - $(Get-Date -Format 'ddMMMMyyyy_hh-mm-ss_tt').txt" ) Begin { if ($AzLogon) { if (-not ($Login = Login-AzSubscription -LoginName $LoginName -SubscriptionName $SubscriptionName -LogFile $LogFile)) { break } } if (-not (Get-Module Import-Excel -ListAvailable)) { Install-Module ImportExcel -Force } if ($OutputFile -match '/' ) { $OutputFile = $OutputFile.Replace('/','_') } } Process { $VMList = Get-AzVM -WA 0 -EA 0 if (-not $VMList) { Write-Log 'No ARM VMs found in subscription',$SubscriptionName Green,Yellow $LogFile break } $LocationList = $VMList.Location | select -Unique $VMTagList = $VMList | % { $_.Tags.Keys } | % { $_.ToString().ToLower().Trim() } | select -Unique | sort $ResourceGroupList = $VMList.ResourceGroupName | select -Unique Write-Log 'Identified',$VMList.Count,'VMs' Green,Cyan,Green $Logfile Write-Log 'Identified Azure site(s)',($LocationList -join ', ') Green,Cyan $Logfile Write-Log 'Identified',$ResourceGroupList.Count,'Resource Groups' Green,Cyan,Green $Logfile $myVMList = foreach ($VM in $VMList) { Write-Verbose "Processing VM ($($VM.Name)) in Resource Group ($($VM.ResourceGroupName))" $VMSize = Get-AzVMSize -Location $VM.Location | where { $_.Name -eq $VM.HardWareProfile.VmSize } $myOutput = [PSCustomObject][Ordered]@{ VMName = $VM.Name ResourceGroup = $VM.ResourceGroupName Status = (Get-AzVM -ResourceGroupName $VM.ResourceGroupName -Name $VM.Name -Status -WA 0).Statuses[1].DisplayStatus Subscription = $SubscriptionName Size = $VM.HardWareProfile.VmSize Cores = $VMSize.NumberOfCores 'RAM(GB)' = $VMSize.MemoryInMB/1KB HybridLicense = $(if ($VM.LicenseType -eq 'Windows_Server') { $true } else { $false }) Location = $VM.Location MACAddress = ($VM.NetworkProfile.NetworkInterfaces.id | foreach { (Get-AzResource -ResourceId $_).Properties.MacAddress }) -join ', ' IPv4Address = ((Get-AzNetworkInterface -ResourceGroupName $VM.ResourceGroupName | where {$PSItem.virtualmachine.id -match $VM.Name } | Get-AzNetworkInterfaceIpConfig).PrivateIPAddress) -join ', ' AdminName = $VM.OSProfile.AdminUsername OperatingSystem = $VM.StorageProfile.OsDisk.OsType 'OSDiskSize(GB)' = $VM.StorageProfile.OsDisk.DiskSizeGB DataDisks = $( if ($VM.StorageProfile.DataDisks) { ($VM.StorageProfile.DataDisks | foreach { "($($_.Name), $($_.DiskSizeGB) GB, LUN $($_.Lun))" }) -join ', ' } else { 'None' } ) } foreach ($TagName in $VMTagList) { $myOutput | Add-Member -MemberType NoteProperty -Name "Tag: $TagName" -Value $( $myTagList = $VM.Tags.Keys | foreach { "$_=$($VM.Tags.$_)" } if ($FoundTag = $myTagList | where { $_ -match $TagName }) { $FoundTag.Split('=')[1] } ) } $myOutput } } End { try { if (Test-Path $OutputFile) { Remove-Item -Path $OutputFile -Force -Confirm:$false -EA 1 } $myVMList | Export-Excel -Path $OutputFile -ConditionalText $( ($myVMList | Get-Member -MemberType NoteProperty).Name | foreach { New-ConditionalText $_ White SteelBlue } ) -AutoSize -FreezeTopRowFirstColumn } catch { Write-Log 'Output file',$OutputFile,'already open!!??' Magenta,Yellow,Magenta $Logfile } $myVMList } } function Set-AzVMHybridLicense { # Requires -Modules Az # Requires -Version 5 <# .SYNOPSIS Function to enable/disable Windows Hybrid Licensing feature on a given Azure VM .DESCRIPTION Function to enable/disable Windows Hybrid Licensing feature on a given Azure VM This function uses Az PowerShell module available in the PowerShell gallery .PARAMETER LoginName The username required to authenticate to Azure Example: samb@mydomain.com .PARAMETER SubscriptionName The Azure subscription name such as 'My Dev EA subscription' .PARAMETER VMName The name of the VM. This is a required parameter .PARAMETER ResourceGroupName The name of the Resource Group where the VM lives. This is only required if you have more than1 VM with the same name in the provided subscription .PARAMETER EnableHybridLicensing This is a switch that defaults to true causing the function to enable Windows Hybrid Licensing feature When set to false, the function disables the Windows Hybrid Licensing feature for the given VM .PARAMETER LogFile This is an optional parameter that specifies the path to the log file where the script logs its progress This defaults to a file in the current folder where the script is running .EXAMPLE Set-AzVMHybridLicense -LoginName 'samb@mydomain.com' -SubscriptionName 'my azure subscription name here' -VMName 'myvm1' .LINK https://superwidgets.wordpress.com/category/powershell/ .NOTES Function by Sam Boutros v0.1 - 23 January 2019 v0.2 - 25 January 2019 - Added logic to weed out Linux VMs v0.3 - 3 June 2019 - Updated to use Az module instead of AzureRM #> [CmdletBinding(ConfirmImpact='Low')] Param( [Parameter(Mandatory=$true)][String]$LoginName, [Parameter(Mandatory=$true)][String]$SubscriptionName, [Parameter(Mandatory=$true)][String]$VMName, [Parameter(Mandatory=$false)][String]$ResourceGroupName, [Parameter(Mandatory=$false)][Switch]$EnableHybridLicensing = $true, [Parameter(Mandatory=$false)][String]$LogFile = ".\Set-AzVMHybridLicense - $VMName - $(Get-Date -Format 'ddMMMMyyyy_hh-mm-ss_tt').txt" ) Begin { if (-not ($Login = Login-AzSubscription -LoginName $LoginName -SubscriptionName $SubscriptionName -LogFile $LogFile)) { break } } Process { $Proceed = $false if ($VM = Get-AzVM | where Name -EQ $VMName) { if ($VM.Count -gt 1) { if ($ResourceGroupName) { if ($VM = Get-AzVM -ResourceGroupName $ResourceGroupName -Name $VMName) { $Proceed = $true } else { Write-Log 'No VM named',$VMName,'found in subscription',$SubscriptionName,'under Resource Group',$ResourceGroupName Magenta,Yellow,Magenta,Yellow,Magenta,Yellow $LogFile } } else { Write-Log 'More than 1 VM named',$VMName,'found in subscription', $SubscriptionName Magenta,Yellow,Magenta,Yellow $LogFile Write-Log ' You must specify ''ResourceGroupName'' parameter for this VM' Yellow $LogFile } } else { $Proceed = $true } } else { Write-Log 'VM',$VMName,'not found in subscription', $SubscriptionName Magenta,Yellow,Magenta,Yellow $LogFile } if ($VM.StorageProfile.OsDisk.OsType -ne 'Windows') { Write-Log 'VM',$VM.Name,'has OS',$VM.StorageProfile.OsDisk.OsType,'skipping..' Green,Cyan,Green,Yellow,Green $LogFile $Proceed = $false } if ($Proceed) { if ($VM.LicenseType -eq 'Windows_Server') { if ($EnableHybridLicensing) { Write-Log 'Windows hybrid licensing for VM',$VM.Name,'in Resource Group',$VM.ResourceGroupName,'is already enabled' Green,Cyan,Green,Cyan,Yellow $LogFile } else { Write-Log 'Disabling Windows hybrid licensing for VM',$VM.Name,'in Resource Group',$VM.ResourceGroupName Green,Cyan,Green,Cyan $LogFile -NoNewLine $VM.LicenseType = 'None' Update-AzVM -ResourceGroupName $VM.ResourceGroupName -VM $VM | Out-Null $VM = Get-AzureRmVM -ResourceGroupName $VM.ResourceGroupName -Name $VM.Name if ($VM.LicenseType -eq 'Windows_Server') { Write-Log 'failed' Yellow $LogFile } else { Write-Log 'done and validated' Green $LogFile } } } else { if ($EnableHybridLicensing) { Write-Log 'Enabling Windows hybrid licensing for VM',$VM.Name,'in Resource Group',$VM.ResourceGroupName Green,Cyan,Green,Cyan $LogFile -NoNewLine $VM.LicenseType = 'Windows_Server' Update-AzVM -ResourceGroupName $VM.ResourceGroupName -VM $VM | Out-Null $VM = Get-AzVM -ResourceGroupName $VM.ResourceGroupName -Name $VM.Name if ($VM.LicenseType -eq 'Windows_Server') { Write-Log 'done and validated' Green $LogFile } else { Write-Log 'failed' Yellow $LogFile } } else { Write-Log 'Windows hybrid licensing for VM',$VM.Name,'in Resource Group',$VM.ResourceGroupName,'is already disabled' Green,Cyan,Green,Cyan,Yellow $LogFile } } } } End { } } function Report-AzureClassicResources { # Requires -Modules AzureRM # Requires -Version 5 <# .SYNOPSIS Function to report on Azure classic ASM in a given Azure subscription .DESCRIPTION Function to report on Azure classic ASM in a given Azure subscription This function uses AzureRM PowerShell module available in the PowerShell gallery .PARAMETER LoginName The username required to authenticate to Azure Example: samb@mydomain.com .PARAMETER SubscriptionName The Azure subscription name such as 'My Dev EA subscription' .PARAMETER LogFile This is an optional parameter that specifies the path to the log file where the script logs its progress This defaults to a file in the current folder where the script is running .EXAMPLE Report-AzureClassicResources -LoginName 'samb@mydomain.com' -SubscriptionName 'my azure subscription name here' -Verbose .OUTPUTS Microsoft.Azure.Commands.ResourceManager.Cmdlets.SdkModels.PSResource objects for each classic ASM resource found Example: Name : txxxxxxxx8 ResourceGroupName : aaaServer ResourceType : Microsoft.ClassicCompute/virtualMachines Location : eastus ResourceId : /subscriptions/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/resourceGroups/aaaServer/providers/Microsoft.ClassicCompute/virtualMachines/txxxxxxxx8 .LINK https://superwidgets.wordpress.com/category/powershell/ .NOTES Function by Sam Boutros v0.1 - 6 February 2019 #> [CmdletBinding(ConfirmImpact='Low')] Param( [Parameter(Mandatory=$true)][String]$LoginName, [Parameter(Mandatory=$true)][String]$SubscriptionName, [Parameter(Mandatory=$false)][String]$LogFile = ".\Report-AzureClassicResources - $SubscriptionName - $(Get-Date -Format 'ddMMMMyyyy_hh-mm-ss_tt').txt" ) Begin { if (-not ($Login = Login-AzureRMSubscription -LoginName $LoginName -SubscriptionName $SubscriptionName -LogFile $LogFile)) { break } } Process { $ResourceProviderList = Get-AzureRmResourceProvider -ListAvailable | where ProviderNamespace -Match 'Microsoft' | select ProviderNamespace,ResourceTypes $ResourceTypeList = foreach ($Provider in $ResourceProviderList) { foreach ($Type in $Provider.ResourceTypes) { "$($Provider.ProviderNamespace)/$($Type.ResourceTypeName)" } } $ClassicTypes = $ResourceTypeList -match 'classic' | sort Write-Log 'Reporting on',$ClassicTypes.Count,'classic ASM resources types' Green,Cyan,Green $LogFile Write-Verbose ($ClassicTypes | Out-String).Trim() if ($ClassicResourceList = Get-AzureRmResource | where { $_.ResourceType -in $ClassicTypes }) { Write-Log 'Identified',$ClassicResourceList.Count,'classic ASM resources in subscription',$SubscriptionName Green,Yellow,Green,Yellow $LogFile $ClassicResourceList } else { Write-Log 'No classic ASM resources found in subscription', $SubscriptionName Green,Cyan $LogFile } } End { } } function Report-AzureResourceTags { # Requires -Modules AZ,ImportExcel # Requires -Version 5 <# .SYNOPSIS Function to report on Azure Tags of ARM resources in a given Azure subscription .DESCRIPTION Function to report on Azure Tags of ARM resources in a given Azure subscription This function uses and depends on Az and ImportExcel PowerShell modules available in the PowerShell gallery .PARAMETER SubscriptionId The Azure subscription Id such as 'My Dev EA subscription' that can be obtained by Get-AZSubscription .PARAMETER LogFile This is an optional parameter that specifies the path to the log file where the script logs its progress This defaults to a file in the current folder where the script is running .PARAMETER Output This is an optional parameter that specifies the path to the XLSX file where the script Excel output report is saved This defaults to a file in the current folder where the script is running .EXAMPLE Report-AzureResourceTags -LoginName 'samb@mydomain.com' -SubscriptionName 'my azure subscription name here' .OUTPUTS PowerShell object for each ARM resource found with the following properties/example Example: SubscriptionName : my azure subscription name here ResourceName : wxxx9170 ResourceGroupName : Wxxxr ResourceType : Microsoft.Storage/storageAccounts ResourceLocation : eastus Note that there will be an additional property for each Azure tag found in the given subscription .LINK https://superwidgets.wordpress.com/category/powershell/ .NOTES Function by Sam Boutros v0.1 - 6 February 2019 v0.2 - 9 May 2019 - update for AZ module instead of AzureRM v0.3 - 13 August 2020 - Switching to SubscriptionId instead SubscriptionName input since Subscription Name is not necessarily unque within the same Azure tenant - Removing the Azure login requirement/check, expecting to be logged into an Azure tenant before invoking this function #> [CmdletBinding(ConfirmImpact='Low')] Param( [Parameter(Mandatory=$true)][String]$SubscriptionId, [Parameter(Mandatory=$false)][String]$OutputFile = ".\Report-AzureResourceTags - $SubscriptionName - $(Get-Date -Format 'ddMMMMyyyy_hh-mm-ss_tt').xlsx", [Parameter(Mandatory=$false)][String]$LogFile = ".\Report-AzureResourceTags - $SubscriptionName - $(Get-Date -Format 'ddMMMMyyyy_hh-mm-ss_tt').txt" ) Begin { try { } catch { } } Process { <# Write-Log 'Checking if there are any classic ASM resources..' Green $LogFile if ($ClassicResources = Report-AzureClassicResources -LoginName $LoginName -SubscriptionName $SubscriptionName) { Write-Log 'skipping classic ASM reources..' Green $LogFile } #> if ($ResourceList = Get-AzResource | where ResourceType -NotMatch 'classic') { $TagList = @() $TagList += ($ResourceList | % { $_.Tags | % { $_.Keys } }) -notmatch 'hidden' | select -Unique if ($TagList) { Write-Log 'Identified',$ResourceList.Count,'ARM resources bearing',$TagList.Count,'unique tag(s)..' Green,Cyan,Green,Cyan,Green $LogFile # Create output object definition with dynamic property list (tags) $Proplist = @('SubscriptionName','ResourceName','ResourceGroupName','ResourceType','Location') $TagList | foreach { $Proplist += "Tag:$_" -as [String] } $myOutput = foreach ($Resource in $ResourceList) { # Instantiate output object with dynamic property list (tags) $myObj = New-Object -TypeName PSObject $Proplist | foreach { Add-Member -InputObject $myObj -MemberType NoteProperty -Name $_ -Value $null -EA 0 } # Populate output object properties $myObj.SubscriptionName = $SubscriptionName $myObj.ResourceName = $Resource.Name $myObj.ResourceGroupName = $Resource.ResourceGroupName $myObj.ResourceType = $Resource.ResourceType $myObj.Location = $Resource.Location foreach ($Tag in $TagList) { $myObj.("Tag:$Tag") = $Resource.Tags.$Tag } $myObj } Remove-Item -Path $OutputFile -Force -Confirm:$false -EA 0 try { $myOutput | Export-Excel -Path $OutputFile -ConditionalText $( ($myOutput | Get-Member -MemberType NoteProperty).Name | foreach { New-ConditionalText $_ White SteelBlue } ) -AutoSize -FreezeTopRowFirstColumn } catch { Write-Log 'Output file',$OutputFile,'already open!!??' Magenta,Yellow,Magenta $Logfile } $myOutput } else { Write-Log 'Identified',$ResourceList.Count,'ARM resources bearing','NO','tags..' Green,Cyan,Green,yellow,Green $LogFile } # if $TagList } else { Write-Log 'No ARM resources found in subscription',$SubscriptionName Magenta,Yellow $LogFile } # if $ResourceList } End { } } function Report-AzureCustomRBACRoles { # Requires -Modules AZ,ImportExcel # Requires -Version 5 <# .SYNOPSIS Function to report on Azure custom RBAC roles in one or more Azure subscriptions .DESCRIPTION Function to report on Azure custom RBAC roles in one or more Azure subscriptions This function uses and depends on Az and ImportExcel PowerShell modules available in the PowerShell gallery This function expects to be authenticated to Azure before it's invoked (Connect-AzAccount) .PARAMETER SubscriptionId One or more Azure subscription Ids such as 'abcdabcd-abcd-abcd-abcd-abcdabcdabcd' .PARAMETER LogFile This is an optional parameter that specifies the path to the log file where the script logs its progress This defaults to a file in the current folder where the script is running .PARAMETER Output This is an optional parameter that specifies the path to the XLSX file where the script Excel output report is saved This defaults to a file in the current folder where the script is running .EXAMPLE Report-AzureCustomRBACRoles -SubscriptionId 'abcdabcd-abcd-abcd-abcd-abcdabcdabcd' .EXAMPLE $CustomRoles = Report-AzureCustomRBACRoles -SubscriptionId (Get-AzSubscription).Id .OUTPUTS PowerShell object for each ARM resource found with the following properties/example Example: SubscriptionName : my azure subscription name here SubscriptionId : abcdabcd-abcd-abcd-abcd-abcdabcdabcd Role : Azure Infra Admin AssignedTo : user1@domain1.com, user2@domain1.com, AD-Group1 Actions : * NotActions : Microsoft.Authorization/*/Delete, Microsoft.Authorization/*/Write, Note that there will be an additional property for each Azure tag found in the given subscription .LINK https://superwidgets.wordpress.com/category/powershell/ .NOTES Function by Sam Boutros v0.1 - 10 May 2019 #> [CmdletBinding(ConfirmImpact='Low')] Param( [Parameter(Mandatory=$true)][String[]]$SubscriptionId, [Parameter(Mandatory=$false)][String]$OutputFile = ".\Report-AzureCustomRBACRoles - $(Get-Date -Format 'ddMMMMyyyy_hh-mm-ss_tt').xlsx", [Parameter(Mandatory=$false)][String]$LogFile = ".\Report-AzureCustomRBACRoles - $(Get-Date -Format 'ddMMMMyyyy_hh-mm-ss_tt').txt" ) Begin { try { $AllSubscriptionList = Get-AzSubscription -EA 1 } catch { Write-Log 'Unable to list subscriptions','are we logged on to Azure?' Magenta,Yellow $LogFile break } if ($SubscriptionList = $SubscriptionId | where { $_ -in $AllSubscriptionList.Id } | select -Unique) { $SubscriptionList = $SubscriptionList | foreach { Get-AzSubscription -SubscriptionId $_ } Write-Log 'The following',$SubscriptionList.Count,'subscriptions are found under the current tenant:' Green,Cyan,Green $LogFile Write-Log ($SubscriptionList.Name | Out-String).Trim() Cyan $LogFile } else { Write-Log 'The provided subscription Id(s)','are not found under the current tenant' Magenta,Yellow $LogFile break } } Process { $CustomRoles = foreach ($Subscription in $SubscriptionList) { Get-AzSubscription -SubscriptionId $Subscription.Id | Set-AzContext | Out-Null Write-Log 'Checking for RBAC custom roles in subscription',$Subscription.Name Green,Cyan $LogFile -NoNewLine try { $RoleList = Get-AzRoleDefinition -Custom -EA 1 } catch { Write-Log 'no access -' Magenta $LogFile -NoNewLine } if ($RoleList) { Write-Log 'found',$RoleList.count,'custom roles' Green,Yellow,Green $LogFile foreach ($Role in $RoleList) { [PSCustomObject][Ordered]@{ SubscriptionName = $Subscription.Name SubscriptionId = $Subscription.Id Role = $Role.Name AssignedTo = (Get-AzRoleAssignment -RoleDefinitionName $Role.Name).DisplayName -join ', ' Actions = $Role.Actions -join ', ' NotActions = $Role.NotActions -join ', ' } } } else { Write-Log 'found','no','custom roles' Green,Yellow,Green $LogFile } } } End { Remove-Item -Path $OutputFile -Force -Confirm:$false -EA 0 if ($CustomRoles) { try { $CustomRoles | Export-Excel -Path $OutputFile -EA 1 -AutoSize -FreezeTopRowFirstColumn -ConditionalText $( ($CustomRoles | Get-Member -MemberType NoteProperty).Name | foreach { New-ConditionalText $_ White SteelBlue } ) } catch { Write-Log 'Output file',$OutputFile,'already open!!??' Magenta,Yellow,Magenta $Logfile } } $CustomRoles } } function Deploy-AzRBACRoleDefinition { <# .SYNOPSIS Function to deploy custom RBAC role definitions in one or more Azure subscriptions .DESCRIPTION Function to deploy the following custom RBAC role definitions in one or more Azure subscriptions 1. Network Admin: Manage Vnets, Subnets, Express Routes and Routing and Switching Manage NSGs and ASGs, Manage WAF Devices, Manage Internal and External Load Balancers "Actions": "Microsoft.Network/*", "Microsoft.Compute/*/read", "Microsoft.Resources/deployments/*", "Microsoft.Resources/deployments/validate/action", "Microsoft.Resources/subscriptions/resourceGroups/read", "Microsoft.Support/*" 2. Infra Admin: Access to all Resources except Networking and user access administration, Manage VMs, Availability Sets, Assign Static IP, Static MAC, Add or Remove NICs "Actions": "*" "NotActions": "Microsoft.Authorization/*/Delete", "Microsoft.Authorization/*/Write", "Microsoft.Authorization/elevateAccess/Action", "Microsoft.Network/applicationGateways/delete", "Microsoft.Network/dnsZones/delete", "Microsoft.Network/expressRouteCrossConnections/delete", "Microsoft.Network/expressRouteGateways/delete", "Microsoft.Network/expressRouteCircuits/delete", "Microsoft.Network/expressRoutePorts/delete", "Microsoft.Network/frontDoors/delete", "Microsoft.Network/networkWatchers/delete", "Microsoft.Network/routeFilters/delete", "Microsoft.Network/routeTables/delete", "Microsoft.Network/serviceEndpointPolicies/delete", "Microsoft.Network/trafficManagerProfiles/delete", "Microsoft.Network/virtualNetworkGateways/delete", "Microsoft.Network/loadBalancers/delete", "Microsoft.Network/networkSecurityGroups/delete", "Microsoft.Network/virtualNetworks/delete", "Microsoft.Network/localNetworkGateways/delete", "Microsoft.Network/applicationGateways/write", "Microsoft.Network/dnsZones/write", "Microsoft.Network/expressRouteCrossConnections/write", "Microsoft.Network/expressRouteGateways/write", "Microsoft.Network/expressRouteCircuits/write", "Microsoft.Network/expressRoutePorts/write", "Microsoft.Network/frontDoors/write", "Microsoft.Network/networkWatchers/write", "Microsoft.Network/routeFilters/write", "Microsoft.Network/routeTables/write", "Microsoft.Network/serviceEndpointPolicies/write", "Microsoft.Network/trafficManagerProfiles/write", "Microsoft.Network/virtualNetworkGateways/write", "Microsoft.Network/loadBalancers/write", "Microsoft.Network/networkSecurityGroups/write", "Microsoft.Network/virtualNetworks/write", "Microsoft.Network/localNetworkGateways/write", "Microsoft.Blueprint/blueprintAssignments/write", "Microsoft.Blueprint/blueprintAssignments/delete" 3. Tag Editor: Manage (Add/modify/delete) Azure tags for VMs, VM disks, and VM NICs "Actions": "*/read", "Microsoft.Compute/VirtualMachines/write", "Microsoft.Compute/Disks/write", "Microsoft.Network/networkInterfaces/write", "Microsoft.Resources/subscriptions/resourceGroups/read", "Microsoft.Support/*" This function uses and depends on Az PowerShell module available in the PowerShell gallery This function expects to be authenticated to Azure before it's invoked (Connect-AzAccount) .PARAMETER SubscriptionId One or more Azure subscription Ids such as 'abcdabcd-abcd-abcd-abcd-abcdabcdabcd' .PARAMETER RoleList One or more of the roles defined in this script .PARAMETER LogFile This is an optional parameter that specifies the path to the log file where the script logs its progress This defaults to a file in the current folder where the script is running .EXAMPLE Deploy-AzRBACRoleDefinition -SubscriptionId 'abcdabcd-abcd-abcd-abcd-abcdabcdabcd' .LINK https://superwidgets.wordpress.com/category/powershell/ .NOTES Function by Sam Boutros v0.1 - 14 May 2019 v0.2 - 3 June 2019 - Updated built-in help to provide role definition details v0.3 - 10 April, 2020 - Added TagEditor role, added RoleList parameter #> [CmdletBinding(ConfirmImpact='Low')] Param( [Parameter(Mandatory=$true)][String[]]$SubscriptionId, [Parameter(Mandatory=$true)][ValidateSet('NetworkAdmin','InfraAdmin','TagEditor')][String[]]$RoleList, [Parameter(Mandatory=$false)][String]$LogFile = ".\Deploy-AzRBACRoleDefinition - $(Get-Date -Format 'ddMMMMyyyy_hh-mm-ss_tt').txt" ) Begin { try { $AllSubscriptionList = Get-AzSubscription -EA 1 } catch { Write-Log 'Unable to list subscriptions','are we using AZ module and logged on to Azure?' Magenta,Yellow $LogFile break } if ($SubscriptionList = $SubscriptionId | where { $_ -in $AllSubscriptionList.Id } | select -Unique) { $SubscriptionList = $SubscriptionList | foreach { Get-AzSubscription -SubscriptionId $_ } Write-Log 'The following',$SubscriptionList.Count,'subscriptions are found under the current tenant:' Green,Cyan,Green $LogFile Write-Log ($SubscriptionList.Name | Out-String).Trim() Cyan $LogFile } else { Write-Log 'The provided subscription Id(s)','are not found under the current tenant' Magenta,Yellow $LogFile break } } Process { foreach ($Subscription in $SubscriptionList) { $Subscription | Set-AzContext | Out-Null $JSONFile = New-TemporaryFile if ('TagEditor' -in $RoleList) { # Putting the subscription Id in the Role definition name since they must be all unique in AAD !!?? $RoleName = "TagEditor_($($Subscription.Id.ToCharArray()[-8..-1] -join ''))" @" { "Name": "$RoleName", "Description": "Manage (Add/modify/delete) Azure tags for VMs, VM disks, and VM NICs", "Actions": [ "*/read", "Microsoft.Compute/VirtualMachines/write", "Microsoft.Compute/Disks/write", "Microsoft.Network/networkInterfaces/write", "Microsoft.Resources/subscriptions/resourceGroups/read", "Microsoft.Support/*" ], "NotActions": [ ], "AssignableScopes": [ "/subscriptions/$($Subscription.Id)" ] } "@ | Out-File $JSONFile try { $Result = New-AzRoleDefinition -InputFile $JSONFile -EA 1 Write-Log ($Result|Out-String).Trim() Green $LogFile } catch { Write-Log 'Unable to deploy role defintion',$RoleName,'in subscription',$Subscription.Name Magenta,Yellow,Magenta,Yellow $LogFile Write-log " $($_.Exception.Message)" Yellow $LogFile } } if ('NetworkAdmin' -in $RoleList) { # Putting the subscription Id in the Role definition name since they must be all unique in AAD !!?? $RoleName = "NetworkAdmin_($($Subscription.Id.ToCharArray()[-8..-1] -join ''))" @" { "Name": "$RoleName", "Description": "Manage (Add/modify/delete) network resources", "Actions": [ "Microsoft.Network/*", "Microsoft.Compute/*/read", "Microsoft.Resources/deployments/*", "Microsoft.Resources/deployments/validate/action", "Microsoft.Resources/subscriptions/resourceGroups/read", "Microsoft.Support/*" ], "NotActions": [ ], "AssignableScopes": [ "/subscriptions/$($Subscription.Id)" ] } "@ | Out-File $JSONFile try { $Result = New-AzRoleDefinition -InputFile $JSONFile -EA 1 Write-Log ($Result|Out-String).Trim() Green $LogFile } catch { Write-Log 'Unable to deploy role defintion',$RoleName,'in subscription',$Subscription.Name Magenta,Yellow,Magenta,Yellow $LogFile Write-log " $($_.Exception.Message)" Yellow $LogFile } } if ('InfraAdmin' -in $RoleList) { # Putting the subscription Id in the Role definition name since they must be all unique in AAD !!?? $RoleName = "InfraAdmin ($($Subscription.Id.ToCharArray()[-8..-1] -join ''))" @" { "Name": "$RoleName", "IsCustom": true, "Description": "Access to (Create/Modify/Delete) all Resources except Networking and User Access administration", "Actions": [ "*" ], "NotActions": [ "Microsoft.Authorization/*/Delete", "Microsoft.Authorization/*/Write", "Microsoft.Authorization/elevateAccess/Action", "Microsoft.Network/applicationGateways/delete", "Microsoft.Network/dnsZones/delete", "Microsoft.Network/expressRouteCrossConnections/delete", "Microsoft.Network/expressRouteGateways/delete", "Microsoft.Network/expressRouteCircuits/delete", "Microsoft.Network/expressRoutePorts/delete", "Microsoft.Network/frontDoors/delete", "Microsoft.Network/networkWatchers/delete", "Microsoft.Network/routeFilters/delete", "Microsoft.Network/routeTables/delete", "Microsoft.Network/serviceEndpointPolicies/delete", "Microsoft.Network/trafficManagerProfiles/delete", "Microsoft.Network/virtualNetworkGateways/delete", "Microsoft.Network/loadBalancers/delete", "Microsoft.Network/networkSecurityGroups/delete", "Microsoft.Network/virtualNetworks/delete", "Microsoft.Network/localNetworkGateways/delete", "Microsoft.Network/applicationGateways/write", "Microsoft.Network/dnsZones/write", "Microsoft.Network/expressRouteCrossConnections/write", "Microsoft.Network/expressRouteGateways/write", "Microsoft.Network/expressRouteCircuits/write", "Microsoft.Network/expressRoutePorts/write", "Microsoft.Network/frontDoors/write", "Microsoft.Network/networkWatchers/write", "Microsoft.Network/routeFilters/write", "Microsoft.Network/routeTables/write", "Microsoft.Network/serviceEndpointPolicies/write", "Microsoft.Network/trafficManagerProfiles/write", "Microsoft.Network/virtualNetworkGateways/write", "Microsoft.Network/loadBalancers/write", "Microsoft.Network/networkSecurityGroups/write", "Microsoft.Network/virtualNetworks/write", "Microsoft.Network/localNetworkGateways/write", "Microsoft.Blueprint/blueprintAssignments/write", "Microsoft.Blueprint/blueprintAssignments/delete" ], "AssignableScopes": [ "/subscriptions/$($Subscription.Id)" ] } "@ | Out-File $JSONFile try { $Result = New-AzRoleDefinition -InputFile $JSONFile -EA 1 Write-Log ($Result|Out-String).Trim() Green $LogFile } catch { Write-Log 'Unable to deploy role',$RoleName,'defintion in subscription',$Subscription.Name Magenta,Yellow,Magenta,Yellow $LogFile Write-log " $($_.Exception.Message)" Yellow $LogFile } } } } End { } } function Deploy-AzPolicy { # Requires -Modules AZ # Requires -Version 5 <# .SYNOPSIS Function to deploy custom RBAC role definitions in one or more Azure subscriptions .DESCRIPTION Function to deploy custom RBAC role definitions in one or more Azure subscriptions This function uses and depends on Az PowerShell module available in the PowerShell gallery This function expects to be authenticated to Azure before it's invoked (Connect-AzAccount) .PARAMETER SubscriptionId One or more Azure subscription Ids such as 'abcdabcd-abcd-abcd-abcd-abcdabcdabcd' .PARAMETER LogFile This is an optional parameter that specifies the path to the log file where the script logs its progress This defaults to a file in the current folder where the script is running .EXAMPLE Deploy-AzPolicy -SubscriptionId 'abcdabcd-abcd-abcd-abcd-abcdabcdabcd' .LINK https://superwidgets.wordpress.com/category/powershell/ .NOTES Function by Sam Boutros v0.1 - 14 May 2019 #> [CmdletBinding(ConfirmImpact='Low')] Param( [Parameter(Mandatory=$true)][String[]]$SubscriptionId, [Parameter(Mandatory=$false)][String]$LogFile = ".\Deploy-AzPolicy - $(Get-Date -Format 'ddMMMMyyyy_hh-mm-ss_tt').txt" ) Begin { try { $AllSubscriptionList = Get-AzSubscription -EA 1 } catch { Write-Log 'Unable to list subscriptions','are we using AZ module and logged on to Azure?' Magenta,Yellow $LogFile break } if ($SubscriptionList = $SubscriptionId | where { $_ -in $AllSubscriptionList.Id } | select -Unique) { $SubscriptionList = $SubscriptionList | foreach { Get-AzSubscription -SubscriptionId $_ } Write-Log 'The following',$SubscriptionList.Count,'subscriptions are found under the current tenant:' Green,Cyan,Green $LogFile Write-Log ($SubscriptionList.Name | Out-String).Trim() Cyan $LogFile } else { Write-Log 'The provided subscription Id(s)','are not found under the current tenant' Magenta,Yellow $LogFile break } } Process { foreach ($Subscription in $SubscriptionList) { $Subscription | Set-AzContext | Out-Null $JSONFile = New-TemporaryFile #region Network Admin # Putting the subscription Id in the Role definition name since they must be all unique in AAD !!?? $RoleName = "Azure Network Admin ($($Subscription.Id.ToCharArray()[-8..-1] -join ''))" @" { "Name": "$RoleName", "Description": "Manage (Add/modify/delete) network resources", "Actions": [ "Microsoft.Network/*", "Microsoft.Compute/*/read", "Microsoft.Resources/deployments/*", "Microsoft.Resources/deployments/validate/action", "Microsoft.Resources/subscriptions/resourceGroups/read", "Microsoft.Support/*" ], "NotActions": [ ], "AssignableScopes": [ "/subscriptions/$($Subscription.Id)" ] } "@ | Out-File $JSONFile try { $Result = New-AzRoleDefinition -InputFile $JSONFile -EA 1 Write-Log ($Result|Out-String).Trim() Green $LogFile } catch { Write-Log 'Unable to deploy role',$RoleName,'defintion in subscription',$Subscription.Name Magenta,Yellow,Magenta,Yellow $LogFile Write-log " $($_.Exception.Message)" Yellow $LogFile } #endregion #region Infra Admin # Putting the subscription Id in the Role definition name since they must be all unique in AAD !!?? $RoleName = "Azure Infra Admin ($($Subscription.Id.ToCharArray()[-8..-1] -join ''))" @" { "Name": "$RoleName", "IsCustom": true, "Description": "Access to (Create/Modify/Delete) all Resources except Networking and User Access administration", "Actions": [ "*" ], "NotActions": [ "Microsoft.Authorization/*/Delete", "Microsoft.Authorization/*/Write", "Microsoft.Authorization/elevateAccess/Action", "Microsoft.Network/applicationGateways/delete", "Microsoft.Network/dnsZones/delete", "Microsoft.Network/expressRouteCrossConnections/delete", "Microsoft.Network/expressRouteGateways/delete", "Microsoft.Network/expressRouteCircuits/delete", "Microsoft.Network/expressRoutePorts/delete", "Microsoft.Network/frontDoors/delete", "Microsoft.Network/networkWatchers/delete", "Microsoft.Network/routeFilters/delete", "Microsoft.Network/routeTables/delete", "Microsoft.Network/serviceEndpointPolicies/delete", "Microsoft.Network/trafficManagerProfiles/delete", "Microsoft.Network/virtualNetworkGateways/delete", "Microsoft.Network/loadBalancers/delete", "Microsoft.Network/networkSecurityGroups/delete", "Microsoft.Network/virtualNetworks/delete", "Microsoft.Network/localNetworkGateways/delete", "Microsoft.Network/applicationGateways/write", "Microsoft.Network/dnsZones/write", "Microsoft.Network/expressRouteCrossConnections/write", "Microsoft.Network/expressRouteGateways/write", "Microsoft.Network/expressRouteCircuits/write", "Microsoft.Network/expressRoutePorts/write", "Microsoft.Network/frontDoors/write", "Microsoft.Network/networkWatchers/write", "Microsoft.Network/routeFilters/write", "Microsoft.Network/routeTables/write", "Microsoft.Network/serviceEndpointPolicies/write", "Microsoft.Network/trafficManagerProfiles/write", "Microsoft.Network/virtualNetworkGateways/write", "Microsoft.Network/loadBalancers/write", "Microsoft.Network/networkSecurityGroups/write", "Microsoft.Network/virtualNetworks/write", "Microsoft.Network/localNetworkGateways/write", "Microsoft.Blueprint/blueprintAssignments/write", "Microsoft.Blueprint/blueprintAssignments/delete" ], "AssignableScopes": [ "/subscriptions/$($Subscription.Id)" ] } "@ | Out-File $JSONFile try { $Result = New-AzRoleDefinition -InputFile $JSONFile -EA 1 Write-Log ($Result|Out-String).Trim() Green $LogFile } catch { Write-Log 'Unable to deploy role',$RoleName,'defintion in subscription',$Subscription.Name Magenta,Yellow,Magenta,Yellow $LogFile Write-log " $($_.Exception.Message)" Yellow $LogFile } #endregion } } End { } } function Assign-AzPolicy { # Requires -Modules AZ # Requires -Version 5 <# .SYNOPSIS Function to assign an Azure Policy definition to an Azure subscription scope .DESCRIPTION Function to assign an Azure Policy definition to an Azure subscription scope This function uses and depends on Az PowerShell module available in the PowerShell gallery This function expects to be authenticated to Azure before it's invoked (Connect-AzAccount) .PARAMETER Subscription Azure subscription object obtained from Get-AzSubscription Cmdlet of the Az PS module .PARAMETER PolicyDefinition PS Custom object obtained from New-AzPolicyDefinition Cmdlet of the Az PS module .PARAMETER LogFile This is an optional parameter that specifies the path to the log file where the script logs its progress This defaults to a file in the current folder where the script is running .EXAMPLE Connect-AzAccount $Subscription = Get-AzSubscription -SubscriptionName 'My Subscription Name here' $PolicyName = 'Policy (Standardization) > Resource Group names start with AZ-' # '1234567890123456789012345678901234567890123456789012345678901234' 64 characters max $ParameterSet = @{ Name = $PolicyName DisplayName = $PolicyName Description = $PolicyName Mode = 'All' Policy = @' { "if": { "allOf": [ { "field": "type", "equals": "Microsoft.Resources/subscriptions/resourceGroups" }, { "not": { "field": "name", "Like": "AZ-*" } }, ] }, "then": { "effect": "deny" } } '@ ErrorAction = 1 } $PolicyDefinition = New-AzPolicyDefinition @ParameterSet AssignAzPolicy -Subscription $Subscription -PolicyDefinition $PolicyDefinition .OUTPUTS TypeName: System.Management.Automation.PSCustomObject Name MemberType Definition ---- ---------- ---------- Equals Method bool Equals(System.Object obj) GetHashCode Method int GetHashCode() GetType Method type GetType() ToString Method string ToString() Name NoteProperty string Name=test Policy (Standardization) start with AZ- PolicyAssignmentId NoteProperty string PolicyAssignmentId=/subscriptions/f0caexxxx142/providers/Microsoft.Authorization/policyAssignmen... Properties NoteProperty System.Management.Automation.PSCustomObject Properties=@{displayName=test Policy (Standardization) start with AZ-; policyDefini... ResourceId NoteProperty string ResourceId=/subscriptions/f0caexxxxx142/providers/Microsoft.Authorization/policyAssignments/test ... ResourceName NoteProperty string ResourceName=test Policy (Standardization) start with AZ- ResourceType NoteProperty string ResourceType=Microsoft.Authorization/policyAssignments Sku NoteProperty System.Management.Automation.PSCustomObject Sku=@{name=A0; tier=Free} SubscriptionId NoteProperty string SubscriptionId=f0caexxxxx142 .LINK https://superwidgets.wordpress.com/category/powershell/ .NOTES Function by Sam Boutros v0.1 - 5 June 2019 #> [CmdletBinding(ConfirmImpact='Low')] Param( [Parameter(Mandatory=$true)][Microsoft.Azure.Commands.Profile.Models.PSAzureSubscription]$Subscription, [Parameter(Mandatory=$true)][PSCustomObject]$PolicyDefinition, [Parameter(Mandatory=$false)][String]$LogFile = ".\Assign-AzPolicy - $(Get-Date -Format 'ddMMMMyyyy_hh-mm-ss_tt').txt" ) Begin { } Process { $ParameterSet = @{ Name = $PolicyDefinition.Name DisplayName = $PolicyDefinition.Name Description = $PolicyDefinition.Name Scope = "/subscriptions/$($Subscription.Id)" PolicyDefinition = $PolicyDefinition ErrorAction = 1 } try { New-AzPolicyAssignment @ParameterSet Write-Log 'Assigned policy definition',$PolicyDefinition.Name,'in subscription',$Subscription.Name,'to scope',"/subscriptions/$($Subscription.Id)" Green,Cyan,Green,Cyan,Green,Cyan $LogFile } catch { Write-Log 'Unable to assign policy definition',$PolicyDefinition.Name,'to scope',"/subscriptions/$($Subscription.Id)",'for subscription',$Subscription.Name Magenta,Yellow,Magenta,Yellow,Magenta,Yellow $LogFile Write-Log $_.Exception.Message Yellow $LogFile } } End { } } function Test-AzVMConnection { # Requires -Modules Az # Requires -Version 5 <# .SYNOPSIS Function to test TCP connectivity between 2 Azure VMs over one or more ports .DESCRIPTION Function to test TCP connectivity between 2 Azure VMs over one or more ports This function uses Az PowerShell module available in the PowerShell gallery This function will display color-coded console output similar to: Testing connectivity from AZ-Jump1-VM (10.5.255.164) to AZ-myApp1SQL-VM (10.6.2.4) TCP Port 111 failed TCP Port 135 failed TCP Port 22 failed TCP Port 3389 passed TCP Port 25 failed TCP Port 80 failed TCP Port 443 failed TCP Port 5985 passed TCP Port 5986 failed This function will test connectivity from/to private IPs only not public IPs If a source or target VMs has more than 1 NIC, all NICs will be tested .PARAMETER FromVM This is the source VM. This object can be obtained via the Get-AzVM cmdlet .PARAMETER ToVM This is the target VM. This object can be obtained via the Get-AzVM cmdlet .PARAMETER TCPPortList One or more TCP ports. If not provided the following ports will be tested: TCP Port 111 ==> Linux VM connectivity TCP Port 135 ==> Windows VM connectivity TCP Port 22 ==> SSH TCP Port 3389 ==> RDP TCP Port 25 ==> SMTP TCP Port 80 ==> HTTP TCP Port 443 ==> HTTPS TCP Port 5985 ==> PS Remoting (WinRM) over HTTP TCP Port 5986 ==> PS Remoting (WinRM) over HTTPS .PARAMETER LogFile This is an optional parameter that specifies the path to the log file where the script logs its progress This defaults to a file in the current folder where the script is running .EXAMPLE Test-AzVMConnection -FromVM (Get-AzVM -Name AZ-Jump1-VM) -ToVM (Get-AzVM -Name AZ-myApp1SQL-VM) .OUTPUTS This function returns a PS Custom object similar to: SourceComputer SourceIP TargetComputer TargetIP TCPPort CanConnect -------------- -------- -------------- -------- ------- ---------- AZ-Jump1-VM 10.5.255.164 AZ-myApp1SQL-VM 10.6.2.4 111 False AZ-Jump1-VM 10.5.255.164 AZ-myApp1SQL-VM 10.6.2.4 135 False AZ-Jump1-VM 10.5.255.164 AZ-myApp1SQL-VM 10.6.2.4 22 False AZ-Jump1-VM 10.5.255.164 AZ-myApp1SQL-VM 10.6.2.4 3389 True AZ-Jump1-VM 10.5.255.164 AZ-myApp1SQL-VM 10.6.2.4 25 False AZ-Jump1-VM 10.5.255.164 AZ-myApp1SQL-VM 10.6.2.4 80 False AZ-Jump1-VM 10.5.255.164 AZ-myApp1SQL-VM 10.6.2.4 443 False AZ-Jump1-VM 10.5.255.164 AZ-myApp1SQL-VM 10.6.2.4 5985 True AZ-Jump1-VM 10.5.255.164 AZ-myApp1SQL-VM 10.6.2.4 5986 False .LINK https://superwidgets.wordpress.com/category/powershell/ .NOTES Function by Sam Boutros v0.1 - 14 June 2019 #> [CmdletBinding(ConfirmImpact='Low')] Param( [Parameter(Mandatory=$true)][Microsoft.Azure.Commands.Compute.Models.PSVirtualMachine]$FromVM, [Parameter(Mandatory=$true)][Microsoft.Azure.Commands.Compute.Models.PSVirtualMachine]$ToVM, [Parameter(Mandatory=$false)][Int[]]$TCPPortList = @(111,135,22,3389,25,80,443,5985,5986), [Parameter(Mandatory=$false)][String]$LogFile = ".\Test-AzVMConnection - $FromVM - $ToVM - $(Get-Date -Format 'ddMMMMyyyy_hh-mm-ss_tt').txt" ) Begin { } Process { $TempFile = New-TemporaryFile $FromInterfaceNameList = $FromVM.NetworkProfile.NetworkInterfaces.Id | foreach { Split-Path $_ -Leaf } $myOutput = foreach ($FromInterfaceName in $FromInterfaceNameList) { $FromPrivateIP = (Get-AzNetworkInterface -ResourceGroupName $FromVM.ResourceGroupName -Name $FromInterfaceName).IpConfigurations.PrivateIpAddress $ToInterfaceNameList = $ToVM.NetworkProfile.NetworkInterfaces.Id | foreach { Split-Path $_ -Leaf } foreach ($ToInterfaceName in $ToInterfaceNameList) { $ToPrivateIP = (Get-AzNetworkInterface -ResourceGroupName $ToVM.ResourceGroupName -Name $ToInterfaceName).IpConfigurations.PrivateIpAddress Write-Log 'Testing connectivity from',"$($FromVM.Name) ($FromPrivateIP)","to $($ToVM.Name) ($ToPrivateIP)" DarkYellow,Green,Cyan $LogFile foreach ($Port in $TCPPortList) { "Test-SBNetConnection -ComputerName $ToPrivateIP -Port $Port -WA 0" | Out-File $TempFile $Result = Invoke-AzVMRunCommand -ResourceGroupName $FromVM.ResourceGroupName -Name $FromVM.Name -CommandId 'RunPowerShellScript' -ScriptPath $TempFile if ($Result.Value[0].Message -match 'True') { Write-Log " TCP Port $Port".PadRight(20,' '),'passed' Green,Cyan $LogFile [PSCustomObject]@{ SourceComputer = $FromVM.Name SourceIP = $FromPrivateIP TargetComputer = $ToVM.Name TargetIP = $ToPrivateIP TCPPort = $Port CanConnect = $true } } else { Write-Log " TCP Port $Port".PadRight(20,' '),"failed $($Result.Value[1].Message)" Green,Yellow $LogFile [PSCustomObject]@{ SourceComputer = $FromVM.Name SourceIP = $FromPrivateIP TargetComputer = $ToVM.Name TargetIP = $ToPrivateIP TCPPort = $Port CanConnect = $false } } } } } } End { $myOutput } } function Fix-Json { <# .SYNOPSIS Function to fix bug with ConvertTo-Json where nested object appear as a hash table - see example .DESCRIPTION Function to fix bug with ConvertTo-Json where nested object appear as a hash table - see example .PARAMETER FilePath Path to JSON File. This is expected to be a file similar in syntax to the example below. .EXAMPLE @' { "$schema": "https://schema.management.azure.com/schemas/2018-05-01/deploymentTemplate.json#", "contentVersion": "1.0.0.0", "parameters": { "ApplicationName": { "type": "string", "maxLength": 3, "metadata": { "description": "my desc" } }, "plan_name": { "type": "String" } }, "variables": { "resourceNames": { "name": "EDSENDGRID06", "commonResourceGroup": "[tolower(concat(parameters('ApplicationName'),'-',parameters('Environment'),'-',parameters('shortlocation'),'-',parameters('tenant'),'-rgp-','01'))]" }, "TemplateURLs": { "sendgrid": "[concat(parameters('artifacts_baseUri'),'/ArmTemplates/master/Public/lib/linkedTemplates/sendgrid.json')]" } } } '@ | ConvertFrom-Json | ConvertTo-Json This shows the bug where the 'metadata' object is not represented properly: "metadata": "@{description=my desc}" instead of it should be: "metadata": { "description": "my desc" } as seen in the source input. This function fixes this issue as in: $TempFile = New-TemporaryFile @' { "$schema": "https://schema.management.azure.com/schemas/2018-05-01/deploymentTemplate.json#", "contentVersion": "1.0.0.0", "parameters": { "ApplicationName": { "type": "string", "maxLength": 3, "metadata": { "description": "my desc" } }, "plan_name": { "type": "String" } }, "variables": { "resourceNames": { "name": "EDSENDGRID06", "commonResourceGroup": "[tolower(concat(parameters('ApplicationName'),'-',parameters('Environment'),'-',parameters('shortlocation'),'-',parameters('tenant'),'-rgp-','01'))]" }, "TemplateURLs": { "sendgrid": "[concat(parameters('artifacts_baseUri'),'/ArmTemplates/master/Public/lib/linkedTemplates/sendgrid.json')]" } } } '@ | ConvertFrom-Json | ConvertTo-Json | Out-File $TempFile Fix-Json $TempFile .LINK https://superwidgets.wordpress.com/ .NOTES Function by Sam Boutros v0.1 - 17 July 2019 #> [CmdletBinding(ConfirmImpact='Low')] Param( [Parameter(Mandatory=$true,ValueFromPipeLine=$true,ValueFromPipeLineByPropertyName=$true)] [ValidateScript({Test-Path $_})][String]$FilePath ) Begin { } Process { $myOutput = foreach ($Line in (Get-Content $FilePath)) { if ($Line -match '@') { Write-Log 'Fixing bad line',$Line Green,Cyan $Indent = $Line.Split('"')[0].ToCharArray().Count "$($Line.Split('"')[0])""$($Line.Split('"')[1])""$($Line.Split('"')[2]){" # Line 1 $Temp = $Line.Split('"')[3].replace('@','').replace('{','').replace('}','') ' ' * ($Indent + 4) + '"' + $Temp.Split('=')[0] + '": "' + $Temp.Split('=')[1] + '"' # Line 2 ' ' * $Indent + "}" # Line 3 } else { $Line } } } End { $myOutput } } function Azure-PFC { <# .SYNOPSIS Function to perform the following basic validations for using Azure. .DESCRIPTION Function to perform the following basic validations for using Azure: - Validate/Install AZ and other PowerShell module(s) - Validate connection to Azure - Validate Azure subscription (if SubscriptionId is provided) .PARAMETER SubscriptionId Optional parameter of an Azure Subscription Id. If provided, this function will validate that the subscription exists .PARAMETER Module Optional parameter that informs this function to validate/install additional PowerShell modules. AZ module will always be validated/installed. Valid input is one or more of: 'AZ' 'AzureAD' 'AzureADPreview' 'MSOnline' 'SharePointPnPPowerShellOnline' .PARAMETER LogFile Path to a file where this function will log its output .EXAMPLE if (-not (Azure-PFC)) { Write-Log 'Not connected to Azure, stopping..' Yellow break } .OUTPUTS Function returns $true if all checks pass or $false if any of the checks fail .LINK https://superwidgets.wordpress.com/category/powershell/ .NOTES Function by Sam Boutros v0.1 - 11 February 2020 v0.2 - 20 October 2020 - Minor updates, exposed externally v0.3 - 20 October 2020 - Added validation for additional Azure PowerShell modules #> [CmdletBinding(ConfirmImpact='High')] Param( [Parameter(Mandatory=$false)][String]$SubscriptionId, [Parameter(Mandatory=$false)][ValidateSet('AZ','AzureAD','AzureADPreview','MSOnline','SharePointPnPPowerShellOnline')] [String[]]$Module = 'AZ', [Parameter(Mandatory=$false)][String]$LogFile = ".\Azure-PFC - $(Get-Date -Format 'ddMMMMyyyy_hh-mm-ss_tt').txt" ) Begin { $Go = $true } Process { #region Validate/Install PowerShell module(s) $ModuleList = @('AZ') $ModuleList += $Module $ModuleList = $ModuleList | select -Unique foreach ($ModuleName in $ModuleList) { if ($ModuleName.ToUpper() -eq 'AZ') { $ModuleCheck = 'AZ.*' } else { $ModuleCheck = $ModuleName } if (Get-Module $ModuleCheck -ListAvailable -WA 0) { Write-Log 'Validated module',$ModuleName Green,Cyan $LogFile } else { Write-Log 'PowerShell module',$ModuleName,'is not installed, installing from the PowerShell Gallery..' Yellow,Cyan,Yellow $LogFile -NoNewLine try { # PowerShellGallery dropped Ssl3 and Tls as of 1 April 2020 [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 Install-Module $ModuleName -Scope CurrentUser -AllowClobber -Force -SkipPublisherCheck -EA 1 if (Get-Module $ModuleCheck -ListAvailable) { Write-Log 'done' Green $LogFile } else { Write-Log 'failed' Magenta $LogFile Write-Log $_.Exception.Message Yellow $LogFile $Go = $false } } catch { Write-Log 'failed' Magenta $LogFile Write-Log $_.Exception.Message Yellow $LogFile $Go = $false } } } #endregion #region Validate connection to Azure try { Get-AzSubscription -EA 1 | Out-Null $AzContext = Get-AzContext Write-Log ' Connected to tenant',$AzContext.Name Green,Cyan $LogFile } catch { Write-Log $_.Exception.Message Yellow $LogFile $Go = $false } #endregion #region Validate Azure subscription (if SubscriptionId is provided) if ($SubscriptionId) { try { Get-AzSubscription -SubscriptionId $SubscriptionId -WA 0 -EA 1 | Set-AzContext | Out-Null $AzContext = Get-AzContext Write-Log 'Now connected to subscription',($AzContext.Name).Split('(')[0].Trim(),'Id',$AzContext.Subscription,'as',$AzContext.Account Green,Cyan,Green,Cyan,Green,Cyan $LogFile } catch { Write-Log 'Unable to find SubcriptionId',$SubscriptionId Magenta,Yellow $LogFile Write-Log 'Available subscriptions:' Yellow $LogFile Write-Log (Get-AzSubscription|Out-String).Trim() Yellow $LogFile $Go = $false } } #endregion } End { $Go } } function Deploy-ARMVnet { <# .SYNOPSIS Function to Deploy Vnet to Azure subscription via ARM template .DESCRIPTION Function to Deploy Vnet to Azure subscription via ARM template This function requires PowerShell version 5 and AZ PowerShell module. This function uses API version 2019-09-01 which addresses the issue of having to make each subnet dependent on prior subnets - see https://github.com/Azure/azure-powershell/issues/1817 Caution: Although ARM templates are deployed in 'incremental mode' by default - (https://docs.microsoft.com/en-us/azure/azure-resource-manager/templates/deployment-modes), where resources in the template are added to the resource group, without deleting resources not specified in the ARM template. However, Subnets are considered part of the Vnet resource, meaning that this script may delete existing subnets, and only subnets specified in the input of this function will remain. This function will display verbose details during Template processing. .PARAMETER SubscriptionId Azure subscription Id that can be obtained from Get-AzSubscription Cmdlet of the Az PS module This is an optional parameter. If specified, this function will change context to deploy in the specified subscription. .PARAMETER ResourceGroupName Name of the Resource Group where the Vnet will be deployed .PARAMETER AzureLocation Name of the Azure Location where the Vnet will be deployed For a list of Azure locations use: "(Get-AzLocation).Location" Example: westus .PARAMETER VnetName Name of the Vnet to deploy, example: "Picard_Hub_Vnet" .PARAMETER VnetPrefix IPv4 address space for this Vnet in CIDR notation, example: "10.11.0.0/16" .PARAMETER DdosProtection This is a switch that defaults to False. The False setting enables 'Basic DDoS Protection' The True setting enables 'Standard DDoS Protection' See https://docs.microsoft.com/en-us/azure/virtual-network/ddos-protection-overview for more details .PARAMETER ShowTemplate This is a switch that defaults to False. When set to True, this function will display the resulting ARM template in notepad and will also make it part of the script log file .PARAMETER SubnetList This is an optional parameter that has information on one or more subnets to be provisioned within this Vnet. Only Subnets listed here will remain in the Vnet when this function is invoked. For example, if subnets sub1 and sub2 are specified here, and the Vnet exists with subnets sub1 and sub3, when this function is invoked sub3 will be deleted and sub2 will be added. If no value is provided for this parameter, all existing subnets will be removed from this Vnet. Example (1 subnet): $SubnetList = @{ Name = 'Hub_Gateway_Subnet'; Prefix = '10.11.0.0/27' } Example (3 subnets): $SubnetList = @( @{ Name = 'Hub_Gateway_Subnet'; Prefix = '10.11.0.0/27' } @{ Name = 'Hub_NVA_Subnet'; Prefix = '10.11.0.32/27' } @{ Name = 'Hub_Infra_Subnet'; Prefix = '10.11.0.64/27' } ) .PARAMETER LogFile This is an optional parameter that specifies the path to the log file where the script logs its progress This defaults to a file in the current folder where the script is running .EXAMPLE Connect-AzAccount # To connect to Azure tenant $Subscription = Get-AzSubscription -SubscriptionName 'My Subscription Name here' $ParameterSet = @{ SubscriptionId = $Subscription.Id ResourceGroupName = 'MyOrg_Hub_RG' AzureLocation = 'centralus' VnetName = 'MyOrg_Hub_Vnet' VnetPrefix = '10.11.0.0/16' SubnetList = @( @{ Name = 'Hub_Gateway_Subnet'; Prefix = '10.11.0.0/27' } @{ Name = 'Hub_NVA_Subnet'; Prefix = '10.11.0.32/27' } @{ Name = 'Hub_Infra_Subnet'; Prefix = '10.11.0.64/27' } ) DdosProtection = $false ShowTemplate = $true } Deploy-ARMVnet @ParameterSet .OUTPUTS None .LINK https://superwidgets.wordpress.com/category/powershell/ .NOTES Function by Sam Boutros v0.1 - 11 February 2020 #> [CmdletBinding(ConfirmImpact='High')] Param( [Parameter(Mandatory=$false, HelpMessage='Azure Subscription Id, Use "help Deploy-ARMVnet -Show" for more details')] [String]$SubscriptionId, [Parameter(Mandatory=$true, HelpMessage='Name of the Resource Group where this Vnet will be deployed, example: "Picard_Hub_RG"')] [String]$ResourceGroupName, [Parameter(Mandatory=$true, HelpMessage='For a list of Azure locations use: "(Get-AzLocation).Location"')] [String]$AzureLocation, [Parameter(Mandatory=$true, HelpMessage='Name of the Vnet to deploy, example: "Picard_Hub_Vnet"')] [String]$VnetName, [Parameter(Mandatory=$true, HelpMessage='IPv4 address space for this Vnet in CIDR notation, example: "10.11.0.0/16"')] [String]$VnetPrefix, [Parameter(Mandatory=$false, HelpMessage='True or False, Use "help Deploy-ARMVnet -Show" for more details')] [Switch]$DdosProtection = $false, [Parameter(Mandatory=$false, HelpMessage='True or False, Use "help Deploy-ARMVnet -Show" for more details')] [Switch]$ShowTemplate = $false, [Parameter(Mandatory=$false, HelpMessage='One or more Subnets, Use "help Deploy-ARMVnet -Show" to see example')] [Hashtable[]]$SubnetList, [Parameter(Mandatory=$false)][String]$LogFile = ".\Deploy-ARMVnet - $(Get-Date -Format 'ddMMMMyyyy_hh-mm-ss_tt').txt" ) Begin { if (-not (Azure-PFC -SubscriptionId $SubscriptionId -LogFile $LogFile)) { break } } Process { try { New-AzResourceGroup -Name $ResourceGroupName -Location $AzureLocation -Force -EA 1 | Out-Null Write-Log 'Created/Validated Resource Group',$ResourceGroupName Green,Cyan $LogFile } catch { Write-Log 'Failed to create Resource Group',$ResourceGroupName Magenta,Yellow $LogFile; break } #region Build ARM template $TemplateFile = New-TemporaryFile @' { "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#", "contentVersion": "1.0.0.0", '@ | Out-File $TemplateFile @" "parameters": { "vnetName": { "type": "string", "DefaultValue": "$VnetName", }, "location": { "type": "string", "DefaultValue": "$AzureLocation", }, "resourceGroup": { "type": "string", "DefaultValue": "$ResourceGroupName", }, "vnetAddressPrefix": { "type": "string", "DefaultValue": "$VnetPrefix", }, "enableDdosProtection": { "type": "bool", "DefaultValue": $(if ($DdosProtection) { 'true' } else { 'false' }), }, "@ | Out-File $TemplateFile -Append $n = 0 foreach ($Subnet in $SubnetList) { $n++ @" "subnet$($n)Name": { "type": "string", "DefaultValue": "$($Subnet.Name)", }, "subnet$($n)Prefix": { "type": "string", "DefaultValue": "$($Subnet.Prefix)", }, "@ | Out-File $TemplateFile -Append } @" }, "resources": [ { "apiVersion": "2019-09-01", "name": "[parameters('vnetName')]", "type": "Microsoft.Network/virtualNetworks", "location": "[parameters('location')]", "properties": { "addressSpace": { "addressPrefixes": [ "[parameters('vnetAddressPrefix')]" ] }, "subnets": [ "@ | Out-File $TemplateFile -Append $n = 0 foreach ($Subnet in $SubnetList) { $n++ @" { "name": "[parameters('subnet$($n)Name')]", "properties": { "addressPrefix": "[parameters('subnet$($n)Prefix')]", "addressPrefixes": [] } }, "@ | Out-File $TemplateFile -Append } @" ], "enableDdosProtection": "[parameters('enableDdosProtection')]" } } ] } "@ | Out-File $TemplateFile -Append #endregion if ($ShowTemplate) { Write-Log (Get-Content $TemplateFile | Out-String) Green $LogFile notepad $TemplateFile } try { New-AzResourceGroupDeployment -ResourceGroupName $ResourceGroupName -TemplateFile $TemplateFile -Verbose -EA 1 | Out-Null } catch { Write-Log 'failed' Magenta $LogFile Write-Log $_.Exception.Message Yellow $LogFile } } End { } } function Deploy-ARMNIC { <# .SYNOPSIS Function to Deploy a network interface to Azure subscription via ARM template .DESCRIPTION Function to Deploy network interface to Azure subscription via ARM template This function requires PowerShell version 5 and AZ PowerShell module. This is typically done before deploying a VM (Virtual Machine) This function will display verbose details during Template processing. .PARAMETER SubscriptionId Azure subscription Id that can be obtained from Get-AzSubscription Cmdlet of the Az PS module This is a required parameter. This function will change context to deploy in the specified subscription. .PARAMETER ResourceGroupName Name of the Resource Group where the NIC will be deployed .PARAMETER AzureLocation Name of the Azure Location where the NIC will be deployed NIC must be deployed in the same Azure location where the VNet is For a list of Azure locations use: "(Get-AzLocation).Location" Example: westus .PARAMETER NICName Name of the NIC to deploy, example: "Picard-DC01-NIC" .PARAMETER VnetName Name of the Vnet to deploy this NIC into, example: "Picard_Hub_Vnet" .PARAMETER SubnetName Name of the Subnet to deploy this NIC into, example: "Hub_Infra_Subnet" .PARAMETER ShowTemplate This is a switch that defaults to False. When set to True, this function will display the resulting ARM template in notepad and will also make it part of the script log file .PARAMETER TagList Zero or more tags, each in a hashtable containing Name and Value keys - see example below .PARAMETER LogFile This is an optional parameter that specifies the path to the log file where the script logs its progress This defaults to a file in the current folder where the script is running .EXAMPLE Connect-AzAccount # To connect to Azure tenant $Subscription = Get-AzSubscription -SubscriptionName 'My Subscription Name here' $ParameterSet = @{ SubscriptionId = $Subscription.Id ResourceGroupName = 'Picard_Hub_RG' AzureLocation = 'centralus' NICName = 'Picard-DC01-NIC' VnetName = 'Picard_Hub_Vnet' SubnetName = 'Hub_Infra_Subnet' ShowTemplate = $true TagList = @( @{ Name = 'Owner'; Value = 'Sam Boutros' } @{ Name = 'CostCenter'; Value = 'My Azure Demo' } @{ Name = 'DateProvisioned'; Value = $(Get-Date -Format 'ddMMMMyyyy_hh-mm-ss_tt') } ) } Deploy-ARMNIC @ParameterSet .OUTPUTS None .LINK https://superwidgets.wordpress.com/category/powershell/ .NOTES Function by Sam Boutros v0.1 - 12 February 2020 #> [CmdletBinding(ConfirmImpact='Low')] Param( [Parameter(Mandatory=$true, HelpMessage='Azure Subscription Id, Use "help Deploy-ARMNIC -Show" for more details')] [String]$SubscriptionId, [Parameter(Mandatory=$true, HelpMessage='Name of the Resource Group where this NIC will be deployed, example: "Picard_Hub_RG"')] [String]$ResourceGroupName, [Parameter(Mandatory=$true, HelpMessage='For a list of Azure locations use: "(Get-AzLocation).Location"')] [String]$AzureLocation, [Parameter(Mandatory=$true, HelpMessage='Name of the NIC to deploy, example: "Picard-DC01-NIC"')] [String]$NICName, [Parameter(Mandatory=$true, HelpMessage='Name of the Vnet to attach this NIC to, example: "Picard_Hub_Vnet"')] [String]$VnetName, [Parameter(Mandatory=$true, HelpMessage='Name of the Subnet to attach this NIC to, example: "Hub_Infra_Subnet"')] [String]$SubnetName, [Parameter(Mandatory=$false, HelpMessage='True or False, Use "help Deploy-ARMNIC -Show" for more details')] [Switch]$ShowTemplate = $false, [Parameter(Mandatory=$false, HelpMessage='One or more Tags, Use "help Deploy-ARMNIC -Show" to see example')] [Hashtable[]]$TagList, [Parameter(Mandatory=$false)][String]$LogFile = ".\Deploy-ARMNIC - $(Get-Date -Format 'ddMMMMyyyy_hh-mm-ss_tt').txt" ) Begin { if (-not (Azure-PFC -SubscriptionId $SubscriptionId -LogFile $LogFile)) { break } } Process { try { New-AzResourceGroup -Name $ResourceGroupName -Location $AzureLocation -Force -EA 1 | Out-Null Write-Log 'Created/Validated Resource Group',$ResourceGroupName Green,Cyan $LogFile } catch { Write-Log 'Failed to create Resource Group',$ResourceGroupName Magenta,Yellow $LogFile; break } #region Build ARM template $TemplateFile = New-TemporaryFile @' { "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#", "contentVersion": "1.0.0.0", '@ | Out-File $TemplateFile @" "parameters": { "networkInterfaceName": { "type": "string", "defaultvalue": "$NICName" }, "location": { "type": "string", "defaultvalue": "$AzureLocation" }, "subnetId": { "type": "string", "defaultvalue": "/subscriptions/$SubscriptionId/resourceGroups/$ResourceGroupName/providers/Microsoft.Network/virtualNetworks/$VnetName/subnets/$SubnetName" }, "privateIPAllocationMethod": { "type": "string", "defaultvalue": "Dynamic" } }, "resources": [ { "name": "[parameters('networkInterfaceName')]", "type": "Microsoft.Network/networkInterfaces", "apiVersion": "2019-07-01", "location": "[parameters('location')]", "dependsOn": [], "properties": { "ipConfigurations": [ { "name": "ipconfig1", "properties": { "privateIpAddressVersion": "IPv4", "privateIPAllocationMethod": "[parameters('privateIPAllocationMethod')]", "subnet": { "id": "[parameters('subnetId')]" } } } ] }, "@ | Out-File $TemplateFile -Append if ($TagList) { @' "tags": { '@ | Out-File $TemplateFile -Append foreach ($Tag in $TagList) { @" "$($Tag.Name)": "$($Tag.Value)", "@ | Out-File $TemplateFile -Append } @' } '@ | Out-File $TemplateFile -Append } @" } ] } "@ | Out-File $TemplateFile -Append #endregion if ($ShowTemplate) { Write-Log (Get-Content $TemplateFile | Out-String) Green $LogFile notepad $TemplateFile } try { New-AzResourceGroupDeployment -ResourceGroupName $ResourceGroupName -TemplateFile $TemplateFile -Verbose -EA 1 | Out-Null } catch { Write-Log 'failed' Magenta $LogFile Write-Log $_.Exception.Message Yellow $LogFile } } End { } } function Deploy-ARMStorageAccount { <# .SYNOPSIS Function to Deploy a Storage Account to Azure subscription via ARM template .DESCRIPTION Function to Deploy Storage Account to Azure subscription via ARM template This function requires PowerShell version 5 and AZ PowerShell module. This is typically done before deploying a VM (Virtual Machine) This function will display verbose details during Template processing. .PARAMETER SubscriptionId Azure subscription Id that can be obtained from Get-AzSubscription Cmdlet of the Az PS module This is an optional parameter. This function will change context to deploy in the specified subscription. .PARAMETER ResourceGroupName Name of the Resource Group where the Storage Account will be deployed .PARAMETER AzureLocation Name of the Azure Location where the Storage Account will be deployed For a list of Azure locations use: "(Get-AzLocation).Location" Example: westus .PARAMETER storageAccountName Name of the Storage Account to deploy, example: "picard02122020" Storage account names must be between 3 and 24 characters in length and may contain numbers and lowercase letters only. Storage account name must be unique within Azure. No two storage accounts can have the same name. See https://docs.microsoft.com/en-us/azure/storage/common/storage-account-overview#naming-storage-accounts .PARAMETER storageAccountType As of 12 February 2020, this can be either: Premium_LRS Premium_ZRS Standard_LRS Standard_ZRS Standard_GRS Standard_RAGRS This is an optional parameter that defaults to Standard_LRS .PARAMETER storageAccountKind As of 12 February 2020, this can be either: BlobStorage BlockBlobStorage FileStorage Storage StorageV2 This is an optional parameter that defaults to StorageV2 .PARAMETER ShowTemplate This is a switch that defaults to False. When set to True, this function will display the resulting ARM template in notepad and will also make it part of the script log file .PARAMETER TagList Zero or more tags, each in a hashtable containing Name and Value keys - see example below .PARAMETER LogFile This is an optional parameter that specifies the path to the log file where the script logs its progress This defaults to a file in the current folder where the script is running .EXAMPLE Connect-AzAccount # To connect to Azure tenant $Subscription = Get-AzSubscription -SubscriptionName 'My Subscription Name here' $ParameterSet = @{ SubscriptionId = $Subscription.Id ResourceGroupName = 'Picard_Hub_RG' AzureLocation = 'centralus' storageAccountName = "picardhubdisks$(Get-Random -Minimum 111111 -Maximum 999999)" ShowTemplate = $true TagList = @( @{ Name = 'Owner'; Value = 'Sam Boutros' } @{ Name = 'CostCenter'; Value = 'My Azure Demo' } @{ Name = 'DateProvisioned'; Value = $(Get-Date -Format 'ddMMMMyyyy_hh-mm-ss_tt') } ) } Deploy-ARMStorageAccount @ParameterSet .OUTPUTS None .LINK https://superwidgets.wordpress.com/category/powershell/ .NOTES Function by Sam Boutros v0.1 - 12 February 2020 #> [CmdletBinding(ConfirmImpact='Low')] Param( [Parameter(Mandatory=$false, HelpMessage='Azure Subscription Id, Use "help Deploy-ARMStorageAccount -Show" for more details')] [String]$SubscriptionId, [Parameter(Mandatory=$true, HelpMessage='Name of the Resource Group where this Storage Account will be deployed, example: "Picard_Hub_RG"')] [String]$ResourceGroupName, [Parameter(Mandatory=$true, HelpMessage='For a list of Azure locations use: "(Get-AzLocation).Location"')] [String]$AzureLocation, [Parameter(Mandatory=$true, HelpMessage='Name of the Storage Account to deploy, example: "picard02122020"')] [String]$storageAccountName, [Parameter(Mandatory=$false, HelpMessage='The type of storage account, example: "Standard_LRS"')] [ValidateSet('Premium_LRS','Premium_ZRS','Standard_LRS','Standard_ZRS','Standard_GRS','Standard_RAGRS')] [String]$storageAccountType = 'Standard_LRS', [Parameter(Mandatory=$false, HelpMessage='The kind of storage account, example: "StorageV2"')] [ValidateSet('BlobStorage','BlockBlobStorage','FileStorage','Storage','StorageV2')] [String]$storageAccountKind = 'StorageV2', [Parameter(Mandatory=$false, HelpMessage='True or False, Use "help Deploy-ARMStorageAccount -Show" for more details')] [Switch]$ShowTemplate = $false, [Parameter(Mandatory=$false, HelpMessage='One or more Tags, Use "help Deploy-ARMStorageAccount -Show" to see example')] [Hashtable[]]$TagList, [Parameter(Mandatory=$false)][String]$LogFile = ".\Deploy-ARMStorageAccount - $(Get-Date -Format 'ddMMMMyyyy_hh-mm-ss_tt').txt" ) Begin { if (-not (Azure-PFC -SubscriptionId $SubscriptionId -LogFile $LogFile)) { break } } Process { try { New-AzResourceGroup -Name $ResourceGroupName -Location $AzureLocation -Force -EA 1 | Out-Null Write-Log 'Created/Validated Resource Group',$ResourceGroupName Green,Cyan $LogFile } catch { Write-Log 'Failed to create Resource Group',$ResourceGroupName Magenta,Yellow $LogFile; break } #region Build ARM template $TemplateFile = New-TemporaryFile @' { "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#", "contentVersion": "1.0.0.0", '@ | Out-File $TemplateFile @" "parameters": { "location": { "type": "string", "defaultvalue": "$AzureLocation" }, "storageAccountName": { "type": "string", "defaultvalue": "$storageAccountName" }, "storageAccountType": { "type": "string", "defaultvalue": "$storageAccountType" }, "storageAccountKind": { "type": "string", "defaultvalue": "$storageAccountKind" } }, "resources": [ { "name": "[parameters('storageAccountName')]", "type": "Microsoft.Storage/storageAccounts", "apiVersion": "2019-06-01", "location": "[parameters('location')]", "properties": {}, "kind": "[parameters('storageAccountKind')]", "sku": { "name": "[parameters('storageAccountType')]" }, "@ | Out-File $TemplateFile -Append if ($TagList) { @' "tags": { '@ | Out-File $TemplateFile -Append foreach ($Tag in $TagList) { @" "$($Tag.Name)": "$($Tag.Value)", "@ | Out-File $TemplateFile -Append } @' } '@ | Out-File $TemplateFile -Append } @" } ] } "@ | Out-File $TemplateFile -Append #endregion if ($ShowTemplate) { Write-Log (Get-Content $TemplateFile | Out-String) Green $LogFile notepad $TemplateFile } try { New-AzResourceGroupDeployment -ResourceGroupName $ResourceGroupName -TemplateFile $TemplateFile -Verbose -EA 1 | Out-Null } catch { Write-Log 'failed' Magenta $LogFile Write-Log $_.Exception.Message Yellow $LogFile } } End { } } function New-AzureSPSecret { <# .SYNOPSIS Function to create a new secret for a given Azure Service Principal. .DESCRIPTION Function to create a new secret (Password/Key) for a given Azure Service Principal. This function depends on and requires the AZ and AzureAD PowerShell modules. .PARAMETER ServicePrincipalId This is a required parameter. This can be obtained via the cmdlet Get-AzADServicePrincipal. .PARAMETER SecretDays This is an optional parameter that defaults to 365 days. This is used to set the expiration date of the new secret. Valid values are from 1 to 7305 days (20 years). .PARAMETER RemoveExisting When this optional parameter is set to True, this function will delete all existing secrets for this Service Principal. .PARAMETER Length This is an optional parameter that defaults to 24. This is used to determine how long the password will be. Valid values are from 2 to 256. .PARAMETER Include This optional parameter determines which characters are used to create the random password. Valid values are one or more of: 'UpperCase','LowerCase','Numbers','SpecialCharacters'. When not provided, the password will contain characters from all four groups. For example, if 'UpperCase' is only provided, the password will contain upper case letters only. .PARAMETER CodeFriendly This optional parameter defaults to True. It excludes the following 4 characters from the 'SpecialCharacters' list of the password " ==> ASCII 34 $ ==> ASCII 36 ' ==> ASCII 39 ` ==> ASCII 96 .PARAMETER LogFile Path to a file where this function will log its console output. .EXAMPLE $SP = Get-AzADServicePrincipal -DisplayName SamTestSP01 $mySP = New-AzureSPSecret -ServicePrincipalId $SP.Id $mySP.Secret This example will create new secret, and output a list of all this SP secrets. Only the new secret value (password) will be displayed. .EXAMPLE $SP = Get-AzADServicePrincipal -DisplayName SamTestSP01 $mySP = New-AzureSPSecret -ServicePrincipalId $SP.Id -RemoveExisting $mySP.Secret This example will create new secret, deletes all existing secrets for this SP if any, and output the new secret including its value (password). .OUTPUTS This cmdlet returns a collection of objects - one for each secret similar to: KeyId Expires Secret ----- ------- ------ a85f7748-2203-49f8-937a-843cbe40c720 21 October 2021 xywd5\CevjK2E-}{:Vgr!/(9 1b6ac347-48fc-40d4-9d4d-aa0303ff536b 21 October 2021 eb2d74b3-5740-48f6-b7a5-059567c02266 21 October 2021 623efb41-74b5-4777-bd45-91174a8776ed 21 October 2021 .LINK https://superwidgets.wordpress.com/category/powershell/ .NOTES Function by Sam Boutros v0.1 - 20 October 2020 #> [CmdletBinding(ConfirmImpact='High')] Param( [Parameter(Mandatory=$true)][String]$ServicePrincipalId, [Parameter(Mandatory=$false)][ValidateRange(1,7305)][String]$SecretDays = 365, [Parameter(Mandatory=$false)][Switch]$RemoveExisting, [Parameter(Mandatory=$false)][ValidateRange(2,256)][Int32]$Length = 24, [Parameter(Mandatory=$false)][ValidateSet('UpperCase','LowerCase','Numbers','SpecialCharacters')] [String[]]$Include = @('UpperCase','LowerCase','Numbers','SpecialCharacters'), [Parameter(Mandatory=$false)][Switch]$CodeFriendly = $true, [Parameter(Mandatory=$false)][String]$LogFile = ".\New-AzureSPSecret_$(Get-Date -Format 'ddMMMMyyyy_hh-mm-ss_tt').txt" ) Begin { if (-not (Azure-PFC -Module AzureAD -LogFile $LogFile)) { Write-Log 'Not connected to Azure, stopping..' Yellow $LogFile break } try { $SP = Get-AzADServicePrincipal -ObjectId $ServicePrincipalId -EA 1 Write-Log 'Validated Azure Service Principal:' Green $LogFile ($SP | Get-Member -MemberType Properties).Name | foreach { Write-Log $(if ($SP.$_) {" $($_.PadRight(25)) : $($SP.$_ -join ', ')"}) Cyan $LogFile } } catch { Write-Log 'Unable to validate the provided Service Principal Id',$ServicePrincipalId Magenta,Yellow $LogFile Write-Log $_.Exception.Message Yellow $LogFile break } } Process { #region Remove Existing Secret(s) <# Unfortuantely, as of 20 October, 2020, the default 1 year Secret created via $sp = New-AzADServicePrincipal -DisplayName ServicePrincipalName which can be viewed via $BSTR = [System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($sp.Secret) $UnsecureSecret = [System.Runtime.InteropServices.Marshal]::PtrToStringAuto($BSTR) as outlined in https://docs.microsoft.com/en-us/powershell/azure/create-azure-service-principal-azureps is not visible via either Get-AzureADServicePrincipalPasswordCredential Get-AzADServicePrincipalCredential which makes deleting it not possible at this time #> if ($RemoveExisting) { try { $SecretList = Get-AzureADServicePrincipalPasswordCredential -ObjectId $SP.Id -EA 1 foreach ($Secret in $SecretList) { try { Remove-AzureADServicePrincipalPasswordCredential -ObjectId $SP.Id -KeyId $Secret.KeyId -EA 1 Write-Log 'Removed Service Principal Secret:' Green $LogFile Write-Log ($Secret | Out-String).Trim() Cyan $LogFile } catch { Write-Log 'Failed to remove Service Principal Secret:' Magenta $LogFile Write-Log ($Secret | Out-String).Trim() Yellow $LogFile Write-Log $_.Exception.Message Yellow $LogFile } } } catch { Write-Log $_.Exception.Message Yellow $LogFile break } } #endregion #region Add new Secret $SecretList = Get-AzureADServicePrincipalPasswordCredential -ObjectId $SP.Id $ParameterSet = @{ ObjectId = $SP.Id StartDate = Get-Date EndDate = (Get-Date).AddDays($SecretDays) Value = New-Password -CodeFriendly:$CodeFriendly -Length $Length -Include $Include ErrorAction = 'STOP' } try { $Result = New-AzureADServicePrincipalPasswordCredential @ParameterSet $NewSecret = Get-AzureADServicePrincipalPasswordCredential -ObjectId $SP.Id | where { $_.KeyId -notin $SecretList.KeyId } $SecretList = Get-AzureADServicePrincipalPasswordCredential -ObjectId $SP.Id Write-Log 'Added new Secret, expiring',$Result.EndDate Green,Cyan $LogFile New-Object -TypeName PSObject -Property ([Ordered]@{ DisplayName = $SP.DisplayName Id = $SP.Id ApplicationId = $SP.ApplicationId Secret = $( foreach ($Secret in $SecretList) { $Secret | select KeyId, @{n='Expires';e={Get-Date($_.EndDate) -Format 'dd MMMM yyyy'}}, @{n='Secret' ;e={if ($_.KeyId -eq $NewSecret.KeyId) { $ParameterSet.Value }}} } ) }) } catch { Write-Log 'Failed to add new Secret:' Magenta $LogFile Write-Log $_.Exception.Message Yellow $LogFile } #endregion } End { } } function Convert-ObjectGuid2ImmutableId { <# .SYNOPSIS Function to convert (Active Directory) Object Guid to (Azure Active Directory) Immutable Id .DESCRIPTION Function to convert (Active Directory) Object Guid to (Azure Active Directory) Immutable Id .PARAMETER ObjectGuid This is a required parameter of type System.Guid This can be obtained by (Get-ADUser samb).ObjectGuid .EXAMPLE Convert-ObjectGuid2ImmutableId (New-Guid) .OUTPUTS This cmdlet returns a base 64 encoded string like QBLtiithN0yENM4ji3SYjw== .LINK https://superwidgets.wordpress.com/category/powershell/ .NOTES Function by Sam Boutros v0.1 - 10 December 2020 #> [CmdletBinding(ConfirmImpact='Low')] Param( [Parameter(Mandatory=$true)][Guid]$ObjectGuid ) Begin { } Process { [Convert]::ToBase64String($ObjectGuid.ToByteArray()) } End { } } function Convert-ImmutableId2ObjectGuid { <# .SYNOPSIS Function to convert (Azure Active Directory) Immutable Id to (Active Directory) Object Guid .DESCRIPTION Function to convert (Azure Active Directory) Immutable Id to (Active Directory) Object Guid .PARAMETER ObjectGuid This is a required parameter. It should be a base 64 encoded string of a Guid. This can be obtained by Convert-ObjectGuid2ImmutableId (New-Guid) .EXAMPLE Convert-ImmutableId2ObjectGuid lg6ze0F/fkO2kImEstdJgA== .EXAMPLE Convert-ImmutableId2ObjectGuid (Convert-ObjectGuid2ImmutableId (New-Guid)) This is the same thing as New-Guid but it illustrates the function use.. .OUTPUTS This cmdlet returns a Guid like 7bb30e96-7f41-437e-b690-8984b2d74980 .LINK https://superwidgets.wordpress.com/category/powershell/ .NOTES Function by Sam Boutros v0.1 - 10 December 2020 #> [CmdletBinding(ConfirmImpact='Low')] Param( [Parameter(Mandatory=$true)][String]$ImmutableId ) Begin { } Process { try { [Guid]([Convert]::FromBase64String($ImmutableId)) } catch { Write-Log 'Convert-ImmutableId2ObjectGuid Error: bad input received',$ImmutableId, 'expecting a base 64 encoded string like','QBLtiithN0yENM4ji3SYjw==' Magenta,Yellow,Magenta,Yellow } } End { } } function Remove-AzureUserProxyAddresses { <# .SYNOPSIS Function to delete unwanted proxy addresses from an Azure user .DESCRIPTION Function to delete unwanted proxy addresses from an Azure user that is synchronized from an on-premises AD user via ADConnect This function depends on and requires the following PowerShell modules: ActiveDirectory AzureAD MsOnline .PARAMETER SamAccountName This is a required parameter. It is the samAccountName of the AD user such as 'abcdef'. .PARAMETER LogFile This is an optional parameter. It's a path to a file where this script saves time-stamped entries of its console output. If not provided, it defaults to a file in the current folder. .EXAMPLE Remove-AzureUserProxyAddresses -samAccountName 'abcdef' .OUTPUTS None .LINK https://superwidgets.wordpress.com/category/powershell/ .NOTES Function by Sam Boutros v0.1 - 23 March 2021 #> [CmdletBinding(ConfirmImpact='Low')] Param( [Parameter(Mandatory=$True)][String]$samAccountName, [Parameter(Mandatory=$false)][String]$LogFile = ".\Remove-AzureUserProxyAddresses_$(Get-Date -Format 'ddMMMMyyyy_hh-mm-ss_tt').log" ) Begin { Write-Warning 'Please ensure that the AD Sync Scheduler is stopped before running this script.' Write-Warning 'On your ADConnect server run:' Write-Warning 'Set-ADSyncScheduler -SyncCycleEnabled $false' Write-Warning 'You will also need to stop or wait for any synchronization in progress (In the Synchronization Service Manager GUI tool).' try { Get-MsolUser -MaxResults 1 -EA 1 | Out-Null Write-Log 'Validated connection to Microsoft Online Service.' Green $LogFile } catch { Write-Log $_.Exception.Message Yellow $LogFile Connect-MsolService | Out-Null } try { Get-AzureADUser -Top 1 -EA 1 | Out-Null Write-Log 'Validated connection to Azure AD.' Green $LogFile } catch { Write-Log $_.Exception.Message Yellow $LogFile Connect-AzureAD | Out-Null } try { $ADUser = Get-ADUser -Identity $samAccountName -Properties proxyaddresses,objectguid -EA 1 Write-Log 'Identified AD user',"'$($ADUser.DisplayName)' ($($ADUser.DistinguishedName))" Green,Cyan $LogFile } catch { Write-Log 'Remove-AzureUserProxyAddresses Error: User samAccountName',$samAccountName,'not found' Magenta,Yellow,Magenta $LogFile Write-Log $_.Exception.Message Yellow $LogFile break } if (-not ($AzureUser = Get-AzureADUser -Filter "ImmutableId eq '$(Convert-ObjectGuid2ImmutableId -ObjectGuid $ADUser.ObjectGUID)'")) { Write-Log 'Remove-AzureUserProxyAddresses Error:','Azure user with ImmutableId',(Convert-ObjectGuid2ImmutableId -ObjectGuid $ADUser.ObjectGUID),'corresponding to AD user with ObjectGUID',$ADUser.ObjectGUID,'not found' Magenta,Yellow,Magenta,Yellow,Magenta,Yellow $LogFile break } if ($ExtraProxyAddressLit = (compare $AzureUser.proxyaddresses $ADUser.proxyaddresses | where SideIndicator -EQ '<=').InputObject) { Write-Log 'Identified the following proxy addresses to be removed' Green $LogFile $ExtraProxyAddressLit | foreach { Write-Log " $_ " Cyan $LogFile } } else { Write-Log 'No extra proxy addresses found in the Azure user that don''t exist in the AD user' Yellow $LogFile break } } Process { #region Soft delete the user Write-Log 'Deleting the user',$AzureUser.UserPrincipalName Green,Cyan $LogFile -NoNewLine try { Remove-Msoluser -UserPrincipalName $AzureUser.UserPrincipalName -Force -EA 1 Write-Log 'done' DarkYellow -NoNewLine } catch { } try { Get-MsolUser -UserPrincipalName $AzureUser.UserPrincipalName -EA 1 | Out-Null Write-Log 'Remove-AzureUserProxyAddresses Error:','failed to delete user - UPN:',$AzureUser.UserPrincipalName Magenta,Yellow,Magenta $LogFile break } catch { Write-Log 'and validated' Green $LogFile } #endregion #region Create temp cloud user(s) with the email addresses to be removed $PasswordProfile = New-Object -TypeName Microsoft.Open.AzureAD.Model.PasswordProfile $PasswordProfile.Password = New-Password -Length 16 -Include LowerCase,UpperCase,Numbers # This temporary user will be deleted as part of this script foreach ($UserUPN in $ExtraProxyAddressLit) { $UserUPN = ($UserUPN -split ':')[1] Write-Log 'Creating temp user',$UserUPN Green,Cyan $LogFile -NoNewLine try { New-AzureADUser -AccountEnabled $True -DisplayName $AzureUser.DisplayName -PasswordProfile $PasswordProfile -MailNickName $AzureUser.MailNickName -UserPrincipalName $UserUPN -EA 1 | Out-Null Write-Log 'done' DarkYellow $LogFile } catch { Write-Log 'failed' Magenta $LogFile Write-Log $_.Exception.Message Yellow $LogFile # break } # To add proxy addresses, assign a license Start-Sleep -Seconds 2 Set-AzureADUser -ObjectID $UserUPN -UsageLocation 'US' Start-Sleep -Seconds 2 Set-MsolUserLicense -UserPrincipalName $UserUPN -AddLicenses 'cignatlp:STANDARDPACK' } #endregion #region Restore the user Write-Log 'Restoring the user',$AzureUser.UserPrincipalName Green,Cyan $LogFile -NoNewLine try { Restore-MsolUser -UserPrincipalName $AzureUser.UserPrincipalName -AutoReconcileProxyConflicts -EA 1 | Out-Null Write-Log 'done' DarkYellow $LogFile } catch { Write-Log 'failed' Magenta $LogFile Write-Log $_.Exception.Message Yellow $LogFile } Start-Sleep -Seconds 3 $NewUser = Get-AzureADUser -Filter "UserPrincipalName eq '$($AzureUser.UserPrincipalName)'" Write-Log 'New Proxy Address list:' Green $LogFile Write-Log ($NewUser.ProxyAddresses -match 'smtp:' | sort | Out-String).Trim() Cyan $LogFile #endregion # Re-enable ADConnect scheduler Write-Warning 'Now re-enable the AD Sync Scheduler.' Write-Warning 'On your ADConnect server run:' Write-Warning 'Set-ADSyncScheduler -SyncCycleEnabled $true' #region Clean up the temp cloud user(s) foreach ($UserUPN in $ExtraProxyAddressLit) { $UserUPN = ($UserUPN -split ':')[1] Write-Log 'Removing temp user',$UserUPN Green,Cyan $LogFile -NoNewLine Set-MsolUserLicense -UserPrincipalName $UserUPN -RemoveLicenses 'cignatlp:STANDARDPACK' -EA 0 Start-Sleep -Seconds 3 Get-AzureADUser -Filter "UserPrincipalName eq '$UserUPN'" | Remove-AzureADUser if ($StillThere = Get-AzureADUser -Filter "UserPrincipalName eq '$UserUPN'") { Write-Log 'failed' Magenta $LogFile } else { Write-Log 'done' DarkYellow $LogFile } } #endregion } End { } } function Get-AzSBSubscription { <# .SYNOPSIS Function to return Azure subscription information including parent Management Group(s) .DESCRIPTION Function to return Azure subscription information including parent Management Group(s) This function also requires prior login to an Azure tenant via Connect-AzAccount cmdlet of the Az.Accounts PowerShell module. This function depends on the following PowerShell modules: - Az.Accounts - Az.Resources .PARAMETER ManagementGroupName This is an optional parameter. This function expects a valid Management Group Name for the currently logged on Azure tenant. It's mainly used for the recursive feature of this function. .PARAMETER SubscriptionList This is an optional parameter. This function expects the output of Get-AzSubscription cmdlet of the Az.Accounts PowerShell module. It's mainly used for the recursive feature of this function to reduce the repetition of invoking Get-AzSubscription cmdlet. .PARAMETER MGTree This is an optional parameter. This function expects a string of comma separated Management Group names representing the parents of the current Management Group. Example: Root, MxxxA, mxxxxl, mxxxxxxs, mxxxxxxd It's mainly used for the recursive feature of this function. .PARAMETER Silent This is an optional Switch. When set to True, this function will suppress console progress messages. .PARAMETER ExcludeDisabled This is an optional Switch. When set to True, this function will not return information on disabled subscriptions. .PARAMETER ExcludeAADSub This is an optional Switch. When set to True, this function will not return information on subscription(s) named 'Access to Azure Active Directory'. .PARAMETER MGTree This is an optional parameter. It defaults to a file name in the current folder where this function will save its console output. .EXAMPLE $mySublist = Get-AzSBSubscription This will show console output similar to: Identified 23 subscriptions in Azure tenant 7xxxxxxxxxxxxxxxxxxxxxxxxxf Processing MG 7xxxxxxxxxxxxxxxxxxxxxxxxf Processing MG Sxxxxxxxxxxxd Processing MG Sxxxxxxxxxxxxxxs Processing MG Pxxxxxxxxxxxxxs Processing MG Fxxxxxxxxxxs Processing MG Dxxxxxxxxxxxxxxs Processing MG Fxxxxxxxxxxxxxs .EXAMPLE Get-AzSBSubscription -Silent .OUTPUTS This function returns a PowerShell object for each subscription such as: Id Name MGTree -- ---- ------ exxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx7 Access to Azure Active Directory Root 6xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx4 hxxxxxxxxv Root, MxxxxxxA, mxxxxxxxxp, mxxxxxxxxxxxxxxxxxs, mxxxxxxxxxxxxxxxxxxxxxv cxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx4 hxxxxxxxxxd Root, MxxxxxxA, mxxxxxxxxp, mxxxxxxxxxxxxxxxxxs, mxxxxxxxxxxxxxxxxxxxxxxd exxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx5 hxxxxxxxxxv Root, MxxxxxxA, mxxxxxxxxp, mxxxxxxxxxxxxxxxxxxs, mxxxxxxxxxxxxxxxxxxxxxxv .LINK https://superwidgets.wordpress.com/category/powershell/ .NOTES Function by Sam Boutros v0.1 - 27 July 2021 v0.2 - 27 July 2021 - Added ExcludeDisabled and ExcludeAADSub switches. v0.3 - 3 August 2021 - Added State property to output. Known Issues: ExcludeDisabled and ExcludeAADSub switches don't seem to work past the Root management group #> [CmdletBinding(ConfirmImpact='Low')] Param( [Parameter(Mandatory=$false)][String]$ManagementGroupName, [Parameter(Mandatory=$false)][String]$MGTree, [Parameter(Mandatory=$false)][String[]]$SubscriptionList, [Parameter(Mandatory=$false)][Switch]$Silent, [Parameter(Mandatory=$false)][Switch]$ExcludeDisabled, [Parameter(Mandatory=$false)][Switch]$ExcludeAADSub, [Parameter(Mandatory=$false)][String]$LogFile = ".\Get-AzSBSubscription_$($env:COMPUTERNAME)_$(Get-Date -Format 'ddMMMMyyyy_hh-mm-ss_tt').log" ) Begin { } Process { $Context = Get-AzContext if (-not $SubscriptionList) { $SubscriptionList = Get-AzSubscription } if ($SubscriptionList) { if (-not $ManagementGroupName) { $ManagementGroupName = $Context.Tenant.Id if (-not $Silent) { Write-Log 'Identified',$SubscriptionList.Count,'subscriptions in Azure tenant',$Context.Tenant Green,Cyan,Green,Cyan $LogFile } } try { if (-not $Silent) { Write-Log ' Processing MG',$ManagementGroupName Green,Cyan $LogFile } $ChildList = Get-AzManagementGroup -Recurse -GroupName $ManagementGroupName -Expand -WA 0 -EA 1 | Select Id,Name,Children,MGTree | sort Name $NextMGTree = if ($MGTree) { $MGTree,$ManagementGroupName -join ', ' } else { 'Root' } $SubList = foreach ($Sub in ($ChildList.Children | where Type -Match 'subscriptions')) { New-Object -TypeName PSObject -Property ([Ordered]@{ Id = $Sub.Name Name = $Sub.DisplayName State = ($SubscriptionList | where Id -EQ $Sub.Name).State MGTree = $NextMGTree }) } foreach ($Sub in $SubList) { if ($ExcludeAADSub -and $Sub.Name -eq 'Access to Azure Active Directory') { # Suppress the Output } elseif ($ExcludeDisabled -and ($SubscriptionList | where Id -EQ $Sub.Id).State -eq 'Disabled') { # Suppress the Output } else { $Sub } } $MGList = $ChildList.Children | where Type -Match 'managementGroups' | Select Name,Id,MGTree $MGList | foreach { $_.MGTree = $NextMGTree } $MGList | foreach { $ParamList = @{ ManagementGroupName = $_.Name SubscriptionList = $SubscriptionList MGTree = $NextMGTree LogFile = $LogFile } if ($Silent) { $ParamList += @{ Silent = $true } } if ($ExcludeAADSub) { $ParamList += @{ ExcludeAADSub = $true } } if ($ExcludeDisabled) { $ParamList += @{ ExcludeDisabled = $true } } Get-AzSBSubscription @ParamList } } catch { Write-Log 'Get-AzSBSubscription Error:','Bad ManagementGroupName provided',$ManagementGroupName Magenta,Yellow,Magenta $LogFile Write-Log $_.Exception.Message Yellow $LogFile } } else { Write-Log 'Get-AzSBSubscription Error:','No subscriptions found in the tenant',$Context.Tenant Magenta,Yellow,Magenta $LogFile Write-Log ' or this user/service principal does not have enough permission to list subscriptions, recommend running under a user with Reader Resource RBAC role at the Root Management group scope' Yellow $LogFile Write-Log 'Current Azure Context:' Magenta $LogFile Write-Log ($Context | FL * | Out-String).Trim() Yellow $LogFile break } } End { } } function New-AzureServicePrincipal { <# .SYNOPSIS Function to provision an Azure Service Principal. .DESCRIPTION Function to provision an Azure Service Principal secured by a secret (password). This function also requires prior login to an Azure tenant via Connect-AzAccount cmdlet of the Az.Accounts PowerShell module and Connect-AzureAD cmdlet of the AzureAD PowerShell Module. This function depends on the following PowerShell modules: - Az.Accounts - Az.Resources - AzureAD .PARAMETER ServicePrincipalName Name of the Azure Service Principal to be provisioned. .PARAMETER SecretDays This optional parameter determines the life of the secret (password) of the Service Principal. It defaults to 365 days, or 1 year from the time it's provisioned. .PARAMETER SecretLength This optional parameter determines the length of the secret (password). It defaults to the maximum of 256 characters in view of the SSO Brute force Vulnerability https://www.secureworks.com/research/undetected-azure-active-directory-brute-force-attacks .PARAMETER IncludeSpecialCharacters When this Switch is set to True, this function will include special characters in the random 25 character secret (password) it generates for the new Service Principal. Otherwise, the secret will include upper case letters, lower case letters, and numbers only. .PARAMETER SaveSecretToLog When this Switch is set to True, this function will write the Service Principal secret (password) to the log file (PLAIN TEXT). This function will always display the Service Principal secret (password) on the console. .PARAMETER SubscriptionList One or more subscription names. This parameter is used with ResourceRoleList parameter. When both are provided, this function will assign the provided Resource Roles to the new Service Principal at scope of each of the provided subscription names. .PARAMETER ResourceRoleList One or more Resource Role names such as 'Owner' or 'Reader'. This parameter is used with ResourceRoleList parameter. When both are provided, this function will assign the provided Resource Roles to the new Service Principal at scope of each of the provided subscription names. .PARAMETER NewSecret When this Switch is set to True, this function will issue a new secret for an existing Service Principal. .PARAMETER RemoveExpiredSecrets When this Switch is set to True, this function will remove expired secrets if any are found for an existing Service Principal. .PARAMETER RemoveExpiredCerts When this Switch is set to True, this function will remove expired certificates if any are found for an existing Service Principal. .PARAMETER LogFile This is an optional parameter. It defaults to a file name in the current folder where this function will save its console output. .EXAMPLE $ServicePrincipal = New-AzureServicePrincipal -ServicePrincipalName samtest6 This will create a new Azure Service Principal, display its secret on the console but not in the log file. $ServicePrincipal variable will contain details on the new Service Principal including the secret. .EXAMPLE $ServicePrincipal = New-AzureServicePrincipal -ServicePrincipalName samtest6 This will create a new Azure Service Principal called samtest6, display its secret on the console but not in the log file. $ServicePrincipal variable will contain details on the new Service Principal including the secret. $CredFile = ".\$($ServicePrincipal.SPName.Replace('\','_').Replace('/','_')).txt" $Cred = New-Object -TypeName PSCredential -ArgumentList $ServicePrincipal.SPName , (ConvertTo-SecureString -String $ServicePrincipal.SPSecret -AsPlainText -Force) $Cred.Password | ConvertFrom-SecureString | Out-File $CredFile These 3 lines will save the secret/password securely to disk. It can only be decrypted by the same Windows user who saved it. $Pwd = Get-Content ".\$($ServicePrincipal.SPName.Replace('\','_').Replace('/','_')).txt" | ConvertTo-SecureString $Cred = New-Object -TypeName PSCredential -ArgumentList $ServicePrincipal.SPName , $Pwd $Cred.GetNetworkCredential().Password # Display the plain text secret/password These 3 lines will retrieve the secret/password from disk. .EXAMPLE $ServicePrincipal = New-AzureServicePrincipal -ServicePrincipalName samtest6 -NewSecret This will create a new secret for an existing Azure Service Principal, display its secret on the console but not in the log file. $ServicePrincipal variable will contain details on the Service Principal including the new secret. .EXAMPLE $ServicePrincipal = New-AzureServicePrincipal -ServicePrincipalName samtest6 -NewSecret -RemoveExpiredSecrets This will create a new secret for an existing Azure Service Principal, display its secret on the console but not in the log file. It will also delete any existing expired secrtes for this existing Service Principal. $ServicePrincipal variable will contain details on the Service Principal including the new secret. .EXAMPLE New-AzureServicePrincipal -ServicePrincipalName samtest6 -RemoveExpiredCerts For this existing Service Principal, this function will delete any existing expired certificates. .OUTPUTS This function returns a PowerShell object such as: SPName : samtest5 SPId : bxxxxxx1-8185-43e4-99ef-cxxxxxxxxxx8 SPAppId : bxxxxxx7-ed64-49da-b2ac-3xxxxxxxxxx9 SPSecret : krMioq4v0EjLe3AY5D6udPRy Expires : 9/28/2022 8:56:38 AM ResourceRoleAssignments : {@{RoleName=Reader; RoleScopeById=/subscriptions/6xxxxxx3-1xxx-xxxx-xxxx-1xxxxxxxxa; RoleScopeByName=/subscriptions/MySubscrioptionName}} .LINK https://superwidgets.wordpress.com/category/powershell/ https://superwidgets.wordpress.com/2018/03/15/new-sbazserviceprincipal-cmdlet-to-create-new-azure-ad-service-principal-added-to-azsbtools-powershell-module/ .NOTES Function by Sam Boutros v0.1 - 28 September 2021 - Original release. v0.2 - 30 September 2021 Added -NewSecret and -RemoveExpiredSecrets and -RemoveExpiredCerts switches and related code v0.3 - 4 October 2021 Added -SecretLength Parameter and related code. Added a GUID to the secret for identification. Upcoming improvements: - Ability to assign Resource Roles at resource group level. - Ability to assign Azure AD roles directly. - Ability to assign Azure AD roles via Azure group membership. #> [CmdletBinding(ConfirmImpact='Low')] Param( [Parameter(Mandatory=$true)][String]$ServicePrincipalName, [Parameter(Mandatory=$false)][ValidateRange(1,4000)][Int32]$SecretDays = 365, [Parameter(Mandatory=$false)][ValidateRange(1,256)][Int32]$SecretLength = 256, [Parameter(Mandatory=$false)][Switch]$IncludeSpecialCharacters, [Parameter(Mandatory=$false)][Switch]$SaveSecretToLog, [Parameter(Mandatory=$false)][Switch]$NewSecret, [Parameter(Mandatory=$false)][Switch]$RemoveExpiredSecrets, [Parameter(Mandatory=$false)][Switch]$RemoveExpiredCerts, [Parameter(Mandatory=$false)][String[]]$ResourceRoleList, [Parameter(Mandatory=$false)][String[]]$SubscriptionList, [Parameter(Mandatory=$false)][String]$LogFile = ".\New-AzureServicePrincipal_$(Get-Date -Format 'ddMMMMyyyy_hh-mm-ss_tt').log" ) Begin { $ModuleList = @( 'Az.Accounts' 'az.resources' 'AzureAD -or AzureADPreview' ) $ModuleGo = $true foreach ($Module in $ModuleList) { if ($Module -match '-or') { $FoundCount = 0 $ModSubList = ($Module -split '-or').Trim() foreach ($ModuleName in $ModSubList) { try { Import-Module $ModuleName -Force -EA 1 | out-null; $FoundCount ++ } catch {} } if ($FoundCount -eq 0) { Write-Log 'Error:','unable to load any of the required modules',$Module Magenta,Yellow,Magenta $LogFile; $ModuleGo = $false } } else { try { Import-Module $Module -Force -EA 1 | out-null } catch { Write-Log 'Error:','unable to load required module',$Module Magenta,Yellow,Magenta $LogFile Write-Log $_.Exception.Message Yellow $LogFile $ModuleGo = $false } } } if (-not $ModuleGo) { break } } Process { if ($FoundSP = Get-AzADServicePrincipal -DisplayName $ServicePrincipalName) { Write-Log 'Identified Service Principal:' Green $LogFile Write-Log ($FoundSP | FL DisplayName,Id,ApplicationId | Out-String).Trim() Cyan $LogFile try { $APP = Get-AzureADApplication -Filter "DisplayName eq '$ServicePrincipalName'" -EA 1 Write-Log 'Identified App:' Green $LogFile Write-Log ($APP | FL DisplayName,ObjectId,AppId | Out-String).Trim() Cyan $LogFile if ($SecretList = Get-AzureADApplicationPasswordCredential -ObjectId $APP.ObjectId) { Write-Log 'Identified App secret(s):' Green $LogFile Write-Log ($SecretList | FL KeyId,EndDate | Out-String).Trim() Cyan $LogFile if ($RemoveExpiredSecrets) { foreach ($Secret in ($SecretList | where {($_.EndDate - (Get-Date)) -le 0})) { Write-Log 'Removing expired secret',"$($Secret.KeyId) (Expired $($Secret.EndDate))" Green,Cyan $LogFile -NoNewLine try { $Removed = Remove-AzureADApplicationPasswordCredential -ObjectId $APP.ObjectId -KeyId $Secret.KeyId -EA 1 Write-Log 'done' DarkYellow $LogFile } catch { Write-Log 'failed' Magenta $LogFile Write-Log $_.Exception.Message Yellow $LogFile } } } # Remove Expired Secrets } else { Write-Log 'No App secrets found!?' Yellow $LogFile } if ($CertList = Get-AzureADApplicationKeyCredential -ObjectId $APP.ObjectId) { Write-Log 'Identified App certificate(s):' Green $LogFile Write-Log ($CertList | FL KeyId,EndDate,@{n='CustomKeyId';e={ $_.CustomKeyIdentified -join ',' }} | Out-String).Trim() Cyan $LogFile if ($RemoveExpiredCerts) { foreach ($Cert in ($CertList | where {($_.EndDate - (Get-Date)) -le 0})) { Write-Log 'Removing expired certificate',"$($Cert.KeyId) (Expired $($Cert.EndDate))" Green,Cyan $LogFile -NoNewLine try { $Removed = Remove-AzureADApplicationKeyCredential -ObjectId $APP.ObjectId -KeyId $Cert.KeyId -EA 1 Write-Log 'done' DarkYellow $LogFile } catch { Write-Log 'failed' Magenta $LogFile Write-Log $_.Exception.Message Yellow $LogFile } } } # Remove Expired Certificates } else { Write-Log 'No App certificates found.' Green $LogFile } } catch { Write-Log 'New-AzureServicePrincipal Error:','Get-AzureADApplication cmdlet failed:' Magenta,Yellow $LogFile Write-Log $_.Exception.Message Yellow $LogFile break } # Get-AzureADApplication if ($NewSecret) { $Pwd = if ($IncludeSpecialCharacters) { New-Password -Length $SecretLength } else { New-Password -Length $SecretLength -Include LowerCase,UpperCase,Numbers } $ParameterSet = @{ ObjectId = $APP.ObjectId Value = $Pwd StartDate = Get-Date EndDate = (Get-Date).AddDays($SecretDays) } Write-Log 'Creating new secret for Service Principal',$ServicePrincipalName Green,Cyan $LogFile -NoNewLine try { $CreatedSecret = New-AzureADApplicationPasswordCredential @ParameterSet -EA 1 Write-Log 'done' DarkYellow $LogFile } catch { Write-Log 'failed' Magenta $LogFile Write-Verbose '$CreatedSecret:' Write-Verbose ($CreatedSecret | FL * | Out-String).Trim() Write-Log $_.Exception.Message Yellow $LogFile break } $mySP = New-Object -TypeName PSObject -Property ([Ordered]@{ SPName = $FoundSP.DisplayName SPId = $FoundSP.Id SPAppId = $FoundSP.ApplicationId SPSecret = $Pwd Expires = (Get-Date).AddDays($SecretDays) ResourceRoleAssignments = @() }) Write-Verbose '$mySP:' Write-Verbose ($mySP | FL * | Out-String).Trim() } else { Write-Log 'New-AzureServicePrincipal Error: Service Principal',$ServicePrincipalName,'already exists.' Magenta,Yellow,Magenta $LogFile Write-Log 'To provision a new secret for an existing Service Principal, use -NewSecret switch.' Yellow $LogFile break } # NewSecret } else { if ($NewSecret) { Write-Log 'New-AzureServicePrincipal Error: -NewSecret switch used, but Service Principal',$ServicePrincipalName,'is not found' Magenta,Yellow,Magenta $LogFile Write-Log 'Use the -NewSecret switch with existing Service Principals only.' Yellow $LogFile break } else { #region Create SP $Pwd = if ($IncludeSpecialCharacters) { New-Password -Length $SecretLength } else { New-Password -Length $SecretLength -Include LowerCase,UpperCase,Numbers } $ParameterSet = @{ DisplayName = $ServicePrincipalName PasswordCredential = New-Object -TypeName Microsoft.Azure.Commands.ActiveDirectory.PSADPasswordCredential -Property @{ StartDate = Get-Date EndDate = (Get-Date).AddDays($SecretDays) Password = $Pwd KeyId = New-Guid } } Write-Log 'Creating Azure Service Principal',$ServicePrincipalName Green,Cyan $LogFile -NoNewLine try { $SP = New-AzADServicePrincipal @ParameterSet -EA 1 Write-Log 'done' DarkYellow $LogFile } catch { Write-Log 'failed' Magenta $LogFile Write-Log $_.Exception.Message Yellow $LogFile break } # Create SP $mySP = New-Object -TypeName PSObject -Property ([Ordered]@{ SPName = $sp.DisplayName SPId = $sp.Id SPAppId = $sp.ApplicationId SPSecret = $Pwd Expires = (Get-Date).AddDays($SecretDays) ResourceRoleAssignments = @() }) #endregion #region Assign Resource Roles if ($ResourceRoleList) { foreach ($SubscriptionName in $SubscriptionList) { Write-Log 'Setting to context to subscription',$SubscriptionName Green,Cyan $LogFile -NoNewLine try { $Result = Set-AzContext -Subscription $SubscriptionName -EA 1 Write-Log 'done' DarkYellow $LogFile foreach ($ResourceRole in $ResourceRoleList) { try { $Role = Get-AzRoleDefinition -Name $ResourceRole -EA 1 Write-Log ' Identified Role',$Role.Name,'Description:',$Role.Description Green,Cyan,Green,Cyan $LogFile $Scope = "/subscriptions/$($Result.Subscription.Id)" #region retry for 60 sec while the Azure API catches up.. $RetryTotalSeconds = 60 $RetryWaitSeconds = 5 $StartRetry = Get-Date $Assigned = $False while (-not $Assigned -and (New-TimeSpan -Start $StartRetry -End (Get-Date)).TotalSeconds -le $RetryTotalSeconds) { try { $Temp = New-AzRoleAssignment -ObjectId $mySP.SPId -RoleDefinitionId $Role.Id -Scope $Scope -EA 1 $Assigned = $true Write-Log ' Assigned Resource Role',$Role.Name,'to Service Principal',"$($mySP.SPName) ($($mySP.SPId))" Green,Cyan,Green,Cyan $LogFile $mySP.ResourceRoleAssignments += New-Object -TypeName PSObject -Property ([Ordered]@{ RoleName = $Role.Name RoleScopeById = $Scope = "/subscriptions/$($Result.Subscription.Id)" RoleScopeByName = $Scope = "/subscriptions/$SubscriptionName" }) } catch { if ($_.Exception.Message -match 'does not exist in the directory') { Write-Log ' Waiting on Azure API to catch up before assigning Resource Role',$Role.Name,'to Service Principal',"$($mySP.SPName) ($($mySP.SPId))" Yellow,Cyan,Green,Cyan $LogFile Start-Sleep -Seconds $RetryWaitSeconds } else { Write-Log 'New-AzureServicePrincipal Error:','Azure Resource Role Assignment failed, details of command used:' Magenta,Yellow $LogFile Write-Log "New-AzRoleAssignment -ObjectId $($mySP.SPId) -RoleDefinitionId $($Role.Id) -Scope $Scope" Yellow $LogFile Write-Log $_.Exception.Message Yellow $LogFile $Temp = $_.Exception.Message } } # New-AzRoleAssignment } if (-not $Temp.DisplayName) { Write-Log 'New-AzureServicePrincipal Error:','Azure Resource Role Assignment failed, details of command used:' Magenta,Yellow $LogFile Write-Log "New-AzRoleAssignment -ObjectId $($mySP.SPId) -RoleDefinitionId $($Role.Id) -Scope $Scope" Yellow $LogFile Write-Log $Temp Yellow $LogFile } #endregion } catch { Write-Log 'New-AzureServicePrincipal Error:','No role definition for Resource Role',$ResourceRole,'found in subscription',$SubscriptionName Magenta,Yellow,Cyan,Yellow,Cyan $LogFile Write-Log $_.Exception.Message Yellow $LogFile } # Get-AzRoleDefinition } # foreach $ResourceRole } catch { Write-Log 'failed' Magenta $LogFile Write-Log $_.Exception.Message Yellow $LogFile } # Set-AzContext } # foreach $SubscriptionName } # if ($ResourceRoleList #endregion } # New SP } } End { if ($SaveSecretToLog) { Write-Log ($mySP | FL SPName,SPId,SPAppId,SPSecret,Expires | Out-String).Trim() Cyan $LogFile } else { Write-Log ($mySP | FL SPName,SPId,SPAppId,Expires | Out-String).Trim() Cyan $LogFile Write-Host "SPSecret: $($mySP.SPSecret)" -ForegroundColor Cyan } foreach ($RoleAssignment in $mySP.ResourceRoleAssignments) { Write-Log ' Resource Role Assigned:', "'$($RoleAssignment.RoleName)'",'at scope',"'$($RoleAssignment.RoleScopeById)' ($($RoleAssignment.RoleScopeByName))" Green,Cyan,Green,Cyan $LogFile } $mySP } } #endregion #region Hyper-V functions function Get-ParentPath { <# .Synopsis Function to get parent disk/path tree of VHD(x) file .Description Function to get parent disk/path tree of VHD(x) file .Parameter VHDPath Full local path to the VHD(x) file. For example: 'e:\VMs\v-2012R2-G2a\v-2012R2-G2a-C_55DB25B0-EFA9-415F-A5D1-738A62742B4E.avhdx' .Parameter ComputerName Name of Hyper-V host where the VHD(x) file resides If absent, defaults to localhost .Parameter Silent Switch parameter, when set to True, this function will supress its console output. This parameter will NOT suppress error messages. .Example Get-ParentPath -VHDPath 'e:\VMs\v-2012R2-G2a\v-2012R2-G2a-C_55DB25B0-EFA9-415F-A5D1-738A62742B4E.avhdx' -ComputerName 'xhost16' Retunrs an array of the disk path and all its parents paths .Example $VMName = 'v-2012R2-G2a' $HVName = 'xHost16' $VMDisks = Invoke-Command -ComputerName $HVName -ArgumentList $VMName -ScriptBlock { Param($VMName) Get-VMHardDiskDrive -VMName $VMName } ($VMDisks.Path | Where { $_ -match ':' }) | foreach { Get-ParentPath -VHDPath $_ -ComputerName $HVName } Retunrs an array for each disk attached to the VM $VMName, containing the disk tree. Sample output: e:\VMs\v-2012R2-G2a\v-2012R2-G2a-C_55DB25B0-EFA9-415F-A5D1-738A62742B4E.avhdx e:\VMs\v-2012R2-G2a\v-2012R2-G2a-C_0020A6D3-0371-48E3-B67D-DE2ADF0BEDF1.avhdx e:\VMs\v-2012R2-G2a\v-2012R2-G2a-C_C2BA8DE5-8FE6-49AD-B12B-789853306524.avhdx e:\VMs\v-2012R2-G2a\v-2012R2-G2a-C_79E7AFEB-F867-4068-A3DA-6BF64F49E819.avhdx e:\VMs\v-2012R2-G2a\v-2012R2-G2a-C_232D8FD8-5855-4518-800C-2659385521FA.avhdx e:\VMs\v-2012R2-G2a\v-2012R2-G2a-C.VHDX e:\VMs\v-2012R2-G2a\v-2012R2-G2a-D_C492420D-011D-42F4-862A-A04A7222B7FC.avhdx e:\VMs\v-2012R2-G2a\v-2012R2-G2a-D_7B0A54C4-36E5-4229-9746-49566B0566AF.avhdx e:\VMs\v-2012R2-G2a\v-2012R2-G2a-D_460EDAAF-F30A-4744-8396-87449EAF1A8C.avhdx e:\VMs\v-2012R2-G2a\v-2012R2-G2a-D_9D3805B2-79F5-4E5F-865D-D69620E31B6A.avhdx e:\VMs\v-2012R2-G2a\v-2012R2-G2a-D_F37513BE-AD00-41FD-B24E-CBB3E0BDEFC4.avhdx e:\VMs\v-2012R2-G2a\v-2012R2-G2a-D.vhdx .Link https://superwidgets.wordpress.com/2014/11/11/powershell-script-to-merge-hyper-v-virtual-machine-disks .Notes Function by Sam Boutros v0.1 - 1 November 2014 v0.2 - 23 August 2021 - Rewrite for AZSBTools after Microsoft retired the Technet Gallery effective June 2020 – see https://docs.microsoft.com/en-us/teamblog/technet-gallery-retirement v0.3 - 27 August 2021 - Added 'silent parameter to supress console output. #> [CmdletBinding(ConfirmImpact='Low')] Param( [Parameter(Mandatory=$true)][ValidateNotNullorEmpty()][String]$VHDPath, [Parameter(Mandatory=$false)][alias('HVName','HyperVHostName')][String]$ComputerName = '.', [Parameter(Mandatory=$false)][Switch]$Silent, [Parameter(Mandatory=$false)][String]$LogFile = ".\Get-ParentPath_$(Get-Date -format yyyyMMdd_hhmmsstt).Log" ) Begin { } Process { if (-not $Silent) { Write-Log 'Getting disk information for file',$VHDPath,'on computer',$ComputerName Green,Cyan,Green,Cyan $LogFile } try { $VHDSVC = Get-WmiObject -ComputerName $ComputerName -Namespace root\virtualization\v2 -Class Msvm_ImageManagementService -ErrorAction Stop $VHDInfo = [xml]$VHDSVC.GetVirtualHardDiskSettingData($VHDPath).SettingData if ($VHDInfo) { $ParentPath = ($VHDInfo.INSTANCE.PROPERTY | Where { $_.Name -eq 'ParentPath' }).Value if ($ParentPath) { $Result = @($VHDPath,$ParentPath) While ($ParentPath.Split(".")[1] -match 'avhd') { $VHDInfo = [xml]$VHDSVC.GetVirtualHardDiskSettingData($ParentPath).SettingData $ParentPath = ($VHDInfo.INSTANCE.PROPERTY | Where { $_.Name -eq "ParentPath" }).Value $Result += $ParentPath } if (-not $Silent) { Write-Log 'Got disk chain information:' Green $LogFile } if (-not $Silent) { $Result | foreach { Write-Log " $_" Cyan $LogFile } } } else { $Result = $VHDPath Write-Log 'Disk',$Result,'is not a differencing disk - does not have any parent..' Magenta,Yellow,Cyan $LogFile } } else { Write-Log 'Disk file',$VHDPath,'does not exist on computer',$ComputerName Magenta,Yellow,Magenta,Yellow $LogFile } $Result } catch { Write-Log 'Computer',$ComputerName,'is offline or cannot be contacted.' Magenta,Yellow,Magenta $LogFile Write-Log $_.Exception.Message Yellow $LogFile } } End { } } function Merge-VMDisks { <# .Synopsis Function to merge VM disks .Description Function/script to merge VM disks. The script will power down the VM in the process. This script requires to be invoked under credentials that have permission to remote into the VM and its Hyper-V host. .Parameter VMName Name of the VM whose VHD(x) disks are to be merged. .Parameter HyperVHoatName Name of the Hyper-V Host where the VM resides. If absent, this function will try to query the VM for its Hyper-V host name. .Parameter LogFile Name and path of the file where the script will log its steps and progress. .Example Merge-VMDisks -VMName 'myVMName' This will merge the VM disks if needed, and will require user manual confirmation before stopping the VM. .Example Merge-VMDisks -VMName 'myVMName' -Confirm:$false This will merge the VM disks if needed, without requiring user manual confirmation before stopping the VM. .Link https://superwidgets.wordpress.com/2014/11/11/powershell-script-to-merge-hyper-v-virtual-machine-disks .Notes Function by Sam Boutros v0.1 - 1 November 2014 v0.2 - 23 August 2021 - Rewrite for AZSBTools after Microsoft retired the Technet Gallery effective June 2020 – see https://docs.microsoft.com/en-us/teamblog/technet-gallery-retirement V0.3 - 27 August 2021 - Update to allow this function to work on powered off VMs. #> [CmdletBinding(SupportsShouldProcess=$true,ConfirmImpact='High')] Param( [Parameter(Mandatory=$true)][String]$VMName, [Parameter(Mandatory=$false)][String]$HyperVHostName, [Parameter(Mandatory=$false)][String]$LogFile = ".\Merge-VMDisks_$(Get-Date -format yyyyMMdd_hhmmsstt).Log" ) Begin { if ($VMName) { Write-Log 'Received input: VMName:',$VMName Green,Cyan $LogFile } if ($HyperVHostName) { Write-Log 'Received input: HyperVHostName:',$HyperVHostName Green,Cyan $LogFile } if (-not $VMName -and -not $HyperVHostName) { Write-Log 'Merge-VMDisks Error: parameters VMName and HyperVHostName not provided. Need at least one of the two.' Magenta $LogFile break } } Process{ #region Get Hyper-V host name from VM if (-not ($HyperVHostName)) { Write-Log 'HyperVHostName not provided, trying to get it from the VM',$VMName Green,Cyan $LogFile try { $HyperVHostName = Invoke-Command -ComputerName $VMName -ErrorAction Stop -ScriptBlock { try { (Get-ItemProperty -Path 'HKLM:\SOFTWARE\Microsoft\Virtual Machine\Guest\Parameters' -EA 1).PhysicalHostName } catch { "Failed: $($_.Exception.Message)" } } if ($HyperVHostName.IndexOf('Failed') -ge 0) { Write-Log 'Merge-VMDisks Error: failed to get Hyper-V host name from VM, VM returned:' Magenta $LogFile Write-Log $HyperVHostName Yellow $LogFile break } else { Write-Log 'Identified Hyper-V host',$HyperVHostName Green,Cyan $LogFile } } catch { Write-Log 'Merge-VMDisks Error' Magenta $LogFile Write-Log $_.Exception.Message Yellow $LogFile break } } #endregion #region Get VM disk information from Hyper-V host try { $VMDisks = Invoke-Command -ComputerName $HyperVHostName -EA 1 -ScriptBlock { Get-VMHardDiskDrive -VMName $Using:VMName } $VMDisks = $VMDisks | Where Path -match ':' # Get Disk parent path information foreach ($Disk in $VMDisks) { $DiskTree = Get-ParentPath -Silent -VHDPath $Disk.Path -ComputerName $HyperVHostName -LogFile $LogFile if ($DiskTree.Count -gt 1) { $Differencing = $true } else { $Differencing = $false } $Disk | Add-Member -MemberType NoteProperty -Name DiskTree -Value $DiskTree -EA 0 $Disk | Add-Member -MemberType NoteProperty -Name Differencing -Value $Differencing -EA 0 } Write-Log 'Identified VM disk(s):' Green $LogFile Write-Log ($VMDisks | FL Name,Path,Differencing,@{n='DiskTree';e={$_.DiskTree -join ', '}}| Out-String).Trim() Cyan $LogFile if ($VMDisks.Differencing -match 'True') {} else { Write-Log 'No differencing disks found, nothing to merge, stopping..' Yellow $LogFile break } } catch { Write-Log 'Merge-VMDisks Error: unable to identify VM disks' Magenta $LogFile Write-Log $_.Exception.Message Yellow $LogFile break } #endregion #region Stop VM if running $VMState = Invoke-Command -ComputerName $HyperVHostName -ScriptBlock { (Get-VM -Name $Using:VMName).State } if ($VMState.Value -eq 'Running') { Write-Log 'Stopping VM',$VMName,'on Hyper-V host',$HyperVHostName Green,Cyan,Yellow,Cyan $LogFile -NoNewLine } if ($VMState.Value -eq 'Running' -and $PSCmdlet.ShouldProcess($VMName)) { $Result = Invoke-Command -ComputerName $HyperVHostName -ScriptBlock { try { Stop-VM -Name $Using:VMName -Force -EA 1 } catch { $_.Exception.Message } } if ($Result) { Write-Log 'Merge-VMDisks error: Unable to stop VM',$VMName,'on hyper-V host',$HyperVHostName Magenta,Yellow,Magenta,Yellow $LogFile Write-Log $Result Yellow $LogFile break } else { Write-Log 'done' Green $LogFile $OriginalVMState = 'Running' } } $VMState = Invoke-Command -ComputerName $HyperVHostName -ScriptBlock { (Get-VM -Name $Using:VMName).State } if ($VMState.Value -eq 'Running') { Write-Log 'aborting based on user input' Yellow $LogFile break } #endregion #region Merge disks, attach new merged disks to VM foreach ($Disk in $VMDisks) { Write-Log 'Processing Disk',$Disk.Path Green,Cyan $LogFile for ($i=0; $i -lt $DiskTree.Count-1; $i++) { Write-Log 'Merging file',$Disk.DiskTree[$i],'#',($i+1),'of',($Disk.DiskTree.Count-1) Green,Cyan,Green,Cyan,Green,Cyan $LogFile Invoke-Command -ComputerName $HyperVHostName -ArgumentList $Disk.DiskTree[$i] -ScriptBlock { Param($DiskFile) Merge-VHD -Path $DiskFile -Confirm:$false -Force } } Write-Log 'Attaching merged disk',($Disk.DiskTree[$Disk.DiskTree.Count-1]) Green,Cyan $LogFile Invoke-Command -ComputerName $HyperVHostName -ArgumentList $VMName,$Disk.DiskTree,$Disk -ScriptBlock { Param($VMName,$DiskTree,$Disk) $Splat = @{ VMName = $VMName ControllerType = $Disk.ControllerType ControllerNumber = $Disk.ControllerNumber ControllerLocation = $Disk.ControllerLocation } Remove-VMHardDiskDrive @Splat $Splat += @{ Path = $DiskTree[$DiskTree.Count-1] } Add-VMHardDiskDrive @Splat } } Write-Log 'Done merging disks' Green $LogFile #endregion #region Start VM if it was running if ($OriginalVMState -eq 'Running') { Write-Log 'Starting VM',$VMName Green,Cyan $LogFile -NoNewLine $Result = Invoke-Command -ComputerName $HyperVHostName -ScriptBlock { try { Start-VM -VMName $Using:VMName -EA 1 } catch { $_.Exception.Message } } if ($Result) { Write-Log 'failed' Magenta $LogFile Write-Log $Result Yellow $LogFile break } else { Write-Log 'done' Green $LogFile } } #endregion } # end process end { } } #endregion #region Core functions function Function-Template { <# .SYNOPSIS Function to return the Geographical location of an Internet IP address .DESCRIPTION Function to return the Geographical location of an Internet IP address This function depends on ip-api.com and ipinfo.io .PARAMETER Source One or more URLs This is an optional parameter. These URLs will be queried for WAN IP. .EXAMPLE Get-MyWANIP .OUTPUTS This cmdlet returns a System.Net.IPAddress object such as: Address : 1132553623 AddressFamily : InterNetwork ScopeId : IsIPv6Multicast : False IsIPv6LinkLocal : False IsIPv6SiteLocal : False IsIPv6Teredo : False IsIPv4MappedToIPv6 : False IPAddressToString : 151.101.129.67 .LINK https://superwidgets.wordpress.com/category/powershell/ .NOTES Function by Sam Boutros v0.1 - 12 April 2020 #> [CmdletBinding(ConfirmImpact='Low')] Param( [Parameter(Mandatory=$false)][Alias('IPsToBlock')][IPAddress[]]$IPAddress = (Get-MyWANIP), [Parameter(Mandatory=$false)][String]$LogFile = ".\Function-Template_$($env:COMPUTERNAME)_$(Get-Date -Format 'ddMMMMyyyy_hh-mm-ss_tt').log" ) Begin { } Process { } End { } } function Write-Log { <# .SYNOPSIS Function to log input string to file and display it to screen .DESCRIPTION Function to log input string to file and display it to screen. Log entries in the log file are time stamped. Function allows for displaying text to screen in different colors. .PARAMETER String The string to be displayed to the screen and saved to the log file .PARAMETER Color The color in which to display the input string on the screen Default is White 16 valid options for [System.ConsoleColor] type are Black Blue Cyan DarkBlue DarkCyan DarkGray DarkGreen DarkMagenta DarkRed DarkYellow Gray Green Magenta Red White Yellow .PARAMETER LogFile Path to the file where the input string should be saved. Example: c:\log.txt If absent, the input string will be displayed to the screen only and not saved to log file .EXAMPLE Write-Log -String "Hello World" -Color Yellow -LogFile c:\log.txt This example displays the "Hello World" string to the console in yellow, and adds it as a new line to the file c:\log.txt If c:\log.txt does not exist it will be created. Log entries in the log file are time stamped. Sample output: 2014.08.06 06:52:17 AM: Hello World .EXAMPLE Write-Log "$((Get-Location).Path)" Cyan This example displays current path in Cyan, and does not log the displayed text to log file. .EXAMPLE "$((Get-Process | select -First 1).name) process ID is $((Get-Process | select -First 1).id)" | Write-Log -color DarkYellow Sample output of this example: "MDM process ID is 4492" in dark yellow .EXAMPLE Write-Log 'Found',(Get-ChildItem -Path .\ -File).Count,'files in folder',(Get-Item .\).FullName Green,Yellow,Green,Cyan .\mylog.txt Sample output will look like: Found 520 files in folder D:\Sandbox - and will have the listed foreground colors .EXAMPLE Write-Log (Get-Volume | sort DriveLetter | Out-String).Trim() Cyan .\mylog.txt Sample output will look like (in Cyan, and will also be written to .\mylog.txt): DriveLetter FriendlyName FileSystemType DriveType HealthStatus OperationalStatus SizeRemaining Size ----------- ------------ -------------- --------- ------------ ----------------- ------------- ---- Recovery NTFS Fixed Healthy OK 101.98 MB 450 MB C NTFS Fixed Healthy OK 7.23 GB 39.45 GB D Unknown CD-ROM Healthy Unknown 0 B 0 B E Data NTFS Fixed Healthy OK 26.13 GB 49.87 GB .LINK https://superwidgets.wordpress.com/2014/12/01/powershell-script-function-to-display-text-to-the-console-in-several-colors-and-save-it-to-log-with-timedate-stamp/ .NOTES Function by Sam Boutros v1.0 - 6 August 2014 v1.1 - 1 December 2014 - added multi-color display in the same line v1.2 - 8 August 2016 - updated date time stamp format, protect against bad LogFile name v1.3 - 22 September 2017 - Re-write: Error handling for no -String parameter, bad color(s), and bad -LogFile without errors Add Verbose messages v1.4 - 27 March 2020 - Update to skip writing to file if LogFile parameter is not provided v1.5 - 15 May 2020 - Update to fix bug related to colors (thanks Stephen) #> [CmdletBinding(ConfirmImpact='Low')] Param( [Parameter(Mandatory=$false, ValueFromPipeLine=$true, ValueFromPipeLineByPropertyName=$true, Position=0)] [String[]]$String, [Parameter(Mandatory=$false,Position=1)][String[]]$Color, [Parameter(Mandatory=$false,Position=2)][String]$LogFile, [Parameter(Mandatory=$false,Position=3)][Switch]$NoNewLine ) if ($String) { #region Write to Console $i=0 foreach ($item in $String) { try { Write-Host "$item " -ForegroundColor $Color[$i] -NoNewline -EA 1 } catch { Write-Host "$item " -NoNewline } $i++ } if (-not $NoNewLine) { Write-Host ' ' } #endregion #region Write to file if ($LogFile) { try { "$(Get-Date -format 'dd MMMM yyyy hh:mm:ss tt'): $($String -join ' ')" | Out-File -Filepath $Logfile -Append -ErrorAction Stop } catch { Write-Warning "Write-Log: Bad LogFile name ($LogFile). Will not save input string(s) to log file.." } } else { Write-Verbose 'Write-Log: Missing -LogFile parameter. Will not save input string(s) to log file..' } #endregion } else { Write-Verbose 'Write-Log: Missing -String parameter - nothing to write or log..' } } function Get-SBCredential { <# .SYNOPSIS Function to get a credential, save encrypted password to file for future automation .DESCRIPTION Function to get a credential, save encrypted password to file for future automation The function will use saved password if the password file exists The function will prompt for the password if the password file does not exist, or the -Refresh switch is used Note that the function does not validate whether the UserName exists in any directory, or that the password entered is valid. It merely creates a PSCredential object to be used securely for future automation, eleminating the need to type in the password every time the function is needed, or the need to save passwords in clear text in scripts. .PARAMETER UserName This can be in the format 'myusername' or 'domain\username' If not provided, the function assumes the username under which the function is executed .PARAMETER CredPath This is the folder where this function will save the pwd encrypted file. It defaults to $env:Temp folder, like C:\Users\myname\AppData\Local\Temp .PARAMETER Refresh This switch will force the function to prompt for the password and over-write the password file .OUTPUTS The function returns a PSCredential object that can be used with other cmdlets that use the -Credential parameter .EXAMPLE $MyCred = Get-SBCredential .EXAMPLE $Cred2 = Get-SBCredential -UserName 'sboutros' -Verbose -Refresh .EXAMPLE $Cred3 = 'domain2\ADSuperUser' | Get-SBCredential Disable-ADAccount -Identity 'Someone' -Server 'MyDomainController' -Credential $Cred3 This example obtains and saves credential of 'domain2\ADSuperUser' in $Cred3 varialble Second line uses that credential to disable an AD account of 'Someone' .LINK https://superwidgets.wordpress.com/2016/08/05/powershell-script-to-provide-a-ps-credential-object-saving-password-securely/ .NOTES Function by Sam Boutros 5 August 2016 - v0.1 1 April 2020 - v0.2 - Parameterized CredPath 30 September 2021 - v0.3 - Added error handling for bad Cred file. #> [CmdletBinding(ConfirmImpact='Low')] Param( [Parameter(Mandatory=$false, ValueFromPipeLine=$true, ValueFromPipeLineByPropertyName=$true, Position=0)] [String]$UserName = "$env:USERDOMAIN\$env:USERNAME", [Parameter(Mandatory=$false,Position=1)][ValidateScript({Test-Path $_})][String]$CredPath = $env:TEMP, [Parameter(Mandatory=$false,Position=2)][Switch]$Refresh ) Begin { $CredFile = "$CredPath\$($UserName.Replace('\','_').Replace('/','_')).txt" if ($Refresh) { Remove-Item -Path $CredFile -Force -Confirm:$false -ErrorAction SilentlyContinue } } Process { if (-not (Test-Path -Path $CredFile)) { Read-Host "Enter the pwd for $UserName" -AsSecureString | ConvertFrom-SecureString | Out-File $CredFile } try { $Pwd = Get-Content $CredFile | ConvertTo-SecureString -EA 1 } catch { if ($_.Exception.Message -match 'Key not valid for use in specified state') { Write-Log 'Get-SBCredential Error:','The provided Credential File',$CredFile,'was encrypted/saved by other than the current user',"$env:USERDNSDOMAIN\$env:USERNAME" Magenta,Yellow,Cyan,Yellow,cyan } else { Write-Log 'Get-SBCredential Error:',$_.Exception.Message Magenta,Yellow } break } } End { New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList $UserName, $Pwd } } function ConvertTo-EnhancedHTML { <# .SYNOPSIS Provides an enhanced version of the ConvertTo-HTML command that includes inserting an embedded CSS style sheet, JQuery, and JQuery Data Tables for interactivity. Intended to be used with HTML fragments that are produced by ConvertTo-EnhancedHTMLFragment. This command does not accept pipeline input. .PARAMETER jQueryURI A Uniform Resource Indicator (URI) pointing to the location of the jQuery script file. You can download jQuery from www.jquery.com; you should host the script file on a local intranet Web server and provide a URI that starts with http:// or https://. Alternately, you can also provide a file system path to the script file, although this may create security issues for the Web browser in some configurations. Tested with v1.8.2. Defaults to http://ajax.aspnetcdn.com/ajax/jQuery/jquery-1.8.2.min.js, which will pull the file from Microsoft's ASP.NET Content Delivery Network. .PARAMETER jQueryDataTableURI A Uniform Resource Indicator (URI) pointing to the location of the jQuery Data Table script file. You can download this from www.datatables.net; you should host the script file on a local intranet Web server and provide a URI that starts with http:// or https://. Alternately, you can also provide a file system path to the script file, although this may create security issues for the Web browser in some configurations. Tested with jQuery DataTable v1.9.4 Defaults to http://ajax.aspnetcdn.com/ajax/jquery.dataTables/1.9.3/jquery.dataTables.min.js, which will pull the file from Microsoft's ASP.NET Content Delivery Network. .PARAMETER CssStyleSheet The CSS style sheet content - not a file name. If you have a CSS file, you can load it into this parameter as follows: -CSSStyleSheet (Get-Content MyCSSFile.css) Alternately, you may link to a Web server-hosted CSS file by using the -CssUri parameter. .PARAMETER CssUri A Uniform Resource Indicator (URI) to a Web server-hosted CSS file. Must start with either http:// or https://. If you omit this, you can still provide an embedded style sheet, which makes the resulting HTML page more standalone. To provide an embedded style sheet, use the -CSSStyleSheet parameter. .PARAMETER Title A plain-text title that will be displayed in the Web browser's window title bar. Note that not all browsers will display this. .PARAMETER PreContent Raw HTML to insert before all HTML fragments. Use this to specify a main title for the report: -PreContent "<H1>My HTML Report</H1>" .PARAMETER PostContent Raw HTML to insert after all HTML fragments. Use this to specify a report footer: -PostContent "Created on $(Get-Date)" .PARAMETER HTMLFragments One or more HTML fragments, as produced by ConvertTo-EnhancedHTMLFragment. -HTMLFragments $part1,$part2,$part3 .EXAMPLE The following is a complete example script showing how to use ConvertTo-EnhancedHTMLFragment and ConvertTo-EnhancedHTML. The example queries 6 pieces of information from the local computer and produces a report in C:\. This example uses most of the avaiable options. It relies on Internet connectivity to retrieve JavaScript from Microsoft's Content Delivery Network. This example uses an embedded stylesheet, which is defined as a here-string at the top of the script. $computername = 'localhost' $path = 'c:\' $style = @" <style> body { color:#333333; font-family:Calibri,Tahoma; font-size: 10pt; } h1 { text-align:center; } h2 { border-top:1px solid #666666; } th { font-weight:bold; color:#eeeeee; background-color:#333333; cursor:pointer; } .odd { background-color:#ffffff; } .even { background-color:#dddddd; } .paginate_enabled_next, .paginate_enabled_previous { cursor:pointer; border:1px solid #222222; background-color:#dddddd; padding:2px; margin:4px; border-radius:2px; } .paginate_disabled_previous, .paginate_disabled_next { color:#666666; cursor:pointer; background-color:#dddddd; padding:2px; margin:4px; border-radius:2px; } .dataTables_info { margin-bottom:4px; } .sectionheader { cursor:pointer; } .sectionheader:hover { color:red; } .grid { width:100% } .red { color:red; font-weight:bold; } </style> "@ function Get-InfoOS { [CmdletBinding()] param( [Parameter(Mandatory=$True)][string]$ComputerName ) $os = Get-WmiObject -class Win32_OperatingSystem -ComputerName $ComputerName $props = @{'OSVersion'=$os.version; 'SPVersion'=$os.servicepackmajorversion; 'OSBuild'=$os.buildnumber} New-Object -TypeName PSObject -Property $props } function Get-InfoCompSystem { [CmdletBinding()] param( [Parameter(Mandatory=$True)][string]$ComputerName ) $cs = Get-WmiObject -class Win32_ComputerSystem -ComputerName $ComputerName $props = @{'Model'=$cs.model; 'Manufacturer'=$cs.manufacturer; 'RAM (GB)'="{0:N2}" -f ($cs.totalphysicalmemory / 1GB); 'Sockets'=$cs.numberofprocessors; 'Cores'=$cs.numberoflogicalprocessors} New-Object -TypeName PSObject -Property $props } function Get-InfoBadService { [CmdletBinding()] param( [Parameter(Mandatory=$True)][string]$ComputerName ) $svcs = Get-WmiObject -class Win32_Service -ComputerName $ComputerName ` -Filter "StartMode='Auto' AND State<>'Running'" foreach ($svc in $svcs) { $props = @{'ServiceName'=$svc.name; 'LogonAccount'=$svc.startname; 'DisplayName'=$svc.displayname} New-Object -TypeName PSObject -Property $props } } function Get-InfoProc { [CmdletBinding()] param( [Parameter(Mandatory=$True)][string]$ComputerName ) $procs = Get-WmiObject -class Win32_Process -ComputerName $ComputerName foreach ($proc in $procs) { $props = @{'ProcName'=$proc.name; 'Executable'=$proc.ExecutablePath} New-Object -TypeName PSObject -Property $props } } function Get-InfoNIC { [CmdletBinding()] param( [Parameter(Mandatory=$True)][string]$ComputerName ) $nics = Get-WmiObject -class Win32_NetworkAdapter -ComputerName $ComputerName ` -Filter "PhysicalAdapter=True" foreach ($nic in $nics) { $props = @{'NICName'=$nic.servicename; 'Speed'=$nic.speed / 1MB -as [int]; 'Manufacturer'=$nic.manufacturer; 'MACAddress'=$nic.macaddress} New-Object -TypeName PSObject -Property $props } } function Get-InfoDisk { [CmdletBinding()] param( [Parameter(Mandatory=$True)][string]$ComputerName ) $drives = Get-WmiObject -class Win32_LogicalDisk -ComputerName $ComputerName ` -Filter "DriveType=3" foreach ($drive in $drives) { $props = @{'Drive'=$drive.DeviceID; 'Size'=$drive.size / 1GB -as [int]; 'Free'="{0:N2}" -f ($drive.freespace / 1GB); 'FreePct'=$drive.freespace / $drive.size * 100 -as [int]} New-Object -TypeName PSObject -Property $props } } foreach ($computer in $computername) { try { $everything_ok = $true Write-Verbose "Checking connectivity to $computer" Get-WmiObject -class Win32_BIOS -ComputerName $Computer -EA Stop | Out-Null } catch { Write-Warning "$computer failed" $everything_ok = $false } if ($everything_ok) { $filepath = Join-Path -Path $Path -ChildPath "$computer.html" $params = @{'As'='List'; 'PreContent'='<h2>OS</h2>'} $html_os = Get-InfoOS -ComputerName $computer | ConvertTo-EnhancedHTMLFragment @params $params = @{'As'='List'; 'PreContent'='<h2>Computer System</h2>'} $html_cs = Get-InfoCompSystem -ComputerName $computer | ConvertTo-EnhancedHTMLFragment @params $params = @{'As'='Table'; 'PreContent'='<h2>♦ Local Disks</h2>'; 'EvenRowCssClass'='even'; 'OddRowCssClass'='odd'; 'MakeTableDynamic'=$true; 'TableCssClass'='grid'; 'Properties'='Drive', @{n='Size(GB)';e={$_.Size}}, @{n='Free(GB)';e={$_.Free};css={if ($_.FreePct -lt 80) { 'red' }}}, @{n='Free(%)';e={$_.FreePct};css={if ($_.FreeePct -lt 80) { 'red' }}}} $html_dr = Get-InfoDisk -ComputerName $computer | ConvertTo-EnhancedHTMLFragment @params $params = @{'As'='Table'; 'PreContent'='<h2>♦ Processes</h2>'; 'MakeTableDynamic'=$true; 'TableCssClass'='grid'} $html_pr = Get-InfoProc -ComputerName $computer | ConvertTo-EnhancedHTMLFragment @params $params = @{'As'='Table'; 'PreContent'='<h2>♦ Services to Check</h2>'; 'EvenRowCssClass'='even'; 'OddRowCssClass'='odd'; 'MakeHiddenSection'=$true; 'TableCssClass'='grid'} $html_sv = Get-InfoBadService -ComputerName $computer | ConvertTo-EnhancedHTMLFragment @params $params = @{'As'='Table'; 'PreContent'='<h2>♦ NICs</h2>'; 'EvenRowCssClass'='even'; 'OddRowCssClass'='odd'; 'MakeHiddenSection'=$true; 'TableCssClass'='grid'} $html_na = Get-InfoNIC -ComputerName $Computer | ConvertTo-EnhancedHTMLFragment @params $params = @{'CssStyleSheet'=$style; 'Title'="System Report for $computer"; 'PreContent'="<h1>System Report for $computer</h1>"; 'HTMLFragments'=@($html_os,$html_cs,$html_dr,$html_pr,$html_sv,$html_na)} ConvertTo-EnhancedHTML @params | Out-File -FilePath $filepath } } .Notes Function by Don Jones Generated on: 9/10/2013 For more information see Powershell.org included in AZSBTools module with permission by Don Jones #> [CmdletBinding()] param( [string]$jQueryURI = 'http://ajax.aspnetcdn.com/ajax/jQuery/jquery-1.8.2.min.js', [string]$jQueryDataTableURI = 'http://ajax.aspnetcdn.com/ajax/jquery.dataTables/1.9.3/jquery.dataTables.min.js', [Parameter(ParameterSetName='CSSContent')][string[]]$CssStyleSheet, [Parameter(ParameterSetName='CSSURI')][string[]]$CssUri, [string]$Title = 'Report', [string]$PreContent, [string]$PostContent, [Parameter(Mandatory=$True)][string[]]$HTMLFragments ) <# Add CSS style sheet. If provided in -CssUri, add a <link> element. If provided in -CssStyleSheet, embed in the <head> section. Note that BOTH may be supplied - this is legitimate in HTML. #> Write-Verbose "Making CSS style sheet" $stylesheet = "" if ($PSBoundParameters.ContainsKey('CssUri')) { $stylesheet = "<link rel=`"stylesheet`" href=`"$CssUri`" type=`"text/css`" />" } if ($PSBoundParameters.ContainsKey('CssStyleSheet')) { $stylesheet = "<style>$CssStyleSheet</style>" | Out-String } <# Create the HTML tags for the page title, and for our main javascripts. #> Write-Verbose "Creating <TITLE> and <SCRIPT> tags" $titletag = "" if ($PSBoundParameters.ContainsKey('title')) { $titletag = "<title>$title</title>" } $script += "<script type=`"text/javascript`" src=`"$jQueryURI`"></script>`n<script type=`"text/javascript`" src=`"$jQueryDataTableURI`"></script>" <# Render supplied HTML fragments as one giant string #> Write-Verbose "Combining HTML fragments" $body = $HTMLFragments | Out-String <# If supplied, add pre- and post-content strings #> Write-Verbose "Adding Pre and Post content" if ($PSBoundParameters.ContainsKey('precontent')) { $body = "$PreContent`n$body" } if ($PSBoundParameters.ContainsKey('postcontent')) { $body = "$body`n$PostContent" } <# Add a final script that calls the datatable code We dynamic-ize all tables with the .enhancedhtml-dynamic-table class, which is added by ConvertTo-EnhancedHTMLFragment. #> Write-Verbose "Adding interactivity calls" $datatable = "" $datatable = "<script type=`"text/javascript`">" $datatable += '$(document).ready(function () {' $datatable += "`$('.enhancedhtml-dynamic-table').dataTable();" $datatable += '} );' $datatable += "</script>" <# Datatables expect a <thead> section containing the table header row; ConvertTo-HTML doesn't produce that so we have to fix it. #> Write-Verbose "Fixing table HTML" $body = $body -replace '<tr><th>','<thead><tr><th>' $body = $body -replace '</th></tr>','</th></tr></thead>' <# Produce the final HTML. We've more or less hand-made the <head> amd <body> sections, but we let ConvertTo-HTML produce the other bits of the page. #> Write-Verbose "Producing final HTML" ConvertTo-HTML -Head "$stylesheet`n$titletag`n$script`n$datatable" -Body $body Write-Debug "Finished producing final HTML" } function ConvertTo-EnhancedHTMLFragment { <# .SYNOPSIS Creates an HTML fragment (much like ConvertTo-HTML with the -Fragment switch that includes CSS class names for table rows, CSS class and ID names for the table, and wraps the table in a <DIV> tag that has a CSS class and ID name. .PARAMETER InputObject The object to be converted to HTML. You cannot select properties using this command; precede this command with Select-Object if you need a subset of the objects' properties. .PARAMETER EvenRowCssClass The CSS class name applied to even-numbered <TR> tags. Optional, but if you use it you must also include -OddRowCssClass. .PARAMETER OddRowCssClass The CSS class name applied to odd-numbered <TR> tags. Optional, but if you use it you must also include -EvenRowCssClass. .PARAMETER TableCssID Optional. The CSS ID name applied to the <TABLE> tag. .PARAMETER DivCssID Optional. The CSS ID name applied to the <DIV> tag which is wrapped around the table. .PARAMETER TableCssClass Optional. The CSS class name to apply to the <TABLE> tag. .PARAMETER DivCssClass Optional. The CSS class name to apply to the wrapping <DIV> tag. .PARAMETER As Must be 'List' or 'Table.' Defaults to Table. Actually produces an HTML table either way; with Table the output is a grid-like display. With List the output is a two-column table with properties in the left column and values in the right column. .PARAMETER Properties A comma-separated list of properties to include in the HTML fragment. This can be * (which is the default) to include all properties of the piped-in object(s). In addition to property names, you can also use a hashtable similar to that used with Select-Object. For example: Get-Process | ConvertTo-EnhancedHTMLFragment -As Table ` -Properties Name,ID,@{n='VM'; e={$_.VM}; css={if ($_.VM -gt 100) { 'red' } else { 'green' }}} This will create table cell rows with the calculated CSS class names. E.g., for a process with a VM greater than 100, you'd get: <TD class="red">475858</TD> You can use this feature to specify a CSS class for each table cell based upon the contents of that cell. Valid keys in the hashtable are: n, name, l, or label: The table column header e or expression: The table cell contents css or csslcass: The CSS class name to apply to the <TD> tag Another example: @{n='Free(MB)'; e={$_.FreeSpace / 1MB -as [int]}; css={ if ($_.FreeSpace -lt 100) { 'red' } else { 'blue' }} This example creates a column titled "Free(MB)". It will contain the input object's FreeSpace property, divided by 1MB and cast as a whole number (integer). If the value is less than 100, the table cell will be given the CSS class "red." If not, the table cell will be given the CSS class "blue." The supplied cascading style sheet must define ".red" and ".blue" for those to have any effect. .PARAMETER PreContent Raw HTML content to be placed before the wrapping <DIV> tag. For example: -PreContent "<h2>Section A</h2>" .PARAMETER PostContent Raw HTML content to be placed after the wrapping <DIV> tag. For example: -PostContent "<hr />" .PARAMETER MakeHiddenSection Used in conjunction with -PreContent. Adding this switch, which needs no value, turns your -PreContent into clickable report section header. The section will be hidden by default, and clicking the header will toggle its visibility. When using this parameter, consider adding a symbol to your -PreContent that helps indicate this is an expandable section. For example: -PreContent '<h2>♦ My Section</h2>' If you use -MakeHiddenSection, you MUST provide -PreContent also, or the hidden section will not have a section header and will not be visible. .PARAMETER MakeTableDynamic When using "-As Table", makes the table dynamic. Will be ignored if you use "-As List". Dynamic tables are sortable, searchable, and are paginated. You should not use even/odd styling with tables that are made dynamic. Dynamic tables automatically have their own even/odd styling. You can apply CSS classes named ".odd" and ".even" in your CSS to style the even/odd in a dynamic table. .EXAMPLE $fragment = Get-WmiObject -Class Win32_LogicalDisk | Select-Object -Property PSComputerName,DeviceID,FreeSpace,Size | ConvertTo-HTMLFragment -EvenRowClass 'even' ` -OddRowClass 'odd' ` -PreContent '<h2>Disk Report</h2>' ` -MakeHiddenSection ` -MakeTableDynamic You will usually save fragments to a variable, so that multiple fragments (each in its own variable) can be passed to ConvertTo-EnhancedHTML. .NOTES Consider adding the following to your CSS when using dynamic tables: .paginate_enabled_next, .paginate_enabled_previous { cursor:pointer; border:1px solid #222222; background-color:#dddddd; padding:2px; margin:4px; border-radius:2px; } .paginate_disabled_previous, .paginate_disabled_next { color:#666666; cursor:pointer; background-color:#dddddd; padding:2px; margin:4px; border-radius:2px; } .dataTables_info { margin-bottom:4px; } This applies appropriate coloring to the next/previous buttons, and applies a small amount of space after the dynamic table. If you choose to make sections hidden (meaning they can be shown and hidden by clicking on the section header), consider adding the following to your CSS: .sectionheader { cursor:pointer; } .sectionheader:hover { color:red; } This will apply a hover-over color, and change the cursor icon, to help visually indicate that the section can be toggled. .Notes Function by Don Jones Generated on: 9/10/2013 For more information see Powershell.org included in AZSBTools module with permission by Don Jones #> [CmdletBinding()] param( [Parameter(Mandatory=$True,ValueFromPipeline=$True)] [object[]]$InputObject, [string]$EvenRowCssClass, [string]$OddRowCssClass, [string]$TableCssID, [string]$DivCssID, [string]$DivCssClass, [string]$TableCssClass, [ValidateSet('List','Table')] [string]$As = 'Table', [object[]]$Properties = '*', [string]$PreContent, [switch]$MakeHiddenSection, [switch]$MakeTableDynamic, [string]$PostContent ) BEGIN { <# Accumulate output in a variable so that we don't produce an array of strings to the pipeline, but instead produce a single string. #> $out = '' <# Add the section header (pre-content). If asked to make this section of the report hidden, set the appropriate code on the section header to toggle the underlying table. Note that we generate a GUID to use as an additional ID on the <div>, so that we can uniquely refer to it without relying on the user supplying us with a unique ID. #> Write-Verbose "Precontent" if ($PSBoundParameters.ContainsKey('PreContent')) { if ($PSBoundParameters.ContainsKey('MakeHiddenSection')) { [string]$tempid = [System.Guid]::NewGuid() $out += "<span class=`"sectionheader`" onclick=`"`$('#$tempid').toggle(500);`">$PreContent</span>`n" } else { $out += $PreContent $tempid = '' } } <# The table will be wrapped in a <div> tag for styling purposes. Note that THIS, not the table per se, is what we hide for -MakeHiddenSection. So we will hide the section if asked to do so. #> Write-Verbose "DIV" if ($PSBoundParameters.ContainsKey('DivCSSClass')) { $temp = " class=`"$DivCSSClass`"" } else { $temp = "" } if ($PSBoundParameters.ContainsKey('MakeHiddenSection')) { $temp += " id=`"$tempid`" style=`"display:none;`"" } else { $tempid = '' } if ($PSBoundParameters.ContainsKey('DivCSSID')) { $temp += " id=`"$DivCSSID`"" } $out += "<div $temp>" <# Create the table header. If asked to make the table dynamic, we add the CSS style that ConvertTo-EnhancedHTML will look for to dynamic-ize tables. #> Write-Verbose "TABLE" $_TableCssClass = '' if ($PSBoundParameters.ContainsKey('MakeTableDynamic') -and $As -eq 'Table') { $_TableCssClass += 'enhancedhtml-dynamic-table ' } if ($PSBoundParameters.ContainsKey('TableCssClass')) { $_TableCssClass += $TableCssClass } if ($_TableCssClass -ne '') { $css = "class=`"$_TableCSSClass`"" } else { $css = "" } if ($PSBoundParameters.ContainsKey('TableCSSID')) { $css += "id=`"$TableCSSID`"" } else { if ($tempid -ne '') { $css += "id=`"$tempid`"" } } $out += "<table $css>" <# We're now setting up to run through our input objects and create the table rows #> $fragment = '' $wrote_first_line = $false $even_row = $false if ($properties -eq '*') { $all_properties = $true } else { $all_properties = $false } } PROCESS { foreach ($object in $inputobject) { Write-Verbose "Processing object" $datarow = '' $headerrow = '' <# Apply even/odd row class. Note that this will mess up the output if the table is made dynamic. That's noted in the help. #> if ($PSBoundParameters.ContainsKey('EvenRowCSSClass') -and $PSBoundParameters.ContainsKey('OddRowCssClass')) { if ($even_row) { $row_css = $OddRowCSSClass $even_row = $false Write-Verbose "Even row" } else { $row_css = $EvenRowCSSClass $even_row = $true Write-Verbose "Odd row" } } else { $row_css = '' Write-Verbose "No row CSS class" } <# If asked to include all object properties, get them. #> if ($all_properties) { $properties = $object | Get-Member -MemberType Properties | Select -ExpandProperty Name } <# We either have a list of all properties, or a hashtable of properties to play with. Process the list. #> foreach ($prop in $properties) { Write-Verbose "Processing property" $name = $null $value = $null $cell_css = '' <# $prop is a simple string if we are doing "all properties," otherwise it is a hashtable. If it's a string, then we can easily get the name (it's the string) and the value. #> if ($prop -is [string]) { Write-Verbose "Property $prop" $name = $Prop $value = $object.($prop) } elseif ($prop -is [hashtable]) { Write-Verbose "Property hashtable" <# For key "css" or "cssclass," execute the supplied script block. It's expected to output a class name; we embed that in the "class" attribute later. #> if ($prop.ContainsKey('cssclass')) { $cell_css = $Object | ForEach $prop['cssclass'] } if ($prop.ContainsKey('css')) { $cell_css = $Object | ForEach $prop['css'] } <# Get the current property name. #> if ($prop.ContainsKey('n')) { $name = $prop['n'] } if ($prop.ContainsKey('name')) { $name = $prop['name'] } if ($prop.ContainsKey('label')) { $name = $prop['label'] } if ($prop.ContainsKey('l')) { $name = $prop['l'] } <# Execute the "expression" or "e" key to get the value of the property. #> if ($prop.ContainsKey('e')) { $value = $Object | ForEach $prop['e'] } if ($prop.ContainsKey('expression')) { $value = $tObject | ForEach $prop['expression'] } <# Make sure we have a name and a value at this point. #> if ($name -eq $null -or $value -eq $null) { Write-Error "Hashtable missing Name and/or Expression key" } } else { <# We got a property list that wasn't strings and wasn't hashtables. Bad input. #> Write-Warning "Unhandled property $prop" } <# When constructing a table, we have to remember the property names so that we can build the table header. In a list, it's easier - we output the property name and the value at the same time, since they both live on the same row of the output. #> if ($As -eq 'table') { Write-Verbose "Adding $name to header and $value to row" $headerrow += "<th>$name</th>" $datarow += "<td$(if ($cell_css -ne '') { ' class="'+$cell_css+'"' })>$value</td>" } else { $wrote_first_line = $true $headerrow = "" $datarow = "<td$(if ($cell_css -ne '') { ' class="'+$cell_css+'"' })>$name :</td><td$(if ($cell_css -ne '') { ' class="'+$cell_css+'"' })>$value</td>" $out += "<tr$(if ($row_css -ne '') { ' class="'+$row_css+'"' })>$datarow</tr>" } } <# Write the table header, if we're doing a table. #> if (-not $wrote_first_line -and $as -eq 'Table') { Write-Verbose "Writing header row" $out += "<tr>$headerrow</tr><tbody>" $wrote_first_line = $true } <# In table mode, write the data row. #> if ($as -eq 'table') { Write-Verbose "Writing data row" $out += "<tr$(if ($row_css -ne '') { ' class="'+$row_css+'"' })>$datarow</tr>" } } } END { <# Finally, post-content code, the end of the table, the end of the <div>, and write the final string. #> Write-Verbose "PostContent" if ($PSBoundParameters.ContainsKey('PostContent')) { $out += "`n$PostContent" } Write-Verbose "Done" $out += "</tbody></table></div>" Write-Output $out } } Function Get-SBWMI { <# .SYNOPSIS Function query WMI with Timeout .DESCRIPTION Function query WMI with Timeout .PARAMETER Class Class name such as 'Win32_computerSystem' .PARAMETER Property Property name such as 'NumberofLogicalProcessors' .PARAMETER Filter In the format Property=Value such as DriveLetter=G: .PARAMETER ComputerName Computer name .PARAMETER NameSpace Default is 'root\cimv2' To see name spaces type: (Get-WmiObject -Namespace 'root' -Class '__Namespace').Name .PARAMETER Cred PS Credential object .PARAMETER TimeOut In seconds .EXAMPLE Get-SBWMI -Class Win32_computerSystem -Property NumberofLogicalProcessors .EXAMPLE Get-SBWMI -Class Win32_Volume -Filter 'DriveType=3' .LINK https://superwidgets.wordpress.com/category/powershell/ .NOTES Function by Sam Boutros v0.1 - 20 September 2017 v0.2 - 29 September 2017 - Added parameter to use a different credential other than the one running the script Added error checking for failure to WMI connect #> [CmdletBinding(ConfirmImpact='Low')] Param( [Parameter(Mandatory=$true, ValueFromPipeLine=$true, ValueFromPipeLineByPropertyName=$true, Position=0)][string]$Class, [Parameter(Mandatory=$false)][String[]]$Property = '*', [Parameter(Mandatory=$false)][String]$Filter, [Parameter(Mandatory=$false)][String]$ComputerName = $env:COMPUTERNAME, [Parameter(Mandatory=$false)][String]$NameSpace = 'root\cimv2', [Parameter(Mandatory=$false)][PSCredential]$Cred, [Parameter(Mandatory=$false)][int]$TimeOut=20 ) Begin { if ($Filter) { if ($Filter -match '=') { $FilterProperty = $Filter.Split('=')[0].Trim() $FilterValue = $Filter.Split('=')[1].Trim() } else { Write-Log 'Get-SBWMI Input Error:','Filter',', supported syntax is','Property=Value','such as','DriveLetter=G' Magenta,Yellow,Magenta,Yellow,Magenta,Yellow Write-Log ' ignoring filter',$Filter Magenta,Yellow } } } Process{ $ConnOpt = New-Object System.Management.ConnectionOptions if ($ComputerName -ne $env:COMPUTERNAME -and $Cred) { # User credentials cannot be used for local connections $ConnOpt.EnablePrivileges = $true $ConnOpt.Username = $Cred.UserName $ConnOpt.SecurePassword = $Cred.Password } $Scope = New-Object System.Management.ManagementScope “\\$ComputerName\$NameSpace", $ConnOpt try { $Scope.Connect() } catch { $Message = $_.Exception.InnerException } if ($Scope.IsConnected) { $EnumOptions = New-Object System.Management.EnumerationOptions $EnumOptions.set_timeout((New-TimeSpan -seconds $TimeOut)) $Search = New-Object System.Management.ManagementObjectSearcher $Search.set_options($EnumOptions) $Search.Query = “SELECT $Property FROM $Class” $Search.Scope = $Scope $Result = $Search.get() } else { Write-Warning "Get-SBWMI: Error: $(($Message|Out-String).Trim())" } } End { if ($Result){ if ($Filter) { if ($FilterProperty -in ($Result | Get-Member -MemberType Property).Name) { $Result | where { $_.$FilterProperty -eq $FilterValue } } else { Write-Log 'Class',$Class,'doesn''t contain filter property',$FilterProperty Magenta,Yellow,Magenta,Yellow Write-Log 'Class',$Class,'has the following properties:' Cyan,Yellow,Cyan Write-Log (($Result | Get-Member -MemberType Property).Name | ? { $_ -notmatch '__' } | Out-String).Trim() Cyan } } else { $Result } } } } function Get-SBDisk { <# .SYNOPSIS Function to get disk information including block (allocation unit) size .DESCRIPTION Function to get disk information including block (allocation unit) size Function returns information on all fixed disks (Type 3) Function will fail to return computer disk information if: - Target computer is offline or name is misspelled - Function/script is run under an account with no read permission on the target computer - WMI services not running on the target computer - Target computer firewall or AntiVirus blocks WMI or RPC calls .PARAMETER ComputerName The name or IP address of computer(s) to collect disk information on Default value is local computer name .PARAMETER WMITimeOut Timeout in seconds. The default value is 20 .PARAMETER Cred PS Credential object .PARAMETER IncludeRecoveryVolume This parameter takes a $true or $false value, and is set to $false by default When set to $true the script will return information on Recovery Volume .EXAMPLE Get-SBDisk Returns fixed disk information of local computer .EXAMPLE Get-SBDisk computer1, 192.168.19.26, computer3 -Verbose Returns fixed disk information of the 3 listed computers The 'verbose' parameter will display a message if the target computer cannot be reached .OUTPUTS The script returns a PS Object with the following properties: ComputerName VolumeName DriveLetterOrMountPoint BlockSizeKB SizeGB FreeGB 'Free%' FileSystem Compressed .LINK https://superwidgets.wordpress.com/2017/01/09/powershell-script-to-get-disk-information-including-block-size/ .NOTES Function by Sam Boutros - v1.0 - 9 January 2017 v2.0 - 24 January 2017 Used WMI object Win32_Volume instead of Win32_LogicalDisk to capture mount points as well Added parameter to skip Recovery Volume Updated output object properties v3.0 - 12 July 2017 Updated output object to change data types to Int32 instead of the default String for BlockSizeKB,SizeGB,FreeGB,'Free%' v4.0 - 20 September 2017 - Used Get-SBWMI instead to take advanrage of the default 20 sec Timeout v4.1 - 22 September 2017 - Added WMITimeout parameter, removed -Filter parameter from Get-SBWMI call and filtered via updated if statement to speed processing by 200% #> [CmdletBinding(ConfirmImpact='Low')] Param( [Parameter(Mandatory=$false, ValueFromPipeLine=$true, ValueFromPipeLineByPropertyName=$true, Position=0)] [String[]]$ComputerName = $env:COMPUTERNAME, [Parameter(Mandatory=$false)][Int32]$WMITimeOut = 20, [Parameter(Mandatory=$false)][System.Management.Automation.PSCredential]$Cred = (Get-SBCredential -UserName "$env:USERDOMAIN\$env:USERNAME"), [Parameter(Mandatory=$false)][Switch]$IncludeRecoveryVolume ) foreach ($Computer in $ComputerName) { try { Get-SBWMI -ComputerName $Computer -Class Win32_Volume -TimeOut $WMITimeOut -Cred $Cred -ErrorAction Stop | % { if ($_.DriveType -eq 3 -and ($_.Label-notlike'Recovery' -or $IncludeRecoveryVolume)) { [PSCustomObject][Ordered]@{ ComputerName = $Computer VolumeName = $_.Label DriveLetterOrMountPoint = $(if ($_.Name.Contains(':')) {$_.Name} else {'<Not mounted>'}) BlockSizeKB = [Int32]($_.Blocksize/1KB) SizeGB = [Math]::Round($_.Capacity/1GB,1) FreeGB = [Math]::Round($_.FreeSpace/1GB,1) 'Free%' = [Math]::Round($_.FreeSpace/$_.Capacity*100,1) FileSystem = $_.FileSystem Compressed = $_.Compressed Indexed = $_.IndexingEnabled Automount = $_.Automount QuotasEnabled = $_.QuotasEnabled PageFilePresent = $_.PageFilePresent BootVolume = $_.BootVolume SystemVolume = $_.SystemVolume } # PSCustomObject } # if } # Get-SBWMI } catch { Write-Verbose "Unable to read disk information from computer $Computer" } } } function Format-SBCounter { <# .SYNOPSIS Function to format the output of Get-Counter cmdlet .DESCRIPTION Function to format the output of Get-Counter cmdlet of the Microsoft.PowerShell.Diagnostics PS module .PARAMETER CounterSample This is of type Microsoft.PowerShell.Commands.GetCounter.PerformanceCounterSampleSet which can be obtained from the output of the Get-Counter cmdlet .EXAMPLE Get-Counter | Format-SBCounter .OUTPUTS The script returns a PS Object with the following properties/example: DateTime : 3/1/2019 12:43:57 PM ComputerName : mycomputernamehere CounterSet : physicaldisk(_total) Counter : current disk queue length Value : 0 .LINK https://superwidgets.wordpress.com/category/powershell/ .NOTES Function by Sam Boutros - v0.1 - 1 March 2019 #> [CmdletBinding(ConfirmImpact='Low')] param ( [Parameter(Mandatory,ValueFromPipeline)] [Microsoft.PowerShell.Commands.GetCounter.PerformanceCounterSampleSet]$CounterSample ) Begin {} Process { foreach ($Counter in $CounterSample.CounterSamples){ $Temp = $Counter.Path.Split('\') [PSCustomObject][Ordered]@{ DateTime = $Counter.Timestamp ComputerName = $Temp[2] CounterSet = $Temp[3] Counter = $Temp[4] Value = $Counter.CookedValue } } } End {} } function Validate-WindowsCredential { <# .SYNOPSIS Function to validate whether a provided Credential is correct on a provided target Windows Computer .DESCRIPTION Function to validate whether a provided Credential is correct on a provided target Windows Computer .PARAMETER Credential PSCredential object. This can be obtained from the Get-Credential cmdlet of the Microsoft.PowerShell.Security, or the Get-SBCredential function of the SB-Tools PS module .PARAMETER Session PSSession object. This can be obtained via the New-PSSession cmdlet of the Microsoft.PowerShell.Core .OUTPUTS The script outputs a TRUE/FALSE result if the provided PSSession is valid and opened. .EXAMPLE $Session = New-PSSession -ComputerName test-vm0116.test.domain.com -Credential (Get-SBCredential 'test\superuser') Validate-WindowsCredential -Credential (Get-SBCredential '.\administrator') -Session $Session A 'TRUE' result indicates that the local administrator account of the test-vm0116.test.domain.com is valid (name and password) A 'FALSE' result indicates failure to authenticate. This can be due to bad username or password, or locked or disabled account.. .EXAMPLE $Session = New-PSSession -ComputerName test-vm0116.test.domain.com -Credential (Get-SBCredential 'test\superuser') Validate-WindowsCredential -Credential (Get-SBCredential 'test\OtherUser') -Session $Session A 'TRUE' result indicates that the test\OtherUser account on the test-vm0116.test.domain.com is valid (name and password) .LINK https://superwidgets.wordpress.com/2017/11/28/validate-windowscredential-and-validate-linuxcredential-powershell-functions/ .NOTES Function by Sam Boutros v0.1 - 20 November 2017 v0.2 - 17 May 2019 - Added feature to work against local computer making $Session an optional parameter #> [CmdletBinding(ConfirmImpact='Low')] Param( [Parameter(Mandatory=$true)][System.Management.Automation.PSCredential]$Credential, [Parameter(Mandatory=$false)][System.Management.Automation.Runspaces.PSSession]$Session ) Begin { } Process{ if ($Session) { if ($Session.State -eq 'Opened') { Invoke-Command -Session $Session -ScriptBlock { $Credential = $Using:Credential Add-Type -AssemblyName System.DirectoryServices.AccountManagement $DS = New-Object System.DirectoryServices.AccountManagement.PrincipalContext('domain') $DS.ValidateCredentials($Credential.UserName.Split('\')[1], $Credential.GetNetworkCredential().Password) } } else { Write-Log 'Validate-WindowsCredential: Error: Session provided is not ''opened'':' Magenta Write-Log ($Session|FT -a|Out-String).Trim() Yellow } } else { Add-Type -AssemblyName System.DirectoryServices.AccountManagement $DS = New-Object System.DirectoryServices.AccountManagement.PrincipalContext('domain') $DS.ValidateCredentials($Credential.UserName.Split('\')[1], $Credential.GetNetworkCredential().Password) } } End { } } function Validate-LinuxCredential { <# .SYNOPSIS Function to validate whether a provided Credential is correct on a provided target Linux Computer .DESCRIPTION Function to validate whether a provided Credential is correct on a provided target Linux Computer .PARAMETER Credential PSCredential object. This can be obtained from the Get-Credential cmdlet of the Microsoft.PowerShell.Security, or the Get-SBCredential function of the SB-Tools PS module .PARAMETER Session SSH.SshSession object. This can be obtained via the New-SSHSession cmdlet of the POSH-SSH PS module .OUTPUTS The script outputs a TRUE/FALSE result if the provided SSHSession is valid and Connected. .EXAMPLE $Session = New-SSHSession -ComputerName test-vm0112.test.domain.com -Credential (Get-SBCredential 'opsuser') -AcceptKey Validate-LinuxCredential -Credential (Get-SBCredential 'root') -Session $Session A 'TRUE' result indicates that the local administrator account of the test-vm0116.test.domain.com is valid (name and password) A 'FALSE' result indicates failure to authenticate. This can be due to bad username or password, or locked or disabled account.. .LINK https://superwidgets.wordpress.com/2017/11/28/validate-windowscredential-and-validate-linuxcredential-powershell-functions/ .NOTES Function by Sam Boutros v0.1 - 20 November 2017 #> [CmdletBinding(ConfirmImpact='Low')] Param( [Parameter(Mandatory=$true)][System.Management.Automation.PSCredential]$Credential, [Parameter(Mandatory=$true)][SSH.SshSession]$Session ) Begin { } Process{ if ($Session.Connected) { [String]$ConnectedUserName = (Invoke-SSHCommand -SessionId $Session.SessionId -Command 'whoami').Output $ConnectedCred = Get-SBCredential $ConnectedUserName $myCommand = "echo '$($ConnectedCred.GetNetworkCredential().Password)' | sudo -S cat /etc/shadow | grep $($Credential.UserName)" $Result = Invoke-SSHCommand -SessionId $Session.SessionId -Command $myCommand if ($Result.ExitStatus) { Write-Log 'Validate-LinuxCredential: Error:' Magenta $LogFile if ($Result.Output) { Write-Log ($Result.Output | Out-String).Trim() Yellow } } else { if ($Hash = $Result.Output) { Write-Log 'Obtained user',$Credential.UserName,'hash',$Hash Green,Cyan,Green,Cyan $Salt = $Hash.Split('$')[2] $myCommand = "echo '$($Credential.GetNetworkCredential().Password)' | openssl passwd -1 -salt $Salt" $Result = Invoke-SSHCommand -SessionId $Session.SessionId -Command $myCommand if ($Result.ExitStatus) { Write-Log 'Validate-LinuxCredential: Error:' Magenta $LogFile if ($Result.Output) { Write-Log ($Result.Output | Out-String).Trim() Yellow } } else { $Hash.Split('$')[3].Split(':')[0] -eq $Result.Output.Split('$')[3] } } } } else { Write-Log 'Validate-LinuxCredential: Error: Session provided is not ''Connected'':' Magenta Write-Log ($Session|FT -a|Out-String).Trim() Yellow } } End { } } function Flatten-XML { <# .SYNOPSIS Function to flatten the heirachical structure of an XML input .DESCRIPTION Function to flatten the heirachical structure of an XML input This produces a collection of PS Custom Objects that can be combined into a single PS Custom Object using the Combine-Objects function of this PS module .PARAMETER XML This is the required XML input. For example this can be obtained via [XML]$XML = SCHTASKS /Query /XML /TN '\Microsoft\Windows\Time Synchronization\SynchronizeTime' .PARAMETER SkipElement Optional one or more elements to be ignored. This defaults to 'version','xmlns', and 'xml' .EXAMPLE [XML]$XML = SCHTASKS /Query /XML /TN '\Microsoft\Windows\Time Synchronization\SynchronizeTime' Flatten-XML -XML $XML | Combine-Objects This example prvides the details of a given scheduled task as an easy to use PS object such as: StopIfGoingOnBatteries : true Period : P1D Deadline : P2D Description : $(@%systemroot%\system32\w32time.dll,-201) Source : $(@%systemroot%\system32\w32time.dll,-200) UserId : S-1-5-19 Author : $(@%systemroot%\system32\w32time.dll,-202) Context : LocalService MultipleInstancesPolicy : IgnoreNew DisallowStartIfOnBatteries : true Arguments : start w32time task_started UseUnifiedSchedulingEngine : true URI : \Microsoft\Windows\Time Synchronization\SynchronizeTime StopOnIdleEnd : true RunLevel : HighestAvailable id : LocalService RunOnlyIfNetworkAvailable : true Command : %windir%\system32\sc.exe RestartOnIdle : false Triggers : StartWhenAvailable : true .LINK https://superwidgets.wordpress.com/category/powershell/ .NOTES Function by Sam Boutros v0.1 - 29 July 2019 #> param( [Parameter(Mandatory=$true)]$XML, [Parameter(Mandatory=$false)][String[]]$SkipElement = @('version','xmlns','xml') ) Begin { } Process { foreach ($Property in ($XML | Get-Member -MemberType Property).Name) { $Value = $XML.$Property if ($Value.GetType().Name -ne 'XmlElement') { if ($Property -in $SkipElement) { Write-Log 'Skipping property',$Property,'value',$Value Green,Yellow,Green,Yellow } else { Write-Log 'Processing property',$Property,'value',$Value Green,cyan,Green,Cyan [PSCustomObject]@{ $Property = $Value } } } else { Flatten-XML -XML $Value } } } End { } } function Combine-Objects { <# .SYNOPSIS Function to combine a collection of PS Custom Objects into one. .DESCRIPTION Function to combine a collection of PS Custom Objects into one. This is often used with Flatten-XML function of this PS module .PARAMETER Object One or more PS Custom Object .EXAMPLE [XML]$XML = SCHTASKS /Query /XML /TN '\Microsoft\Windows\Time Synchronization\SynchronizeTime' Flatten-XML -XML $XML | Combine-Objects This example prvides the details of a given scheduled task as an easy to use PS object such as: StopIfGoingOnBatteries : true Period : P1D Deadline : P2D Description : $(@%systemroot%\system32\w32time.dll,-201) Source : $(@%systemroot%\system32\w32time.dll,-200) UserId : S-1-5-19 Author : $(@%systemroot%\system32\w32time.dll,-202) Context : LocalService MultipleInstancesPolicy : IgnoreNew DisallowStartIfOnBatteries : true Arguments : start w32time task_started UseUnifiedSchedulingEngine : true URI : \Microsoft\Windows\Time Synchronization\SynchronizeTime StopOnIdleEnd : true RunLevel : HighestAvailable id : LocalService RunOnlyIfNetworkAvailable : true Command : %windir%\system32\sc.exe RestartOnIdle : false Triggers : StartWhenAvailable : true .LINK https://superwidgets.wordpress.com/category/powershell/ .NOTES Function by Sam Boutros v0.1 - 29 July 2019 #> param( [Parameter(ValueFromPipeline,Mandatory=$true)][PSCustomObject[]]$Object ) Begin { } Process { foreach ($Item in $Object) { foreach ($Property in ($Item | Get-Member -MemberType NoteProperty).Name){ $ArgumentList += @{ $Property = $Item.$Property } } } } End { [PSCustomObject]$ArgumentList } } function Grant-UserRight { <# .SYNOPSIS Function to grant the provided local user(s) the provided user right .DESCRIPTION Function to grant the provided local user(s) the provided user right This function modifies Local Security Policy - see secpol.msc .PARAMETER UserName One or more local users .EXAMPLE Grant-UserRight -UserName samb,notthere -UserRight 'SeManageVolumePrivilege' .LINK https://superwidgets.wordpress.com/category/powershell/ .NOTES Function by Sam Boutros 3 October 2019 - v0.1 #> param( [Parameter(Mandatory=$true)][String[]]$UserName, # Must be local user [Parameter(Mandatory=$true)][ValidateSet( 'SeAssignPrimaryTokenPrivilege', 'SeAuditPrivilege', 'SeBackupPrivilege', 'SeBatchLogonRight', 'SeChangeNotifyPrivilege', 'SeCreateGlobalPrivilege', 'SeCreatePagefilePrivilege', 'SeCreateSymbolicLinkPrivilege', 'SeDebugPrivilege', 'SeDelegateSessionUserImpersonatePrivilege', 'SeImpersonatePrivilege', 'SeIncreaseBasePriorityPrivilege', 'SeIncreaseQuotaPrivilege', 'SeIncreaseWorkingSetPrivilege', 'SeLoadDriverPrivilege', 'SeManageVolumePrivilege', 'SeNetworkLogonRight', 'SeProfileSingleProcessPrivilege', 'SeRemoteInteractiveLogonRight', 'SeRemoteShutdownPrivilege', 'SeRestorePrivilege', 'SeSecurityPrivilege', 'SeServiceLogonRight', 'SeShutdownPrivilege', 'SeSystemEnvironmentPrivilege', 'SeSystemProfilePrivilege', 'SeSystemtimePrivilege', 'SeTakeOwnershipPrivilege', 'SeTimeZonePrivilege', 'SeUndockPrivilege' )][String]$userRight, [Parameter(Mandatory=$false)][String]$LogFile = ".\Grant-UserRight - $(Get-Date -Format 'ddMMMMyyyy_hh-mm-ss_tt').txt" ) Begin { } Process { Write-Log 'Backing up current Local Security Policy..' Green -NoNewLine $Logfile $FileName = "$env:TEMP\policies-$(Get-Date -Format 'ddMMMMyyyy_hh-mm-ss_tt').inf" # No spaces $ExitCode = (Start-Process secedit -ArgumentList "/export /areas USER_RIGHTS /cfg $FileName" -Wait -PassThru).ExitCode if ($ExitCode -eq 0) { Write-Log 'done',(Get-Item $FileName).FullName Cyan,DarkYellow $LogFile } else { Write-Log 'failed, stopping..' Yellow $LogFile break } $Policy = Get-Content $FileName # $Policy | % { if ($_ -match '= \*') { "'$($_.Split('=')[0].Trim())'," } } | sort foreach ($LocalUser in $UserName) { try { $Sid = ((Get-LocalUser $LocalUser -EA 1).SID).Value Write-Log 'Identified local user',$LocalUser,'Sid',$Sid Green,Cyan,Green,Cyan $LogFile $Policy = foreach ($Line in $Policy) { if ($Line -match $userRight) { if ($Line -match $Sid) { Write-Log ' Local user',$LocalUser,'already has the right',$userRight Green,Cyan,Green,Cyan $LogFile $Line } else { Write-Log 'Granting local user',$LocalUser,'the right',$userRight Green,Cyan,Green,Cyan $LogFile "$Line,*$Sid" } } else { $Line } } } catch { Write-Log ' Local user',$LocalUser,'not found, skipping..' Magenta,Yellow,Magenta $LogFile } } $Policy | Out-File $FileName -Force $ExitCode = (Start-Process secedit -ArgumentList "/configure /db $env:windir\security\database\secedit.sdb /cfg $FileName /areas USER_RIGHTS /log $($FileName.Replace('.inf','.log'))" -Wait -PassThru).ExitCode if ($ExitCode -eq 0) { Write-Log ' done' Cyan $LogFile } else { Write-Log ' failed','no changes made to Local Policies' Yellow,Magenta $LogFile Write-Log (Get-Content $FileName.Replace('.inf','.log') | Out-String).Trim() Yellow $LogFile } <# Error 1208: An extended error has occurred. Error creating database. Error 12: The access code is invalid. https://social.technet.microsoft.com/Forums/en-US/0c888948-3a0d-49e4-ac81-e71138c8d5b8/facing-an-issue-while-running-quotseceditquot-command?forum=ws2016 https://docs.microsoft.com/en-us/windows-server/administration/windows-commands/secedit https://support.microsoft.com/en-us/help/324383/troubleshooting-scecli-1202-events #> Remove-Item -Path $FileName -Force } End { } } Function Monitor-Service { <# .SYNOPSIS Function to query one or more TCP ports .DESCRIPTION Function query WMI with Timeout .PARAMETER Class Class name such as 'Win32_computerSystem' .PARAMETER Property Property name such as 'NumberofLogicalProcessors' .PARAMETER Filter In the format Property=Value such as DriveLetter=G: .PARAMETER ComputerName Computer name .PARAMETER NameSpace Default is 'root\cimv2' To see name spaces type: (Get-WmiObject -Namespace 'root' -Class '__Namespace').Name .PARAMETER Cred PS Credential object .PARAMETER TimeOut In seconds .EXAMPLE Get-SBWMI -Class Win32_computerSystem -Property NumberofLogicalProcessors .EXAMPLE Get-SBWMI -Class Win32_Volume -Filter 'DriveType=3' .LINK https://superwidgets.wordpress.com/category/powershell/ .NOTES Function by Sam Boutros v0.1 - 20 September 2017 v0.2 - 29 September 2017 - Added parameter to use a different credential other than the one running the script Added error checking for failure to WMI connect #> [CmdletBinding(ConfirmImpact='Low')] Param( [Parameter(Mandatory=$false)][Object[]]$MontitoredPort = @( [PSCustomObject]@{ FromIP = 'Any' # 'Any' Or valid IPv4 address ToIP = 'cnn.com' # FQDN or IPv4 ToPort = 80 # TCP port (0-65536) } ), [Parameter(Mandatory=$false)][Int]$Frequency = 5, # Number of minutes between checks [Parameter(Mandatory=$false)][Int]$CountToAlert = 2, # Number of failed checks before alert is triggered [Parameter(Mandatory=$true)][String]$SenderEmail, [Parameter(Mandatory=$true)][String]$AlertTo, # one or more email addresses [Parameter(Mandatory=$true)][String]$SMTPRelayServer = 20, # IP address or FQDN of SMTP relay server [Parameter(Mandatory=$false)][PSCrednetial]$SMTPCred # Credential needed to use SMTP server ) Begin { # Validate input $ProprtyList = @('FromIP','ToIP','ToPort') $PortList = foreach ($PortSpec in $MontitoredPort) { $ThisPropList = ($PortSpec | Get-Member -MemberType NoteProperty).Name $Keep = $true foreach ($Property in $ProprtyList) { if ($Property -notin $ThisPropList) { Write-Log 'Input MonitoredPort missing required property',$Property Magenta,Yellow $Keep = $false } } if ($Keep) { $PortSpec } } } Process{ if ($PortList) { Write-Log 'Monitoring' Green Write-Log ($PortList | FT -a | Out-String).Trim() Cyan foreach ($PortSpec in $PortList) { if ($PortSpec.FromIP.ToLower() -eq 'any') { $Result = Test-SBNetConnection -ComputerName $PortSpec.ToIP -PortNumber $PortSpec.ToPort foreach ($Ping in $Result) { if ($Ping.TcpTestSucceeded) { if ($PortSpec.ToIP -eq $Ping.ComputerName) { Write-Log "$($Ping.ComputerName)",'online' Cyan,Green } else { Write-Log "$($Ping.ComputerName)($($PortSpec.ToIP))",'online' Cyan,Green } } else { if ($PortSpec.ToIP -eq $Ping.ComputerName) { Write-Log "$($Ping.ComputerName)",'unreacheable' Cyan,Yellow } else { Write-Log "$($Ping.ComputerName)($($PortSpec.ToIP))",'unreacheable' Cyan,Yellow } } } } else { } } } } End { } } function Get-VssWriters { <# .Synopsis Function to get information about VSS Writers on one or more computers .Description Function will parse information from VSSAdmin tool and return object containing WriterName, StateID, StateDesc, and LastError .PARAMETER ComputerName This is the name of one or more computers. If absent, localhost is assumed. .Example Get-VssWriters This example will return a list of VSS Writers on localhost .Example # Get VSS Writers on localhost, sort list by WriterName $VssWriters = Get-VssWriters | Sort "WriterName" $VssWriters | FT -AutoSize # Displays it on screen $VssWriters | Out-GridView # Displays it in GridView $VssWriters | Export-CSV ".\myVSSWriterReport.csv" -NoTypeInformation # Exports it to CSV .Example # Get VSS Writers on the list of $Computers, sort list by ComputerName $Computers = "xHost11","notThere","xHost12",$env:ComputerName $VssWriters = Get-VssWriters -ComputerName $Computers -Verbose | Sort "ComputerName" $VssWriters | FT -AutoSize # Displays it on screen $VssWriters | Out-GridView # Displays it in GridView $VssWriters | Export-CSV ".\myVSSWriterReport.csv" -NoTypeInformation # Exports it to CSV .Example # Reports any errors on VSS Writers on the computers listed in MyComputerList.txt, sorts list by ComputerName $Computers = Get-Content ".\MyComputerList.txt" $VssWriters = Get-VssWriters $Computers -Verbose | Where { $_.StateDesc -ne 'Stable' } | Sort "ComputerName" $VssWriters | FT -AutoSize # Displays it on screen $VssWriters | Out-GridView # Displays it in GridView $VssWriters | Export-CSV ".\myVSSWriterReport.csv" -NoTypeInformation # Exports it to CSV .Example # Get VSS Writers on all computers in current AD domain, sort list by ComputerName $Computers = (Get-ADComputer -Filter *).Name $VssWriters = Get-VssWriters $Computers -Verbose | Sort "ComputerName" $VssWriters | Out-GridView # Displays it in GridView $VssWriters | Export-CSV ".\myVSSWriterReport.csv" -NoTypeInformation # Exports it to CSV .EXAMPLE # Get VSS Writers on all Hyper-V hosts in current AD domain, sort list by ComputerName $Computers = (Get-ADComputer -Filter *).Name $FilteredComputerList = Foreach ($Computer in $Computers) { if (Get-WindowsFeature -ComputerName $Computer -ErrorAction SilentlyContinue | where { $_.Name -eq "Hyper-V" -and $_.InstallState -eq "Installed"}) { $Computer } } $VssWriters = Get-VssWriters $FilteredComputerList -Verbose | Sort "ComputerName" $VssWriters | FT -AutoSize # Displays it on screen $VssWriters | Out-GridView # Displays it in GridView $VssWriters | Export-CSV ".\myVSSWriterReport.csv" -NoTypeInformation # Exports it to CSV .OUTPUTS Scripts returns a PS Object with the following properties: ComputerName WriterName StateID StateDesc LastError .LINK https://superwidgets.wordpress.com/category/powershell/ https://gallery.technet.microsoft.com/scriptcenter/Powershell-ScriptFunction-415e9e70 .NOTES Function by Sam Boutros v1.0 - 17 September 2014 v1.1 - 12 February 2020 - Rewrite, improved parsing and error handling #> [CmdletBinding(ConfirmImpact='Low')] Param( [Parameter(Mandatory=$false, ValueFromPipeLine=$true, ValueFromPipeLineByPropertyName=$true, Position=0)] [ValidateNotNullorEmpty()] [String[]]$ComputerName = $env:COMPUTERNAME, [Parameter(Mandatory=$false)][String]$LogFile = ".\Get-VssWriters - $(Get-Date -Format 'ddMMMMyyyy_hh-mm-ss_tt').txt" ) Begin { } Process { foreach ($Computer in $ComputerName) { Write-Log 'Processing computer',$Computer Green,Cyan $LogFile $Raw = if ($Computer -eq $env:COMPUTERNAME) { try { VssAdmin List Writers } catch { $_.Exception.Message } } else { try { Invoke-Command -ComputerName $Computer -EA 1 -ScriptBlock { try { VssAdmin List Writers } catch { $_.Exception.Message } } } catch { $_.Exception.Message } } # Parse $Raw # $n=0; $Raw | % { "Line $($n): $_"; $n++ } if ($Raw -match "The term 'VssAdmin' is not recognized" -or $Raw -match "Connecting to remote server $Computer failed with the following error message") { Write-Log 'Error with Computer',$Computer Magenta,Yellow $LogFile Write-Log ($Raw | Out-String).Trim() Yellow $LogFile } elseif ($Raw[3] -match "Error: You don't have the correct permissions to run this command") { Write-Log 'Error with Computer',$Computer Magenta,Yellow $LogFile Write-Log ("$($Raw[3]) $($Raw[4])").Trim() Yellow $LogFile } else { if ($Raw -match 'Writer Name') { $n=0; $WriterStartLines = foreach ($Line in $Raw) { if ($Line -match 'Writer Name') { $n }; $n++ } foreach ($Writer in $WriterStartLines) { [PSCustomObject]@{ ComputerName = $Computer WriterName = $Raw[$Writer].Split(':')[1].Trim().Replace("'","") StateId = $Raw[$Writer+3].Split(':')[1].Trim().Split(']')[0].Replace('[','') StateDesc = $Raw[$Writer+3].Split(':')[1].Trim().Split(']')[1].Trim() LastError = $Raw[$Writer+4].Split(':')[1].Trim() } } } else { Write-Log 'No VSS Writers identified on Computer',$Computer,'- details:' Magenta,Yellow,Magenta $LogFile Write-Log ($Raw | Out-String).Trim() Yellow $LogFile } } } } End { } } function Get-DayOfMonth { <# .SYNOPSIS Function to get a given day of the week such as Sunday of a given Month/Year like March/2020 .DESCRIPTION Function to get a given day of the week such as Sunday of a given Month/Year like March/2020 .PARAMETER DayOfWeek Optional parameter that defaults to 'Sunday' Valid options are 'Sunday','Monday','Tuesday','Wednesday','Thursday','Friday','Saturday' .PARAMETER First Optional switch parameter. By default it retuns the first day of the month When set to $true, it returns the last day of month .PARAMETER Month Optional parameter from 1 to 12 .PARAMETER Year Optional parameter from 1 to 10,000 .EXAMPLE Get-DayOfMonth This will return the last Sunday of the current Month/Year as in: Sunday, March 29, 2020 12:26:49 PM .EXAMPLE Get-DayOfMonth -DayofWeek Monday This will return the last Monday of the current Month/Year as in: Monday, March 30, 2020 12:27:34 PM .EXAMPLE Get-DayOfMonth -DayofWeek Saturday -First This will return the first Saturday of the current Month/Year as in: Saturday, March 7, 2020 12:28:25 PM .EXAMPLE Get-DayOfMonth -DayofWeek Friday -Month 3 -Year 1945 This will return the last Friday of March 1945 as in: Friday, March 30, 1945 12:29:54 PM .OUTPUTS This cmdlet returns a DateTime object .LINK https://superwidgets.wordpress.com/category/powershell/ .NOTES Function by Sam Boutros v0.1 - 26 March 2020 #> [CmdletBinding(ConfirmImpact = 'Low')] Param( [Parameter(Mandatory=$false)][ValidateSet('Sunday','Monday','Tuesday','Wednesday','Thursday','Friday','Saturday')][String]$DayofWeek = 'Sunday', [Parameter(Mandatory=$false)][Switch]$First, [Parameter(Mandatory=$false)][ValidateRange(1,12)][Int]$Month = (Get-Date).Month, [Parameter(Mandatory=$false)][ValidateRange(1,10000)][Int]$Year = (Get-Date).Year ) Begin { } Process { $Days = 0..31 | foreach { (Get-Date -Year $Year -Month $Month -Day 1).AddDays($_) | where { $_.Month -eq $Month -and $_.DayOfWeek -eq $DayofWeek } } } End { if ($First) { $Days | select -First 1 } else { $Days | select -last 1 } } } function Get-PCInfo { <# .SYNOPSIS Function to ping and report on given one or more Windows computers. .DESCRIPTION Function to ping and report on given one or more Windows computers. If the computer has more than one network interface, this function will report all IP and MAC addresses .PARAMETER ComputerName One or more computer names to be reported on. This defaults to the current computer. .PARAMETER Cred PS Credential object that can be obtained from Get-Credential or Get-SBCredential .PARAMETER Refresh This switch will supress progress messages to speed up processing. .OUTPUTS The function returns a PS object that has the following properties/example: ComputerName : WIN10G2-Sam1 Status : Online IPAddress : 192.168.214.118 MACAddress : 00:xx:xx:xx:xx:xx DateBuilt : 9/6/2019 10:38:13 AM OSVersion : 10.0.18363 OSCaption : Microsoft Windows 10 Enterprise OSArchitecture : 64-bit Model : Virtual Machine Manufacturer : Microsoft Corporation VM : True LastBootTime : 3/26/2020 9:38:45 PM .EXAMPLE Get-PCInfo This returns the current PC information .EXAMPLE $PCInfo = Get-PCInfo -ComputerName @('PC1','PC2','PC3') This checks the listed computers and saves the collected information in $PCInfo variable .EXAMPLE (Import-Csv .\ComputerList1.csv).ComputerName | Get-PCInfo | Export-Csv .\ComputerReport.csv -NoType This example will read a list of computer names from the CSV file provided which has a 'ComputerName' column, gather each computer information and save it to the provided CSV output file. .EXAMPLE Get-PCInfo -ComputerName Server111 -Cred (Get-SBCredential 'domain\user') This example will report on information of the provided computer using the provided credentials .LINK https://superwidgets.wordpress.com/2017/01/04/powershell-script-to-report-on-computer-inventory/ .NOTES Function by Sam Boutros 31 October 2014 v0.1 4 January 2017 v0.2 17 March 2017 v0.3 - chnaged the logic to output 1 record per computer even when it has several NICs 2 April 2020 v0.4 - Added Silent switch to speed up processing of large number of computers Switched to using Get-SBWMI instead of Get-WMIObject Added Cred Parameter to be able to query computers outside the domain #> [CmdletBinding(ConfirmImpact='Low')] Param( [Parameter(Mandatory=$false,ValueFromPipeline=$true,ValueFromPipelineByPropertyName=$true)] [String[]]$ComputerName = $env:COMPUTERNAME, [Parameter(Mandatory=$false)][PSCredential]$Cred, [Parameter(Mandatory=$false)][Switch]$Silent ) Begin { } Process { foreach ($PC in $ComputerName) { if (-not $Silent) { Write-Log 'Checking computer',$PC Green,Cyan -NoNewLine } try { $Result = Test-Connection -ComputerName $PC -Count 2 -ErrorAction Stop if ($Cred) { $OS = Get-SBWMI -ComputerName $PC -Class Win32_OperatingSystem -Cred $Cred -EA 0 $Mfg = Get-SBWMI -ComputerName $PC -Class Win32_ComputerSystem -Cred $Cred -EA 0 $IPs = (Get-SBWMI -ComputerName $PC -Class Win32_NetworkAdapterConfiguration -Cred $Cred -EA 0 | Where { $_.IpEnabled }).IPAddress | where { $_ -match "\." } # IPv4 only } else { $OS = Get-SBWMI -ComputerName $PC -Class Win32_OperatingSystem -EA 0 $Mfg = Get-SBWMI -ComputerName $PC -Class Win32_ComputerSystem -EA 0 $IPs = (Get-SBWMI -ComputerName $PC -Class Win32_NetworkAdapterConfiguration -EA 0 | Where { $_.IpEnabled }).IPAddress | where { $_ -match "\." } # IPv4 only } $MACs = foreach ($IPAddress in $IPs) { if ($Cred) { (Get-SBWMI -ComputerName $PC -Class Win32_NetworkAdapterConfiguration -Cred $Cred -EA 0 | Where { $_.IPAddress -eq $IPAddress }).MACAddress } else { (Get-SBWMI -ComputerName $PC -Class Win32_NetworkAdapterConfiguration -EA 0 | Where { $_.IPAddress -eq $IPAddress }).MACAddress } } if (-not $Silent) { Write-Log 'done' Green } [PSCustomObject]@{ ComputerName = $PC Status = 'Online' IPAddress = $IPs -join ', ' MACAddress = $MACs -join ', ' DateBuilt = ([WMI]'').ConvertToDateTime($OS.InstallDate) OSVersion = $OS.Version OSCaption = $OS.Caption OSArchitecture = $OS.OSArchitecture Model = $Mfg.model Manufacturer = $Mfg.Manufacturer VM = $(if ($Mfg.Manufacturer -match 'vmware' -or $Mfg.Manufacturer -match 'microsoft') { $true } else { $false }) LastBootTime = ([WMI]'').ConvertToDateTime($OS.LastBootUpTime) } } catch { # either ping failed or access denied if ($Result) { if (-not $Silent) { Write-Log 'done' Magenta } [PSCustomObject]@{ ComputerName = $PC Status = $Error[0].Exception } } else { if (-not $Silent) { Write-Log 'done' Yellow } [PSCustomObject]@{ ComputerName = $PC Status = 'No response to ping' } } } } } End { } } function Parse-String { <# .Synopsis Function to parse an input string returning values between Start Marker and End Marker strings .Description Function to parse an input string returning values between Start Marker and End Marker strings Start and End marker strings cannot be the same This function will return multiple values if the $InputString has several occurances of the Start and End Markers For useful results look for unqiue Start and End markers in your $InputString This function can be useful in parsing the Message property of Windows Event Logs .PARAMETER InputString The input string .PARAMETER StartMarker The Start Marker string .PARAMETER EndMarker The End Marker string .PARAMETER OpenEnded When this switch is set to True, this function will respond if one of the two markers is not provided. If EndMarker is not provided, and StartMarker is provided, and the OpenEnded switch is set, this function will return the string from the StarMarker to the end of the Input String. If StartMarker is not provided, and EndMarker is provided, and the OpenEnded switch is set, this function will return the string from the beginning of the Input String to the StarMarker. .Example $InputString = 'A sleek red fox emerged from its deep under ground burrow A sleek green fox emerged from its deep under ground burrow' Parse-String -InputString $InputString -StartMarker 'sleek' -EndMarker 'emerged' This example will parse the input string and return values between 'sleek' and 'emerged' .Example if ($LogEntry = Get-EventLog -LogName Security -EntryType FailureAudit | select -First 1) { $LogonType = Parse-String -InputString $LogEntry.Message -StartMarker 'Logon Type:' -EndMarker 'Account For Which Logon Failed:' $AccountAttempted = Parse-String -InputString $LogEntry.Message -StartMarker 'Account Name:' -EndMarker 'Account Domain:' $IPAttemptedFrom = Parse-String -InputString $LogEntry.Message -StartMarker 'Source Network Address:' -EndMarker 'Source Port:' "Logon Type: $LogonType (2 = interactive, 3 = network)" "Account Attempted: $($AccountAttempted | where { $_ -ne '-' })" "IP Address from which Logon was attempted: $IPAttemptedFrom" } This example will find the first AuditFailure event in the Security EventLog, and will parse its Message property to show Logon Type, Account Attempted, and IP Address from which Logon was attempted .Example $InputString = 'A sleek red fox emerged from its deep under ground burrow A sleek green fox emerged from its deep under ground burrow' Parse-String -InputString $InputString -StartMarker 'sleek' -OpenEnded This example will parse the input string and return values between the first 'sleek' to the end of the string, such as: 'red fox emerged from its deep under ground burrow A sleek green fox emerged from its deep under ground burrow' .Example $InputString = 'A sleek red fox emerged from its deep under ground burrow A sleek green fox emerged from its deep under ground burrow' Parse-String -InputString $InputString -EndMarker 'emerged' -OpenEnded This example will parse the input string and return values between the beginning of the string to the first 'emerged', such as: 'A sleek red fox' .OUTPUTS This function returns one or more strings .LINK https://superwidgets.wordpress.com/category/powershell/ .NOTES Function by Sam Boutros v0.1 - 12 April 2020 v0.2 - 14 April 2020 Updated logic to report errors as verbose output Updated logic to continue on error v0.3 - 29 September 2021 Added OpenEnded switch, allowing to omit either StartMarker or EndMarker. #> [CmdletBinding(ConfirmImpact='Low')] Param( [Parameter(Mandatory=$true)][String]$InputString, [Parameter(Mandatory=$false)][String]$StartMarker, [Parameter(Mandatory=$false)][String]$EndMarker, [Parameter(Mandatory=$false)][Switch]$OpenEnded ) Begin { Write-Verbose "InputString: $InputString" Write-Verbose "StartMarker: $StartMarker" Write-Verbose "EndMarker: $EndMarker" Write-Verbose "OpenEnded: $OpenEnded" if ($StartMarker -eq $EndMarker) { Write-Warning "Parse-String Error: 'StartMarker' and 'EndMarker' parameter values cannot be the same" break } } Process { # Both markers provided, both markers found if ($StartMarker) { if ($EndMarker) { if ($InputString -match $StartMarker -and $InputString -match $EndMarker) { $StartMarkerCount = ($InputString -split $StartMarker).Count - 1 $EndMarkerCount = ($InputString -split $EndMarker).Count - 1 foreach ($Occurance in (1..$StartMarkerCount)) { (($InputString -split $StartMarker)[$Occurance].Trim() -split $EndMarker)[0].Trim() } # Foreach } # Match } # $EndMarker } # $StartMarker # StartMarker only provided and found if ($StartMarker -and -not $EndMarker) { if ($InputString -match $StartMarker -and $OpenEnded) { $MarkerCharNumber = $InputString.ToLower().IndexOf($StartMarker.ToLower()) $InputString.Substring($MarkerCharNumber+$StartMarker.Length,$InputString.Length-($MarkerCharNumber+$StartMarker.Length)).Trim() } # Match } # $StartMarker # EndMarker only provided and found if ($EndMarker -and -not $StartMarker) { if ($InputString -match $EndMarker -and $OpenEnded) { ($InputString -split $EndMarker)[0].Trim() } # Match } # $EndMarker } End { } } function Update-PSModule { <# .SYNOPSIS Function to update one or more PowerShell Modules from the PowerShellGalery.com .DESCRIPTION Function to update one or more PowerShell Modules from the PowerShellGalery.com .PARAMETER ModuleList One or more Module names This is an optional parameter that defaults to AZSBTools .EXAMPLE Update-PSModule -ModuleList AZSBTools,ImportExcel .OUTPUTS This cmdlet returns PS Objects for each module such as: Name Version ---- ------- AZSBTools 1.173.107 ImportExcel 7.1.0 .LINK https://superwidgets.wordpress.com/category/powershell/ .NOTES Function by Sam Boutros v0.1 - 13 April 2020 #> [CmdletBinding(ConfirmImpact='Low')] Param( [Parameter(Mandatory=$false)][String[]]$ModuleList = @('AZSBTools') ) Begin { Set-PSRepository -Name PSGallery -InstallationPolicy Trusted -EA 0 [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 $Elevated = (New-Object Security.Principal.WindowsPrincipal([Security.Principal.WindowsIdentity]::GetCurrent())).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator) $Length = $ModuleList | foreach { $_.Length } | sort | select -Last 1 } Process { $myOutput = foreach ($Module in $ModuleList) { try { $NewModule = Find-Module $Module -EA 1 $CurrentModule = Get-Module $Module -ListAvailable | sort Version | select -Last 1 $CurrentVersion = if ($CurrentModule) {$CurrentModule.Version.ToString()} else {'None'} Write-Log 'Validating PS module',"$Module".PadRight($Length+1),'version',($NewModule.Version.ToString()).PadRight(10) Green,Cyan,Green,Cyan -NoNewLine if ($CurrentVersion -eq $NewModule.Version) { Write-Log 'Validated' DarkYellow } else { Write-Log "Not (Current Version $CurrentVersion), installing.." Yellow -NoNewline if ($Elevated) { Install-Module $Module -Force -AllowClobber Remove-Module $Module -Force -EA 0 # To allow for auto-loading the latest version # Remove older copies of the module under 'CurrentUser' scope, because they get prioritized for auto-loading: Remove-Item "$([Environment]::GetFolderPath('MyDocuments'))\WindowsPowerShell\Modules\$Module" -Recurse -Force -EA 0 } else { Install-Module $Module -Force -AllowClobber -Scope CurrentUser Remove-Module $Module -Force -EA 0 # To allow for auto-loading the latest version } Write-Log 'done' Green } $CurrentModule = Get-Module $Module -ListAvailable | sort Version | select -Last 1 $CurrentVersion = if ($CurrentModule) {$CurrentModule.Version.ToString()} else {'None'} } catch { $CurrentVersion = 'Not found in PS Gallery' Write-Log $_.Exception.Message Magenta } [PSCustomObject][Ordered]@{ Name = $Module Version = $CurrentVersion } } } End { $myOutput } } function New-PSProfile { <# .SYNOPSIS Function to create a PS profile .DESCRIPTION Function to create a PS profile If a profile file exists, this function appends $FileContent to it .PARAMETER FileContent This is an optional parameter that defaults 'Update-PSModule', which defaults to updating AZSBTools and ImportExcel PS Modules .EXAMPLE New-PSProfile .OUTPUTS None .LINK https://superwidgets.wordpress.com/category/powershell/ .NOTES Function by Sam Boutros v0.1 - 13 April 2020 #> [CmdletBinding(ConfirmImpact='Low')] Param( [Parameter(Mandatory=$false)][String]$Content = 'Update-PSModule' ) Begin { } Process { if (Test-Path $profile) { $OldContent = Get-Content $profile Write-Log 'Current PS Profile',$profile Green,Cyan Write-Log (Get-Content $profile -Raw) Yellow if (-not ($OldContent -match $Content)) { Write-Log 'Updating PS Profile file',$profile Green,Cyan $NewContent = $OldContent += $Content $NewContent | Out-File $profile -Force # Over write the content to avoid file encoding issues New-PSProfile -Content $Content } } else { Write-Log 'Creating new PS Profile file',$profile Green,Cyan New-Item "$([Environment]::GetFolderPath('MyDocuments'))\WindowsPowerShell" -ItemType Directory -Force -EA 0 | Out-Null $Content | Out-File $profile -Force New-PSProfile -Content $Content } } End { } } function Get-IPLocation { <# .SYNOPSIS Function to return the Geographical location of an Internet IP address .DESCRIPTION Function to return the Geographical location of an Internet IP address This function depends on ip-api.com and/or ipinfo.io This function defaults to querying ipinfo.io because it also provides reverse dns .PARAMETER Uri One or more URLs This is an optional parameter. These URLs will be queried for WAN IP. .PARAMETER IPAddress One or more IP addresses This is an optional parameter that defaults to the current WAN IP. .PARAMETER ReportAll This is an optional switch. When set to True, this function will return information from every Uri source on every provided IP address .EXAMPLE Get-IPLocation (Resolve-DnsName CNN.com -Type A).IPAddress -Verbose This example will return information of all IP addresses of CNN.com from ipinfo.io .EXAMPLE Get-IPLocation (Resolve-DnsName Google.com -Type A).IPAddress -ReportAll -Verbose This example will return information of the IP address of Google.com from ipinfo.io and ip-api.com .EXAMPLE Get-IPLocation -ReportAll 192.168.1.1 -Verbose This example returns no data. This function returns no data for Private IP addresses .OUTPUTS This cmdlet returns aa object such as: IPAddress : 172.217.11.46 ReverseDNS : lga25s61-in-f14.1e100.net Country : US Region : New York City : New York City ZipCode : 10004 Coords : 40.7143,-74.0060 TimeZone : America/New_York Org : AS15169 Google LLC .LINK https://superwidgets.wordpress.com/category/powershell/ .NOTES Function by Sam Boutros v0.1 - 14 April 2020 v0.2 - 15 April 2020 - Manually validate that the IP input is a valid IP Address #> [CmdletBinding(ConfirmImpact='Low')] Param( [Parameter(Mandatory=$false)][String[]]$IPAddress = (Get-MyWANIP).IPAddressToString, [Parameter(Mandatory=$false)][String[]]$Uri = @('http://ip-api.com/json','http://ipinfo.io'), [Parameter(Mandatory=$false)][Switch]$ReportAll = $false ) Begin { function GetInfoFrom-IPAPI { [CmdletBinding(ConfirmImpact='Low')] Param( [Parameter(Mandatory=$true)][String]$Uri) try { $Result = Invoke-RestMethod -Method Get -Uri $Uri -UseBasicParsing -EA 1 if ($Result.status -eq 'success') { [PSCustomObject][Ordered]@{ IPAddress = $Result.query Country = $Result.country Region = $Result.regionname City = $Result.city ZipCode = $Result.zip Coords = "$($Result.lat),$($Result.lon)" TimeZone = $Result.timezone Org = "$($Result.as) ($($Result.org))" } } } catch { Write-Verbose $_.Message.Exception } } function GetInfoFrom-IPINFO { [CmdletBinding(ConfirmImpact='Low')] Param( [Parameter(Mandatory=$true)][String]$Uri) try { $Result = Invoke-RestMethod -Method Get -Uri $Uri -UseBasicParsing -EA 1 if (-not $Result.bogon) { [PSCustomObject][Ordered]@{ IPAddress = $Result.ip ReverseDNS = $Result.hostname Country = $Result.country Region = $Result.region City = $Result.city ZipCode = $Result.postal Coords = $Result.loc TimeZone = $Result.timezone Org = $Result.org } } } catch { Write-Verbose $_.Message.Exception } } Write-Verbose 'Received input:' Write-Verbose "IPAddress: $($IPAddress -join ', ')" Write-Verbose "Uri: $($Uri -join ', ')" Write-Verbose "ReportAll: $ReportAll" } Process { foreach ($IP in $IPAddress) { try { $IP = [IPAddress]$IP.trim() # Manually validate that the IP input is a valid IP Address $IP = $IP.IPAddressToString if ($ReportAll) { foreach ($1Uri in $Uri) { switch ($1Uri) { 'http://ip-api.com/json' { GetInfoFrom-IPAPI "$1Uri/$IP" } 'http://ipinfo.io' { GetInfoFrom-IPINFO "$1Uri/$IP" } default { Invoke-RestMethod -Method Get -Uri "$1Uri/$IP" -UseBasicParsing } } } } else { # Prefer ipinfo.io because it also provides reverse dns if ($Uri -match 'ipinfo.io') { GetInfoFrom-IPINFO "$($Uri -match 'ipinfo.io')/$IP" } elseif ($Uri -match 'ip-api.com') { GetInfoFrom-IPAPI "$($Uri -match 'ip-api.com')/$IP" } else { # return raw Invoke-RestMethod -Method Get -Uri "$($Uri | select -First 1)/$IP" -UseBasicParsing } } } catch { Write-Verbose "Get-IPLocation Error: invalid IP address input received: $IP" } } } End { } } function Get-EventLogNames { [CmdletBinding()] Param() [System.Diagnostics.Eventing.Reader.EventLogSession]::GlobalSession.GetLogNames() } function Backup-EventLog { <# .SYNOPSIS Function to backup one or more Windows event logs .DESCRIPTION Function to backup one or more Windows event logs .PARAMETER EventLogName One or more Windows event logs To see a list of Windows Event Logs: (Get-WinEvent -ListLog '*' -EA 0).LogName | sort This parameter features auto-complete This is an optional parameter that defaults to 'Application' Note that some event logs like 'Security' event log require elevation .PARAMETER BackupFolder Path to the folder where this function will make a backup of the provided Windows event log .PARAMETER LogFile Path to a file where this function will log its console output .OUTPUTS This function returns a list of successfully backed up Windows event logs .EXAMPLE Backup-EventLog -EventLogName Application,Security,Microsoft-Windows-RemoteDesktopServices-RdpCoreTS/Operational -BackupFolder c:\Logs\Test .LINK https://superwidgets.wordpress.com/category/powershell/ .NOTES Function by Sam Boutros v0.1 - 29 April 2020 #> [CmdletBinding(ConfirmImpact='Low')] Param( [Parameter(Mandatory=$false)] [ArgumentCompleter( { param($Command, $Parameter, $WordToComplete, $CommandAst, $FakeBoundParams) Get-EventLogNames } )] [ValidateScript( { $_ -in (Get-EventLogNames) } )] [String[]]$EventLogName = 'Application', [Parameter(Mandatory=$false)][String]$BackupFolder, [Parameter(Mandatory=$false)][String]$LogFile = ".\Backup-EventLog_$($env:COMPUTERNAME)_$(Get-Date -Format 'ddMMMMyyyy_hh-mm-ss_tt').txt" ) Begin { if (-not $BackupFolder) { Write-Log '$BackupFolder parameter not provided, using current folder' Yellow $LogFile -NoNewLine $BackupFolder = (Get-Location).Path Write-Log $BackupFolder Cyan $LogFile } if (-not (Test-Path $BackupFolder)) { Write-Log '$BackupFolder',$BackupFolder,'does not exist, using current folder' Yellow,Cyan,Yellow $LogFile -NoNewLine $BackupFolder = (Get-Location).Path Write-Log $BackupFolder Cyan $LogFile } $BackupFolder = (Get-Item $BackupFolder).FullName } Process { $EventSession = New-Object System.Diagnostics.Eventing.Reader.EventLogSession $Succeeded = foreach ($LogName in $EventLogName) { if ($LogName -in (Get-EventLogNames)) { $Destination = "$BackupFolder\$($LogName.Replace('/','_'))_$($env:COMPUTERNAME)_$(Get-Date -Format 'ddMMMMyyyy_hh-mm-ss_tt').evtx" Write-Log 'Backing up',$LogName,'Windows event log to',$Destination Green,Cyan,Green,Cyan $LogFile -NoNewLine try { $EventSession.ExportLogAndMessages($LogName,'LogName','*',$Destination) Write-Log 'done' Green $LogFile -NoNewLine if (Test-Path $Destination) { $LogName; Write-Log 'and validated' Cyan $LogFile } else { Write-Log 'but failed validation' Magenta $LogFile } } catch { # ExportLogAndMessages works but gives this error message if not running under elevated permissions $msg = 'Exception calling "ExportLogAndMessages" with "4" argument(s): "The directory name is invalid"' if ($_.Exception.Message -eq $msg) { Write-Log 'done' Green $LogFile -NoNewLine if (Test-Path $Destination) { $LogName; Write-Log 'and validated' Cyan $LogFile } else { Write-Log 'but failed validation' Magenta $LogFile } } else { Write-Log 'failed' Magenta $LogFile Write-Log $_.Exception.Message Magenta $LogFile } } } else { Write-Log 'Backup-EventLog Error: bad log name provided:', $LogName Yellow,Cyan $LogFile } } } End { $Succeeded } } function Clear-SBEventLog { <# .SYNOPSIS Function to clear one or more Windows event logs .DESCRIPTION Function to clear one or more Windows event logs Unlike the native Clear-EventLog, this function can clear all Windows event logs This function requires elevated permissions .PARAMETER EventLogName One or more Windows event logs To see a list of Windows Event Logs: (Get-WinEvent -ListLog '*' -EA 0).LogName | sort This parameter features auto-complete This is an optional parameter that defaults to 'Application' .PARAMETER LogFile Path to a file where this function will log its console output .EXAMPLE Clear-SBEventLog -EventLogName Application .EXAMPLE Clear-SBEventLog -EventLogName Application,Security,Microsoft-Windows-RemoteDesktopServices-RdpCoreTS/Operational -Confirm:$false This example will clear the listed Windows event logs without interactive confirmation .EXAMPLE $EventLogList = @('Application','Security','Microsoft-Windows-RemoteDesktopServices-RdpCoreTS/Operational') Backup-EventLog -EventLogName $EventLogList -BackupFolder c:\Sandbox\Logs\Test Clear-SBEventLog -EventLogName $EventLogList -Confirm:$false This example backs up and clears the listed event logs .LINK https://superwidgets.wordpress.com/category/powershell/ .NOTES Function by Sam Boutros v0.1 - 29 April 2020 #> [CmdletBinding(SupportsShouldProcess=$true,ConfirmImpact='High')] Param( [Parameter(Mandatory=$false)] [ArgumentCompleter( { param($Command, $Parameter, $WordToComplete, $CommandAst, $FakeBoundParams) Get-EventLogNames } )] [ValidateScript( { $_ -in (Get-EventLogNames) } )] [String[]]$EventLogName = 'Application', [Parameter(Mandatory=$false)][String]$LogFile = ".\Clear-SBEventLog_$($env:COMPUTERNAME)_$(Get-Date -Format 'ddMMMMyyyy_hh-mm-ss_tt').txt" ) Begin { # Check elevation if (-NOT ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]'Administrator')) { Write-Log 'Clear-SBEventLog Error: This function requires elevation (run as administrator)' Magenta $LogFile Break } } Process { $EventSession = New-Object System.Diagnostics.Eventing.Reader.EventLogSession foreach ($LogName in $EventLogName) { if ($LogName -in (Get-EventLogNames)) { $LogInfo = $EventSession.GetLogInformation("$LogName",'LogName') Write-Log 'Clearing',$LogInfo.RecordCount,'events in',$LogName,'Windows event log..' Green,Cyan,Green,Cyan,Green $LogFile -NoNewLine If ($PSCmdlet.ShouldProcess("$LogName", "Clear log file")) { try { $EventSession.ClearLog("$LogName") Write-Log 'done' DarkYellow $LogFile } catch { Write-Log 'failed' Magenta $LogFile Write-Log $_.Exception.Message Magenta $LogFile } } } else { Write-Log 'Clear-SBEventLog Error: bad log name provided:', $LogName Yellow,Cyan $LogFile } } } End { } } function Get-FileShareInfo { <# .SYNOPSIS Script to report on file share information .DESCRIPTION Function to provide file share information. This function also obtains and saves the registry entries for file shares under the current user Temp folder. USer the -Verbose switch for more details .PARAMETER IncludeDefaultShares This is an optional Switch parameter. When set to True, this function will report on default shares such as c$ .EXAMPLE Get-FileShareInfo .EXAMPLE cls; $Result = Get-FileShareInfo -Verbose -IncludeDefaultShares; $Result | Out-GridView .OUTPUTS This cmdlet returns a PS object for each share permission such as: ComputerName : myComputerName ShareName : myShareName Path : x:\myFolderName Description : ConnectedUsers : 2 DriveTotalGB : 1788 DriveUsedGB : 1457 DriveFreeGB : 331 DriveFree% : 19 SharePrincipal : myDomainName\Domain Users SharePermission : Modify, Synchronize ShareAccess : AccessAllowed Note that several objects may be returned for the same share if it has multiple share permissions assigned .LINK https://superwidgets.wordpress.com/category/powershell/ https://superwidgets.wordpress.com/2015/03/11/file-share-migration-phase-1-discovery/ .NOTES Function by Sam Boutros v0.1 - 9 February 2015 - Original version https://gallery.technet.microsoft.com/scriptcenter/Powershell-script-to-get-39c73c74 Microsoft is retiring the Technet Gallery by June 2020, see https://docs.microsoft.com/en-us/teamblog/technet-gallery-retirement v0.2 - 2 May 2020 - Rewrite #> [CmdletBinding(ConfirmImpact='Low')] Param( [Parameter(Mandatory=$false)][Switch]$IncludeDefaultShares ) Begin { } Process { #region LANMAN registry key dump REG export HKLM\SYSTEM\CurrentControlSet\Services\LanmanServer\Shares "$env:TEMP\$env:COMPUTERNAME-Shares.reg" /y | Out-Null Write-Verbose "Shares' registry info saved to file '$env:TEMP\$env:COMPUTERNAME-Shares.reg', details:" Write-Verbose (Get-Content "$env:TEMP\$env:COMPUTERNAME-Shares.reg" | Out-String).Trim() #endregion #region Drive info $DriveInfo = Get-PSDrive | where { $_.Free } | sort $_.Root | foreach { [PSCustomObject][Ordered]@{ Drive = $_.Root UsedBytes = $_.Used UsedGB = [Math]::Round($_.Used/1GB, 0) FreeBytes = $_.Free FreeGB = [Math]::Round($_.Free/1GB, 0) 'Free%' = [Math]::Round((100 * $_.Free/($_.Used + $_.Free)), 0) TotalBytes = $_.Used + $_.Free TotalGB = [Math]::Round(($_.Used + $_.Free)/1GB, 0) } } Write-Verbose 'Drive info:' Write-Verbose ($DriveInfo | FT Drive,UsedGB,FreeGB,Free%,TotalGB -a | Out-String).Trim() #endregion #region Fileshare info $FileShareInfo = Get-WmiObject -Class Win32_Share | select Name, Path, Description | sort Path if (-not $IncludeDefaultShares) { $FileShareInfo = $FileShareInfo | where { -not $_.Description.StartsWith('Default') -and $_.Path } } Write-Verbose 'Fileshare info:' Write-Verbose ($FileShareInfo | FT -a | Out-String).Trim() #endregion #region ConnectedUsers info $ConnectedUsers = Get-WmiObject -Class Win32_ServerConnection -Namespace 'root\CIMV2' | select ShareName, UserName, ComputerName, @{n='ActiveTimeSec';e={$_.ActiveTime}} | sort ShareName Write-Verbose ($ConnectedUsers | FT -a | Out-String).Trim() $ConnectedUsersTallies = $ConnectedUsers | group ShareName | Sort Count -Descending | select @{n='Share';e={$_.Name}},@{n='Connections';e={$_.Count}} Write-Verbose ($ConnectedUsersTallies | FT -a | Out-String).Trim() #endregion #region SharePermissions $SharePermissions = foreach ($ShareSecuritySetting in (Get-WmiObject -Class Win32_LogicalShareSecuritySetting)) { foreach ($DACL in ($ShareSecuritySetting.GetSecurityDescriptor()).Descriptor.DACL) { [PSCustomObject][ordered]@{ ShareName = $ShareSecuritySetting.Name SecurityPrincipal = $( try { "$($DACL.Trustee.Domain)\$($DACL.Trustee.Name)" } catch { $DACL.Trustee.Name } ) FileSystemRights = ($DACL.AccessMask -as [Security.AccessControl.FileSystemRights]) AccessType = [Security.AccessControl.AceType]$DACL.AceType } } } $SharePermissions = $SharePermissions | sort ShareName Write-Verbose 'Share (not NTFS) permissions:' Write-Verbose ($SharePermissions | FT -a | Out-String).Trim() #endregion #region Summary $SummaryShareInfo = foreach ($thisFileShare in $FileShareInfo) { Write-Verbose "Processing $($thisFileShare.Name)" if ($SharePermissions | where ShareName -EQ $thisFileShare.Name) { foreach ($thisSharePermission in ($SharePermissions | where ShareName -EQ $thisFileShare.Name)) { Write-Verbose "Processing $($thisSharePermission.SecurityPrincipal)" [PSCustomObject][ordered]@{ ComputerName = $env:COMPUTERNAME ShareName = $thisFileShare.Name Path = $thisFileShare.Path Description = $thisFileShare.Description ConnectedUsers = ($ConnectedUsersTallies | where Share -EQ $thisFileShare.Name).Connections DriveTotalGB = ($DriveInfo | where { $_.Drive[0] -eq $thisFileShare.Path[0] }).TotalGB DriveUsedGB = ($DriveInfo | where { $_.Drive[0] -eq $thisFileShare.Path[0] }).UsedGB DriveFreeGB = ($DriveInfo | where { $_.Drive[0] -eq $thisFileShare.Path[0] }).FreeGB 'DriveFree%' = ($DriveInfo | where { $_.Drive[0] -eq $thisFileShare.Path[0] }).'Free%' SharePrincipal = $thisSharePermission.SecurityPrincipal SharePermission = $thisSharePermission.FileSystemRights ShareAccess = $thisSharePermission.AccessType } } } else { [PSCustomObject][ordered]@{ ComputerName = $env:COMPUTERNAME ShareName = $thisFileShare.Name Path = $thisFileShare.Path Description = $thisFileShare.Description ConnectedUsers = ($ConnectedUsersTallies | where Share -EQ $thisFileShare.Name).Connections DriveTotalGB = ($DriveInfo | where { $_.Drive[0] -eq $thisFileShare.Path[0] }).TotalGB DriveUsedGB = ($DriveInfo | where { $_.Drive[0] -eq $thisFileShare.Path[0] }).UsedGB DriveFreeGB = ($DriveInfo | where { $_.Drive[0] -eq $thisFileShare.Path[0] }).FreeGB 'DriveFree%' = ($DriveInfo | where { $_.Drive[0] -eq $thisFileShare.Path[0] }).'Free%' SharePrincipal = 'None' SharePermission = 'None' ShareAccess = 'None' } } } $SummaryShareInfo = $SummaryShareInfo | Sort ConnectedUsers -Descending #endregion } End { $SummaryShareInfo } } function Where-AMI { <# .SYNOPSIS Function to return the output of different variables to indicate where a cmdlet/script is invoked from in the file system .DESCRIPTION Function to return the output of different variables to indicate where a cmdlet/script is invoked from in the file system .PARAMETER ShowCommandDefinition Optional Switch parameter. when set to True this funtion will also display $MyInvocation.MyCommand.Definition .EXAMPLE Where-AMI .EXAMPLE Where-AMI -ShowCommandDefinition .OUTPUTS PS Object containing the following properties: Command Direct Function .LINK https://superwidgets.wordpress.com/category/powershell/ .NOTES Function by Sam Boutros v0.1 - 3 May 2020 #> [CmdletBinding(ConfirmImpact='Low')] Param( [Parameter(Mandatory=$false)][Switch]$ShowCommandDefinition ) Begin { function PSCommandPath1() { $PSCommandPath } function ScriptName() { $MyInvocation.ScriptName } function MyCommandName() { $MyInvocation.MyCommand.Name } function MyCommandDefinition() { $MyInvocation.MyCommand.Definition } function PSCommandPath2() { $MyInvocation.PSCommandPath } } Process { $CommandList = @( [PScustomObject][Ordered]@{Command='$PSCommandPath';Direct=$PSCommandPath;Function=(PSCommandPath1)} [PScustomObject][Ordered]@{Command='$MyInvocation.ScriptName';Direct=$MyInvocation.ScriptName;Function=(ScriptName)} [PScustomObject][Ordered]@{Command='$MyInvocation.MyCommand.Name';Direct=$MyInvocation.MyCommand.Name;Function=(MyCommandName)} [PScustomObject][Ordered]@{Command='$MyInvocation.PSCommandPath';Direct=$MyInvocation.PSCommandPath;Function=(PSCommandPath2)} ) if ($ShowCommandDefinition) { $CommandList += [PScustomObject][Ordered]@{Command='$MyInvocation.MyCommand.Definition';Direct=$MyInvocation.MyCommand.Definition;Function=(MyCommandDefinition)} } Write-Log ' ' Write-Log 'PS Version:',$PSVersionTable.PSVersion Green,Cyan $Result = foreach ($Command in $CommandList) { [PSCustomObject][Ordered]@{ Command = $Command.Command Direct = $Command.Direct Function = $Command.Function } Write-Log ' ' Write-Log 'Command: ',$Command.Command Green,Cyan Write-Log 'Direct: ',$Command.Direct Green,Cyan Write-Log 'Function: ',$Command.Function Green,Cyan } } End { $Result } } function Get-FolderSize { <# .SYNOPSIS Function to return total folder size .DESCRIPTION Function to return total folder size This function can also return the size of subfolders This function uses Robocopy.exe (https://docs.microsoft.com/en-us/windows-server/administration/windows-commands/robocopy) .PARAMETER Folder Path to folder. This parameter defaults to the current folder .PARAMETER Depth A number that can be 0-999 and defaults to 0. This is how deep into the subfolders this function will report on. This is a recursive function. .EXAMPLE Get-FolderSize .EXAMPLE Get-FolderSize -Folder C:\Windows -Depth 3 | sort SizeGB -Descending | FT -a .OUTPUTS This cmdlet returns a PS object for each folder (and subfolder if depth > 0) such as: FolderName FolderCount FileCount SizeGB DurationSec ---------- ----------- --------- ------ ----------- C:\Windows 52043 183425 18.44 17 C:\Windows\WinSxS 23946 65516 8.13 46 C:\Windows\System32 1454 13775 3.09 53 .LINK https://superwidgets.wordpress.com/category/powershell/ .NOTES Function by Sam Boutros v0.1 - 23 September 2020 #> [CmdletBinding(ConfirmImpact='Low')] Param( [Parameter(Mandatory=$false)][String]$Folder = '.\', [Parameter(Mandatory=$false)][ValidateRange(0,999)][Int]$Depth = 0 ) Begin { } Process { if (Test-Path $Folder) { $Duration = Measure-Command { $RawOutput = robocopy /l /nfl /ndl /e /bytes /ia:RASHCNETO /W:0 /R:0 $Folder \Whatevs } New-Object -TypeName PSObject -Property ([Ordered]@{ FolderName = (Get-Item $Folder).FullName FolderCount = $( if ($Found = ($RawOutput -match 'Dirs :')) { [Math]::Round($Found.Split(':')[1].Trim().Split(' ')[0]) } ) FileCount = $( if ($Found = ($RawOutput -match 'Files : ')) { [Math]::Round($Found.Split(':')[1].Trim().Split(' ')[0]) } ) SizeGB = $( if ($Found = ($RawOutput -match 'Bytes :')) { [Math]::Round($Found.Split(':')[1].Trim().Split(' ')[0]/1GB,2) } ) DurationSec = [Math]::Round($Duration.TotalSeconds,2) }) if ($Depth -gt 0) { if ($FolderList = Get-ChildItem -Path $Folder -Directory -EA 0) { foreach ($FolderName in $FolderList.FullName) { Get-FolderSize -Folder $FolderName -Depth ($Depth -1) } } } } else { Write-Log 'Folder',$Folder,'not found' Magenta,Yellow,Magenta } } End { } } function Self-Elevate { <# .SYNOPSIS Function to self-elevate a PowerShell script .DESCRIPTION Function to self-elevate a PowerShell script .PARAMETER Exe Optional parameter that indicates which executable to use for the new elevated PowerShell session. Valid options are either PowerShell.exe or PowerShell_Ise.exe This defaults to PowerShell.exe .EXAMPLE if (-not $IsElevated) { Self-Elevate } .LINK https://superwidgets.wordpress.com/category/powershell/ .NOTES Function by Sam Boutros v0.1 - 26 December 2020 #> [CmdletBinding(ConfirmImpact='High')] Param( [Parameter(Mandatory=$false)][ValidateSet('PowerShell_ise.exe','PowerShell.exe')][String]$Exe = 'PowerShell.exe' ) Begin { } Process { if ([int]($thisOS.BuildNumber) -ge 6000) { $ArgumentList = "-File ""$($MyInvocation.MyCommand.Path)"" $($MyInvocation.UnboundArguments)" Start-Process -FilePath $Exe -Verb Runas -ArgumentList $ArgumentList } else { Write-Log 'This OS',$thisOs.Caption,"(Version $($thisOS.Version))",'does not support elevation' Magenta,Yellow,Magenta,Yellow } } End { } } function Truncate-String { <# .SYNOPSIS Function to truncate a given string according to the given maximum. .DESCRIPTION Function to truncate a given string according to the given maximum. .PARAMETER String Any string like 'hello there'. .PARAMETER Maximum The number of characters to truncate the given string at. .EXAMPLE Truncate-String 'Hello there' 4 .OUTPUTS String object .LINK https://superwidgets.wordpress.com/category/powershell/ .NOTES Function by Sam Boutros v0.1 - 17 February 2021 #> [CmdletBinding(ConfirmImpact='Low')] Param( [Parameter(Mandatory=$true)][String]$String, [Parameter(Mandatory=$true)][Int16]$Maximum ) Begin { } Process { $String.Substring(0, ($String.Length,$Maximum | measure -Minimum).Minimum ) } End { } } function New-FileSeed { <# .SYNOPSIS Function to return the Geographical location of an Internet IP address .DESCRIPTION Function to return the Geographical location of an Internet IP address This function depends on ip-api.com and ipinfo.io .PARAMETER Source One or more URLs This is an optional parameter. These URLs will be queried for WAN IP. .EXAMPLE Get-MyWANIP .OUTPUTS This cmdlet returns a System.Net.IPAddress object such as: Address : 1132553623 AddressFamily : InterNetwork ScopeId : IsIPv6Multicast : False IsIPv6LinkLocal : False IsIPv6SiteLocal : False IsIPv6Teredo : False IsIPv4MappedToIPv6 : False IPAddressToString : 151.101.129.67 .LINK https://superwidgets.wordpress.com/category/powershell/ .NOTES Function by Sam Boutros v0.1 - 12 April 2020 #> [CmdletBinding(ConfirmImpact='Low')] Param( [Parameter(Mandatory=$true)][ValidateSet('10KB','100KB','1MB','10MB','100MB','1GB','10GB','100GB','1TB')][String]$SeedSize, [Parameter(Mandatory=$true)][ValidateScript({ Test-Path $_ })][String]$WorkFolder ) Begin { } Process { $SizeList = @('10KB','100KB','1MB','10MB','100MB','1GB','10GB','100GB','1TB') $Seed = $SizeList.IndexOf($SeedSize.ToUpper()) $WorkFolder = (Get-Item $WorkFolder).FullName $SeedInt64 = Switch ($SeedSize) { '10KB' { 10KB } '100KB' { 100KB } '1MB' { 1MB } '10MB' { 10MB } '100MB' { 100MB } '1GB' { 1GB } '10GB' { 10GB } '100GB' { 100GB } '1TB' { 1TB } } $SeedFileName = "$WorkFolder\Seed$SeedSize.txt" if ($SeedInt64 -eq 10KB) { # Smallest seed starts from scratch $Missing = try { (Get-Item $SeedFileName -EA 1).length -ne $SeedInt64 } catch { $true } if ($Missing) { $Duration = Measure-Command { do { Get-Random -Minimum 100000000 -Maximum 999999999 | Out-File -Filepath $SeedFileName -append } while ((Get-Item $SeedFileName).length -lt ($SeedInt64-8)) Get-Random -Minimum 10 -Maximum 99 | Out-File -Filepath $SeedFileName -append # + 8 bytes } } } else { # Each subsequent seed depends on the prior one $PriorSeed = "$WorkFolder\Seed$($SizeList[$Seed-1]).txt" if (-not (Test-Path $PriorSeed)) { New-FileSeed -SeedSize $SizeList[$Seed-1] -WorkFolder $WorkFolder } # Recursive function :) $Missing = try { (Get-Item $SeedFileName -EA 1).length -ne $SeedInt64 } catch { $true } if ($Missing) { $Duration = Measure-Command { $command = "cmd.exe /C copy $PriorSeed+$PriorSeed+$PriorSeed+$PriorSeed+$PriorSeed+$PriorSeed+$PriorSeed+$PriorSeed+$PriorSeed+$PriorSeed $SeedFileName /y" Invoke-Expression -Command:$command | Out-Null Get-Random -Minimum 1000000 -Maximum 9999999 | Out-File -Filepath $SeedFileName -append # + 18 bytes } } } Write-Log 'Created/Validated',($SeedSize).PadRight(5),'seed file',($SeedFileName).PadRight(33),'in',('{0:N2}' -f $Duration.TotalSeconds).PadLeft(6),'seconds.' Green,Cyan,Cyan,Green,Green,Cyan,Green } End { } } function Test-Disk { <# .SYNOPSIS Function to return the Geographical location of an Internet IP address .DESCRIPTION Function to return the Geographical location of an Internet IP address This function depends on ip-api.com and ipinfo.io .PARAMETER Source One or more URLs This is an optional parameter. These URLs will be queried for WAN IP. .EXAMPLE Get-MyWANIP .OUTPUTS This cmdlet returns a System.Net.IPAddress object such as: Address : 1132553623 AddressFamily : InterNetwork ScopeId : IsIPv6Multicast : False IsIPv6LinkLocal : False IsIPv6SiteLocal : False IsIPv6Teredo : False IsIPv4MappedToIPv6 : False IPAddressToString : 151.101.129.67 .LINK https://superwidgets.wordpress.com/category/powershell/ .NOTES Function by Sam Boutros v0.1 - 12 April 2020 #> [CmdletBinding(ConfirmImpact='Low')] param( [Parameter (Mandatory=$true,HelpMessage='WorkFolder to run this test in, like c:\support')][String]$WorkFolder, [Parameter (Mandatory=$true,HelpMessage='Maximum amount of disk space to use for this test')][Int64]$MaxSpaceToUseOnDisk, [Parameter (Mandatory=$false)][Int32]$Threads = 1, [Parameter (Mandatory=$false)][Int32]$Cycles = 3, [Parameter (Mandatory=$false)][Int32]$SmallestFile = 4 ) Begin { } Process { } End { } } function Get-PendingReboot { <# .SYNOPSIS Function to test if a remote computer is pending reboot .DESCRIPTION Function to test if a remote computer is pending reboot This function performs three tests against each reachable computer provided: 1. Pending Reboot: HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Component Based Servicing 2. Reboot Required: HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\WindowsUpdate\Auto Update 3. Pending File Rename Operations: HKLM:\SYSTEM\CurrentControlSet\Control\Session Manager .PARAMETER ComputerName One or more computer names .PARAMETER Cred Optional PSCredential object that can be obtained via Get-Credential or Get-SBCredential .PARAMETER Detailed Optional switch. When set to True, this function returns detailed results on each of the three tests attempted. .PARAMETER LogFile Path to file where this function will log its console output. .EXAMPLE Get-PendingReboot Comp1,Comp2 .OUTPUTS This function returns a PS object for test against each reachable computer such as: ComputerName : Comp1 OS : Microsoft Windows Server 2016 Datacenter (10.0.14393 64-bit) PendingReboot : False If the 'Detailed' switch is set to True, the ouput looks like: This function returns a PS object for test against each reachable computer such as: ComputerName : Comp1 OS : Microsoft Windows Server 2016 Datacenter (10.0.14393 64-bit) TestName : PendingFileRenameOperations TestType : NonNullValue TestResult : False .LINK https://superwidgets.wordpress.com/category/powershell/ .NOTES Function by Sam Boutros v0.1 - 7 April 2021 v0.2 - 10 April 2021 - Added Detailed switch, and summary output. #> [CmdletBinding(ConfirmImpact='Low')] Param( [Parameter(Mandatory=$true)][String[]]$ComputerName, [Parameter(Mandatory=$false)][PSCredential]$Cred, [Parameter(Mandatory=$false)][Switch]$Detailed, [Parameter(Mandatory=$false)][String]$LogFile = ".\Get-PendingReboot_$($env:COMPUTERNAME)_$(Get-Date -Format 'ddMMMMyyyy_hh-mm-ss_tt').log" ) Begin { $pendingRebootTests = @( New-Object -TypeName PSObject -Property @{ Name = 'RebootPending' Test = { Get-ItemProperty -Path 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Component Based Servicing' -Name 'RebootPending' -EA 0 } TestType = 'ValueExists' } New-Object -TypeName PSObject -Property @{ Name = 'RebootRequired' Test = { Get-ItemProperty -Path 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\WindowsUpdate\Auto Update' -Name 'RebootRequired' -EA 0 } TestType = 'ValueExists' } New-Object -TypeName PSObject -Property @{ Name = 'PendingFileRenameOperations' Test = { Get-ItemProperty -Path 'HKLM:\SYSTEM\CurrentControlSet\Control\Session Manager' -Name 'PendingFileRenameOperations' -EA 0 } TestType = 'NonNullValue' } ) } Process { $DetailedResult = foreach ($Computer in $ComputerName) { try { if ($Cred) { $session = New-PSSession -Computer $Computer -Credential $Cred -EA 1 } else { $session = New-PSSession -Computer $Computer -EA 1 } foreach ($test in $pendingRebootTests) { $result = Invoke-Command -Session $session -ScriptBlock $test.Test $OS = Invoke-Command -Session $session -ScriptBlock { Get-WmiObject -Class Win32_OperatingSystem -EA 0 } $TestResult = if ($test.TestType -eq 'ValueExists' -and $result) { $true } elseif ($test.TestType -eq 'NonNullValue' -and $result -and $result.($test.Name)) { $true } else { $false } New-Object -TypeName PSObject -Property ([Ordered]@{ ComputerName = $Computer OS = "$($OS.Caption) ($($OS.Version) $($OS.OSArchitecture))" TestName = $test.Name TestType = $test.TestType TestResult = $TestResult }) } $session | Remove-PSSession } catch { Write-Log 'Get-PendingReboot Error:' Magenta $LogFile Write-Log $_.Exception.Message Yellow $LogFile New-Object -TypeName PSObject -Property ([Ordered]@{ ComputerName = $Computer OS = 'Unreachable' TestName = 'N/A' TestType = 'N/A' TestResult = 'N/A' }) } } } End { if ($Detailed) { $DetailedResult } else { $DetailedResult | Group-Object -Property ComputerName | foreach { if ($Found = $_.Group | where { $_.TestResult }) { New-Object -TypeName PSObject -Property ([Ordered]@{ ComputerName = $Found.ComputerName | select -First 1 OS = $Found.OS | select -First 1 PendingReboot = $Found.TestName -join ', ' }) } else { New-Object -TypeName PSObject -Property ([Ordered]@{ ComputerName = $_.Group.ComputerName | select -First 1 OS = $_.Group.OS | select -First 1 PendingReboot = $false }) } } } } } function Cleanup-WindowsFolder { <# .SYNOPSIS Function to clean up Windows folder by deleting unused components and service packs. .DESCRIPTION Function to clean up Windows folder by deleting unused components and service packs. This function uses DISM.EXE and requires elevation. https://docs.microsoft.com/en-us/windows-hardware/manufacture/desktop/clean-up-the-winsxs-folder .PARAMETER Level This optional parameter takes 0, 1, or 2 values and defaults to 0. 0 ==> Delete files with attribute 'Temporary' under the Windows Font Cache folder. This is typically C:\WINDOWS\ServiceProfiles\LocalService\AppData\Local 1 ==> Delete older unused components, safely cleanup c:\Windows\WinSXS No 30 day grace period. 2 ==> Remove all superseded versions of every component in the component store All existing service packs and updates cannot be uninstalled 3 ==> Remove any backup components needed for uninstallation of the service pack The service pack cannot be uninstalled after this command is completed .PARAMETER LogFile Path to a file where this function will save its console output. .EXAMPLE Cleanup-WindowsFolder This example will invoke this function at level 0, which will delete older unused components, and safely cleanup c:\Windows\WinSXS. No 30 day grace period. .EXAMPLE Cleanup-WindowsFolder -Level 2 This example will invoke this function at level 2, which will delete older unused components, safely cleanup c:\Windows\WinSXS, with no 30 day grace period. It will also remove all superseded versions of every component in the component store. All existing service packs and updates cannot be uninstalled. It will also remove any backup components needed for uninstallation of the service pack. The service pack cannot be uninstalled after this command is completed. .OUTPUTS This function displays command details, the time it took, and the disk space savings, to the console and log file. .LINK https://superwidgets.wordpress.com/category/powershell/ .NOTES Function by Sam Boutros v0.1 - 2 October 2021 v0.2 - 9 October 2021 Added DISM error trapping Added Level to delete temporary files in Windows Font Cache folder #> [CmdletBinding(ConfirmImpact='Low')] Param( [Parameter(Mandatory=$false)][ValidateRange(0,3)][Int16]$Level = 0, [Parameter(Mandatory=$false)][String]$LogFile = ".\Cleanup-WindowsFolder_$($env:COMPUTERNAME)_$(Get-Date -Format 'ddMMMMyyyy_hh-mm-ss_tt').log" ) Begin { if (-not $IsElevated) { Write-Log 'Cleanup-WindowsFolder Error:','This function requires elevation.' Magenta,Yellow $LogFile break } Write-Log 'Performing Windows folder cleanup, this will take several minutes..' Green $LogFile } Process { switch ($Level) { 0 { $FolderPath = "$env:windir\ServiceProfiles\LocalService\AppData\Local" if (Test-Path $FolderPath) { Write-Log 'Listing files with','Temporary','attribute under the Windows Font Cache folder',$FolderPath Green,Cyan,Green,Cyan $LogFile $Before = Get-Volume -DriveLetter $WinDrive $Duration = measure-Command { $FileList = Get-ChildItem -Path $FolderPath -Recurse $TotalSize = (($FileList | foreach { $_.Length }) | Measure -Sum).Sum Write-Log ' Identified',('{0:N0}' -f $FileList.Count),'files under',$FolderPath,'Total size:',('{0:N2}' -f ($TotalSize/1GB)),'GB' Green,Cyan,Green,Cyan,Green,Cyan,Green $LogFile $TempList = $FileList | where Attributes -match 'Temp' $TempSize = (($TempList | foreach { $_.Length }) | Measure -Sum).Sum Write-Log ' of which',('{0:N0}' -f $TempList.Count),'files have the','Temporary','attribute. Total size:',('{0:N2}' -f ($TempSize/1GB)),'GB' Green,Cyan,Green,Cyan,Green,Cyan,Green $LogFile if ($TempList) { Write-Log ' Deleting..' Green -NoNewLine Remove-Item $TempList.Fullname -Force -Confirm:$false Write-Log 'done' Cyan $LogFile } else { Write-Log ' Nothing to cleanup here.' Green $LogFile } } $After = Get-Volume -DriveLetter $WinDrive Write-Log 'done in',"$($Duration.Hours):$($Duration.Minutes):$($Duration.Seconds)",'(hh:mm:ss)' Green,Cyan,Green $LogFile Write-Log ' Free disk space before cleanup:',('{0:N2}' -f ($Before.SizeRemaining/1GB)).PadLeft(9),'GB' Green,Cyan,Green $LogFile Write-Log ' Free disk space after cleanup: ',('{0:N2}' -f ($After.SizeRemaining/1GB)).PadLeft(9),'GB' Green,Cyan,Green $LogFile Write-Log ' Amount of freed disk space: ',('{0:N2}' -f (($After.SizeRemaining-$Before.SizeRemaining)/1GB)).PadLeft(9),'GB' Green,Cyan,Green $LogFile } else { Write-Log 'Windows Font Cache folder',$FolderPath,'not found' Magenta,Yellow,Magenta $LogFile } } 1 { Write-Log 'Invoking','Dism.exe /online /Cleanup-Image /StartComponentCleanup' Green,Cyan $LogFile Write-Log " To delete older unused components and safely cleanup $env:windir\WinSXS",'No 30 day grace period..' Green,Cyan $LogFile -NoNew $Before = Get-Volume -DriveLetter $WinDrive $Duration = measure-Command { $Result = Dism.exe /online /Cleanup-Image /StartComponentCleanup } $After = Get-Volume -DriveLetter $WinDrive Write-Log 'done in',"$($Duration.Hours):$($Duration.Minutes):$($Duration.Seconds)",'(hh:mm:ss)' Green,Cyan,Green $LogFile Write-Log ' Free disk space before cleanup:',('{0:N2}' -f ($Before.SizeRemaining/1GB)).PadLeft(9),'GB' Green,Cyan,Green $LogFile Write-Log ' Free disk space after cleanup: ',('{0:N2}' -f ($After.SizeRemaining/1GB)).PadLeft(9),'GB' Green,Cyan,Green $LogFile Write-Log ' Amount of freed disk space: ',('{0:N2}' -f (($After.SizeRemaining-$Before.SizeRemaining)/1GB)).PadLeft(9),'GB' Green,Cyan,Green $LogFile if (-not ($Result -match 'The operation completed successfully.')) { Write-Log 'DISM issue(s) encountered:' Magenta $LogFile Write-Log ($Result | Out-String).Trim() Yellow $LogFile } } 2 { Write-Log 'Invoking','Dism.exe /online /Cleanup-Image /StartComponentCleanup /ResetBase' Green,Cyan $LogFile Write-Log " To delete older unused components and safely cleanup $env:windir\WinSXS",'No 30 day grace period,' Green,Cyan $LogFile Write-Log ' AND remove all superseded versions of every component in the component store','All existing service packs and updates cannot be uninstalled' Green,Cyan $LogFile -NoNew $Before = Get-Volume -DriveLetter $WinDrive $Duration = measure-Command { $Result = Dism.exe /online /Cleanup-Image /StartComponentCleanup /ResetBase } $After = Get-Volume -DriveLetter $WinDrive Write-Log 'done in',"$($Duration.Hours):$($Duration.Minutes):$($Duration.Seconds)",'(hh:mm:ss)' Green,Cyan,Green $LogFile Write-Log ' Free disk space before cleanup:',('{0:N2}' -f ($Before.SizeRemaining/1GB)).PadLeft(9),'GB' Green,Cyan,Green $LogFile Write-Log ' Free disk space after cleanup: ',('{0:N2}' -f ($After.SizeRemaining/1GB)).PadLeft(9),'GB' Green,Cyan,Green $LogFile Write-Log ' Amount of freed disk space: ',('{0:N2}' -f (($After.SizeRemaining-$Before.SizeRemaining)/1GB)).PadLeft(9),'GB' Green,Cyan,Green $LogFile if (-not ($Result -match 'The operation completed successfully.')) { Write-Log 'DISM issue(s) encountered:' Magenta $LogFile Write-Log ($Result | Out-String).Trim() Yellow $LogFile } } 3 { Write-Log 'Invoking','Dism.exe /online /Cleanup-Image /StartComponentCleanup /ResetBase' Green,Cyan $LogFile Write-Log " To delete older unused components and safely cleanup $env:windir\WinSXS",'No 30 day grace period,' Green,Cyan $LogFile Write-Log ' AND remove all superseded versions of every component in the component store','All existing service packs and updates cannot be uninstalled' Green,Cyan $LogFile -NoNew $Before = Get-Volume -DriveLetter $WinDrive $Duration = measure-Command { $Result = Dism.exe /online /Cleanup-Image /StartComponentCleanup /ResetBase } $After = Get-Volume -DriveLetter $WinDrive Write-Log 'done in',"$($Duration.Hours):$($Duration.Minutes):$($Duration.Seconds)",'(hh:mm:ss)' Green,Cyan,Green $LogFile Write-Log ' Free disk space before cleanup:',('{0:N2}' -f ($Before.SizeRemaining/1GB)).PadLeft(9),'GB' Green,Cyan,Green $LogFile Write-Log ' Free disk space after cleanup: ',('{0:N2}' -f ($After.SizeRemaining/1GB)).PadLeft(9),'GB' Green,Cyan,Green $LogFile Write-Log ' Amount of freed disk space: ',('{0:N2}' -f (($After.SizeRemaining-$Before.SizeRemaining)/1GB)).PadLeft(9),'GB' Green,Cyan,Green $LogFile if (-not ($Result -match 'The operation completed successfully.')) { Write-Log 'DISM issue(s) encountered:' Magenta $LogFile Write-Log ($Result | Out-String).Trim() Yellow $LogFile } Write-Log 'Invoking','Dism.exe /online /Cleanup-Image /SPSuperseded' Green,Cyan $LogFile Write-Log ' To remove any backup components needed for uninstallation of the service pack.','The service pack cannot be uninstalled after this command is completed.' Green,Cyan $LogFile -NoNew $Before = Get-Volume -DriveLetter $WinDrive $Duration = measure-Command { $Result = Dism.exe /online /Cleanup-Image /SPSuperseded } $After = Get-Volume -DriveLetter $WinDrive Write-Log 'done in',"$($Duration.Hours):$($Duration.Minutes):$($Duration.Seconds)",'(hh:mm:ss)' Green,Cyan,Green $LogFile Write-Log ' Free disk space before cleanup:',('{0:N2}' -f ($Before.SizeRemaining/1GB)).PadLeft(9),'GB' Green,Cyan,Green $LogFile Write-Log ' Free disk space after cleanup: ',('{0:N2}' -f ($After.SizeRemaining/1GB)).PadLeft(9),'GB' Green,Cyan,Green $LogFile Write-Log ' Amount of freed disk space: ',('{0:N2}' -f (($After.SizeRemaining-$Before.SizeRemaining)/1GB)).PadLeft(9),'GB' Green,Cyan,Green $LogFile if (-not ($Result -match 'The operation completed successfully.')) { Write-Log 'DISM issue(s) encountered:' Magenta $LogFile Write-Log ($Result | Out-String).Trim() Yellow $LogFile } } default { Write-Log 'Cleanup-WindowsFolder Error:','Unrecognized Level',$Level Magenta,Yellow,Cyan $LogFile } } } End { } } #endregion #region Networking function Validate-NameResolution { <# .SYNOPSIS Function to validate that a given computer name resolves to the same IP address by all domain controllers .DESCRIPTION Function to validate that a given computer name resolves to the same IP address by all domain controllers .PARAMETER ComputerName One or more computer names .EXAMPLE Validate-NameResolution -ComputerName 'myTestPC' .EXAMPLE $DNSValidationResult = Validate-NameResolution @('comp1','comp2','comp3') .OUTPUTS This cmdlet returns PSCustom Objects, one for each resolved IP address with the following properties/example: ComputerName ResolvesTo DNSServer ------------ ---------- --------- devtestaaav47 10.70.122.134 {DEVaaaDCRWV01.dev.tst.local, DEVaaaDCRWV02.dev.tst.local, tstCJRDCRWV01.tst.local, tstJUNDCRWV01.tst.local...} devtestaaav47 10.19.133.168 {DEVCJRDCRWV01.dev.tst.local, tstaaaDCRWV03.tst.local} .LINK https://superwidgets.wordpress.com/category/powershell/ .NOTES Function by Sam Boutros v0.1 - 20 July 2018 #> [CmdletBinding(ConfirmImpact='Low')] Param([Parameter(Mandatory=$true,ValueFromPipeLineByPropertyName=$true)][String[]]$ComputerName) Begin { $DCList = Get-DCList } Process { $myOutput = foreach ($Computer in $ComputerName) { $NameResolutionList = foreach ($DC in ($DCList | where { $_.Forest })) { Resolve-DnsName -Name $Computer -Server $DC.Name | select @{n='ComputerName';e={$_.Name}},Type,TTL,IPAddress,@{n='DNSServer';e={$DC.Name}} | sort IPAddress } if (($Groups = $NameResolutionList | group IPAddress).Count.Count -gt 1) { # Yes .Count twice, not a typo :) Write-Log 'Identified name resolution inconsistency:',$Computer,'resolves to',(($NameResolutionList.IPAddress | select -Unique) -join ', ') Magenta,Yellow,Magenta,Yellow } else { Write-Log 'All DNS servers resolved',$Computer,'to the same IP address',($NameResolutionList.IPAddress | select -Unique) Green,Cyan,Green,Cyan } $Groups | foreach { [PSCustomObject][Ordered]@{ ComputerName = $Computer ResolvesTo = $_.Name DNSServer = $_.Group.DNSServer } } } } End { $myOutput } } function Test-SBNetConnection { <# .SYNOPSIS Function to test open TCP ports .DESCRIPTION Function to test open TCP ports Compared to the Test-NetConnection native function of the NetTCPIP module, this command is much faster particularly when it comes across closed ports. In addition, the timeout value is adjustable by using the TimeoutSec parameter. .PARAMETER ComputerName This parameter accepts a computer name or IPv4 Address. If a computer name is provided, the function attempts to resolve it to an IP address .PARAMETER PortNumber This is one or more TCP port number(s) with valid values from 1 to 65535 It defaults to 111,135,22,3389,25,80,5985,5986 Ports 111,135 help identify the system as a Linux or Windows system respectively Ports 22,3389 are Linux/SSH and Windows/RDP ports Ports 25,80 are SMTP and HTTP ports Ports 5895,5986 are PowerShell/WinRM ports over HTTP and HTTPS respectively .PARAMETER TimeoutSec Time out in seconds This defaults to 1, and accepts valid values from 1 to 300 seconds. .OUTPUTS The script outputs a PS array of objects, one for each open port including the following properties/example: ComputerName RemotePort TcpTestSucceeded ------------ ---------- ---------------- 10.127.73.195 53 True 10.127.73.195 135 True 10.127.73.195 389 True 10.127.73.195 443 False 10.127.73.195 5723 False 10.127.73.195 5985 True 10.127.73.195 5986 True .EXAMPLE Test-SBNetConnection -ComputerName 10.127.73.195 .EXAMPLE $Cred = Get-SBCredential 'domain\admin' $Session = New-PSSession -ComputerName 'Remote1' -Credential $Cred Export-SessionCommand -Command Test-SBNetConnection -Session $Session $IP = (Resolve-DnsName -Name 'Remote2' -Type A).IPAddress Invoke-Command -Session $Session -ScriptBlock { Test-SBNetConnection -ComputerName $Using:IP -Port 1234 } This example illustrates using functions of the AZSBTools PS module to test TCP port connectivity from 'Remote1' computer to 'Remote2' computer over TCP port 1234, where 'Remote1' has PS version 2 and does not have the cmdlets Test-NetConnection or Resolve-DNSName, or the underlying .NET libraries. .LINK https://superwidgets.wordpress.com/category/powershell/ .NOTES Function by Sam Boutros v0.1 - 18 October 2017 v0.2 - 5 January 2018 - Fixed bug to account for computers that resolve to more than 1 IP v0.3 - 20 December 2019 - Added code to exclude IPv6 addresses v0.4 - 10 September 2021 - Made this function work with PS version 2 #> [CmdletBinding(ConfirmImpact='Low')] Param( [Parameter(Mandatory=$true)][String]$ComputerName, [Parameter(Mandatory=$false)][uInt16[]]$PortNumber = @(111,135,22,3389,25,80,5985,5986), [Parameter(Mandatory=$false)][ValidateRange(1,300)][Int16]$TimeoutSec = 1 ) Begin { } Process{ if (-not ($IPv4Address = $Computername -as 'IPAddress')) { try { [IPAddress[]]$IPv4Address = (Resolve-DnsName -Name $ComputerName -Type A -EA 1).IPAddress } catch { Write-Warning "Unable to resolve computer name '$ComputerName'" Write-Warning $_.Exception.Message } } if ($IPv4Address) { foreach ($IP in $IPv4Address.IPAddressToString) { foreach ($Item in $PortNumber) { $TCP = New-Object System.Net.Sockets.TcpClient $AsyncResult = $TCP.BeginConnect("$IP","$Item",$null,$null) $PortOpen = $false if ($AsyncResult.AsyncWaitHandle.WaitOne($TimeoutSec*1000,$false)) { try { $TCP.EndConnect($AsyncResult) $PortOpen = $true } catch { Write-Warning $_.Exception.InnerException } } else { Write-Warning "TCP connect to $($IP):$Item timed out ($TimeoutSec sec)" } # if $AsyncResult $TCP.Close() New-Object -TypeName PSObject -Property @{ ComputerName = $IP RemotePort = $Item TcpTestSucceeded = $PortOpen } } # foreach port } # foreach IP } # if $IPv4Address } # Process End { } } function Convert-IpAddressToMaskLength { <# .SYNOPSIS Function to return the length of an IPv4 subnet mask .DESCRIPTION Function to return the length of an IPv4 subnet mask For example, 255.255.255.0 will return 24 .PARAMETER DottedDecimalIP Dotted IPv4 address (subnet mask) such as 255.255.224.0 .EXAMPLE Convert-IpAddressToMaskLength -DottedDecimalIP 255.255.255.0 This will return 24 .EXAMPLE Convert-IpAddressToMaskLength 255.0.0.0,255.192.0.0,255.255.255.224 This will return 8, 10, and 27 .LINK https://superwidgets.wordpress.com/category/powershell/ .NOTES Function by Sam Boutros v0.1 - 4 October 2018 #> [CmdletBinding(ConfirmImpact='Low')] Param( [Parameter(Mandatory=$true, ValueFromPipeLine=$true, ValueFromPipeLineByPropertyName=$true, Position=0)] [String[]]$DottedDecimalIP ) Begin { } Process{ foreach ($Address in $DottedDecimalIP) { $Result = 0 [IPAddress]$IPv4 = $Address foreach ($Octet in ($IPv4.IPAddressToString.Split('.'))) { while ($Octet -ne 0) { $Octet = ($Octet -shl 1) -band [byte]::MaxValue $Result ++ } # while } # foreach $Result } # foreach } # Process End { } } function Convert-MaskLengthToIpAddress { <# .SYNOPSIS Function to return the IPv4 subnet mask provided a mask length .DESCRIPTION Function to return the IPv4 subnet mask provided a mask length For example, 10 will return 255.192.0.0 .PARAMETER MaskLength IPv4 subnet mask length. Valid values are 1 to 32 .EXAMPLE Convert-MaskLengthToIpAddress -MaskLength 12 This will return 255.240.0.0 .EXAMPLE 8,10,20,27 | Convert-MaskLengthToIpAddress This will return 255.0.0.0 255.192.0.0 255.255.240.0 255.255.255.224 .LINK https://superwidgets.wordpress.com/category/powershell/ .NOTES Function by Sam Boutros v0.1 - 4 October 2018 #> [CmdletBinding(ConfirmImpact='Low')] Param( [Parameter(Mandatory=$true, ValueFromPipeLine=$true, ValueFromPipeLineByPropertyName=$true, Position=0)] [ValidateRange(1,32)] [UInt32[]]$MaskLength ) Begin { } Process{ foreach ($Item in $MaskLength) { if ($Item -lt 9) { "$((1..$Item | % { [math]::Pow(2,8-$_) } | measure -Sum).Sum).0.0.0" } elseif ($Item -lt 17) { "255.$((1..$($Item-8) | % { [math]::Pow(2,8-$_) } | measure -Sum).Sum).0.0" } elseif ($Item -lt 25) { "255.255.$((1..$($Item-16) | % { [math]::Pow(2,8-$_) } | measure -Sum).Sum).0" } else { "255.255.255.$((1..$($Item-24) | % { [math]::Pow(2,8-$_) } | measure -Sum).Sum)" } } # foreach } # Process End { } } function Get-IPv4Details { <# .SYNOPSIS Function to return the details of a given IPv4 address .DESCRIPTION Function to return the details of a given IPv4 address .PARAMETER CIDRAddress IPv4 address in CIDR notation such as 11.12.13.64/27 Part of the 'CIDR' Parameter Set. When provided, IPAddress and SubnetMask are not required .PARAMETER IPAddress Dotted decimal IPv4 address such as 11.12.13.14 Part of the 'Mask' Parameter Set. .PARAMETER SubnetMask Dotted decimal IPv4 subnet mask such as 255.255.0.0 Part of the 'Mask' Parameter Set. .OUTPUTS This function returns a PS object with the following properties (and example): IPDottedDecimal : 10.120.30.11 IPDecimal : 186546186 IPBitLength : 12 IPDottedBinary : 00001010.01111000.00011110.00001011 MaskDottedDecimal : 255.255.240.0 MaskDecimal : 15794175 MaskBitLength : 20 MaskDottedBinary : 11111111.11111111.11110000.00000000 NetDottedDecimal : 10.120.16.0 NetDecimal : 1079306 NetCIDR : 255.255.240.0/20 NetDottedBinary : 00001010.01111000.00010000.00000000 HostDottedDecimal : 0.0.14.11 HostDecimal : 185466880 HostDottedBinary : 00000000.00000000.00001110.00001011 FirstSubnetIP : 10.120.16.1 LastSubnetIP : 10.120.31.254 SubnetMaximumHosts : 4094 .EXAMPLE Get-IPv4Details -IPAddress 10.120.30.11 -SubnetMask 255.255.240.0 .EXAMPLE Get-IPv4Details -CIDRAddress 10.120.30.64/27 .LINK https://superwidgets.wordpress.com/category/powershell/ .NOTES Function by Sam Boutros v0.1 - 4 October 2018 v0.2 - 1 July 2019 - updates to properly address /32 mask v0.3 - 12 February 2020 - Added Parameter Set to accept IP input in CIDR format Known issue: Extreme cases are not detailed properly such as /31 and /32 mask v0.4 - 18 April 2020 - Updated to not Terminate upon input error, so it can be used to detect valid input CIDR format #> [CmdletBinding(ConfirmImpact='Low')] Param( [Parameter(Mandatory=$true,ParameterSetName='CIDR')][String]$CIDRAddress, [Parameter(Mandatory=$true,ParameterSetName='Mask')][Alias('IPv4','IP')][IPAddress]$IPAddress, [Parameter(Mandatory=$true,ParameterSetName='Mask')][Alias('Mask','NetMAsk')][IPAddress]$SubnetMask ) Begin { # Extract $IPAddress, $MaskLength, and $SubnetMask from $CIDRAddress if provided $Go = $true if ($CIDRAddress) { if ($CIDRAddress -match '/') { if ($CIDRAddress.Split('/').Count -eq 2) { $MaskLength = $CIDRAddress.Split('/')[1] -as [Int] if ($MaskLength -gt 32 -or $MaskLength -lt 0) { Write-Verbose "Get-IPv4Details Error: CIDRAddress '$CIDRAddress' must have a mask length between 0 and 32" $Go = $false } [IPAddress]$SubnetMask = Convert-MaskLengthToIpAddress -MaskLength $MaskLength if (-not ($IPAddress = $CIDRAddress.Split('/')[0] -as [IPAddress])) { Write-Verbose "Get-IPv4Details Error: CIDRAddress '$CIDRAddressv' must be in the format DottedDecimalIPv4Address/MaskLength as in 10.1.2.0/24" $Go = $false } else { [IPAddress]$IPAddress = $CIDRAddress.Split('/')[0] -as [IPAddress] } } else { Write-Verbose "Get-IPv4Details Error: CIDRAddress '$CIDRAddressv' must be in the format DottedDecimalIPv4Address/MaskLength as in 10.1.2.0/24" $Go = $false } } else { Write-Verbose "Get-IPv4Details Error: CIDRAddress '$CIDRAddressv' must be in the format DottedDecimalIPv4Address/MaskLength as in 10.1.2.0/24" $Go = $false } } } Process{ if ($Go) { if (-not ($MaskLength)) { $MaskLength = 0 foreach($Octet in ($SubnetMask.GetAddressBytes())) { while ($Octet -ne 0) { $Octet = $Octet*2 -band 255; $MaskLength ++ } } } $IPLength = 32 - $MaskLength $IPBinary = $IPAddress.GetAddressBytes() | % { [Convert]::ToString($_,2).PadLeft(8,'0') } $MaskBinary = $SubnetMask.GetAddressBytes() | % { [Convert]::ToString($_,2).PadLeft(8,'0') } $NetAddress = [IPAddress]($IPAddress.Address -band $SubnetMask.Address) $NetBinary = $NetAddress.GetAddressBytes() | % { [Convert]::ToString($_,2).PadLeft(8,'0') } $Temp = foreach ($Octet in $MaskBinary) { 0..7 | % { if ($Octet[$_] -eq '1') { '0' } else { '1' } } } $MaskMirrorBinary = @(); 0,8,16,24 | % {$MaskMirrorBinary += ($Temp -join '').Substring($_,8) } [IPAddress]$MaskMirror = ($MaskMirrorBinary | % { [Convert]::ToInt32($_,2) }) -join '.' $HostAddress = [IPAddress]($IPAddress.Address -band $MaskMirror.Address) $HostBinary = $HostAddress.GetAddressBytes() | % { [Convert]::ToString($_,2).PadLeft(8,'0') } $FirstSubnetIP = Next-IP -IPAddress $NetAddress.IPAddressToString if (([Math]::Pow(2,$IPLength) - 2) -lt 0) { $LastSubnetIP = $FirstSubnetIP } else { $LastSubnetIP = Next-IP -IPAddress $NetAddress.IPAddressToString -Increment ([Math]::Pow(2,$IPLength) - 2) } [PSCustomObject]@{ IPDottedDecimal = $IPAddress.IPAddressToString IPDecimal = $IPAddress.Address IPBitLength = $IPLength IPDottedBinary = $IPBinary -join '.' MaskDottedDecimal = $SubnetMask.IPAddressToString MaskDecimal = $SubnetMask.Address MaskBitLength = $MaskLength MaskDottedBinary = $MaskBinary -join '.' NetDottedDecimal = $NetAddress.IPAddressToString NetDecimal = $NetAddress.Address NetCIDR = "$($NetAddress.IPAddressToString)/$MaskLength" NetDottedBinary = $NetBinary -join '.' HostDottedDecimal = $HostAddress.IPAddressToString HostDecimal = $HostAddress.Address HostDottedBinary = $HostBinary -join '.' FirstSubnetIP = $FirstSubnetIP LastSubnetIP = $LastSubnetIP SubnetMaximumHosts = if (([Math]::Pow(2,$IPLength) - 2) -lt 0) { 0 } else { ([Math]::Pow(2,$IPLength) - 2) } } } } End { } } function Next-IP { <# .SYNOPSIS Function to return an IP address relative to the input IP address .DESCRIPTION Function to return an IP address relative to the input IP address .PARAMETER IPAddress Dotted IPv4 address such as 10.12.13.15 .PARAMETER Increment A whole number between -4294967294 and 4294967295 For example when using 1, the function will return the next IP address This defaults to 1 .EXAMPLE Next-IP -IPAddress 10.10.10.11 -Increment 1 Will return 10.10.10.12 .EXAMPLE Next-IP -IPAddress 201.120.252.253 -Verbose Will return 201.120.252.254 .EXAMPLE Next-IP -IPAddress 201.120.252.253 -Increment 100 -Verbose Will return 201.120.253.97 .EXAMPLE Next-IP -IPAddress 201.120.252.253 -Increment -500 -Verbose Will return 201.120.251.9 .LINK https://superwidgets.wordpress.com/category/powershell/ .NOTES Function by Sam Boutros v0.1 - 4 October 2018 #> [CmdletBinding(ConfirmImpact='Low')] Param( [Parameter(Mandatory=$true)][Alias('IPv4','IP')][IPAddress]$IPAddress, [Parameter(Mandatory=$false)][ValidateRange(-4294967294,4294967295)][Int64]$Increment = 1 ) Begin { } Process{ $DecimalArray = $IPAddress.GetAddressBytes() [Array]::Reverse($DecimalArray) $Decimal = ([IPAddress]($DecimalArray -join '.')).Address $Decimal = $Decimal + $Increment if ($Decimal -le 4294967295 -and $Decimal -ge -4294967294) { $DecimalArray = ([IPAddress]$Decimal).GetAddressBytes() [Array]::Reverse($DecimalArray) $DecimalArray -join '.' } else { Write-Verbose "Cannot increment/decrement the provided IP addresses '$($IPAddress.IPAddressToString)' by '$Increment'" Write-Verbose "The resulting address '$Decimal' would exceed a 32-bit address (-4294967294 to 4294967295)" } } End { } } function Test-SameSubnet { <# .SYNOPSIS Function to compare a pair of IPv4 addresess and their subnet masks and identify if they're on the same subnet or not .DESCRIPTION Function to compare a pair of IPv4 addresess and their subnet masks If the 2 IPs are on the same subnet, the function retirns the subnet ID in CIDR format, otherwise it returns False .PARAMETER IP1 Dotted decimal IPv4 address such as 11.12.13.14 .PARAMETER Mask1 Dotted decimal IPv4 subnet mask such as 255.255.0.0 .PARAMETER IP2 Dotted decimal IPv4 address such as 11.12.13.15 .PARAMETER Mask2 Dotted decimal IPv4 subnet mask such as 255.255.240.0 .EXAMPLE Test-SameSubnet -IP1 10.124.170.1 -Mask1 255.255.252.0 -IP2 10.124.170.2 -Mask2 255.255.252.0 This will return 10.124.168.0/22 .EXAMPLE Test-SameSubnet -IP1 10.124.170.117 -Mask1 255.255.255.240 -IP2 10.124.170.2 -Mask2 255.255.255.240 This will return False .LINK https://superwidgets.wordpress.com/category/powershell/ .NOTES Function by Sam Boutros v0.1 - 4 October 2018 #> [CmdletBinding(ConfirmImpact='Low')] Param( [Parameter(Mandatory=$true)][IPAddress]$IP1, [Parameter(Mandatory=$true)][IPAddress]$Mask1, [Parameter(Mandatory=$true)][IPAddress]$IP2, [Parameter(Mandatory=$true)][IPAddress]$Mask2 ) Begin { } Process{ $Network1 = (Get-IPv4Details -IPAddress $IP1 -SubnetMask $Mask1).NetDecimal $Network2 = (Get-IPv4Details -IPAddress $IP2 -SubnetMask $Mask2).NetDecimal if ($Network1 -eq $Network2) { [IPAddress]$IP = 0 $IP.Address = $Network1 "$($IP.IPAddressToString)/$(Convert-IpAddressToMaskLength -DottedDecimalIP $Mask1)" } else { $false } } End { } } function Get-IPv4Summary { <# .SYNOPSIS Function to return IPv4 information of enabled network adapters .DESCRIPTION Function to return IPv4 information of enabled network adapters This function requires the Convert-IpAddressToMaskLength function available in the SB-Tools modules in the PowerShell Gallery .PARAMETER ServiceName This is set to 'netvsc' by default To see available Service Names use: Get-WmiObject -Class Win32_NetworkAdapterConfiguration | FT Description,Index,IPAddress,ServiceName,DefaultIPGateway -a .EXAMPLE Get-IPv4Summary -Verbose .EXAMPLE Get-IPv4Summary -ServiceName 'vmsmp' -Verbose .OUTPUTS This function/cmdlet returns a PS object for each netvsc NIC with the following properties/example: IPv4Address : 192.168.124.44 IPv4Subnet : 255.255.255.0 MaskLength : 24 DefaultGateway : 192.168.124.1 DNSServers : {8.8.8.8,4.4.4.4} Description : Ethernet Network Adapter DHCPEnabled : False .LINK https://superwidgets.wordpress.com/category/powershell/ .NOTES Function by Sam Boutros v0.1 - 4 October 2018 #> [CmdletBinding(ConfirmImpact='Low')] Param([Parameter(Mandatory=$false)][String]$ServiceName = 'netvsc') # 'vmsmp' Begin { } Process{ $AdapterList = Get-WmiObject -Class Win32_NetworkAdapterConfiguration -Filter "servicename = ""$ServiceName""" | ? { $_.IPAddress } if ($AdapterList) { $myOutput = foreach ($NIC in $AdapterList) { Write-Verbose "Get-IPv4Summary: Processing adapter '$($NIC.Description)'" if (($NIC.IPSubnet -match '\.').Count -eq 1) { $IPv4Subnet = $NIC.IPSubnet -match '\.' | select -First 1 $MaskLength = Convert-IpAddressToMaskLength $IPv4Subnet } else { $IPv4Subnet = $NIC.IPSubnet -match '\.' $MaskLength = $IPv4Subnet | % {Convert-IpAddressToMaskLength $_} } if (($NIC.IPAddress -match '\.').Count -eq 1) { $IPv4Address = $NIC.IPAddress -match '\.' | select -First 1 } else { $IPv4Address = $NIC.IPAddress -match '\.' } if (($NIC.DefaultIPGateway -match '\.').Count -eq 1) { $DefaultGateway = $NIC.DefaultIPGateway -match '\.' | select -First 1 } else { $DefaultGateway = $NIC.DefaultIPGateway -match '\.' } if (($NIC.DNSServerSearchOrder -match '\.').Count -eq 1) { $DNSServers = $NIC.DNSServerSearchOrder -match '\.' | select -First 1 } else { $DNSServers = $NIC.DNSServerSearchOrder -match '\.' } [PSCustomObject]@{ IPv4Address = $IPv4Address IPv4Subnet = $IPv4Subnet MaskLength = $MaskLength DefaultGateway = $DefaultGateway DNSServers = $DNSServers Description = $NIC.Description DHCPEnabled = $NIC.DHCPEnabled } # PSCustomObject } # foreach $NIC $myOutput } else { Write-Verbose "Bad ServiceName '$ServiceName' provided, available Service Names are: $( Get-WmiObject -Class Win32_NetworkAdapterConfiguration | FT description,Index,IPAddress,ServiceName,DefaultIPGateway -a | Out-String)" } } End { } } function Get-FTPFileList { <# .SYNOPSIS Function to get file list from FTP site .DESCRIPTION Function to get file list from FTP site .PARAMETER FTPURL For example: ftp://site.domain.com This is the URL to the FTP site .PARAMETER Port Optional parameter that defaults to port 21 .PARAMETER Cred PSCredential object obtained via Get-Credential or Get-SBCredential It is used to authenticate to the FTP site. For anonymous FTP create a credential that has the name 'anonymous' and any password .PARAMETER Recurse Optional switch parameter. When set to True, the function will return all files and subfolders .EXAMPLE Get-FTPFileList -FTPURL ftp://123.45.56.78 -Cred (Get-SBCredential 'samb@mysite.ftpdomain.com') | FT -a This example list the files listed from the given FTP site .EXAMPLE $myFileList = Get-FTPFileList -FTPURL ftp://mysite.ftpsite.com -Cred (Get-SBCredential 'samb@mysite.ftpdomain.com') -Recurse $FileOnlyList = $myFileList | where Type -EQ 'File' Write-log 'File and directory listing contains', $myFileList.Count, 'items' Green,Cyan,Green Write-log ' including', ($myFileList.Count-$FileOnlyList.Count), 'directories' Green,Cyan,Green Write-log ' and', $FileOnlyList.Count, 'files' Green,Cyan,Green Write-log 'Calculating total size...' Green -noNew $SizeBytes = ($myFileList | measure SizeBytes -Sum).Sum Write-log $SizeBytes, 'bytes', "($([Math]::Round($SizeBytes/1GB,2)) GB)" Cyan,Green,Cyan .OUTPUTS The function returns an object for each file/directory found with the following properties/example: Type Name Path SizeBytes Date Permission ---- ---- ---- --------- ---- ---------- File 8xxxx5 ftp://mysite.ftpsite.com//8xxxx5/8xxxx5 47 12/27/2014 12:00:00 AM -r--r--r-- File 8xxxx5.zip ftp://mysite.ftpsite.com//8xxxx5/8xxxx5.zip 61728 12/27/2014 12:00:00 AM -r--r--r-- Directory June Amazon ftp://mysite.ftpsite.com//Amazon/June Amazon 0 6/9/2015 12:00:00 AM drwxr-xr-x File MANIFEST.txt ftp://mysite.ftpsite.com//Amazon/MANIFEST.txt 636 3/18/2015 12:00:00 AM -r--r--r-- .LINK https://superwidgets.wordpress.com/category/powershell/ .NOTES Function by Sam Boutros 17 October 2018 - v0.1 #> [CmdletBinding(ConfirmImpact='Low')] Param( [Parameter(Mandatory=$true,HelpMessage='Such as ftp://site.domain.com')][String]$FTPURL, [Parameter(Mandatory=$false)][Int]$Port = 21, [Parameter(Mandatory=$true)][PSCredential]$Cred, [Parameter(Mandatory=$false)][Switch]$Recurse = $false ) Begin { # Compile URL from FTPURL and Port if (($FTPURL -as [system.uri]).AbsoluteUri) { $Temp = $FTPURL -as [system.uri] [system.uri]$FTPURL = "ftp://$($Temp.Host):$Port$($Temp.LocalPath)" Write-Log 'Get-FTPFileList: Processing URL:',$FTPURL.AbsoluteUri Green,Cyan } else { Write-Log 'Get-FTPFileList: Error: bad FTPURL received:',$FTPURL,"expecting FTP URL such as ftp://site.domain.com" Magenta,Yellow,Magenta break } } Process { try { $FTPRequest = [System.Net.FtpWebRequest]::Create($FTPURL) $FTPRequest.Credentials = $Cred $FTPRequest.Method = [System.Net.WebRequestMethods+Ftp]::ListDirectoryDetails $FTPResponse = $FTPRequest.GetResponse() $ResponseStream = $FTPResponse.GetResponseStream() $StreamReader = New-Object System.IO.StreamReader $ResponseStream $FileList = New-Object System.Collections.ArrayList While ($File = $StreamReader.ReadLine()) { [void]$FileList.add($File) } } catch { Write-Log $_.Exception.InnerException.Message Yellow break } $StreamReader.close() $ResponseStream.close() $FTPResponse.Close() $myOutput = foreach ($FileLine in $FileList) { $Name = $FileLine.Substring(49,$FileLine.Length-49) [PSCustomObject][Ordered]@{ Type = $(if ($FileLine.Substring(0,1) -eq 'd') { 'Directory' } else { 'File' }) Name = $Name Path = "$($FTPURL.AbsoluteUri)/$Name" SizeBytes = $FileLine.Substring(20,15).Trim() -as [Int64] Date = $FileLine.Substring(35,13) -as [DateTime] Permission = $FileLine.Substring(0,10) -as [String] } } if ($Recurse) { foreach ($Directory in ($myOutput | where Type -EQ 'Directory')) { Get-FTPFileList -FTPURL $Directory.Path -Cred $Cred } } } End { $myOutput } } function Listen-Port { <# .SYNOPSIS Function to listen on a given TCP port .DESCRIPTION Function to listen on a given TCP port This is typically useful for testing firewall rules This port listener will auto-shutdown in 1 minute after it's invoked. This duration can be increased via a parameter up to 1440 minutes (1 day) .PARAMETER TCPPort TCP port number - required .PARAMETER IPAddress Optional parameter for the computer IPv4 address .PARAMETER AddFirewallRule Optional parameter to create a windows firewall rule to allow testing that TCP port listener The script will remove this temporary rule upon its completion .PARAMETER AutoShutdownMinutes Optional paramter that defaults to 1 minute Can be as high as 1440 minutes (1 day) .EXAMPLE Listen-Port 12345 .LINK https://superwidgets.wordpress.com/category/powershell/ .NOTES Function by Sam Boutros v0.1 - 19 June 2019 #> [CmdletBinding(ConfirmImpact='Low')] Param( [Parameter(Mandatory=$true)][ValidateRange(0,65535)][Int32]$TCPPort, [Parameter(Mandatory=$false)][String]$IPAddress = 'any', [Parameter(Mandatory=$false)][ValidateRange(1,1440)][Int16]$AutoShutdownMinutes = 1, [Parameter(Mandatory=$false)][Switch]$AddFirewallRule =$true ) Begin { if ($AddFirewallRule) { Write-Log 'Adding',"Listen-Port-$TCPPort",'firewall rule' Green,Cyan,Green -NoNewLine try { $ParameterSet = @{ DisplayName = "Listen-Port-$TCPPort" Direction = 'inbound' LocalPort = $TCPPort Protocol = 'TCP' Action = 'Allow' Enabled = 'True' Profile = 'Any' ErrorAction = 'Stop' } $Rule = New-NetFirewallRule @ParameterSet Write-Log 'done' DarkYellow } catch { Write-Log 'failed' Magenta Write-Log $_.Exception.Message Yellow } } $PingingJob = Start-Job -ScriptBlock { 0..($Using:AutoShutdownMinutes*6+4) | foreach { Test-SBNetConnection -ComputerName $env:COMPUTERNAME -Port $Using:TCPPort -EA 0 -WA 0 Start-Sleep -Seconds 10 } } } Process{ $IPEndPoint = New-Object System.Net.IPEndPoint ([IPAddress]::$IPAddress, $TCPPort) $TcpListener = New-Object System.Net.Sockets.TcpListener $IPEndPoint $TcpListener.Start() $StartTime = Get-Date $Running = $true try { While ($Running) { if (-not $TcpListener.Pending()) { Start-Sleep -Seconds 1 } $TCPClient = $TcpListener.AcceptTcpClient() $TimeRemaining = New-TimeSpan -Start (Get-Date) -End $StartTime.AddMinutes($AutoShutdownMinutes) if ($TimeRemaining -le 0) { $Running = $false Write-Log 'Auto-shutdown duration exceeded, shutting down..' Green } else { Write-Log 'Listening on port',"$TCPPort,",'auto-shutdown in',"$($TimeRemaining.Hours):$($TimeRemaining.Minutes):$($TimeRemaining.Seconds)",'''hh:mm:ss''' Green,Cyan,Green,Yellow,Green } $TCPClient.Close() } } catch { Write-Log $_.Exception.Message Yellow } finally { $TcpListener.Stop() } } End { if ($AddFirewallRule) { Remove-NetFirewallRule -DisplayName "Listen-Port-$TCPPort" -EA 0 } $PingingJob | Remove-Job -Force } } function Get-MyWANIP { <# .SYNOPSIS Function to return current WAN IP address .DESCRIPTION Function to return current WAN IP address .PARAMETER Source One or more URLs This is an optional parameter. These URLs will be queried for WAN IP. .EXAMPLE Get-MyWANIP .OUTPUTS This cmdlet returns a System.Net.IPAddress object such as: Address : 1132553623 AddressFamily : InterNetwork ScopeId : IsIPv6Multicast : False IsIPv6LinkLocal : False IsIPv6SiteLocal : False IsIPv6Teredo : False IsIPv4MappedToIPv6 : False IPAddressToString : 151.101.129.67 .LINK https://superwidgets.wordpress.com/category/powershell/ .NOTES Function by Sam Boutros v0.1 - 20 December 2019 v0.2 - 12 April 2020 - Added -UseBasicParsing Switch to Invoke-WebRequest Cmdlet call #> [CmdletBinding(ConfirmImpact='Low')] Param( [Parameter(Mandatory=$false)][String[]]$Source = @( 'http://ipinfo.io/ip' 'http://ifconfig.me/ip' 'http://icanhazip.com' 'http://ident.me' 'http://smart-ip.net/myip' ) ) Begin { } Process { Remove-Variable FoundIP -Force -EA 0 foreach ($SourceURL in $Source) { $FoundIP = (Invoke-WebRequest -uri $SourceURL -EA 0 -UseBasicParsing).Content $FoundIP = $FoundIP.Trim() if ($FoundIP -as [IPAddress]) { $FoundIP = [IPaddress]$FoundIP break } } } End { $FoundIP } } function Get-RDPDetails { <# .SYNOPSIS Function to return details on Terminal Services process .DESCRIPTION Function to return details on Terminal Services process including process ID and listening port .EXAMPLE Get-RDPDetails -Verbose .OUTPUTS If there are established RDP sessions this function will return a PS object for each session like: ComputerName : myComputerName ProcessId : 1160 Port : 3389 RemoteAddress : 123.23.34.45 RemotePort : 56916 StartTime : 4/18/2020 6:31:32 AM DurationMinutes : 105 If there is no established RDP sessions this function will return a PS object like: ComputerName : myComputerName ProcessId : 1160 Port : 3389 If Terminal Service is disabled this function will return no output .LINK https://superwidgets.wordpress.com/category/powershell/ .NOTES Function by Sam Boutros v0.1 - 18 April 2020 #> [CmdletBinding(ConfirmImpact='Low')] Param() Begin { } Process { if ($TermId = (Get-SBWMI -Class Win32_TerminalService).ProcessId) { Write-Verbose "Identified 'TerminalService' Process ID '$TermId' on computer '$env:COMPUTERNAME'" Write-Verbose (Get-Process -Id $TermId | FL * | Out-String).Trim() try { $ConnectionList = Get-NetTCPConnection -OwningProcess $TermId -EA 1 if ($Established = $ConnectionList | where State -EQ Established ) { $Established | foreach { [PSCustomObject][Ordered]@{ ComputerName = $env:COMPUTERNAME ProcessId = $TermId Port = $ConnectionList.LocalPort | select -First 1 RemoteAddress = $_.RemoteAddress RemotePort = $_.RemotePort StartTime = $_.CreationTime DurationMinutes = '{0:N0}' -f (New-TimeSpan -Start $_.CreationTime -End (Get-Date)).TotalMinutes } } } else { [PSCustomObject][Ordered]@{ ComputerName = $env:COMPUTERNAME ProcessId = $TermId Port = $ConnectionList.LocalPort | select -First 1 } } } catch { Write-Verbose "TerminalService is disabled (not listening) on computer '$env:COMPUTERNAME'" } } else { Write-Warning 'Win32_TerminalService not found!!??' } } End { } } function Sort-IPList { <# .SYNOPSIS Function to sort a list of IPv4 addresses .DESCRIPTION Function to sort a list of IPv4 addresses .PARAMETER IPAddress Required one or more IPv4 address in dotted decomal format such as 1.2.3.4 .EXAMPLE Sort-IPList @('1.2.3.4','2.3.4.5','10.11.2.13') -Verbose .OUTPUTS Sorted list of IPv4 addresses .LINK https://superwidgets.wordpress.com/category/powershell/ .NOTES Function by Sam Boutros v0.1 - 10 May 2020 #> [CmdletBinding(ConfirmImpact='Low')] Param( [Parameter(Mandatory=$true)][IPAddress[]]$IPAddress ) Begin { Write-Verbose 'Sort-IPList: Input received:' Write-Verbose ($IPAddress -join ', ') } Process { if ($IPAddress) { $SortedList = foreach ($IP in $IPAddress) { [PSCustomObject][Ordered]@{ Address = $IP.Address IPAddressToString = $IP.IPAddressToString IPDottedBinary = (Get-IPv4Details -IPAddress $IP.IPAddressToString -SubnetMask 255.255.255.255).IPDottedBinary } } } else { Write-Log 'Sort-IPList Error: No input provided for parameter (IPAddress)' Yellow } $SortedList = $SortedList | sort IPDottedBinary Write-Verbose ($SortedList | FT -a | Out-String) } End { $SortedList.IPAddressToString } } function New-BlockList { <# .SYNOPSIS Function to return a list of IPv4 address ranges that includes the entire IPv4 address space except the input IPs/IP CIDR ranges. .DESCRIPTION Function to return a list of IPv4 address ranges that includes the entire IPv4 address space except the input IPs/IP CIDR ranges. This function is useful when setting up a Windows Firewall rule that's intended to block all IPs except the list provided in this function's input. .PARAMETER AllowedIP One or more IPv4 addresses or CIDR ranges .EXAMPLE New-BlockList -AllowedIP @( '99.88.77.66' '33.44.55.111' ) Will return: 1.0.0.1-33.44.55.110 33.44.55.112-99.88.77.65 99.88.77.67-255.255.255.255 .EXAMPLE New-BlockList -AllowedIP @( '99.88.77.66' '33.44.55.111' '192.168.11.0/24' '10.0.0.0/12' '66.77.88.48/29' ) Will return: 1.0.0.1-9.255.255.255 10.16.0.0-33.44.55.110 33.44.55.112-66.77.88.47 66.77.88.56-99.88.77.65 99.88.77.67-192.168.10.255 192.168.12.0-255.255.255.255 .EXAMPLE $ParameterSet = @{ RemoteAddress = New-BlockList -AllowedIP @( '99.88.77.66' '33.44.55.111' (Resolve-DnsName -Name mytrustedhost1.mydomain.com).IPAddress '192.168.11.0/24' '10.0.0.0/12' '66.77.88.48/29' ) Direction = 'Inbound' Profile = 'Any' Action = 'Block' Enabled = 'True' Name = 'Allow authorized IPs only' DisplayName = 'Allow authorized IPs only' Description = 'Allow authorized IPs only' } New-NetFirewallRule @ParameterSet This will create a new Windows Firewall rule that blocks all incoming connections except from the provided IP list. .OUTPUTS This cmdlet returns a list of IP address ranges such as: 1.0.0.1-9.255.255.255 10.16.0.0-33.44.55.110 33.44.55.112-66.77.88.47 66.77.88.56-99.88.77.65 99.88.77.67-192.168.10.255 192.168.12.0-255.255.255.255 .LINK https://superwidgets.wordpress.com/category/powershell/ .NOTES Function by Sam Boutros v0.1 - 6 October 2020 #> [CmdletBinding(ConfirmImpact='Low')] Param( [Parameter(Mandatory=$true)][Alias('IPAddress')][String[]]$AllowedIP ) Begin { Write-Verbose "Input IPAddress(s): $($AllowedIP -join ', ')" # Validate IP addresses: $AllowedIP = $AllowedIP | where { $_ } # Remove blanks $IPList = @() foreach ($IP in $AllowedIP) { if ($IP -as [IPAddress]) { $IPList += New-Object -TypeName PsObject -Property @{ IP = $IP Type = 'IP' } } elseif ($CIDR = Get-IPv4Details -CIDRAddress $IP) { # Get CIDR Start and End IPs $IPList += New-Object -TypeName PsObject -Property @{ IP = Next-IP -IPAddress $CIDR.FirstSubnetIP -Increment -1 Type = 'Start' } $IPList += New-Object -TypeName PsObject -Property @{ IP = Next-IP -IPAddress $CIDR.LastSubnetIP -Increment 1 Type = 'End' } } } } Process { try { $SortedIPList = Sort-IPList -IPAddress $IPList.IP | foreach { $IPList | where IP -EQ $_ } $StartIP = '1.0.0.1' $RangeList = @() foreach ($IP in $SortedIPList) { $EndIP = Next-IP -IPAddress $IP.IP -Increment -1 if (-not ($StartIP -eq (Next-IP -IPAddress $EndIP -Increment 1)) -and $IP.Type -ne 'End') { $RangeList += "$StartIP-$EndIP" # Range to block } $StartIP = Next-IP -IPAddress $IP.IP -Increment 1 } $EndIP = '255.255.255.255' $RangeList += "$StartIP-$EndIP" } catch { } } End { $RangeList } } #endregion #region Remoting Function Export-SessionCommand { <# .SYNOPSIS Function to export one or more session commands .DESCRIPTION Function to export one or more session commands This function takes one or more Powershell script functions/commands from current session and exports them to a remote PS session This function will ignore and not export binary functions Exported functions will persist on the remote computer for the user profile used with the PS remote session .PARAMETER Command This is one or more script commands available in the current PS session For example Update-SmbMultichannelConnection cmdlet/function of the SmbShare PS module To see available script commands, you can use: Get-Command | ? { $_.CommandType -eq 'function' } .PARAMETER ModuleName This is the name of the module that this function will create on the remote computer under the user profile of the remote PS session This will over-write prior existing module with the same name .PARAMETER Session PSSession object usually obtained by using New-PSSession cmdlet. .EXAMPLE Export-SessionCommand get-saervice,get-sbdisk,bla,get-bitlockerstatus,get-service -Session $Session -Verbose .OUTPUTS The function returns a list of successfully exported commands/functions, or $false if it fails Example: CommandType Name ModuleName ----------- ---- ---------- Function Get-BitLockerStatus SBjr Function Get-SBDisk SBjr .LINK https://superwidgets.wordpress.com/category/powershell/ .NOTES Function by Sam Boutros v0.1 - 12 July 2018 #> [CmdletBinding(ConfirmImpact='Low')] Param( [Parameter(Mandatory=$true)][string[]]$Command, [Parameter(Mandatory=$false)][String]$ModuleName = 'SBjr', [Parameter(Mandatory=$true)][System.Management.Automation.Runspaces.PSSession]$Session ) Begin { if ($Session.State -ne 'Opened') { Write-Log 'Export-SessionCommand: Error: Session State is not ''opened''' Magenta Write-Log ($Session|Out-String).Trim() Yellow break } $FunctionList = foreach ($Name in $Command) { try { Get-Command $Name -EA 1 | Out-Null if ((Get-Command $Name).ScriptBlock) { $Name } else { Write-Warning "Command '$Name' is not a script command, ignoring" } } catch { Write-Warning "Command '$Name' not found, ignoring" } } $FunctionList = $FunctionList | select -Unique Write-Log 'Exporting function(s):',($FunctionList -join ', ') Green,Cyan } Process{ $FirstCommand = $true $FunctionList | % { $myCommand = Get-Command $_ Write-Verbose "Exporting command '$($myCommand.Name)' to module '$ModuleName'" Invoke-Command -Session $Session -ScriptBlock { $ModPath = "$env:USERPROFILE\Documents\WindowsPowerShell\Modules\$Using:ModuleName" $PSM = "$ModPath\$Using:ModuleName.psm1" if ($Using:FirstCommand) { New-Item -Path $ModPath -ItemType Directory -Force | Out-Null "Function $($Using:myCommand.Name) {" | Out-File $PSM } else { "Function $($Using:myCommand.Name) {" | Out-File $PSM -Append } $Using:myCommand.ScriptBlock,'}',' ' | % { $_ | Out-File $PSM -Append } } $FirstCommand = $false } } # Process End { Invoke-Command -Session $Session -ScriptBlock { ' ','Export-ModuleMember -Function *' | % { $_ | Out-File $PSM -Append } Remove-Module $Using:ModuleName -Force -Confirm:$false -EA 0 Import-Module $Using:ModuleName try { Get-Command -Module $Using:ModuleName -EA 1 | FT -a } catch { $false } } } } function Import-SessionCommands { <# .SYNOPSIS Function to import commands from another computer .DESCRIPTION Function will import commands from remote computer from the module(s) listed. .PARAMETER ModuleName Name(s) of the module(s) that we want to import their commands into the current PS console. Note that session commands will not be available in other PS instances. .PARAMETER ComputerName Computer name that has the module(s) that we need to import their commands. .PARAMETER Keep This is a switch. When selected, the function will export the imported module(s) locally under "C:\Program Files\WindowsPowerShell\Modules" if it's in the PSModulePath, otherwise, it will export it to the default path "$home\Documents\WindowsPowerShell\Modules" - Note 1: Exported modules and their commands can be used directly from any PS instance after a module has been exported with the -keep switch - Note 2: Even though a module has been exported locally, everytime you try to use one of its commands, PS will start an implicit remoting session to the server where the module was imported from. .EXAMPLE Import-SessionCommands -ModuleName ActiveDirectory -ComputerName DC01 This example imports all the commands from the ActiveDirectory module from the DC01 server So, in this PS console instance we can use AD commands like Get-ADComputer without the need to install AD features, tools, or PS modules on this computer! .EXAMPLE Import-SessionCommands SQLPS,Storage V-2012R2-SQL1 -Verbose This example imports all the commands from the PSSQL and Storage modules from the MySQLServer server into the current PS instance .EXAMPLE Import-SessionCommands WebAdministration,BestPractices,MMAgent CM01 -keep This example imports all the commands from the WebAdministration, BestPractices, and MMAgent modules from the CM01 server into the current PS instance, and exports them locally. .LINK https://superwidgets.wordpress.com/category/powershell/ .NOTES Function by Sam Boutros Requires PS 3.0 v1.0 - 08/17/2014 Although we need to eventually run: Remove-PSSession -Session $Session We cannot do that in the function as we'll lose the imported session commands Two things to consider: 1. The session will be automatically removed when the PS console is closed 2. If in the parent script that's using this function a blanket Remove-PSSession command is run, like: Get-PSSession | Remove-PSSession We'll lose this session and its commands, which could cripple the parent script #> [CmdletBinding(SupportsShouldProcess=$true,ConfirmImpact='Low')] Param( [Parameter(Mandatory=$true, ValueFromPipeLineByPropertyName=$true, Position=0)] [String[]]$ModuleName, [Parameter(Mandatory=$true, ValueFromPipeLineByPropertyName=$true, Position=1)] [String]$ComputerName, [Parameter(Mandatory=$false, Position=2)] [Switch]$Keep ) # Get a random session name: Do { $SessionName = "Import" + (Get-Random -Minimum 10000000 -Maximum 99999999) } While (Get-PSSession -Name $SessionName -ErrorAction SilentlyContinue) Write-Verbose "New PSSession name: $SessionName" if ($Env:PSModulePath.Split(';') -contains 'C:\Program Files\WindowsPowerShell\Modules') { $ExportTo = 'C:\Program Files\WindowsPowerShell\Modules' } else { $ExportTo = "$home\Documents\WindowsPowerShell\Modules" } try { Write-Log 'Connecting to computer', $ComputerName Green,Cyan $CurrentSessions = Get-PSSession -ErrorAction SilentlyContinue -ComputerName $ComputerName if ($CurrentSessions.ComputerName -Contains $ComputerName) { $Session = $CurrentSessions[0] } else { $Session = New-PSSession -ComputerName $ComputerName -Name $SessionName -ErrorAction Stop } Write-Verbose "Current PSSessions: $(Get-PSSession)" $RemoteModules = Invoke-Command -ScriptBlock { Get-Module -ListAvailable | Select Name } -Session $Session $LocalModules = Get-Module -ListAvailable | Select Name foreach ($Module in $ModuleName) { if ($LocalModules.Name -Contains $Module -or $LocalModules.Name -Contains "Imported-$Module") { Write-Log 'Module', $Module, 'exists locally, not importing..' Yellow,Cyan,Yellow } else { if ($RemoteModules.Name -Contains $Module) { Write-Log 'Found module', $Module, 'on computer', $ComputerName, 'importing its commands..' Green,Cyan,Green,Cyan,Green Invoke-Command -Session $Session -ArgumentList $Module -ScriptBlock { Param($Module) Import-Module $Module } try { $ImportedModule = Import-PSSession -Session $Session -Module $Module -DisableNameChecking -ErrorAction Stop if ($Keep) { Write-Log 'Keeping module', $Module, 'locally..' Green,Cyan,Green Remove-Module -Name $ImportedModule.Name Export-PSSession -Module $Module -OutputModule "$ExportTo\Imported-$Module" -Session $Session -Force Import-Module -Name "Imported-$Module" } } catch { Write-Log 'Module', $Module, 'already imported, skipping..' Yellow,Cyan,Yellow } } else { Write-Log 'Error: module', $Module, 'not found on server', $ComputerName Magenta,Yellow,Magenta,Yellow } } } } catch { Write-Log 'Error: unable to connect to server', $ComputerName Magenta,Yellow Write-Log ' Check if', $ComputerName, 'exists, is online, ' Magenta,Yellow,Magenta Write-Log ' has WinRM enabled and configured, and ' Magenta Write-Log ' you have sufficient permissions to it' Magenta } } function Connect-Computer { <# .SYNOPSIS Function to establish PowerShell Remoting session with a remote computer that's not domain member .DESCRIPTION Function to establish PowerShell Remoting session with a remote computer that's not domain member .PARAMETER ComputerName This can be a NetBios computer name like'mycomputer' or an IPv4 address like '10.20.30.40' If using a computer name, make sure it can be resolved to an IPv4 address .PARAMETER Credential This is a PSCredential Object not text. .EXAMPLE $Session = Connect-Computer -ComputerName '10.171.120.68' -Credential (Get-SBCredential -UserName '.\Administrator') -Verbose This establishes a session with 10.171.120.68 To see built in help for the Get-SB-Credential function use: Get-Help Get-SBCredential -Show The returned PSSession object is stored in the $Session variable in this example, to used for further automation such as: Invoke-command -Session $Session -ScriptBlock { Get-Service } .OUTPUTS This function returns a PSSession object [System.Management.Automation.Runspaces.PSSession] .LINK https://superwidgets.wordpress.com/category/powershell/ .NOTES Function by Sam Boutros v0.1 - 4 October 2018 #> [CmdletBinding(ConfirmImpact='Low')] Param( [Parameter(Mandatory=$true)] [String]$ComputerName, [Parameter(Mandatory=$true)] [System.Management.Automation.PSCredential]$Credential ) Begin { Write-Verbose 'Connect-Computer: Checking Trusted Hosts list' $TrustedHosts = Get-Item wsman:\localhost\Client\TrustedHosts if ($TrustedHosts.Value -match $ComputerName) { Write-Verbose "Connect-Computer: $ComputerName is already in Trusted Hosts" } else { Write-Verbose "Connect-Computer: Adding $ComputerName to Trusted Hosts" try { Set-Item wsman:\localhost\Client\TrustedHosts $ComputerName -Concatenate -Force -ErrorAction Stop Write-Verbose 'done' } catch { throw "Failed to add $ComputerName to Trusted Hosts" } } } Process{ Write-Verbose "Connect-Computer: Establishing PowerShell Remoting session with $ComputerName using Credential $($Credential.UserName)" try { New-PSSession -ComputerName $ComputerName -Credential $Credential -ErrorAction Stop Write-Verbose 'done' } catch { Write-Error "Failed to establish PowerShell Remoting session with $ComputerName" throw $_ } } End { } } #endregion #region PageFile function Get-PageFile { <# .SYNOPSIS List the drives that have page file(s) and their configuration .DESCRIPTION List the drives that have page file(s) and their configuration Note that 0 value for Initial or Maximum size indicate a system-managed page file This function does not require or accept any parameters .OUTPUTS This function returns a PS object for each drive that has a page file on it, each having the following 3 properties/example: DriveLetter InitialSizeMB MaximumSizeMB ----------- ------------- ------------- C 0 0 D 1024 4096 .EXAMPLE Get-PageFile .LINK https://superwidgets.wordpress.com/category/powershell/ .NOTES Function by Sam Boutros https://superwidgets.wordpress.com/category/powershell/ 18 September 2018 - v0.1 #> [CmdletBinding(ConfirmImpact='Low')] Param() Begin { } Process { Get-WmiObject -Class Win32_PageFileSetting | select @{n='DriveLetter'; e={$_.Name[0]}}, @{n='InitialSizeMB';e={$_.InitialSize}}, @{n='MaximumSizeMB';e={$_.MaximumSize}} Write-Verbose '0 value for Initial or Maximum size indicate a system-managed page file' } End { } } function Set-PageFile { <# .SYNOPSIS Function to set page file to be on a given drive .DESCRIPTION Function to set page file to be on a given drive Function will create page file if it does not exist on the provided drive .PARAMETER PageFile This is a PS Custom Object containing the following 3 properties: DriveLetter such as c InitialSizeMB such as 1024 (0 value indicate system managed page file) MaximumSizeMB such as 4096 (0 value indicate system managed page file) This object can be constructed manually as in: $PageFile = [PSCustomObject]@{ DriveLetter = 'c' InitialSizeMB = 0 MaximumSizeMB = 0 } or obtained from the Get-PageFile function of this PS module .EXAMPLE Set-PageFile -PageFile ([PSCustomObject]@{ DriveLetter = 'c' InitialSizeMB = 0 MaximumSizeMB = 0 }) This example configures a system-managed page file on drive c .EXAMPLE Get-PageFile | foreach { $_.InitialSizeMB = 0; $_.MaximumSizeMB = 0; $_ } | Set-PageFile This example sets every page file to system-managed size .LINK https://superwidgets.wordpress.com/category/powershell/ .NOTES Function by Sam Boutros 20 September 2018 - v0.1 #> [CmdletBinding(ConfirmImpact='Low')] Param( [Parameter(Mandatory=$false,ValueFromPipeline=$true)][PSCustomObject]$PageFile = [PSCustomObject]@{ DriveLetter = ((Get-WmiObject Win32_Volume | where PageFilePresent).DriveLetter | foreach { $_[0] } | select -First 1) InitialSizeMB = 0 # 0 = System Managed Size MaximumSizeMB = 0 # 0 = System Managed Size } ) Begin { Write-Verbose 'Input received:' Write-Verbose ($PageFile | Out-String) $DriveletterList = (Get-WmiObject Win32_Volume | where PageFilePresent).DriveLetter | foreach { $_[0] } if ($PageFile.DriveLetter -notin $DriveletterList) { Write-Log 'Set-PageFile error:','Provided drive letter',$PageFile.DriveLetter, 'does not exist on this computer, available drive letters are',($DriveletterList -join ', ') Magenta,Yellow,Magenta,Yellow,Magenta break } else { Write-Verbose "Validated that provided drive letter '$($PageFile.DriveLetter)' exists on this computer '$($env:COMPUTERNAME)'" } } Process { $CurrentPageFile = Get-PageFile | where { $_.DriveLetter -match $PageFile.DriveLetter } if ($CurrentPageFile.InitialSizeMB -eq $PageFile.InitialSizeMB -and $CurrentPageFile.MaximumSizeMB -eq $PageFile.MaximumSizeMB) { Write-Log 'Existing page file',($CurrentPageFile | Out-String),'already matches provided parameters' Green,Yellow,Green } else { Write-Log 'Updating page file',($CurrentPageFile | Out-String) Green,Cyan #region Disable AutomaticManagedPagefile feature $compObj = Get-WmiObject Win32_ComputerSystem -EnableAllPrivileges if ($compObj.AutomaticManagedPagefile) { $compObj.AutomaticManagedPagefile = $false $compObj.Put() | Out-Null $compObj = Get-WmiObject -Class Win32_compObj -EnableAllPrivileges if ($compObj.AutomaticManagedPagefile) { Write-Log 'Set-PageFile:','Unable to Disable AutomaticManagedPagefile feature','Get-WmiObject -Class Win32_compObj' Magenta,Yellow,Magenta break } else { Write-Log 'Disabled','AutomaticManagedPagefile','feature on',$compObj.Name Green,Cyan,Green,Cyan } } else { Write-Log 'Computer',$compObj.Name,'AutomaticManagedPagefile','feature is already disabled' Green,Cyan,Green,Cyan } #endregion # Change/Create Page File $pageFileSetting = Get-WmiObject -Class Win32_PageFileSetting | where { $_.Name.StartsWith($PageFile.DriveLetter) } if (-not $pageFileSetting) { Set-WmiInstance -Class Win32_PageFileSetting -Arguments @{ Name = "$($PageFile.DriveLetter):\pagefile.sys" InitialSize = 0 MaximumSize = 0 } -EnableAllPrivileges | Out-Null $pageFileSetting = Get-WmiObject -Class Win32_PageFileSetting | where { $_.Name.StartsWith($PageFile.DriveLetter) } } $pageFileSetting.InitialSize = $PageFile.InitialSizeMB $pageFileSetting.MaximumSize = $PageFile.MaximumSizeMB $pageFileSetting.Put() | Out-Null $CurrentPageFile = Get-PageFile | where { $_.DriveLetter -match $PageFile.DriveLetter } Write-Verbose 'PageFile setting:' Write-Verbose ($PageFile | Out-String) Write-Verbose 'CurrentPageFile setting:' Write-Verbose ($CurrentPageFile | Out-String) if ($CurrentPageFile.InitialSizeMB -eq $PageFile.InitialSizeMB -and $CurrentPageFile.MaximumSizeMB -eq $PageFile.MaximumSizeMB) { Write-Log 'Successfully updated page file settings to',($CurrentPageFile | Out-String) Green,Cyan Write-Log 'Remember that a reboot is required to complete this process' Yellow } else { Write-log 'Unable to change Page File setting',($CurrentPageFile | Out-String) Magenta,Yellow } } } End { } } function Remove-PageFile { <# .SYNOPSIS Function to remove page file from a given drive .DESCRIPTION Function to remove page file from a given drive .PARAMETER DriveLetter Drive such as 'c' or 'e' that has a page file to be removed .EXAMPLE Remove-PageFile 'c' .LINK https://superwidgets.wordpress.com/category/powershell/ .NOTES Function by Sam Boutros 20 September 2018 - v0.1 #> [CmdletBinding(ConfirmImpact='Low')] Param( [Parameter(Mandatory=$false,ValueFromPipeline=$true)] [String]$DriveLetter = ((Get-WmiObject Win32_Volume | where PageFilePresent).DriveLetter | foreach { $_[0] } | select -First 1) ) Begin { Write-Verbose "Input received: DriveLetter $DriveLetter" $DriveletterList = (Get-WmiObject Win32_Volume | where PageFilePresent).DriveLetter | foreach { $_[0] } if ($DriveLetter -notin $DriveletterList) { Write-Log 'Remove-PageFile error:','Provided drive letter',$DriveLetter, 'does not exist on this computer, available drive letters are',($DriveletterList -join ', ') Magenta,Yellow,Magenta,Yellow,Magenta break } else { Write-Verbose "Validated that provided drive letter '$($DriveLetter)' exists on this computer '$($env:COMPUTERNAME)'" } } Process { Write-Log 'Current page file(s):', (Get-PageFile|Out-String) Green,Cyan if ($DriveLetter -in (Get-PageFile).DriveLetter) { (Get-WmiObject -Class Win32_PageFileSetting | where { $_.Name.StartsWith($DriveLetter) }).Delete() Write-Log 'Removed page file from drive',$DriveLetter Green,Cyan Write-Log 'Current page file(s):', (Get-PageFile|Out-String) Green,Cyan Write-Log 'Remember that a reboot is required to complete this process' Yellow } else { Write-Log 'No page file found on drive',$DriveLetter Yellow,Cyan } } End { } } #endregion #region Active Directory function Get-DCList { <# .SYNOPSIS Function to provide domain controller information for the current/given AD forest .DESCRIPTION Function to provide domain controller information for the current/given AD forest .PARAMETER DCName Optional parameter to be used to query other than current AD forest .PARAMETER Cred Optional parameter when querying cuurent AD forest (not providing a DCName) Required parameter when querying other than current AD forest. (Will default to current user credential if not provided when required) .EXAMPLE $myDCList = Get-DCList This returns information on the current forest to the console such as: Identified AD Forest ABC.local Identified the following domains: ForestName DomainName DomainLevel PDCEmulator DCCount ---------- ---------- ----------- ----------- ------- ABC.local ABC.local 2012R2 XYZ-DC1.ABC.local 2 as well as a PS object (stored in $myDCList variable) such as: ForestName : ABC.local DomainName : ABC.local DomainLevel : 2012R2 PDCEmulator : XYZ-DC1.ABC.local DCList : {XYZ-DC1.ABC.local, XYZ-DC2.ABC.local} .EXAMPLE $myDCList = Get-DCList -DCName dc1.mydomain.com -Cred (Get-SBCredential 'mydomain\myname') This returns information on the current forest to the console such as: .OUTPUTS This cmdlet returns PSCustom Objects, one for each Domain containing the following properties/example: ForestName : ABC.local DomainName : ABC.local DomainLevel : 2012R2 PDCEmulator : XYZ-DC1.ABC.local DCList : {XYZ-DC1.ABC.local, XYZ-DC2.ABC.local} .LINK https://superwidgets.wordpress.com/category/powershell/ .NOTES Function by Sam Boutros v0.1 - 20 July 2018 v0.2 - 14 January 2020 - Rewrite to speed up processing (not quering individial DCs) - Added parameter 'DCName' and code to query other than current AD forest - Added parameter 'Cred' and code to query other than current AD forest using a different credential #> [CmdletBinding(ConfirmImpact='Low')] Param( [Parameter(Mandatory=$false)][String]$DCName, # Full FQDN like server.domain.com [Parameter(Mandatory=$false)][PSCredential]$Cred ) Begin { if (-not $IsDomainMember) { Write-Log 'Validate-TimeSync error: This cmdlet is designed to run from a domain joined computer' Magenta break } } Process { if ($DCName) { Write-log 'Querying DC',$DCName,'using',$Cred.UserName,'credential..' Green,Cyan,Green,Cyan,Green if (-not $Cred) { $Cred = Get-SBCredential "$env:USERDNSDOMAIN\$env:USERNAME" } $Context = New-Object -TypeName system.directoryservices.activedirectory.directorycontext -ArgumentList @( 'DirectoryServer',$DCName,$Cred.UserName,$Cred.GetNetworkCredential().Password) try { $Forest = [system.directoryservices.activedirectory.Forest]::GetForest($Context) } catch { Write-Log $_.Exception.Message Magenta break } } else { Write-log 'Identifying current AD forest, domains, domain controllers...' Green try { $Forest = [system.directoryservices.activedirectory.Forest]::GetCurrentForest() } catch { Write-Log $_.Exception.Message Magenta break } } if ($Forest) { Write-Log 'Identified AD Forest',$Forest.Name Green,Cyan $DomainList = foreach ($Domain in $Forest.Domains) { [PSCustomObject]@{ ForestName = $Forest.Name DomainName = $Domain.Name DomainLevel = ($Domain.DomainMode | Out-String).Replace('Windows','').Replace('Domain','').Trim() PDCEmulator = $Domain.PdcRoleOwner DCList = $Domain.DomainControllers } } if ($DomainList) { Write-Log ' Identified the following',$DomainList.Count,'domains:' Green,Cyan,Green Write-Log ($DomainList | FT ForestName,DomainName,DomainLevel,PDCEmulator, @{n='DCCount';e={$_.DCList.Count}} -a | Out-String).Trim() Cyan } else { Write-Log ' AD Forest',$Forest.Name,'has no domains' Magenta,Yellow,Magenta } } else { Write-Log ' Failed to identify AD Forest' Magenta break } # $DCList = [system.directoryservices.activedirectory.Forest]::GetCurrentForest().domains.domaincontrollers | # select Forest,Name,CurrentTime,OSVersion,Roles,Domain,IPAddress,SiteName # Write-Log 'Identified',$DCList.Count,'domain controllers in the',(($DCList.Domain.Name | select -Unique) -join ', '), # 'domain(s), in the',(($DCList | select -First 1).Forest),'forest' Green,Cyan,Green,Cyan,Green,Cyan,Green } End { $DomainList } } function Get-SBADComputer { <# .SYNOPSIS Function to get one or all computer objects' information from Active Directory .DESCRIPTION Function to get one or all computer objects' information from Active Directory using LDAP Does not need ActiveDirectory PowerShell module Must be run from a domain joined computer .PARAMETER ComputerName This is an optional parameter that takes a computer name This parameter accepts wild cards such as * .PARAMETER DomainController This is an optional parameter to contain the FQDN of the Domain Controller to query, as DC1.myDomain.com If omitted, the function will query the currently logged on domain controller .PARAMETER OtherAttributeList This is an optional parameter that instructs this function to fetch one or more computer attributes in addition to the ones already provided. .PARAMETER MaxCount This is an optional number. When provided the output is limited to that many computers. .PARAMETER Quiet This is an optional parameter that takes either True or False values and defaults to False When set to True, it supresses console progress messages, speeding up prcessing .EXAMPLE Get-SBADComputer Returns enabled computer information in the current AD domain .EXAMPLE Get-SBADComputer -ComputerName abc* -MaxCount 5 -OtherAttributeList objectsid,objectguid,memberof,dnshostnamelastlogontimestamp,accountexpires Returns the first 5 enabled computers in the current AD domain that start with abc showing the listed additional properties .OUTPUTS Returns a PowerShell object containing the following properties: ComputerName OSName ==> For example: Windows Server 2012 R2 Standard DN ==> Distinguished name, for example: CN=Server10V,OU=Domain Computer,DC=mydomain,DC=com AD_OU ==> Active Directory Organization Unit where the computer object is located LastLogon ==> Date of last time the computer object logged on to AD ADCreated ==> Date the computer object was created in AD SPN ==> The computer's Service Principal Name if any DomainController ==> The DC queried by this function to obtain the computer information Additional properties will be returnd if specified in the OtherAttributeList parameter Returns nothing if the computer name is not found or a matching computer object is found but disabled .LINK https://superwidgets.wordpress.com/category/powershell/ .NOTES Function by Sam Boutros v0.1 - 10 September 2018 v0.2 - 11 april 2020 - Added parameters: ComputerName, MaxCount, OtherAttributeList, DomainController, Quiet #> [CmdletBinding(ConfirmImpact='Low')] Param( [Parameter(Mandatory=$false)][String]$ComputerName, [Parameter(Mandatory=$false)][Int]$MaxCount, [Parameter(Mandatory=$false)][String[]]$OtherAttributeList, [Parameter(Mandatory=$false)][String]$DomainController = "$($env:LOGONSERVER.Replace('\\','')).$($env:USERDNSDOMAIN)", [Parameter(Mandatory=$false)][Switch]$Quiet = $false ) Begin { if (-not $IsDomainMember) { Write-Log 'This function','Get-SBADComputer','must be run from a domain joined computer' Magenta, Yellow, Magenta break } } Process{ if (-not $Quiet) { Write-Log 'Input received:' Green if ($ComputerName) { Write-Log ' ComputerName:',$ComputerName Green,Cyan } if ($OtherAttributeList) { $OtherAttributeList = $OtherAttributeList.ToLower() Write-Log ' OtherAttributeList:',($OtherAttributeList -join ', ') Green,Cyan } Write-Log ' DomainController:',$DomainController Green,Cyan } $adsi = [adsisearcher][adsi]"LDAP://$DomainController" if ($ComputerName) { if (-not $Quiet) { Write-Log 'Processing ComputerName',$ComputerName,'from DC',$DomainController Green,Cyan,Green,Cyan } $adsi.filter = "(&(objectClass=Computer)(name=$ComputerName)(!userAccountControl:1.2.840.113556.1.4.803:=2))" } else { if (-not $Quiet) { Write-Log 'Processing Computer objects from DC', $DomainController Green,Cyan } $adsi.filter = "(&(objectClass=Computer)(!userAccountControl:1.2.840.113556.1.4.803:=2))" # To return only enabled computer objects } $adsi.PageSize = 1000000 $ComputerList = if ($MaxCount) { $adsi.FindAll() | select -First $MaxCount } else { $adsi.FindAll() } $ComputerList | foreach { $obj = $_.Properties $myOutput = [PSCustomObject][ordered]@{ ComputerName = [string]$obj.name OSName = [string]$obj.operatingsystem DN = [string]$obj.distinguishedname AD_OU = [string](($obj.distinguishedname) -replace '^CN=[\w\d-_]+,\w\w=','' -replace ',OU=','/' -replace ',DC=.*') LastLogon = $( try { $Temp1 = [DateTime]::FromFileTime($($obj.lastlogon) -as [int64]) if ($Temp1 -le [DateTime]'1/1/1900') { 'Never' } else { $Temp1 } } catch {'Never'} ) ADCreated = ($obj.whencreated).ToShortDateString() SPN = [string]$obj.serviceprincipalname DomainController = $DomainController } if ($OtherAttributeList) { foreach ($PCAttribute in $OtherAttributeList) { $myOutput | Add-Member -MemberType NoteProperty -Name $PCAttribute -EA 0 -Value $( if ($obj.$PCAttribute -and $PCAttribute -eq 'lastlogontimestamp') { try { $Temp1 = [datetime]::FromFileTime($($obj.lastlogontimestamp) -as [int64]) if ($Temp1 -le [DateTime]'1/1/1900') { 'Never' } else { $Temp1 } } catch {'Never'} } elseif ($obj.$PCAttribute -and $PCAttribute -eq 'accountexpires') { try { $Temp1 = [datetime]::FromFileTime($($obj.accountexpires) -as [int64]) if ($Temp1 -le [DateTime]'1/1/1900') { 'Never' } else { $Temp1 } } catch {'Never'} } elseif ($obj.$PCAttribute -and $PCAttribute -match 'sid') { # Translate sid from Binary Array to String (New-Object System.Security.Principal.SecurityIdentifier($($obj.$PCAttribute),0)).Value } elseif ($obj.$PCAttribute -and $PCAttribute -match 'guid') { # Translate guid from Octet Array to String $i = 0 $($obj.$PCAttribute) | ForEach { $i ++ if ($i -in (5,7,9,11)) { $guidAsString += '-' } $guidAsString += $_.ToString('x2').ToUpper() } $guidAsString } else { $($obj.$PCAttribute) } ) } } $myOutput } } End { } } function Get-SBADUser { <# .SYNOPSIS Function to get user objects information from Active Directory .DESCRIPTION Function to get user objects information from Active Directory using LDAP Does not need ActiveDirectory PowerShell module Must be run from a domain joined computer Used samaccounttype reference: SAM_DOMAIN_OBJECT 0x0 SAM_GROUP_OBJECT 0x10000000 SAM_NON_SECURITY_GROUP_OBJECT 0x10000001 SAM_ALIAS_OBJECT 0x20000000 SAM_NON_SECURITY_ALIAS_OBJECT 0x20000001 SAM_USER_OBJECT 0x30000000 SAM_NORMAL_USER_ACCOUNT 0x30000000 SAM_MACHINE_ACCOUNT 0x30000001 SAM_TRUST_ACCOUNT 0x30000002 SAM_APP_BASIC_GROUP 0x40000000 SAM_APP_QUERY_GROUP 0x40000001 SAM_ACCOUNT_TYPE_MAX 0x7fffffff Used UserAccountControl reference: 0x00000002 ADS_UF_ACCOUNTDISABLE The user account is disabled. 0x00000010 ADS_UF_LOCKOUT The account is currently locked out. 0x00000200 ADS_UF_NORMAL_ACCOUNT This is a default account type that represents a typical user. 0x00000800 ADS_UF_INTERDOMAIN_TRUST_ACCOUNT This is a permit to trust account for a system domain that trusts other domains. 0x00001000 ADS_UF_WORKSTATION_TRUST_ACCOUNT This is a computer account for a computer that is a member of this domain. 0x00002000 ADS_UF_SERVER_TRUST_ACCOUNT This is a computer account for a system backup domain controller that is a member of this domain. 0x00010000 ADS_UF_DONT_EXPIRE_PASSWD The password for this account will never expire. 0x00020000 ADS_UF_MNS_LOGON_ACCOUNT This is an MNS logon account. 0x00040000 ADS_UF_SMARTCARD_REQUIRED The user must log on using a smart card. 0x00080000 ADS_UF_TRUSTED_FOR_DELEGATION The service account (user or computer account), under which a service runs, is trusted for Kerberos delegation. Any such service can impersonate a client requesting the service. 0x00100000 ADS_UF_NOT_DELEGATED The security context of the user will not be delegated to a service even if the service account is set as trusted for Kerberos delegation. 0x00200000 ADS_UF_USE_DES_KEY_ONLY Restrict this principal to use only Data Encryption Standard (DES) encryption types for keys. 0x00400000 ADS_UF_DONT_REQUIRE_PREAUTH This account does not require Kerberos pre-authentication for logon. 0x00800000 ADS_UF_PASSWORD_EXPIRED The user password has expired. This flag is created by the system using data from the Pwd-Last-Set attribute and the domain policy. 0x01000000 ADS_UF_TRUSTED_TO_AUTHENTICATE_FOR_DELEGATION The account is enabled for delegation. This is a security-sensitive setting; accounts with this option enabled should be strictly controlled. This setting enables a service running under the account to assume a client identity and authenticate as that user to other remote servers on the network. .PARAMETER samaccountname This is an optional parameter that takes the user's login name AKA samaccountname If omitted, the function will return all user accounts (excluding computer accounts) This parameter accepts wild cards such as * .PARAMETER firstname This is an optional parameter that takes the user's first name .PARAMETER lastname This is an optional parameter that takes the user's last name .PARAMETER DomainController This is an optional parameter to contain the FQDN of the Domain Controller to query, as DC1.myDomain.com If omitted, the function will query the currently logged on domain controller .PARAMETER OtherAttributeList This is an optional parameter that instructs this function to fetch one or more user attributes in addition to the ones already provided. .PARAMETER Quiet This is an optional parameter that takes either True or False values and defaults to False When set to True, it supresses console progress messages, speeding up prcessing .EXAMPLE Get-SBADUser Returns all users' information in the current AD domain .Example Get-SBADUser *Sam* This will return all users that have 'sam' as part of the login name .Example Get-SBADUser *test* This will return all users that have 'test' as part of the login name .Example $UserList = Get-SBADUser $UserList | where useraccountcontrol -Match 'Normal' # list of normal working accounts $UserList | where useraccountcontrol -Match 'Disabled' # list of disabled accounts $UserList | where useraccountcontrol -Match 'PasswordNeverExpires' # list of account with passswords that never expire $UserList | where useraccountcontrol -Match 'Locked-Out' # list of locked out accounts $UserList | where useraccountcontrol -Match 'PasswordExpired' # list of accounts with expired passwords $UserList | where DN -Match 'OU=Partners,OU=Users,OU=Two,DC=One,DC=Domain,DC=com' | FT -a # list of accounts in the 'OU=Partners,OU=Users,OU=Two,DC=One,DC=Domain,DC=com' OU .Example $UserName = 'samb' # Logon Name / SamName $DCList = Get-DCList # This may take a few minutes in large domains with many DCs across slow wan links $myUserLogins = foreach ($DC in ($DCList)) { Get-SBADUser -samaccountname $UserName -DomainController $DC.Name } $myUserLogins | where LastLogon -ne 'Never' | sort LastLogon | FT UserName,DomainController, @{n='DomainControllerIP';e={($DCList|where Name -eq $_.DomainController).IPAddress}},LastLogon -auto This example queries all domain controllers for a given user's information including lastlogon This is helpful to show where a given user has logged on last. This can be used along with event log analysis to audit user logons. .Example Get-SBADUser -FirstName sam -LastName boutros -OtherAttributeList objectguid,objectsid .OUTPUTS Returns a PowerShell object for each returned user containing the following properties/example: UserName : Small, Robert samaccountname : Robert.Small DateCreated : 2/4/2016 1:04:05 PM useraccountcontrol : {Disabled, Normal} lastlogon : 10/10/2018 1:56:14 PM DateExpires : AccountNeverExpires DN : CN=Small\, Robert,OU=MyOU,DC=Mysubdomain,DC=mydomain,DC=com Additional properties will be returnd if specified in the OtherAttributeList parameter Notice the use of the '\' in the DN (Distinguished Name) as an escape character for the ',' part of the CN (Common Name) Note: DateExpires property speaks to the account expiration not the password expiration Conditions that may appear under useraccountcontrol include one or more of: Normal Disabled Locked-Out PasswordNeverExpires PasswordExpired .LINK https://superwidgets.wordpress.com/category/powershell/ .NOTES Function by Sam Boutros v0.1 - 9 October 2018 v0.2 - 17 May 2019 - improved reporting lastlogon to show 'never' if older then 1/1/1900 (zero value is 1/1/1601 12:00 AM UTC, in EST = GMT-5 - that would show as 12/31/1600 7:00 PM) v0.3 - 12 September 2019 - Added FirstName, LastName, and DisplayName properties Added parameters to allow finding a user by First or Last Name Added parameter to show custom/other user attributes v0.4 - 6 March 2020 - Added Byte Array to String transalation for sid properties Added Octet Array to String transalation for guid properties Added logic to filter by BOTH first and last names when both are provided Known issues: - GUID property translation from Octet Array to String may be inaccurate - SID property translation from Byte Array to String may fail v0.5 - 20 March 2020 - Minor updates to Avoid error message if attribute is provided to OtherAttribute parameter that's already in the user object Display Lastlogontimestamp attribute in DateTime format if requested Add -Quiet parameter to speed up processing by not displaying progress messages to the console v0.6 - 29 July 202 Added sipProxyAddress property Known issues: - This function fails to return 'OtherAttribute' if they're not all lower case - GUID property translation from Octet Array to String may be inaccurate - SID property translation from Byte Array to String may fail #> [CmdletBinding(ConfirmImpact='Low')] Param( [Parameter(Mandatory=$false)][String]$samaccountname, [Parameter(Mandatory=$false)][String]$FirstName, [Parameter(Mandatory=$false)][String]$LastName, [Parameter(Mandatory=$false)][String[]]$OtherAttributeList, [Parameter(Mandatory=$false)][String]$DomainController = "$($env:LOGONSERVER.Replace('\\','')).$($env:USERDNSDOMAIN)", [Parameter(Mandatory=$false)][Switch]$Quiet = $false ) Begin { if (-not $IsDomainMember) { Write-Log 'This function','Get-SBADUser','must be invoked from a domain-joined computer' Magenta, Yellow, Magenta break } } Process { if (-not $Quiet) { Write-Log 'Input received:' Green if ($samaccountname) { Write-Log ' samaccountname:',$samaccountname Green,Cyan } if ($FirstName) { Write-Log ' FirstName:',$FirstName Green,Cyan } if ($LastName) { Write-Log ' LastName:',$LastName Green,Cyan } if ($OtherAttributeList) { # $OtherAttributeList = $OtherAttributeList.ToLower() Write-Log ' OtherAttributeList:',($OtherAttributeList -join ', ') Green,Cyan } Write-Log ' DomainController:',$DomainController Green,Cyan } $adsi = [adsisearcher][adsi]"LDAP://$DomainController" if ($samaccountname) { if (-not $Quiet) { Write-Log 'Processing user - samaccountname',$samaccountname,'from DC',$DomainController Green,Cyan,Green,Cyan } $adsi.filter = "(samaccountname=$samaccountname)" } elseif ($FirstName -and $LastName) { if (-not $Quiet) { Write-Log 'Processing user - FirstName',$FirstName,'- LastName',$LastName,'from DC',$DomainController Green,Cyan,Green,Cyan,Green,Cyan } $adsi.filter = "(&(givenname=$FirstName)(sn=$LastName))" } elseif ($FirstName) { if (-not $Quiet) { Write-Log 'Processing user - FirstName',$FirstName,'from DC',$DomainController Green,Cyan,Green,Cyan } $adsi.filter = "(givenname=$FirstName)" } elseif ($LastName) { if (-not $Quiet) { Write-Log 'Processing user - LastName',$LastName,'from DC',$DomainController Green,Cyan,Green,Cyan } $adsi.filter = "(sn=$LastName)" } else { if (-not $Quiet) { Write-Log 'Processing user objects from DC', $DomainController Green,Cyan } $adsi.filter = "(&(objectClass=person)(samaccounttype=805306368))" # Filtering on person class objects, and type user account (not computer account) } $adsi.PageSize = 1000000 try { $adsi.FindAll() | foreach { $obj = $_.Properties # Property names are CASE SENSITIVE - all lowercase $myOutput = [PSCustomObject][ordered]@{ FirstName = $($obj.givenname) LastName = $($obj.sn) DisplayName = $($obj.displayname) UserName = $($obj.name) samaccountname = $($obj.samaccountname) DateCreated = $($obj.whencreated) useraccountcontrol = $( $UAC = '{0:x}' -f $($obj.useraccountcontrol) $mySubOutput = @() if ($UAC[-1] -eq '2') { $mySubOutput += 'Disabled' } if ($UAC[-2] -eq '1') { $mySubOutput += 'Locked-Out' } if ($UAC[-3] -eq '2') { $mySubOutput += 'Normal' } # if ($UAC[-3] -eq '8') { $mySubOutput += 'INTERDOMAIN_TRUST_ACCOUNT' } # if ($UAC[-4] -eq '1') { $mySubOutput += 'ComputerAccount' } # if ($UAC[-4] -eq '2') { $mySubOutput += 'DomainControllerAccount' } if ($UAC[-5] -eq '1') { $mySubOutput += 'PasswordNeverExpires' } if ($UAC[-6] -eq '8') { $mySubOutput += 'PasswordExpired' } $mySubOutput ) # lastlogontimestamp = [datetime]::FromFileTime($($obj.lastlogontimestamp) -as [int64]) # Replicates after 14 days by default DomainController = $DomainController Lastlogon = $( try { $Temp1 = [datetime]::FromFileTime($($obj.lastlogon) -as [int64]) if ($Temp1 -le [DateTime]'1/1/1900') { 'Never' } else { $Temp1 } } catch {'Never'} ) DateExpires = $(try {[datetime]::FromFileTime($($obj.accountexpires) -as [int64])} catch {'Never'}) DN = $($obj.distinguishedname) Description = $($obj.description) UserWorkstations = $($obj.userworkstations) PasswordLastSet = $(try {[datetime]::FromFileTime($($obj.pwdlastset) -as [int64])} catch {'Never'}) MemberOf = $($obj.memberof) -join ' - ' sipProxyAddress = $( if ($Temp = $obj.proxyaddresses -match 'sip:') { $Temp.Split(':')[1] } ) } if ($OtherAttributeList) { foreach ($UserAttribute in $OtherAttributeList) { $myOutput | Add-Member -MemberType NoteProperty -Name $UserAttribute -EA 0 -Value $( if ($obj.$UserAttribute -and $UserAttribute -eq 'lastlogontimestamp') { try { $Temp1 = [datetime]::FromFileTime($($obj.lastlogontimestamp) -as [int64]) if ($Temp1 -le [DateTime]'1/1/1900') { 'Never' } else { $Temp1 } } catch {'Never'} } elseif ($obj.$UserAttribute -and $UserAttribute -match 'sid') { # Translate sid from Binary Array to String (New-Object System.Security.Principal.SecurityIdentifier($($obj.$UserAttribute),0)).Value } elseif ($obj.$UserAttribute -and $UserAttribute -match 'guid') { # Translate guid from Octet Array to String $i = 0 $($obj.$UserAttribute) | ForEach { $i ++ if ($i -in (5,7,9,11)) { $guidAsString += '-' } $guidAsString += $_.ToString('x2').ToUpper() } $guidAsString } else { $($obj.$UserAttribute) } ) } } $myOutput } } catch { Write-Log $_.Exception.Message Magenta } } End { } } function Get-SBADGroup { <# .SYNOPSIS Function to get details of an AD group .DESCRIPTION Function to get details of an AD group from Active Directory using LDAP Does not need ActiveDirectory PowerShell module Must be run from a domain-joined computer .PARAMETER GroupName Optional parameter that accepts one or more AD group names. It also accepts wild cards, like 'Alaska*' to return all groups starting with 'Alaska' If omitted, all groups are returned. .PARAMETER QUIET Optional switch. When set to True it supresses console output for faster processing. .EXAMPLE Get-SBADGroup -GroupName 'DomainAdmins' Returns details and members of the 'DomainAdmins' AD group in the current AD domain .OUTPUTS Returns a PowerShell object containing the following properties/example: GroupName : My-Azure-Admin DN : CN=My-Azure-Admin,OU=Groups,OU=xxx,OU=xxx,DC=xxx,DC=MyCorp,DC=com AD_OU : Groups/xxx/xxx Scope : Global Category : Security ADCreated : 12/7/2018 ADChanged : 3/6/2019 MemberDNs : {CN=My-nvxxx,OU=xxx,OU=Users,OU=xxx,OU=xxx,DC=xxx,DC=MyCorp,DC=com, CN=My-bgxxx,OU=xxx,OU=Users,OU=xxx,OU=xxx,DC=xxx,DC=MyCorp,DC=com, CN=My-sbxxx,OU=xxx,OU=Users,OU=xxx,OU=xxx,DC=xxx,DC=MyCorp,DC=com, CN=My-pkxxxx,OU=xxx,OU=Users,OU=xxx,OU=xxx,DC=xxx,DC=MyCorp,DC=com...} MemberNames : {My-nvxxx, My-bgxxx, My-sbxxx, My-pkxxx...} Returns nothing if the group name is not found .LINK https://superwidgets.wordpress.com/category/powershell/ .NOTES Function by Sam Boutros v0.1 - 14 March 2019 v0.2 - 22 October 2020 Fixed bug to list all groups when using a wild card for a group name, not just the first 1,000 Added 2 new properties: Scope: Global, Domain Local, or Universal Category: Security or Distribution Added Quiet switch to not display console output speeding up processing #> [CmdletBinding(ConfirmImpact='Low')] Param( [Parameter(Mandatory=$false)][String[]]$GroupName, [Parameter(Mandatory=$false)][Switch]$Quiet ) Begin { } Process{ if ($IsDomainMember) { $adsi = [adsisearcher]"objectcategory=group" $adsi.PageSize = 1000000 if ($GroupName) { $GroupList = foreach ($Group in $GroupName) { $adsi.filter = "(&(objectCategory=group)(cn=$Group))" ($adsi.FindAll()).Properties } } else { $adsi.filter = '(objectCategory=group)' $GroupList = ($adsi.FindAll()).Properties } foreach ($ADGroup in $GroupList) { if (-not $Quiet) { Write-Log 'Processing group',$ADGroup.distinguishedname Green,Cyan } [PSCustomObject][ordered]@{ GroupName = [string]$ADGroup.name DN = [string]$ADGroup.distinguishedname AD_OU = [string](($ADGroup.distinguishedname) -replace '^CN=[\w\d-_]+,\w\w=','' -replace ',OU=','/' -replace ',DC=.*') Scope = $( switch ($ADGroup.grouptype) { { $_ -in (2,(2-2147483648)) } {'Global'} { $_ -in (4,(4-2147483648)) } {'Domain Local'} { $_ -in (8,(8-2147483648)) } {'Universal'} } ) Category = $( switch ($ADGroup.grouptype) { { $_ -in (2,4,8) } {'Distribution'} { $_ -in ((2-2147483648),(4-2147483648),(8-2147483648)) } {'Security'} } ) ADCreated = ($ADGroup.whencreated).ToShortDateString() ADChanged = ($ADGroup.whenchanged).ToShortDateString() MemberDNs = $ADGroup.member MemberNames = $( if ($ADGroup.member) { $ADGroup.member | foreach { $_.Split(',')[0].Split('=')[1] } } ) } } } else { Write-Log 'This function','Get-SBADGroup','must be invoked from a domain-joined computer' Magenta, Yellow, Magenta } } End { } } function Get-SBADGroupMembers { <# .SYNOPSIS Function to get members of AD group including sub-groups .DESCRIPTION Function to get members of AD group including sub-groups using LDAP Does not need ActiveDirectory PowerShell module Must be run from a domain-joined computer .PARAMETER GroupName Name of the AD group - required .PARAMETER Parent Name of the parent AD group - optional - used to enable the recursive use to search sub-groups .PARAMETER Recurse Switch that is set to True by default. It causes this function to search sub-groups .EXAMPLE Get-SBADGroupMembers testgroup1 .OUTPUTS Returns a PowerShell object containing the following properties/example: UserName DN OU MemberOf -------- -- -- -------- testuser1 CN=testuser1,DC=abcd,DC=local abcd testgroup1 testuser2 CN=testuser2,DC=abcd,DC=local abcd testgroup2.testgroup1 Returns nothing if the group name is not found .LINK https://superwidgets.wordpress.com/category/powershell/ .NOTES Function by Sam Boutros v0.1 - 15 June 2019 v0.2 - 25 September 2019 - Fixed bug with Group members, added 'mail' property to to group members #> [CmdletBinding(ConfirmImpact='Low')] Param( [Parameter(Mandatory=$true)][String]$GroupName, [Parameter(Mandatory=$false)][String]$Parent, [Parameter(Mandatory=$false)][Switch]$Recurse = $true ) Begin { } Process{ $myOutput = if ($IsDomainMember) { $adsi = [adsisearcher]"objectcategory=group" $adsi.filter = "(&(objectCategory=group)(cn=$GroupName))" if ($ADGroup = ($adsi.FindAll()).Properties) { if ($Parent) { Write-Log 'Processing child group',$ADGroup.distinguishedname,"(Parent: $Parent)" Green,Cyan,DarkYellow } else { Write-Log 'Processing group ',$ADGroup.distinguishedname Green,Cyan } $GroupObj = [PSCustomObject][ordered]@{ GroupName = [string]$ADGroup.name MemberNames = $( if ($ADGroup.member) { $ADGroup.member | foreach { $_.Split(',')[0].Split('=')[1] } } ) } foreach ($Member in $GroupObj.MemberNames) { $adsi = [adsisearcher]'' $adsi.filter = "cn=$Member" $MemberObj = ($adsi.FindAll()).Properties if ($MemberObj.objectclass -match 'group') { if ($Recurse) { Get-SBADGroupMembers $MemberObj.name -Parent $GroupObj.GroupName } } else { [PSCustomObject][ordered]@{ UserName = [string]$MemberObj.name Mail = [string]$MemberObj.mail DN = [string]$MemberObj.distinguishedname OU = [string](($MemberObj.distinguishedname) -replace '^CN=[\w\d-_]+,\w\w=','' -replace ',OU=','/' -replace ',DC=.*') MemberOf = $( if ($Parent) { "$($GroupObj.GroupName).$Parent" } else { $GroupObj.GroupName } ) } } } } else { Write-Log 'Group',$GroupName,'not found' Green,Yellow,Cyan } } else { Write-Log 'This function','Get-SBADGroupMembers','must be invoked from a domain-joined computer' Magenta, Yellow, Magenta } } End { $myOutput } } function Report-LastLogon { <# .SYNOPSIS Function to report on last logon information for users in a given AD domain .DESCRIPTION Function to report on last logon information for users in a given AD domain This function depends on ImportExcel and ActiveDirectory PowerShell modules This function runs parallel jobs to process the retrieval of last logon information concurrently. If a given domain controller is accessible via PowerShell remoting (TCP 5985), this function will invoke a remote job, otherwise it will invoke a local job. .PARAMETER DomainName Active Directory domain name such as myaddomain.com. .PARAMETER DCName Any accessible domain controller in the above domain. .PARAMETER Cred Credential used to invoke remote Get-ADuser commands against the domain controllers. This can be obtained via the Get-Credential cmdlet or the Get-SBCredential function. .PARAMETER Filter Optional Get-ADuser Filter such as 'Enabled -eq $True -and Mail -like "*" -and ManagerName -like "*" -and EmployeeID -like "*" -and EmployeeID -notlike "*-*"' .PARAMETER ExcludeDC Known offline domain controller list. .PARAMETER ReportFolder Path to existing folder where this function will write its log and Excel reports. .EXAMPLE Report-LastLogon -DomainName $thisDomainName -DCName $thisDomainDCList[0] -Cred (Get-SBCredential "$Env:USERDNSDOMAIN\$env:USERNAME") .OUTPUTS This cmdlet creates an Excel report for the identified users with the following fields/columns: FirstName LastName EmployeeId SamAccountName LastLogon UPN DN DomainController .LINK https://superwidgets.wordpress.com/category/powershell/ .NOTES Function by Sam Boutros v0.1 - 2 February 2021 #> [CmdletBinding(ConfirmImpact='Low')] Param( [Parameter(Mandatory=$true,HelpMessage='Active Directory domain name such as myaddomain.com')][String]$DomainName, [Parameter(Mandatory=$true,HelpMessage='Any accessible domain controller in the above domain')][String]$DCName, [Parameter(Mandatory=$true,HelpMessage='Credential used to invoke remote Get-ADuser commands against the domain controllers')][PSCredential]$Cred, [Parameter(Mandatory=$false,HelpMessage='Optional Get-ADuser Filter such as ''Enabled -eq $True -and Mail -like "*" -and ManagerName -like "*" -and EmployeeID -like "*" -and EmployeeID -notlike "*-*"''')][String]$Filter = '*', [Parameter(Mandatory=$false,HelpMessage='Known offline domain controller list')][String[]]$ExcludeDC, [Parameter(Mandatory=$false)][String]$ReportFolder = '.\' ) Begin { #region Check required PS Modules $StartTime = Get-Date $ModuleList = @('AZSBTools','ImportExcel') foreach ($Module in $ModuleList) { if (-not (Get-Module -Name $Module -ListAvailable)) { Install-Module $ModuleList -Force -AllowClobber -Scope CurrentUser } } Import-Module $ModuleList -DisableNameChecking -Force -WA 0 | Out-Null #endregion #region Check required folders try { Set-Location (Split-Path -Parent $MyInvocation.MyCommand.Path) } catch {} if (-not (Test-Path $ReportFolder)) { Write-Log '$ReportFolder',$ReportFolder,'does not exist, using current folder',(Get-Location).Path,'instead...' Magenta,Yellow,Cyan,Yellow,Cyan $ReportFolder = (Get-Location).Path } New-Item "$ReportFolder\Logs" -ItemType Directory -Force -EA 0 | Out-Null # Quietly create Logs subfolder if it does not exist $LogFile = "$ReportFolder\Logs\Report-LastLogon-$DomainName-$(Get-Date -Format 'ddMMMMyyyy_hh-mm-ss_tt').log" $ReportFile = "$ReportFolder\Report-LastLogon-$DomainName-$(Get-Date -Format 'ddMMMMyyyy_hh-mm-ss_tt').xlsx" #endregion #region Echo input parameters Write-Log 'Current location:',(Get-Location) Green,Cyan $LogFile Write-Log 'Current User: ',(Whoami) Green,Cyan $LogFile Write-Log 'Elevation: ',$IsElevated Green,Cyan $LogFile Write-Log 'Script location: ',$(try { Split-Path -Parent $MyInvocation.MyCommand.Path } catch {}) Green,Cyan $LogFile Write-Log 'Current modules: ',(Get-Module | Out-String).Trim() Green,Cyan $LogFile Write-Log 'Input received: ' Green $LogFile Write-Log ' DomainName: ',$DomainName Green,Cyan $LogFile Write-Log ' ReportFolder: ',$ReportFolder Green,Cyan $LogFile Write-Log ' DCName: ',$DCName Green,Cyan $LogFile Write-Log ' ExcludeDC: ',($ExcludeDC -join ', ') Green,Cyan $LogFile Write-Log ' Filter: ',$Filter Green,Cyan $LogFile Write-Log ' Credential: ',$Cred.UserName Green,Cyan $LogFile #endregion #region Get DC list, check connectivity try { $DCList = Get-DCList -DCName $DCName -Cred $Cred -EA 1 } catch { Write-Log 'Unable to get DC List, do we have the correct credential?' Magenta $LogFile Write-Log $_.Exception.Message Yellow $LogFile break } $ThisDomainDCList = ($DCList | where DomainName -EQ $DomainName).DCList.Name | sort $ThisDomainDCList = $ThisDomainDCList | foreach { if ($_ -notin $ExcludeDC) { $_ } } $thisDCList = foreach ($DC in $ThisDomainDCList) { Write-Log 'Checking if DC',($DC).PadRight(35,' '),'is reachable:' Green,Cyan,Green $LogFile -NoNewLine if ($Result = Test-SBNetConnection -ComputerName $DC -PortNumber 389,5985 -TimeoutSec 10 -WA 0) { [PSCustomObject]@{ Name = $DC Port389Open = $Result[0].TcpTestSucceeded Port5985Open = $Result[1].TcpTestSucceeded } if ($Result[0].TcpTestSucceeded) { Write-Log 'LDAP port 389 OK,' DarkYellow $LogFile -NoNewLine } else { Write-Log 'LDAP port 389 unreachable,' Magenta $LogFile -NoNewLine } if ($Result[1].TcpTestSucceeded) { Write-Log 'PS Remoting port 5985 OK' DarkYellow $LogFile } else { Write-Log 'PS Remoting port 5985 unreachable' Magenta $LogFile } } else { Write-Log 'Unable to reach LDAP port 389 or PS Remoting port 5985' Magenta $LogFile } } if ($thisDCList.Count -lt 1) { Write-Log 'No reachable DCs found !?' Magenta $LogFile break } #endregion $PropertyList = @( 'sn' 'givenname' 'samaccountname' 'lastlogon' 'EmployeeId' 'DistinguishedName' ) # When changing, also change $CombinedUserList output object } Process { $Duration = Measure-Command { Get-Job | Remove-Job -Force Write-Log 'Starting jobs..' Green $LogFile foreach ($DC in $thisDCList) { if ($DC.Port5985Open) { # Remote Job Invoke-Command -AsJob -ComputerName $DC.Name -JobName $DC.Name -Credential $Cred -ScriptBlock { try { Import-Module ActiveDirectory -EA 1 # For Win 2008 servers running PS 2 :( try { Get-ADUser -Filter $Using:Filter -Properties $Using:PropertyList -EA 1 } Catch { $_.Exception.Message } } Catch { $_.Exception.Message } } } elseif ($DC.Port389Open) { # Local Job Start-Job -Name $DC.Name -Credential $Cred -ScriptBlock { try { Get-ADUser -Filter $Using:Filter -Server $Using:DC.Name -Properties $Using:PropertyList -EA 1 } Catch { $_.Exception.Message } } } else { Write-Log ' Skipping inaccessible DC',$DC.Name Green,Cyan $LogFile } } $JobMonitor = foreach ($JobStatus in (Get-Job)) { if ($JobStatus.State -eq 'Running') { $StatusColor = 'DarkYellow' } else { $StatusColor = 'Yellow' } Write-Log 'Remote Job',($JobStatus.Name).PadRight(35,' '),$JobStatus.State Green,Cyan,$StatusColor $LogFile [PSCustomObject]@{ Name = $JobStatus.Name State = $JobStatus.state Changed = $false StartTime = Get-Date Duration = $null } } Write-Log 'Monitoring Jobs'' status..' Green $LogFile $LiveStatus = Get-job $DisplayJobStatusScriptBlock = { $thisJobMonitor = $JobMonitor | where Name -EQ $JobStatus.Name if ($JobStatus.State -ne $thisJobMonitor.State -and -not $thisJobMonitor.Changed) { # Only display changed job status (once) $thisJobMonitor.Changed = $true $thisJobMonitor.Duration = New-TimeSpan -Start $thisJobMonitor.StartTime -End (Get-Date) # Record and display each DC job time if ($JobStatus.State -eq 'Running') { $StatusColor = 'DarkYellow' } else { $StatusColor = 'Yellow' } if ($Jobstatus.PSJobTypeName -eq 'BackgroundJob') { Write-Log 'Local Job' Yellow $LogFile -No } else { Write-Log 'Remote Job' Green $LogFile -No } Write-Log ($JobStatus.Name).PadRight(35,' '),"$($JobStatus.State) in" Cyan,$StatusColor $LogFile -NoNewLine Write-Log "$($thisJobMonitor.Duration.Hours):$($thisJobMonitor.Duration.Minutes):$($thisJobMonitor.Duration.Seconds) (hh:mm:ss)" DarkYellow $LogFile } } while (($LiveStatus | where State -eq 'Running')) { foreach ($JobStatus in $LiveStatus) { if ($JobStatus.State -eq 'Failed' -and $JobStatus.PSJobTypeName -eq 'RemoteJob') { # Remote Job failed, try Local Job $DC = $JobStatus.Name Get-Job -Name $JobStatus.Name | Remove-Job Start-Job -Name $DC -Credential $Cred -ScriptBlock { try { Get-ADUser -Filter $Using:Filter -Server $Using:DC.Name -Properties $Using:PropertyList -EA 1 } Catch { $_.Exception.Message } } Write-Log ($JobStatus.Name).PadRight(35,' '),$JobStatus.State,'trying Local Job..' Cyan,$StatusColor,Red $LogFile $JobStatus = Get-Job -Name $DC if ($JobStatus.State -eq 'Running') { $StatusColor = 'DarkYellow' } else { $StatusColor = 'Yellow' } Write-Log 'Local Job',$DC.PadRight(35,' '),$JobStatus.State Yellow,Cyan,$StatusColor $LogFile } else { & $DisplayJobStatusScriptBlock } } Start-Sleep -Seconds 1 } & $DisplayJobStatusScriptBlock } # Start and wait for Jobs $Duration = Measure-Command { Write-Log 'Receiving job data..' Green $LogFile -NoNewLine $CombinedUserList = foreach ($DC in $thisDCList.Name) { $Temp = Receive-Job -Name $DC if ($Temp.SamAccountName) { # Job returning expected data, accept it $Temp } else { # Job not returning expected data, probably an error, display it Write-Log 'Job error',$DC,$Temp Yellow,Magenta,Yellow $LogFile } } Get-Job | Remove-Job -Force $CombinedUserList = $CombinedUserList | foreach { [PSCustomObject][Ordered]@{ FirstName = $_.GivenName LastName = $_.Surname EmployeeId = $_.EmployeeId SamAccountName = $_.SamAccountName LastLogon = $( try { $Temp1 = [DateTime]::FromFileTime($($_.lastlogon) -as [Int64]) if ($Temp1 -le [DateTime]'1/1/1900') { 'Never' } else { $Temp1 } } catch { 'Never' } ) UPN = $_.UserPrincipalName DN = $_.DistinguishedName DomainController = $_.PSComputerName } } $CombinedUserList = $CombinedUserList | where LastLogon -NE 'Never' } # Receive Job data Write-Log 'Received',$CombinedUserList.Count,'filtered user logins, in',"$($Duration.Hours):$($Duration.Minutes):$($Duration.Seconds) (hh:mm:ss)" Green,Cyan,Green,DarkYellow $LogFile $Duration = Measure-Command { Write-Log 'Processing',$CombinedUserList.Count,'user login time stamps...' Green,Cyan,Green $LogFile -NoNewLine $myOutput = $CombinedUserList | group SamAccountName | foreach { $_.Group | sort LastLogon | select -Last 1 } $myOutput = $myOutput | sort LastName,FirstName } # Process user logins Write-Log 'Done in',"$($Duration.Hours):$($Duration.Minutes):$($Duration.Seconds) (hh:mm:ss)" Cyan,DarkYellow $LogFile } End { Write-Log 'Exporting report to file',$ReportFile Green,Cyan $LogFile -NoNewLine $Duration = Measure-Command { $myOutput | Export-Excel -Path $ReportFile -ConditionalText $( ($myOutput | Get-Member -MemberType NoteProperty).Name | foreach { New-ConditionalText $_ White SteelBlue } ) -AutoSize -FreezeTopRowFirstColumn } Write-Log ' Done in',"$($Duration.Hours):$($Duration.Minutes):$($Duration.Seconds)(hh:mm:ss)" Green,DarkYellow $LogFile $CombinedDuration = New-TimeSpan -Start $StartTime -End (Get-Date) Write-Host ' ' Write-Log 'All done in',"$($CombinedDuration.Hours):$($CombinedDuration.Minutes):$($CombinedDuration.Seconds) (hh:mm:ss)" Cyan,DarkYellow $LogFile Write-Host ' ' } } function Get-OUFromDN { <# .SYNOPSIS Function to return an AD OU (Active Directory Organization Unit) based on a provided Distinguished Name .DESCRIPTION Function to report on last logon information for users in a given AD domain This function depends on ImportExcel and ActiveDirectory PowerShell modules This function runs parallel jobs to process the retrieval of last logon information concurrently. If a given domain controller is accessible via PowerShell remoting (TCP 5985), this function will invoke a remote job, otherwise it will invoke a local job. .PARAMETER DistinguishedName Active Directory Distinguished Name such as CN=Sam Boutros,OU=USA,DC=MyDomain,DC=local .EXAMPLE Get-OUFromDN -DistinguishedName 'CN=Sam Boutros,OU=USA,DC=MyDomain,DC=local' .OUTPUTS This cmdlet returns a string like OU=USA,DC=MyDomain,DC=local .LINK https://superwidgets.wordpress.com/category/powershell/ .NOTES Function by Sam Boutros v0.1 - 29 September 2021 #> [CmdletBinding(ConfirmImpact='Low')] Param( [Parameter(Mandatory=$true,HelpMessage='AD object DistinguishedName Name')][String]$DistinguishedName ) Begin { } Process { if ($DistinguishedName.IndexOf(',') -ge 0) { $PartList = $DistinguishedName -split ',' $OUList = $PartList -match 'OU=' if ($OUList) { ($OUList -join ','),($PartList -match 'DC=' -join ',') -join ',' } else { Write-Warning "Get-OUFromDN Notice: No OU found in the provided DistinguishedName: '$DistinguishedName'" } # Return nothing if no OU is found. } else { Write-Warning "Get-OUFromDN Error: Bad DistinguishedName provided: '$DistinguishedName'" } # Must have a comma. } End { } } #endregion #region SQL functions function Report-SQLServer { <# .SYNOPSIS Function to report of databases of one or more SQL servers .DESCRIPTION Function to report of databases of one or more SQL servers The report is in plain text format The report lists the databases, their tables, columns, and optionally row count .PARAMETER ComputerName One or more computer names This is an optional parameter that defaults to the current computer name .PARAMETER IncludeSystemDatabases This is an optional parameter that defaults to False When set to True, the report includes system databases .PARAMETER IncludeRowCount This is an optional parameter that defaults to False When set to True, the report includes row count of every table found in every database This parameter requires either module SQLPS or SqlServer SqlServer is available in the PowerShell Gallery: Install-Module SqlServer .PARAMETER LogFile This is an optional parameter that contains the path to the log file where this function will log its output .EXAMPLE Report-SQLServer This example reports on all databases on the current server excluding system databases and not showing row counts .EXAMPLE Report-SQLServer -ComputerName SQL1,SQL2 This example reports on all databases on the 2 provided SQL servers excluding system databases and not showing row counts .EXAMPLE Report-SQLServer -IncludeRowCount This example reports on all databases on the current server excluding system databases and showing row counts .LINK https://superwidgets.wordpress.com/category/powershell/ .NOTES Function by Sam Boutros 23 February 2019 - v0.1 #> [CmdletBinding(ConfirmImpact='Low')] Param( [Parameter(Mandatory=$false,ValueFromPipeline=$true)][String[]]$ComputerName = $env:COMPUTERNAME, [Parameter(Mandatory=$false)][Switch]$IncludeSystemDatabases = $false, [Parameter(Mandatory=$false)][Switch]$IncludeRowCount = $false, [Parameter(Mandatory=$false)][String]$LogFile = ".\Report-SQLServer - $(Get-Date -Format 'ddMMMMyyyy_hh-mm-ss_tt').txt" ) Begin { [void][reflection.assembly]::LoadWithPartialName('Microsoft.SqlServer.Smo') if (Get-Module SQLPS,SqlServer -ListAvailable) { $FoundSQL = $true } else { if ($IncludeRowCount) { Write-Log 'Report-SQLServer: Error:','Missing PS module SQLPS and SqlServer (one of which is needed to get row count)' Magenta,Yellow $LogFile Write-Log ' SqlServer module is available in the PowerShell Gallery:','Install-module SqlServer' Yellow,Cyan $LogFile } } } Process { foreach ($Name in $ComputerName) { Write-Log 'Reporting on SQL server',$Name Green,Cyan $LogFile $Server = New-Object ('Microsoft.SqlServer.Management.Smo.Server') $Name if ($IncludeSystemDatabases) { $DatabaseList = $Server.databases } else { $DatabaseList = $Server.databases | Where { -not $_.IsSystemObject } } $DatabaseReport = ".\DBReport-$Name-$(Get-Date -Format 'ddMMMMyyyy_hh-mm-ss_tt').txt" "Database report for server '$env:computername'" | Out-File $DatabaseReport " generated on $(Get-Date)" | Out-File $DatabaseReport -Append ' ' | out-file $DatabaseReport -Append "Database list ($($DatabaseList.Count)):" | Out-File $DatabaseReport -Append foreach ($DB in $DatabaseList) { " $($DB.Name)" | Out-File $DatabaseReport -Append } ' ' | out-file $DatabaseReport -Append $DatabaseReport = (Get-Item $DatabaseReport).FullName foreach ($DB in $DatabaseList) { ' ' | Out-File $DatabaseReport -Append "Database: $($DB.Name)" | Out-File $DatabaseReport -Append foreach ($Table in $DB.Tables) { if ($IncludeRowCount) { if ($FoundSQL) { $RowCount = (Invoke-Sqlcmd -Query "USE $($DB.Name); SELECT COUNT(*) FROM $($Table.Name)" -EA 1).Column1 $Rows = "($RowCount rows)" } else { $Rows = 'Need SqlServer PS module to get row count' } " Table: $($Table.Name) $Rows" | Out-File $DatabaseReport -Append foreach ($Column in $Table.Columns) { " Column: $($Column.Name)" | Out-File $DatabaseReport -Append } # foreach $Column } # if $IncludeRowCount } # foreach $Table } # foreach $DB } # foreach $Name } # Process End { Write-Log 'Report saved to', $DatabaseReport Green,Cyan } } function Enable-SQLPageCompression { <# .SYNOPSIS Function to enable database page compression on one or more databases .DESCRIPTION Function to enable database page compression on one or more databases Page compression is enabled for all database tables and indices https://docs.microsoft.com/en-us/sql/relational-databases/data-compression/page-compression-implementation https://docs.microsoft.com/en-us/sql/relational-databases/data-compression/enable-compression-on-a-table-or-index .PARAMETER DatabaseName This is an optional parameter. If absent, compression is turned on for all databases This function does not alter system databases .PARAMETER LogFile This is an optional parameter that contains the path to the log file where this function will log its output .EXAMPLE Enable-SQLPageCompression This example enables page compression on all non-system databases on the current SQL server .EXAMPLE Enable-SQLPageCompression -DatabaseName badname1,mydb1,badname2 This example enables page compression on mydb1 skipping badname1 and badname2 (database that don;t exist on this SQL server) .LINK https://superwidgets.wordpress.com/category/powershell/ .NOTES Function by Sam Boutros 2 October 2019 - v0.1 #> [CmdletBinding(ConfirmImpact='Low')] Param( [Parameter(Mandatory=$false)][String[]]$DatabaseName, [Parameter(Mandatory=$false)][String]$LogFile = ".\Enable-SQLPageCompression - $(Get-Date -Format 'ddMMMMyyyy_hh-mm-ss_tt').txt" ) Begin { if (Get-Module SQLPS,SqlServer -ListAvailable) { $FoundSQL = $true } else { if ($IncludeRowCount) { Write-Log 'Report-SQLServer: Error:','Missing PS module SQLPS and SqlServer (one of which is needed to get row count)' Magenta,Yellow $LogFile Write-Log ' SqlServer module is available in the PowerShell Gallery:','Install-module SqlServer' Yellow,Cyan $LogFile } } $DatabaseList = (Invoke-Sqlcmd -Query "SELECT * FROM sys.databases" | Where { $_.database_id -gt 4 }).Name if ($DatabaseName) { $DatabaseName = foreach ($DBName in $DatabaseName) { if ($DBName -in $DatabaseList) { $DBName } else { Write-Log 'Database',$DBName,'not found on this SQL server',$env:computername,'skipping..' Magenta,Yellow,Magenta,Yellow,Magenta $LogFile } } } else { $DatabaseName = $DatabaseList Write-Verbose "Database count: $($DatabaseName.Count)" Write-Verbose ($DatabaseName -join ', ') } if ($DatabaseName) { Write-Log 'Enabling page compression on the following database(s):',($DatabaseName -join ', ') Green,Cyan $LogFile } } Process { foreach ($Database in $DatabaseName) { $Query = Invoke-Sqlcmd -Query " USE $Database --Creates the ALTER TABLE Statements SET NOCOUNT ON SELECT 'ALTER TABLE ' + '[' + s.[name] + ']'+'.' + '[' + o.[name] + ']' + ' REBUILD WITH (DATA_COMPRESSION=PAGE);' FROM sys.objects AS o WITH (NOLOCK) INNER JOIN sys.indexes AS i WITH (NOLOCK) ON o.[object_id] = i.[object_id] INNER JOIN sys.schemas AS s WITH (NOLOCK) ON o.[schema_id] = s.[schema_id] INNER JOIN sys.dm_db_partition_stats AS ps WITH (NOLOCK) ON i.[object_id] = ps.[object_id] AND ps.[index_id] = i.[index_id] WHERE o.[type] = 'U' ORDER BY ps.[reserved_page_count] --Creates the ALTER INDEX Statements SET NOCOUNT ON SELECT 'ALTER INDEX '+ '[' + i.[name] + ']' + ' ON ' + '[' + s.[name] + ']' + '.' + '[' + o.[name] + ']' + ' REBUILD WITH (DATA_COMPRESSION=PAGE);' FROM sys.objects AS o WITH (NOLOCK) INNER JOIN sys.indexes AS i WITH (NOLOCK) ON o.[object_id] = i.[object_id] INNER JOIN sys.schemas s WITH (NOLOCK) ON o.[schema_id] = s.[schema_id] INNER JOIN sys.dm_db_partition_stats AS ps WITH (NOLOCK) ON i.[object_id] = ps.[object_id] AND ps.[index_id] = i.[index_id] WHERE o.type = 'U' AND i.[index_id] >0 ORDER BY ps.[reserved_page_count] " Write-Log 'Processing database',$Database Green,Cyan $LogFile -NoNewLine try { Invoke-Sqlcmd -Query "USE $Database; $($Query.Column1 -join ' ')" -EA 1 Write-Log 'done' DarkYellow $LogFile } catch { if ($_.Exception.Message -match 'Execution Timeout Expired') { # Default 30 sec # https://docs.microsoft.com/en-us/dotnet/api/system.data.sqlclient.sqlcommand.commandtimeout Write-Log 'Database page compression set, actual compression in progress..' DarkYellow $LogFile } else { Write-Log 'failed' Magenta $LogFile Write-Log $_.Exception.Message Yellow $LogFile } } } } End { } } function Get-SQLDatabaseFile { <# .SYNOPSIS Function to return a SQL database file information .DESCRIPTION Function to return a SQL database file information .PARAMETER DatabaseName One or more database names. This is an optional parameter. If absent, the function returns information on data files of all databases except system databases. .PARAMETER IncludeSystemDatabases This is an optional switch. If set to TRUE, this function will report on system databases as well. .PARAMETER IncludeLogFiles This is an optional parameter. This is an optional switch. If set to TRUE, this function will report on LOG files as well. .PARAMETER LogFile This is an optional parameter that contains the path to the log file where this function will log its output .EXAMPLE Get-SQLDatabaseFile This example reports on DATA files of all non-system databases on the current SQL server .EXAMPLE Get-SQLDatabaseFile -DatabaseName dmdire -IncludeLogFiles This example returns file information for database 'dmdire' including both DATA and LOG files .LINK https://superwidgets.wordpress.com/category/powershell/ .NOTES Function by Sam Boutros 7 October 2019 - v0.1 #> [CmdletBinding(ConfirmImpact='Low')] Param( [Parameter(Mandatory=$false)][String[]]$DatabaseName, [Parameter(Mandatory=$false)][Switch]$IncludeSystemDatabases, [Parameter(Mandatory=$false)][Switch]$IncludeLogFiles, [Parameter(Mandatory=$false)][String]$LogFile = ".\Get-SQLDatabaseFile - $(Get-Date -Format 'ddMMMMyyyy_hh-mm-ss_tt').txt" ) Begin { if (-not (Get-Module SQLPS,SqlServer -ListAvailable)) { Write-Log 'Get-SQLDatabaseFile: Error:','Missing PS module SQLPS and SqlServer (one of which is needed to get row count)' Magenta,Yellow $LogFile Write-Log ' SqlServer module is available in the PowerShell Gallery:','Install-module SqlServer' Yellow,Cyan $LogFile break } } Process { $Missing = $false $myOutput = $DatabaseList = Invoke-Sqlcmd -Query " SELECT db.name AS DBName, db.is_auto_shrink_on AS AutoShrink, mf.name AS FileName, Physical_Name AS Location, db.database_id, type, size, max_size, growth, is_percent_growth FROM sys.master_files mf INNER JOIN sys.databases db ON db.database_id = mf.database_id" | select DBName,FileName,Location,AutoShrink @{n='Id';e={$_.database_id}}, @{n='Type';e={if ($_.type -eq 0) {'Data'} else {'Log'}}}, @{n='SizeMB';e={[Math]::Round($_.size/128,1)}}, # size is reported in 8 KB pages @{n='MaxSizeMB';e={ if ($_.max_size -gt 0) { [Math]::Round($_.max_size/128,1) # size is reported in 8 KB pages } elseif ($_.max_size -eq 0) { 'None' } else { 'Unlimited' } }}, @{n='Growth';e={ if ($_.is_percent_growth) { if ($_.growth -gt 0) { "$($_.growth)%" } else { 'None' } } elseif ($_.growth -gt 0) { "$([Math]::Round($_.growth/128,1))MB" # growth is reported in 8 KB pages } elseif ($_.growth -eq 0) { 'None' } else { 'Unlimited' } }} if (-not $IncludeSystemDatabases) { $myOutput = $myOutput | where { $_.Id -gt 4 } } if (-not $IncludeLogFiles) { $myOutput = $myOutput | where { $_.Type -eq 'Data' } } $myOutput = if ($DatabaseName) { foreach ($Name in $DatabaseName) { if ($Temp = $myOutput | where {$_.Name -eq $Name}) { $Temp } else { $Missing = $true Write-Log 'Database',$Name,'not found on SQL server',$env:COMPUTERNAME Magenta,Yellow,Magenta,Yellow $LogFile } } } else { $myOutput } if ($Missing) { Write-Log 'Here''s the list of databases on this',$env:COMPUTERNAME,'SQL server' Green,Cyan,Green $LogFile $DatabaseList.Name | select -Unique | sort| foreach { Write-Log " $_" DarkYellow $LogFile } } } End { $myOutput } } function Truncate-SQLLogs { <# .SYNOPSIS Function to truncate SQL log files for databases on one or more SQL servers. .DESCRIPTION Function to truncate SQL log files for all databases except (master, tempdb, model, msdb) on one or more SQL servers. This function depends on SQLPS PS module. .PARAMETER ComputerName One or more SQL servers This is an optional parameter. It defaults to the current computername. .PARAMETER LogFile This is an optional parameter that contains the path to the log file where this function will log its output. .EXAMPLE Truncate-SQLLogs .OUTPUTS This function returns a powershell object for each database processed containing the following properties: SQLServerName DBName DBLogFile ==> physical disk path to the log file BeforeSizeMB AfterSizeMB .LINK https://superwidgets.wordpress.com/category/powershell/ .NOTES Function by Sam Boutros v0.1 - 30 January 2021 #> [CmdletBinding(ConfirmImpact='Low')] Param( [Parameter(Mandatory=$false)][Alias('SQLServerName')][String[]]$ComputerName = $env:COMPUTERNAME, [Parameter(Mandatory=$false)][String]$LogFile = ".\Truncate-SQLLogs-$ComputerName-$(Get-Date -Format 'ddMMMMyyyy_hh-mm-ss_tt').log" ) Begin { } Process { $myLocation = Get-Location $myOutput = foreach ($Server in $ComputerName) { try { $DatabaseList = Invoke-SQLCMD -Query 'SELECT * FROM sysdatabases WHERE dbid > 4' -ServerInstance $Server -EA 1 # skipping first 4 databases: master, tempdb, model, msdb Set-Location $myLocation # This is needed since Invoke-SQLCMD changes location to the SQL drive SQLSERVER:\ which may interfere with logging or/and subsequent automations Write-Log 'Starting to truncate log files for',$DatabaseList.Count,'databases on server',$Server Green,Cyan,Green,Cyan $LogFile foreach ($DB in $DatabaseList) { $DBLog = Invoke-SQLCMD -Query ("SELECT Name,Physical_Name,Size FROM sys.master_files WHERE database_id = $($DB.dbid) AND type = 1") -ServerInstance $Server Write-Log 'Truncating log file',$DBLog.Physical_Name,"($($DBLog.Size*8) KB)",'for database',$DB.name,"(database_id = $($DB.dbid))" Green,Cyan,Green,Cyan,Green,Cyan $LogFile -NoNewLine try { Invoke-SQLCMD -Query (" USE [$($DB.name)]; ALTER DATABASE [$($DB.name)] SET RECOVERY SIMPLE WITH NO_WAIT; ") -EA 1 -ServerInstance $Server $Result = Invoke-SQLCMD -Query (" USE [$($DB.name)]; DBCC SHRINKFILE(N'$($DBLog.Name)', 1); ALTER DATABASE [$($DB.name)] SET RECOVERY FULL WITH NO_WAIT; ") -EA 1 -ServerInstance $Server Write-Log 'done, now',"($($Result.CurrentSize*8) KB)" Green,Cyan $LogFile New-Object -TypeName psobject -Property ([Ordered]@{ SQLServerName = $Server DBName = $DB.Name DBLogFile = $DBLog.Physical_Name BeforeSizeMB = $DBLog.Size/128 AfterSizeMB = $Result.CurrentSize/128 }) } catch { Write-Log 'failed:' Magenta $LogFile Write-Log $_.Exception.Message Yellow $LogFile } } } catch { Write-Log 'Truncate-SQLLogs Error on server',$Server Magenta,Yellow $LogFile Write-Log $_.Exception.Message Yellow $LogFile } } } End { $myOutput | sort SQLServerName,DBName } } function Install-SQLExpress { <# .SYNOPSIS Function to return the Geographical location of an Internet IP address .DESCRIPTION Function to return the Geographical location of an Internet IP address This function depends on ip-api.com and ipinfo.io .PARAMETER Source One or more URLs This is an optional parameter. These URLs will be queried for WAN IP. .EXAMPLE Get-MyWANIP .OUTPUTS This cmdlet returns a System.Net.IPAddress object such as: Address : 1132553623 AddressFamily : InterNetwork ScopeId : IsIPv6Multicast : False IsIPv6LinkLocal : False IsIPv6SiteLocal : False IsIPv6Teredo : False IsIPv4MappedToIPv6 : False IPAddressToString : 151.101.129.67 .LINK https://superwidgets.wordpress.com/category/powershell/ .NOTES Function by Sam Boutros v0.1 - 12 April 2020 #> [CmdletBinding(ConfirmImpact='Low')] Param( [Parameter(Mandatory=$false)][String]$URL = 'https://download.microsoft.com/download/7/f/8/7f8a9c43-8c8a-4f7c-9f92-83c18d96b681/SQL2019-SSEI-Expr.exe', # 'https://go.microsoft.com/fwlink/?linkid=866658', [Parameter(Mandatory=$false)][String]$FileName = 'SQL2019-SSEI-Expr.exe', [Parameter(Mandatory=$false)][String]$FileHash = '095D77F3B46A708D3F3D7763E60EE46805C3B0E3D1F4F821F9DA8A23A40167C8', [Parameter(Mandatory=$false)][Int]$SizeInBytes = 6376336, [Parameter(Mandatory=$false)][String]$TempFolder = $env:TEMP, [Parameter(Mandatory=$false)][String]$LogFile = ".\Install-SQLExpress_$($env:COMPUTERNAME)_$(Get-Date -Format 'ddMMMMyyyy_hh-mm-ss_tt').log" ) Begin { # Validate TempFolder if (-not (Test-Path $TempFolder)) { Write-Log 'Install-SQLExpress Error:','Invalid path provided for TempFolder parameter',$TempFolder Magenta,Yellow,Magenta $LogFile break } # Validate Free Space $FreeSpaceBytes = (Get-Volume -DriveLetter (Get-Item $TempFolder).FullName[0]).SizeRemaining if ($FreeSpaceBytes -le $SizeInBytes+1MB) { Write-Log 'Install-SQLExpress Error:','Not enough disk space at',$TempFolder Magenta,Yellow,Magenta $LogFile Write-Log 'Available KB:',('{0:N0}' -f ($FreeSpaceBytes/1KB)),'Needed KB:',('{0:N0}' -f ($SizeInBytes/1KB+1KB)) Magenta,Yellow,Magenta,Yellow $LogFile break } #region Download if needed $Go = $true if (Test-Path "$TempFolder\$FileName") { $Hash = (Get-FileHash -Path "$TempFolder\$FileName" -Algorithm SHA256).Hash $File = Get-Item "$TempFolder\$FileName" if ($FileHash -eq $Hash -and $SizeInBytes -eq $File.Length) { $Go = $false Write-Log 'Validated existing file',$File.FullName Green,Cyan $LogFile } } if ($Go) { Write-Log 'Downloading file',$FileName,'from',$URL Green,Cyan,Green,Cyan $LogFile -NoNewLine try { Invoke-WebRequest $URL -OutFile "$TempFolder\$FileName" -UseBasicParsing -EA 1 Write-Log 'done' DarkYellow $LogFile $Hash = (Get-FileHash -Path "$TempFolder\$FileName" -Algorithm SHA256).Hash $File = Get-Item "$TempFolder\$FileName" if ($FileHash -eq $Hash -and $SizeInBytes -eq $File.Length) { $Go = $false Write-Log 'Validated file',$File.FullName Green,Cyan $LogFile } else { Write-Log 'Install-SQLExpress Error:','Downloaded file validation failed' Magenta,Yellow $LogFile Write-Log 'Downloaded File Hash:',$Hash,'Expected Hash:',$FileHash Magenta,Yellow,Magenta,Yellow $LogFile Write-Log 'Downloaded File Size:',$File.Length,'Expected Size:',$SizeInBytes Magenta,Yellow,Magenta,Yellow $LogFile break } } catch { Write-Log 'failed' Magenta $LogFile Write-Log $_.Exception.Message Yellow $LogFile break } } #endregion } Process { # Download the full package Start-Process -FilePath "$TempFolder\$FileName" -Args "/ACTION=Download /MEDIAPATH=$TempFolder /MEDIATYPE=Core /QUIET" -Verb RunAs -Wait Set-ItemProperty -Path 'HKLM:\SOFTWARE\Wow6432Node\Microsoft\.NetFramework\v4.0.30319' -Name 'SchUseStrongCrypto' -Value '1' -Type Dword # https://silentinstallhq.com/microsoft-sql-server-2019-express-silent-install-how-to-guide/ # https://techcommunity.microsoft.com/t5/sql-server/2019-express-silent-install/m-p/1115671 # Install - requires elevation if ($IsElevated) { Write-Log 'Installing',"$TempFolder\$FileName" Green,Cyan $LogFile -NoNewLine Start-Process -FilePath "$TempFolder\$FileName" -Args "/ACTION=INSTALL /IACCEPTSQLSERVERLICENSETERMS /QUIET" -Verb RunAs -Wait Write-Log 'done' DarkYellow $LogFile } else { Write-Log 'Install-SQLExpress Error:','This function requires elevation to install' Magenta,Yellow $LogFile break } # Validate Test-Path 'HKLM:\Software\Microsoft\Microsoft SQL Server\Instance Names\SQL' $IsElevated } End { } } #endregion #region IIS functions function Get-WebSiteList { <# .SYNOPSIS Function to provide Web site list from IIS servers on one or many Hyper-V hosts .DESCRIPTION Function to provide Web site list from IIS servers on one or many Hyper-V hosts This is usefull to get a web site list from all IIS servers in a Hyper-V farm This function uses PowerShell remoting which requires that Hyper-V hosts run Server 2016 or above, and IIS VMs run Server 2016 or above, or Windows 10 .PARAMETER HvHostName Required parameter that provides one or many Hyper-V computer names .PARAMETER Cred Required parameter that can be obtained via Get-Credential or Get-SBCredential - see Example .PARAMETER IISVMNameStringMatch Optional parameter that defaults to 'IIS'. This function uses this string to identify which VMs are IIS VMs .PARAMETER IncludeNotStarted Optional parameter. When set to $True, the output will include web sites that are not 'Started' .PARAMETER IncludeDefault Optional parameter. When set to $True, the output will include 'Default web site' .EXAMPLE $myWebSiteList = Get-WebSiteList -HvHostName @('HV123','HV124','HV125') -Cred (Get-SBCredential 'domain\admin') This returns web site information such as: Name VMName HvHostName Bindings ---- ------ ---------- -------- website11111.com vm123-IIS4 HV12345 {https *:443:website11111.com sslFlags=None, https *:443:www.website11111.com sslFlags=None} website11111.com-redirect vm123-IIS4 HV12345 {http *:80:website11111.com, http *:80:www.website11111.com} book.website22222.com vm124-IIS4 HV12346 {http *:80:book.website22222.com} reps-webs1.com vm124-IIS4 HV12346 {http *:80:reps-webs1.com, http *:80:www.reps-webs1.com} .OUTPUTS This cmdlet returns PSCustom Objects, one for each Domain containing the following properties/example: Name : wesiteaaa.com SSL : False VMName : vm222-IIS4 HvHostName : HV345 Bindings : {https *:443:wesiteaaa.com sslFlags=None, https *:443:www.wesiteaaa.com sslFlags=None} .LINK https://superwidgets.wordpress.com/category/powershell/ .NOTES Function by Sam Boutros v0.1 - 23 March 2020 v0.2 - 23 March 2020 - Added SSL True/False property in the output. #> [CmdletBinding(ConfirmImpact='Low')] Param( [Parameter(Mandatory=$true)][String[]]$HvHostName, [Parameter(Mandatory=$true)][PSCredential]$Cred, [Parameter(Mandatory=$false)][String]$IISVMNameStringMatch = 'IIS', [Parameter(Mandatory=$false)][Switch]$IncludeNotStarted, [Parameter(Mandatory=$false)][Switch]$IncludeDefault ) Begin { } Process { $WebSiteList = foreach ($ComputerName in $HvHostName) { try { $VMList = Get-VM -ComputerName $ComputerName -EA 1 Write-Log 'Identified',$VMList.Count,'VMs on Hyper-V host',$ComputerName Green,Cyan,Green,Cyan $IISList = $VMList | where { $_.State -eq 'Running' -and $_.Name -match $IISVMNameStringMatch } Write-Log ' of which, there''s',$IISList.Count,'running IIS VM(s)' Green,Cyan,Green Write-Log ($IISList|Out-String).Trim() Cyan foreach ($VMId in $IISList.VMId) { Invoke-Command -ComputerName $ComputerName -ScriptBlock { try { Invoke-Command -VMId $Using:VMId -Credential $Using:Cred -EA 1 -ScriptBlock { Get-IISSite | select Bindings,Name,State,@{n='VMName';e={$env:COMPUTERNAME}}, @{n='SSL';e={$SSL=$False; $_.Bindings.CertificateHash|foreach{if($_){$SSL=$true}}; $SSL}} } } catch { Write-Log $_.Exception.Message Yellow if ($_.Exception.Message -match 'An error has occurred which Windows PowerShell cannot handle.') { Write-Log ' VM may not be running Server 2016 or Windows 10 OS, and PowerShell Direct won''t work..' DarkYellow } } } } } catch { Write-Log $_.Exception.Message Magenta } } } End { if ($IncludeNotStarted) { $WebSiteList = $WebSiteList | select Name,SSL,VMName,@{n='HvHostName';e={$_.PSComputerName}},Bindings } else { $WebSiteList = $WebSiteList | where State -match 'Started' | select Name,SSL,VMName,@{n='HvHostName';e={$_.PSComputerName}},Bindings } if ($IncludeDefault) { $WebSiteList } else { $WebSiteList | where Name -NotMatch 'Default Web Site' } } } function Report-IISLogs { <# .SYNOPSIS Function to report on IIS log files of the websites of the current computer .DESCRIPTION Function to report on IIS log files of the websites of the current computer .PARAMETER WebSiteName One or more Web Site Names. This should exist on the computer where this function is invoked. If this parameter is not provided, this function will report on the log files of all websites on this computer .EXAMPLE Report-IISLogs -WebSiteName www.mydomain.com This example will report on IIS log files for the provided website on this computer .EXAMPLE Report-IISLogs -WebSiteName www.mysite.com This example will report on log files of www.mysite.com on this computer .EXAMPLE Report-IISLogs This example will report on all log files of all websites on this computer .EXAMPLE Report-IISLogs | Export-Csv ".\Report-IISLogs_$($env:COMPUTERNAME)_$(Get-Date -Format 'ddMMMMyyyy_hh-mm-ss_tt').csv" -NoTypeInformation This example will report on the current server website log files and save them to CSV file .OUTPUTS This cmdlet returns a PS object collection such as: Name Id LogFolder LogFileCount TotalMB ---- -- --------- ------------ ------- domain1.com 7 C:\inetpub\logs\LogFiles\w3svc7 1749 1966.3 www.domain2.com 23 C:\inetpub\logs\LogFiles\w3svc23 1749 985.1 site.domain3.com 11 C:\inetpub\logs\LogFiles\w3svc11 579 229.7 www.domain4.com 2 C:\inetpub\logs\LogFiles\w3svc2 1749 125.2 .LINK https://superwidgets.wordpress.com/category/powershell/ .NOTES Function by Sam Boutros v0.1 - 9 May 2020 #> [CmdletBinding(ConfirmImpact='Low')] Param( [Parameter(Mandatory=$false)][String[]]$WebSiteName ) Begin { Write-Verbose 'Report-IISLogs: received input:' Write-Verbose "WebSiteName: $WebSiteName" } Process { if ($WebSiteName) { $WebSiteInfo = foreach ($WebSite in $WebSiteName) { if ($Info = Get-Website -Name $WebSite) { $Info } else { Write-Log 'Report-IISLogs Error: web site',$WebSite,'not found' Magenta,Yellow,Magenta } } } # If no $WebSiteName(s) are provided, or provided names do not exist, get a list of all web sites if (-not $WebSiteInfo) { $WebSiteInfo = Get-Website } $myOutput = foreach ($WebSite in $WebSiteInfo) { $LogFolder = "$($Website.logFile.directory)\w3svc$($WebSite.id)".replace("%SystemDrive%",$env:SystemDrive) $LogFileList = try { Get-ChildItem $LogFolder -File -Force -EA 1 | select FullName,Length } catch { Write-Log $_.Exception.Message Yellow } $TotalMB = 0 $LogFileList | foreach { $TotalMB += $_.Length } [PSCustomObject][Ordered]@{ Name = $WebSite.Name Id = $WebSite.Id LogFolder = $LogFolder LogFileCount = $LogFileList.Count TotalMB = [Math]::Round($TotalMB/1MB,1) } } } End { $myOutput | sort TotalMB -Descending } } function Parse-IISLogs { <# .SYNOPSIS Function to parse one or more IIS log files .DESCRIPTION Function to parse one or more IIS log files .PARAMETER IISLogFile One or more IIS log files. This should be the full path to the log file(s). If this parameter is provided, the IISLogFolder and WebSiteName parameters will be ignored .PARAMETER IISLogFolder One or more IIS log folders. This should be the full path to the log folder(s). When this parameter is provided, this function will - parse all the files in the provided folder(s), AND - ignore the WebSiteName parameter if present .PARAMETER WebSiteName One or more Web Site Names. This should exist on the computer where this function is invoked. When this parameter is provided, this function will parse all the log files of the provided website(s). If this parameter is not provided, this function will parse all the log files of all websites on this computer .EXAMPLE Parse-IISLogs -IISLogFile C:\inetpub\logs\LogFiles\w3svc1\u_ex161121.log This example will parse the provided log file .EXAMPLE $WebVisits = Parse-IISLogs -IISLogFolder C:\inetpub\logs\LogFiles\w3svc1,C:\inetpub\logs\LogFiles\w3svc2 -Verbose This example will parse all the IIS log files in the provided folders, and save the results to $WebVisits variable .EXAMPLE $myWebSiteName = 'my.website.com' $WebVisits = Parse-IISLogs -WebSiteName $myWebSiteName $WebVisits | Export-Csv ".\Parse-IISLogs_$($myWebSiteName)_$(Get-Date -Format 'ddMMMMyyyy_hh-mm-ss_tt').csv" -NoTypeInformation This example will parse IIS log file for the provided website on this computer, save the results to $WebVisits variable, and export it to CSV file .EXAMPLE Parse-IISLogs This example will parse all the log files of all websites on this computer .EXAMPLE $WebSiteName = 'WWW.MYDOMAIN.com' $LastLogFile = Get-ChildItem (Report-IISLogs -WebSiteName $WebSiteName).LogFolder -File | sort LastWriteTime | select -Last 1 $AccessEventList = Parse-IISLogs -IISLogFile $LastLogFile.FullName $AccessEventList | Export-CSV ".\Parse-IISLogs_$($WebSiteName)_$(Get-Date -Format 'ddMMMMyyyy_hh-mm-ss_tt').csv" -NoType This example will find the provided website's last IIS log, parse it, and export the data to CSV file. .OUTPUTS This cmdlet returns a PS object collection such as: DateTime : 07/30/2015 21:22:02 ServerName : myserver-IIS2 ServerIP : 10.11.12.13 WebSite : my.website.com Method : GET Stem : /robots.txt Query : - Port : 80 UserName : - ClientIP : 54.196.144.100 UserAgent : CCBot/2.0+(http://commoncrawl.org/faq/) Referer : - Status : 404 SubStatus : 0 Win32Status : 2 DurationMS : 6 .LINK https://superwidgets.wordpress.com/category/powershell/ .NOTES Function by Sam Boutros v0.1 - 9 May 2020 v0.2 - 10 May 2020 - Combined Date and Time properties into DateTime property #> [CmdletBinding(ConfirmImpact='Low')] Param( [Parameter(Mandatory=$false)][String[]]$IISLogFile, [Parameter(Mandatory=$false)][String[]]$IISLogFolder, [Parameter(Mandatory=$false)][String[]]$WebSiteName ) Begin { Write-Verbose 'Parse-IISLogs: received input:' Write-Verbose "WebSiteName: $WebSiteName" Write-Verbose "IISLogFile: $IISLogFile" Write-Verbose "IISLogFolder: $IISLogFolder" } Process { #region Get LogFileList depending on what input is provided if ($IISLogFile) { $LogFileList = foreach ($FileName in $IISLogFile) { try { Get-Item $FileName -EA 1 | select FullName,Length } catch { Write-Log 'Parse-IISLogs Error: Provided IISLogFile',$FileName,'not found' Magenta,Yellow,Magenta } } } elseif ($IISLogFolder) { $LogFileList = foreach ($FolderName in $IISLogFolder) { try { Get-ChildItem $FolderName -File -Force -EA 1 | select FullName,Length } catch { Write-Log 'Parse-IISLogs Error: Provided IISLogFolder',$FolderName,'not found' Magenta,Yellow,Magenta } } } else { if ($WebSiteName) { $WebSiteInfo = foreach ($WebSite in $WebSiteName) { if ($Info = Get-Website -Name $WebSite) { $Info } else { Write-Log 'Parse-IISLogs Error: web site',$WebSite,'not found' Magenta,Yellow,Magenta } } } # If no $WebSiteName(s) are provided, or provided names do not exist, get a list of all web sites if (-not $WebSiteInfo) { $WebSiteInfo = Get-Website } $LogFileList = foreach ($WebSite in $WebSiteInfo) { try { Get-ChildItem "$($Website.logFile.directory)\w3svc$($WebSite.id)".replace("%SystemDrive%",$env:SystemDrive) -File -Force -EA 1 | select FullName,Length } catch { Write-Log $_.Exception.Message Yellow } } } #endregion if ($LogFileList) { $WebSiteList = Get-WebSite $LogFileList | foreach { $TotalMB += $_.Length } $TotalMB = [Math]::Round($TotalMB/1MB,1) Write-Log 'Parsing',$LogFileList.Count,'IIS log files',"($TotalMB MB)" Green,Cyan,Green,Cyan $i=0 foreach ($Log in $LogFileList) { $WebSite = $WebSiteList | where Id -EQ ([Int]($Log.FullName.Split('\') -match 'w3svc').Replace('w3svc','')) $i++ Write-Verbose "Processing log file $($Log.FullName)" if ($LogFileList.Count -ge 1) { $Percent = [Math]::Round($i/$LogFileList.Count*100,1) Write-Progress -Activity "Parsing IIS log file # $i of $($LogFileList.Count)" -PercentComplete $Percent } else { Write-Progress -Activity "Parsing IIS log file # $i" -PercentComplete 50 } $ReadLog = (Get-Content $Log.FullName) -notmatch '#' foreach ($Line in $ReadLog) { $Visitor = $Line -split ' ' [PSCustomObject][Ordered]@{ DateTime = [DateTime]"$($Visitor[0]) $($Visitor[1])" -f '' ServerName = $env:COMPUTERNAME ServerIP = $Visitor[2] WebSite = $WebSite.Name Method = $Visitor[3] Stem = $Visitor[4] Query = $Visitor[5] Port = $Visitor[6] UserName = $Visitor[7] ClientIP = $Visitor[8] UserAgent = $Visitor[9] Referer = $Visitor[10] Status = $Visitor[11] SubStatus = $Visitor[12] Win32Status = $Visitor[13] DurationMS = $Visitor[14] } } } } else { Write-Log 'Parse-IISLogs Error: No IIS Log Files provided' Yellow } } End { } } function Report-IISLogs { <# .SYNOPSIS Function to report on, and optionally delete IIS log files of the websites of the current computer .DESCRIPTION Function to report on, and optionally delete IIS log files of the websites of the current computer .PARAMETER WebSiteName One or more Web Site Names. This should exist on the computer where this function is invoked. If this parameter is not provided, this function will report on the log files of all websites on this computer .PARAMETER DeleteLogFiles If this switch is set to True, this function will delete old web site log files. .PARAMETER DeleteHTTPERRFiles If this switch is set to True, this function will delete old DeleteHTTPERRFiles files. These are typically located under C:\Windows\system32\LogFiles\HTTPERR .PARAMETER OlderThanDays This defaults to 30 (days). When set to 30 for example, this function will delete web site log files older than 30 days. .PARAMETER LogFile Path to a file where this function will log its console output. .EXAMPLE Report-IISLogs This example will report on all log files of all websites on this computer .EXAMPLE Report-IISLogs -WebSiteName www.mydomain.com This example will report on IIS log files for the provided website on this computer .EXAMPLE $WebSiteLogReport = Report-IISLogs -DeleteLogFiles -OlderThanDays 120 This example will report on log files of all web sites on this computer, and delete log files older than 120 days. .EXAMPLE $WebSiteLogReport = Report-IISLogs -DeleteLogFiles -DeleteHTTPERRFiles -OlderThanDays 90 This example will report on log files of all web sites on this computer, delete log files older than 90 days, and delete log files under C:\Windows\system32\LogFiles\HTTPERR that are older than 90 days. .EXAMPLE Report-IISLogs | Export-Csv ".\Report-IISLogs_$($env:COMPUTERNAME)_$(Get-Date -Format 'ddMMMMyyyy_hh-mm-ss_tt').csv" -NoTypeInformation This example will report on the current server website log files and save them to CSV file .OUTPUTS This cmdlet returns a PS object collection such as: Name Id LogFolder LogFileCount TotalMB ---- -- --------- ------------ ------- domain1.com 7 C:\inetpub\logs\LogFiles\w3svc7 1749 1966.3 www.domain2.com 23 C:\inetpub\logs\LogFiles\w3svc23 1749 985.1 site.domain3.com 11 C:\inetpub\logs\LogFiles\w3svc11 579 229.7 www.domain4.com 2 C:\inetpub\logs\LogFiles\w3svc2 1749 125.2 .LINK https://superwidgets.wordpress.com/category/powershell/ .NOTES Function by Sam Boutros v0.1 - 9 May 2020 v0.2 - 9 October 2021 Added console progress reports, updated size calculation logic to speed the process up. Added OlderThanDays, DeleteLogFiles, DeleteHTTPERRFiles, and LogFile parameters #> [CmdletBinding(ConfirmImpact='Low')] Param( [Parameter(Mandatory=$false)][String[]]$WebSiteName, [Parameter(Mandatory=$false)][Switch]$DeleteLogFiles, [Parameter(Mandatory=$false)][Switch]$DeleteHTTPERRFiles, [Parameter(Mandatory=$false)][ValidateRange(1,3650)][Int16]$OlderThanDays = 30, [Parameter(Mandatory=$false)][String]$LogFile = ".\Report-IISLogs - $(Get-Date -Format 'ddMMMMyyyy_hh-mm-ss_tt').txt" ) Begin { Write-Verbose 'Report-IISLogs: received input:' Write-Verbose "WebSiteName: $WebSiteName" Write-Verbose "OlderThanDays: $OlderThanDays" Write-Verbose "DeleteLogFiles: $DeleteLogFiles" Write-Verbose "DeleteHTTPERRFiles: $DeleteHTTPERRFiles" Write-Verbose "LogFile: $LogFile" } Process { if ($WebSiteName) { $WebSiteInfo = foreach ($WebSite in $WebSiteName) { if ($Info = Get-Website -Name $WebSite) { $Info } else { Write-Log 'Report-IISLogs Error: web site',$WebSite,'not found' Magenta,Yellow,Magenta $LogFile } } } # If no $WebSiteName(s) are provided, or provided names do not exist, get a list of all web sites if (-not $WebSiteInfo) { Write-Log 'Gathering website info from IIS' Green $LogFile -NoNewLine try { $WebSiteInfo = Get-Website -EA 1 | Select Name,Id,@{n='LogFolder';e={$_.LogFile.Directory}} Write-Log 'done, obtained details on',$WebSiteInfo.Count,'websites' DarkYellow,Cyan,Green $LogFile } catch { Write-Log 'failed' Yellow $LogFile Write-Log 'Report-IISLogs Error:' Magenta $LogFile Write-Log $_.Exception.Message Yellow $LogFile Break } } #region Get log file details $myOutput = foreach ($WebSite in $WebSiteInfo) { Write-Log ' Listing web site',$WebSite.Name.PadRight(30),'log files' Green,Cyan,Green $LogFile -NoNewLine $LogFolder = "$($Website.LogFolder)\w3svc$($WebSite.id)".replace("%SystemDrive%",$env:SystemDrive) try { $LogFileList = Get-ChildItem $LogFolder -File -Force -EA 1 | select FullName,Length,CreationTime Write-Log 'identified',('{0:N0}' -f $LogFileList.Count).PadRight(10),'log files, totalling' DarkYellow,Cyan,Green $LogFile -NoNewLine } catch { Write-Log 'failed' Magenta $LogFile Write-Log $_.Exception.Message Yellow $LogFile } $TotalBytes = ($LogFileList | foreach { $_.Length } | measure -Sum).Sum Write-Log ('{0:N0}' -f ($TotalBytes/1MB)).PadRight(10),'MB' Cyan,Green $LogFile New-Object -TypeName PSObject -Property ([Ordered]@{ Name = $WebSite.Name Id = $WebSite.Id LogFolder = $LogFolder LogFileCount = $LogFileList.Count TotalMB = [Math]::Round($TotalBytes/1MB,1) LogfileList = $LogFileList }) } $myOutput = $myOutput | sort TotalMB -Descending Write-Log ($myOutput | FT Name,Id,LogFolder,LogFileCount,TotalMB -a | Out-String).Trim() Cyan $LogFile #endregion #region Delete log files older than $OlderThanDays days if ($DeleteLogFiles) { Write-Log 'Deleting web site log files older than',$OlderThanDays,'days',"(before $((Get-Date).AddDays(-$OlderThanDays)))" Green,Cyan,Green,Cyan $LogFile foreach ($WebSite in $myOutput) { Write-Log ' Processing web site',$Website.Name.PadRight(30) Green,Cyan $LogFile -NoNewLine $DeleteList = $WebSite.LogfileList | where CreationTime -lt (Get-Date).AddDays(-$OlderThanDays) if ($DeleteList) { Write-Log ' deleting',$DeleteList.Count,'old log files' Green,Cyan,Green $LogFile -NoNewLine Remove-Item $DeleteList.FullName -Force -Confirm:$false Write-Log 'done' DarkYellow $LogFile } else { Write-Log 'no old log files found.' DarkYellow $LogFile } } } #endregion #region Delete old files under C:\Windows\system32\LogFiles\HTTPERR if ($DeleteHTTPERRFiles) { $FolderPath = "$($env:ComSpec -replace 'cmd.exe')LogFiles\HTTPERR" Write-Log 'Deleting log files older than',$OlderThanDays,'days',"(before $((Get-Date).AddDays(-$OlderThanDays)))",'under',$FolderPath Green,Cyan,Green,Cyan,Green,Cyan $LogFile $DeleteList = Get-ChildItem -Path $FolderPath -File | select FullName,CreationTime | where CreationTime -lt (Get-Date).AddDays(-$OlderThanDays) if ($DeleteList) { Write-Log 'deleting',$DeleteList.Count,'old log files' Green,Cyan,Green $LogFile -NoNewLine Remove-Item $DeleteList.FullName -Force -Confirm:$false Write-Log 'done' DarkYellow $LogFile } else { Write-Log 'no old log files found.' DarkYellow $LogFile } } #endregion } End { $myOutput | select Name,Id,LogFolder,LogFileCount,TotalMB } } #endregion #region Security function Report-FailureAudit { <# .Synopsis Function to search and parse Windows Security EventLog for Failure Audit events .Description Function to search and parse Windows Security EventLog for Failure Audit events (EventID 4625, 5061, 140) .PARAMETER MaxCount If an integer value of this optional parameter is provided, this function will limit its search to the newest $MaxCount events of each of the Security and Microsoft-Windows-RemoteDesktopServices-RdpCoreTS/Operational event logs .PARAMETER LogFile Path to a file where this function will log its console output .Example Report-FailureAudit This example will return information of Failure Audit events in the Windows Security EventLog .Example Report-FailureAudit -MaxCount 10 -Verbose This example will return information of the 10 most recent Failure Audit events in the Windows Security EventLog .Example $EventList = Report-FailureAudit -MaxCount 4000 -LogFile "C:\myFolder\Report-FailureAudit_$(Get-Date -Format 'ddMMMMyyyy_hh-mm-ss_tt').txt" This example will return information of the 4000 most recent Failure Audit events .Example $LogFile = ".\Logs\Report-FailureAudit_$($env:COMPUTERNAME)_$(Get-Date -Format 'ddMMMMyyyy_hh-mm-ss_tt').txt" $CSVFile = ".\Reports\Report-FailureAudit_$($env:COMPUTERNAME)_$(Get-Date -Format 'ddMMMMyyyy_hh-mm-ss_tt').csv" $EventList = Report-FailureAudit -LogFile $LogFile $EventList | Export-Csv $CSVFile -NoTypeInformation This example will return information on Failure Audit events, and save them to CSV file .Example Summarize-FailureAudit -FailureAuditData (Report-FailureAudit -MaxCount 1000) -ReportFolder .\Reports This example will return information on top 1000 Failure Audit events, and display summary analysis to the console, and save summary analysis to CSV files under .\Reports folder such as: Summarize-FailureAudit_All_16April2020_04-22-39_PM.CSV ==> This file has all the records from Report-FailureAudit Summarize-FailureAudit_PerLogonType_16April2020_04-22-39_PM.CSV ==> This file has break down per Logon Type Summarize-FailureAudit_PerSourceIP_16April2020_04-22-39_PM.CSV ==> This file has break down per Source IP Summarize-FailureAudit_PerUserName_16April2020_04-22-39_PM.CSV ==> This file has break down per Attemptd Account Summarize-FailureAudit_PerLog_Security_16April2020_04-22-39_PM ==> This file has break down per Security Event Log Summarize-FailureAudit_PerLog_RdpCoreTS_16April2020_04-22-39_PM ==> This file has break down per rdpCoreTS Event Log .OUTPUTS PS Objects for each event such as: EventID : 4625 ComputerName : computername.domain.com LogName : Security Provider : EventType : Audit Failure LogonType : Network Account : \gvradmin SourceIP : 185.202.2.179 TimeCreated : 4/11/2020 10:12:46 PM .LINK https://superwidgets.wordpress.com/category/powershell/ https://superwidgets.wordpress.com/2020/04/17/using-powershell-to-report-on-failed-remote-desktop-logon-attempts/ .NOTES Function by Sam Boutros v0.1 - 12 April 2020 v0.2 - 14 April 2020 Updated summary reporting Added parsing for event 5061 in addition to event 4625 Added reading of event 140 of the Microsoft-Windows-RemoteDesktopServices-RdpCoreTS/Operational event log Added duration tracking of each processing section v0.3 - 15 April 2020 Read event details from $Event.ReplacementStrings instead of parsing $Event.Message Added source IP geolocation details in the IP summary section Known issues, future wish list: - Break off the reporting into a separate function ==> done in v0.4 - 15 April 2020 - Report to HTML - Function to remediate by setting/updating Windows firewall rule or Azure NSG - Function to schedule tasks like reporting/remediation ==> done in Update-WindowsFirewall - 17 April 2020 - Function to optimize Windows firewall rules by super-netting /32 IP entries when possible v0.4 - 15 April 2020 - Removed reporting into a separate function: Summarize-FailureAudit v0.5 - 17 April 2020 - Added code to report on Application event log event 18456 for SQL users failed logon v0.6 - 18 April 2020 - Added handling for RdpCoreTS log event Id 139 v0.7 - 23 April 2020 - Standardize on using Get-WinEvent with FilterHashTable v0.8 - 1 May 2020 - Added handling for Security event 4771 - Kerberos pre-authentication failed - Added feature to dump unrecognized failed logon audit events to text file #> [CmdletBinding(ConfirmImpact='Low')] Param( [Parameter(Mandatory=$false)][Int]$MaxCount, [Parameter(Mandatory=$false)][String]$LogFile = ".\Report-FailureAudit_$($env:COMPUTERNAME)_$(Get-Date -Format 'ddMMMMyyyy_hh-mm-ss_tt').txt" ) Begin { $StartTime = Get-Date function Get-LogonType ($LogonCode) { switch ($LogonCode) { 2 { 'Interactive' } 3 { 'Network' } 4 { 'Batch' } 5 { 'Service' } 7 { 'Unlock' } 8 { 'NetworkCleartext' } 9 { 'NewCredentials' } 10 { 'RemoteInteractive' } 11 { 'CachedInteractive' } default { $LogonCode } } } Write-Verbose "MaxCount: $MaxCount" Write-Verbose "LogFile: $LogFile" Write-Log 'Reading Security Event Log on computer',$env:COMPUTERNAME Green,Cyan $LogFile -NoNewLine $Duration = Measure-Command { try { $EventList = Get-WinEvent -EA 1 -FilterHashtable @{ logname = 'Security' Keywords = ([System.Diagnostics.Eventing.Reader.StandardEventKeywords]::AuditFailure).Value__ } if ($MaxCount) { $EventList = $EventList | select -First $MaxCount } } catch { if ($_.Exception.Message -match 'No events were found') { Write-Log 'No FailureAudit events found in Security Event Log for computer',$env:COMPUTERNAME Green,Cyan $LogFile } else { Write-Log 'Report-FailureAudit Error: unable to read Windows Security EventLog for computer',$env:COMPUTERNAME Magenta,Yellow $LogFile Write-Log 'This function needs to run under elevated permissions' DarkYellow $LogFile Write-Log $_.Exception.Message Magenta $LogFile } } } if ($EventList) { Write-Log '..','read',$EventList.Count,'events in',"$($Duration.Hours):$($Duration.Minutes):$($Duration.Seconds) (hh:mm:ss)" white,Green,Cyan,Green,DarkYellow $LogFile } Write-Log 'Reading ''RdpCoreTS/Operational'' Event Log on computer',$env:COMPUTERNAME Green,Cyan $LogFile -NoNewLine $Duration = Measure-Command { try { $RDPList = Get-WinEvent -EA 1 -FilterHashtable @{ logname = 'Microsoft-Windows-RemoteDesktopServices-RdpCoreTS/Operational' Id = 139,140 } if ($MaxCount) { $RDPList = $RDPList | select -First $MaxCount } } catch { if ($_.Exception.Message -match 'No events were found') { Write-Log 'No RDP 139/140 events found in RdpCoreTS Event Log for computer',$env:COMPUTERNAME Green,Cyan $LogFile } else { Write-Log 'Report-FailureAudit Error: unable to read Windows RdpCoreTS EventLog for computer',$env:COMPUTERNAME Magenta,Yellow $LogFile Write-Log 'This function needs to run under elevated permissions' DarkYellow $LogFile Write-Log $_.Exception.Message Magenta $LogFile } } } if ($RDPList) { Write-Log '..','read',$RDPList.Count,'events in',"$($Duration.Hours):$($Duration.Minutes):$($Duration.Seconds) (hh:mm:ss)" white,Green,Cyan,Green,DarkYellow $LogFile } Write-Log 'Reading ''SQL/Application'' Event Log on computer',$env:COMPUTERNAME Green,Cyan $LogFile -NoNewLine $Duration = Measure-Command { try { $SQLList = Get-WinEvent -EA 1 -FilterHashtable @{ logname = 'Application' Keywords = ([System.Diagnostics.Eventing.Reader.StandardEventKeywords]::AuditFailure).Value__ } if ($MaxCount) { $SQLList = $SQLList | select -First $MaxCount } } catch { if ($_.Exception.Message -match 'No events were found') { Write-Log 'No FailureAudit events found in Application Event Log for computer',$env:COMPUTERNAME Green,Cyan $LogFile } else { Write-Log 'Report-FailureAudit Error: unable to read Windows Application EventLog for computer',$env:COMPUTERNAME Magenta,Yellow $LogFile Write-Log 'This function needs to run under elevated permissions' DarkYellow $LogFile Write-Log $_.Exception.Message Magenta $LogFile } } } if ($SQLList) { Write-Log '..','read',$SQLList.Count,'events in',"$($Duration.Hours):$($Duration.Minutes):$($Duration.Seconds) (hh:mm:ss)" white,Green,Cyan,Green,DarkYellow $LogFile } } Process { $myOutput = $OutOfReportEvents = @() if ($EventList) { $EventList = $EventList | sort TimeCreated Write-Log 'Processing Security Log events 4625 and 5061 on computer',$env:COMPUTERNAME Green,Cyan $LogFile -NoNewLine $Duration = Measure-Command { $myOutput += foreach ($Event in $EventList) { Switch ($Event.Id) { 4625 { $Temp1 = Parse-String -InputString $Event.Message -StartMarker 'Account For Which Logon Failed:' -EndMarker 'Failure Reason:' $AccountName = Parse-String -InputString $Temp1 -StartMarker 'Account Name:' -EndMarker 'Account Domain:' $AccountDomain = Parse-String -InputString $Temp1 -StartMarker 'Account Domain:' -EndMarker 'Failure Information:' [PSCustomObject][Ordered]@{ EventID = $Event.Id ComputerName = $Event.MachineName LogName = $Event.LogName Provider = $Event.ProviderName EventType = $Event.KeywordsDisplayNames -join ', ' LogonType = Get-LogonType (Parse-String -InputString $Event.Message -StartMarker 'Logon Type:' -EndMarker 'Account For Which Logon Failed:') Account = "$AccountDomain\$AccountName" SourceIP = Parse-String -InputString $Event.Message -StartMarker 'Source Network Address:' -EndMarker 'Source Port:' TimeCreated = $Event.TimeCreated } } 4771 { $AccountName = Parse-String -InputString $Event.Message -StartMarker 'Account Name:' -EndMarker 'Service Information:' $AccountDomain = ((Parse-String -InputString $Event.Message -StartMarker 'Service Name:' -EndMarker 'Network Information:') -split '/')[1] [PSCustomObject][Ordered]@{ EventID = $Event.Id ComputerName = $Event.MachineName LogName = $Event.LogName Provider = $Event.ProviderName EventType = $Event.KeywordsDisplayNames -join ', ' LogonType = 'Kerberos pre-authentication' Account = "$AccountDomain\$AccountName" SourceIP = Parse-String -InputString $Event.Message -StartMarker 'Client Address:' -EndMarker 'Client Port:' TimeCreated = $Event.TimeCreated } } 5061 { $AccountName = Parse-String -InputString $Event.Message -StartMarker 'Account Name:' -EndMarker 'Account Domain:' $AccountDomain = Parse-String -InputString $Event.Message -StartMarker 'Account Domain:' -EndMarker 'Logon ID:' [PSCustomObject][Ordered]@{ EventID = $Event.Id ComputerName = $Event.MachineName LogName = $Event.LogName Provider = $Event.ProviderName EventType = $Event.KeywordsDisplayNames -join ', ' LogonType = "Not reported in event $($Event.Id)" Account = "$AccountDomain\$AccountName" SourceIP = "Not reported in event $($Event.Id)" TimeCreated = $Event.TimeCreated } } Default { Write-Log 'Report-FailureAudit: Encountered unknown FailureAudit Event: ID', $Event.Id Yellow,Cyan $LogFile $OutOfReportEvents += $Event } } } } Write-Log '..','done in',"$($Duration.Hours):$($Duration.Minutes):$($Duration.Seconds) (hh:mm:ss)" white,Green,DarkYellow $LogFile } else { Write-Log 'No events of type FailureAudit found in the Windows Security EventLog' Green $LogFile } if ($RDPList) { $RDPList = $RDPList | sort TimeCreated Write-Log 'Processing ''RdpCoreTS/Operational'' Log events 139/140 on computer',$env:COMPUTERNAME Green,Cyan $LogFile -NoNewLine $Duration = Measure-Command { $myOutput += foreach ($Event in $RDPList) { Switch ($Event.Id) { 139 { [PSCustomObject][Ordered]@{ EventID = $Event.Id ComputerName = $Event.MachineName LogName = $Event.LogName Provider = $Event.ProviderName EventType = $( if ($Event.KeywordsDisplayNames) { $Event.KeywordsDisplayNames -join ', ' } else { ($EventKeyWords | where Number -EQ $Event.Keywords).Name } ) LogonType = $( if ($Event.UserId -eq 'S-1-5-20') { 'Network' } else { $Event.UserId # "Not reported in event $($Event.Id)" } ) Account = "Not reported in event $($Event.Id)" SourceIP = Parse-String -InputString $Event.Message -StartMarker ([Regex]::Escape('Client IP:')) -EndMarker ([Regex]::Escape(') has been disconnected')) TimeCreated = $Event.TimeCreated } } 140 { [PSCustomObject][Ordered]@{ EventID = $Event.Id ComputerName = $Event.MachineName LogName = $Event.LogName Provider = $Event.ProviderName EventType = $( if ($Event.KeywordsDisplayNames) { $Event.KeywordsDisplayNames -join ', ' } else { ($EventKeyWords | where Number -EQ $Event.Keywords).Name } ) LogonType = $( if ($Event.UserId -eq 'S-1-5-20') { 'Network' } else { $Event.UserId # "Not reported in event $($Event.Id)" } ) Account = "Not reported in event $($Event.Id)" SourceIP = Parse-String -InputString $Event.Message -StartMarker ([Regex]::Escape('IP address of')) -EndMarker ([Regex]::Escape('failed because')) TimeCreated = $Event.TimeCreated } } } } } Write-Log '..','done in',"$($Duration.Hours):$($Duration.Minutes):$($Duration.Seconds) (hh:mm:ss)" white,Green,DarkYellow $LogFile } else { Write-Log 'No Events 139/140 found in the ''RdpCoreTS/Operational'' EventLog' Green $LogFile } if ($SQLList) { $SQLList = $SQLList | sort TimeCreated Write-Log 'Processing Application Log event 18456 on computer',$env:COMPUTERNAME Green,Cyan $LogFile -NoNewLine $Duration = Measure-Command { $myOutput += foreach ($Event in $SQLList) { Switch ($Event.Id) { 18456 { [PSCustomObject][Ordered]@{ EventID = $Event.Id ComputerName = $Event.MachineName LogName = $Event.LogName Provider = $Event.ProviderName EventType = $( if ($Event.KeywordsDisplayNames) { $Event.KeywordsDisplayNames -join ', ' } else { ($EventKeyWords | where Number -EQ $Event.Keywords).Name } ) LogonType = $( if ($Event.UserId -eq 'S-1-5-20') { 'Network' } else { $Event.UserId # "Not reported in event $($Event.Id)" } ) Account = Parse-String -InputString $Event.Message -StartMarker 'user ''' -EndMarker '''. Reason' SourceIP = Parse-String -InputString $Event.Message -StartMarker '\[CLIENT:' -EndMarker '\]' TimeCreated = $Event.TimeCreated } } Default { Write-Log 'Report-FailureAudit: Encountered unknown FailureAudit Event: ID', $Event.Id Yellow,Cyan $LogFile $OutOfReportEvents += $Event } } } } Write-Log '..','done in',"$($Duration.Hours):$($Duration.Minutes):$($Duration.Seconds) (hh:mm:ss)" white,Green,DarkYellow $LogFile } else { Write-Log 'No events of type FailureAudit found in the Windows Application EventLog' Green $LogFile } } End { if ($myOutput) { $myOutput = $myOutput | sort TimeCreated -Descending $myOutput } if ($OutOfReportEvents) { $OutOfReportEvents = $OutOfReportEvents | sort TimeCreated -Descending $FileName = (Get-Item $LogFile).FullName.Replace('Report-FailureAudit_','Report-FailureAudit_OutOfReportEvents_') $OutOfReportEvents | FL * | Out-String | Out-File $FileName -Force Write-Log $OutOfReportEvents.Count,'Unrecognized events dumped to file:',$OutOfReportEvents Cyan,Green,Cyan $LogFile } } } function Summarize-FailureAudit { <# .SYNOPSIS Function to provide summary report on data returned from Report-FailureAudit function .DESCRIPTION Function to provide summary report on data returned from Report-FailureAudit function This function is designed to aggregate reporting on multiple computers in the same environment Summary reporting is provided by: Event Log: Security and RDP (Microsoft-Windows-RemoteDesktopServices-RdpCoreTS/Operational) Source IP: with the most frequent ones on top Logon Type: such as Network/Interactive/... with the most frequent ones on top Attempted User Name: with the most frequent ones on top .PARAMETER FailureAuditData PS Objects returned from Report-FailureAudit function containing the following required properties: Account ComputerName EventID EventType LogName LogonType Provider SourceIP TimeCreated .PARAMETER ShowTop Optional parameter containing the count of records to report on. Such as show top 10 most frequent IP addresses. This defaults to 10. .PARAMETER ReportFolder Path to a folder where this function will save its CSV output reports .PARAMETER LogFile Optional parameter containing the path to a file to which this function logs its console output .Example Summarize-FailureAudit -FailureAuditData (Report-FailureAudit -MaxCount 1000) -ReportFolder .\Reports This example will return information on top 1000 Failure Audit events, and display summary analysis to the console, and save summary analysis to CSV files under .\Reports folder such as: Summarize-FailureAudit_All_16April2020_04-22-39_PM.CSV ==> This file has all the records from Report-FailureAudit Summarize-FailureAudit_PerLogonType_16April2020_04-22-39_PM.CSV ==> This file has break down per Logon Type Summarize-FailureAudit_PerSourceIP_16April2020_04-22-39_PM.CSV ==> This file has break down per Source IP Summarize-FailureAudit_PerUserName_16April2020_04-22-39_PM.CSV ==> This file has break down per Attempted Account Summarize-FailureAudit_PerLog_Security_16April2020_04-22-39_PM ==> This file has break down per Security Event Log Summarize-FailureAudit_PerLog_RdpCoreTS_16April2020_04-22-39_PM ==> This file has break down per rdpCoreTS Event Log .LINK https://superwidgets.wordpress.com/category/powershell/ https://superwidgets.wordpress.com/2020/04/17/using-powershell-to-report-on-failed-remote-desktop-logon-attempts/ .NOTES Function by Sam Boutros v0.1 - 12 April 2020 v0.2 - 17 April 2020 - Updated to summarize SQL/Application log events v0.3 - 23 April 2020 - Removed SourceName property and added Provider v0.4 - 29 April 2020 - Lookup a maximum of 3 IP locations - IP Location API will lock out source IP if sending too many requests #> [CmdletBinding(ConfirmImpact='Low')] Param( [Parameter(Mandatory=$true)][PSCustomObject[]]$FailureAuditData, [Parameter(Mandatory=$false)][Int]$ShowTop = 10, [Parameter(Mandatory=$false)][Switch]$PerLog, [Parameter(Mandatory=$false)][Switch]$PerSourceIP, [Parameter(Mandatory=$false)][Switch]$PerLogonType, [Parameter(Mandatory=$false)][Switch]$PerUserName, [Parameter(Mandatory=$false)][ValidateScript({Test-Path $_})][String]$ReportFolder = '.\', [Parameter(Mandatory=$false)][String]$LogFile = ".\Summarize-FailureAudit_$(Get-Date -Format 'ddMMMMyyyy_hh-mm-ss_tt').txt" ) Begin { # Validate PS Objects' required properties $RequiredProperties = @('Account','ComputerName','EventID','EventType','LogName','LogonType','Provider','SourceIP','TimeCreated') $ProvidedProperties = ($FailureAuditData | select -First 1 | Get-Member -MemberType NoteProperty).Name $MissingProperties = foreach ($Property in $RequiredProperties) { if ($Property -notin $ProvidedProperties) { $Property } } # If none of the individual summaries is selected, select them all if (-not($PerLog-and$PerSourceIP-and$PerLogonType-and$PerUserName)) { $All = $true } Write-Verbose "FailureAuditData: $($FailureAuditData.Count)" Write-Verbose "ShowTop: $ShowTop" Write-Verbose "PerLog: $PerLog" Write-Verbose "PerSourceIP: $PerSourceIP" Write-Verbose "PerLogonType: $PerLogonType" Write-Verbose "PerUserName: $PerUserName" Write-Verbose "ReportFolder: $ReportFolder" Write-Verbose "LogFile: $LogFile" if ($MissingProperties) { Write-Log 'Summarize-FailureAudit Error: missing one or more input object properties:' Magenta $LogFile Write-Log 'Missing properties:',($MissingProperties -join ',') Magenta,Yellow $LogFile Write-Log 'Expected properties:',($RequiredProperties -join ',') Green,Cyan $LogFile Write-Log 'Provided properties:',($ProvidedProperties -join ',') Green,Yellow $LogFile break } } Process { Write-Log 'Processing summary report' Green $LogFile -NoNewLine $TimeStamp = Get-Date -Format 'ddMMMMyyyy_hh-mm-ss_tt' $FailureAuditData = $FailureAuditData | sort TimeCreated if ($PerLog -or $All) { $EventList = $FailureAuditData | where LogName -EQ Security if ($EventList) { $EventList = $EventList | sort TimeCreated $LastHour = $EventList | where TimeCreated -GT (Get-Date $EventList[-1].TimeCreated).AddHours(-1) $LD = New-TimeSpan -Start $EventList[0].TimeCreated -End $EventList[-1].TimeCreated $SecurityEventSummary = [PSCustomObject][Ordered]@{ EventCount = '{0:N0}' -f $EventList.Count FirstEventTime = $EventList[0].TimeCreated LastEventTime = $EventList[-1].TimeCreated Duration = "$($LD.Days):$($LD.Hours):$($LD.Minutes):$($LD.Seconds) (dd:hh:mm:ss)" AttemptsPerHour = '{0:N0}' -f ($EventList.Count/$LD.TotalHours) AttemptsLastHour = '{0:N0}' -f ($LastHour.Count) EventLog = 'Security' EventType = $(($EventList.EventType | select -Unique) -join ', ') EventId = $(($EventList.EventId | select -Unique) -join ', ') } Write-Host ' ' Write-Log 'Security Event summary:' Green $LogFile Write-Log ($SecurityEventSummary | FL * | Out-String).Trim() Cyan $LogFile $ReportFile = "$ReportFolder\Summarize-FailureAudit_PerLog_Security_$TimeStamp.CSV" $SecurityEventSummary | Export-Csv $ReportFile -NoTypeInformation Write-Log 'Security Event summary exported to',$ReportFile Green,Cyan $LogFile } else { Write-Log 'No Failure Audit Events found in Security event log' Green $LogFile } $RDPList = $FailureAuditData | where LogName -EQ RdpCoreTS if ($RDPList) { $RDPList = $RDPList | sort TimeCreated $LastHour = $RDPList | where TimeCreated -GT (Get-Date $RDPList[-1].TimeCreated).AddHours(-1) $LD = New-TimeSpan -Start $RDPList[0].TimeCreated -End $RDPList[-1].TimeCreated $RDPEventSummary = [PSCustomObject][Ordered]@{ EventCount = '{0:N0}' -f $RDPList.Count FirstEventTime = $RDPList[0].TimeCreated LastEventTime = $RDPList[-1].TimeCreated Duration = "$($LD.Days):$($LD.Hours):$($LD.Minutes):$($LD.Seconds) (dd:hh:mm:ss)" AttemptsPerHour = '{0:N0}' -f ($RDPList.Count/$LD.TotalHours) AttemptsLastHour = '{0:N0}' -f ($LastHour.Count) EventLog = 'RdpCoreTS' EventType = $(($RDPList.EventType | select -Unique) -join ', ') EventId = $(($RDPList.EventId | select -Unique) -join ', ') } Write-Host ' ' Write-Log 'RDP Event summary:' Green $LogFile Write-Log ($RDPEventSummary | FL * | Out-String).Trim() Cyan $LogFile $ReportFile = "$ReportFolder\Summarize-FailureAudit_PerLog_RdpCoreTS_$TimeStamp.CSV" $RDPEventSummary | Export-Csv $ReportFile -NoTypeInformation Write-Log 'RdpCoreTS Event summary exported to',$ReportFile Green,Cyan $LogFile } else { Write-Log 'No Failure Audit Events found in RdpCoreTS event log' Green $LogFile } $SQLList = $FailureAuditData | where LogName -EQ Application if ($SQLList) { $SQLList = $SQLList | sort TimeCreated $LastHour = $SQLList | where TimeCreated -GT (Get-Date $SQLList[-1].TimeCreated).AddHours(-1) $LD = New-TimeSpan -Start $SQLList[0].TimeCreated -End $SQLList[-1].TimeCreated $SQLEventSummary = [PSCustomObject][Ordered]@{ EventCount = '{0:N0}' -f $SQLList.Count FirstEventTime = $SQLList[0].TimeCreated LastEventTime = $SQLList[-1].TimeCreated Duration = "$($LD.Days):$($LD.Hours):$($LD.Minutes):$($LD.Seconds) (dd:hh:mm:ss)" AttemptsPerHour = '{0:N0}' -f ($SQLList.Count/$LD.TotalHours) AttemptsLastHour = '{0:N0}' -f ($LastHour.Count) EventLog = 'Application' EventType = $(($SQLList.EventType | select -Unique) -join ', ') EventId = $(($SQLList.EventId | select -Unique) -join ', ') } Write-Host ' ' Write-Log 'SQL/Application Event summary:' Green $LogFile Write-Log ($SQLEventSummary | FL * | Out-String).Trim() Cyan $LogFile $ReportFile = "$ReportFolder\Summarize-FailureAudit_PerLog_SQL-Application_$TimeStamp.CSV" $SQLEventSummary | Export-Csv $ReportFile -NoTypeInformation Write-Log 'SQL/Application Event summary exported to',$ReportFile Green,Cyan $LogFile } else { Write-Log 'No Failure Audit Events found in Application event log' Green $LogFile } } if ($PerSourceIP -or $All) { $i=0 # Lookup a maximum of 3 IP locations - IP Location API will lock out source IP if sending too many requests $SourceIP = foreach ($Group in ($FailureAuditData | where { $_.SourceIP } | group SourceIP)) { $i++ if ($i -le 3) { $IPLocation = Get-IPLocation $Group.Name } else { Remove-Variable IPLocation -Force -EA 0 } [PSCustomObject][Ordered]@{ IPAddress = $Group.Name ReverseDNS = $IPLocation.ReverseDNS IPLocation = $( if ($IPLocation) { "$($IPLocation.City), $($IPLocation.Region), $($IPLocation.ZipCode) - $($IPLocation.Country) ($($IPLocation.Coords))" } ) IPOrg = $IPLocation.Org IPTimeZone = $IPLocation.TimeZone AttemptCount = $Group.Count Percent = ($Group.Count/$FailureAuditData.Count).tostring("P") } } $SourceIP = $SourceIP | sort AttemptCount -Descending Write-Host ' ' Write-Log "Source IP summary (Top $ShowTop):" Green $LogFile Write-Log ($SourceIP | select -First $ShowTop | FL * | Out-String).Trim() Cyan $LogFile $ReportFile = "$ReportFolder\Summarize-FailureAudit_PerSourceIP_$TimeStamp.CSV" $SourceIP | Export-Csv $ReportFile -NoTypeInformation Write-Log 'Source IP summary exported to',$ReportFile Green,Cyan $LogFile } if ($PerLogonType -or $All) { $LogonType = $FailureAuditData | where { $_.LogonType } | group LogonType | select @{n='LogonType';e={$_.Name}}, @{n='AttemptCount';e={$_.Count}}, @{n='Percent';e={($_.Count/$FailureAuditData.Count).tostring("P")}} | sort AttemptCount -Descending Write-Host ' ' Write-Log "Logon Attempt Type summary (Top $ShowTop):" Green $LogFile Write-Log ($LogonType | select -First $ShowTop | FT -a | Out-String).Trim() Cyan $LogFile $ReportFile = "$ReportFolder\Summarize-FailureAudit_PerLogonType_$TimeStamp.CSV" $LogonType | Export-Csv $ReportFile -NoTypeInformation Write-Log 'Logon Type summary exported to',$ReportFile Green,Cyan $LogFile } if ($PerUserName -or $All) { $Account = $FailureAuditData | where { $_.Account } | group Account | sort count -Descending | select @{n='Account';e={$_.Name}},@{n='AttemptCount';e={$_.Count}}, @{n='Percent';e={($_.Count/$FailureAuditData.Count).tostring("P")}} | sort AttemptCount -Descending Write-Host ' ' Write-Log "Attempted Account summary (Top $ShowTop):" Green $LogFile Write-Log ($Account | select -First $ShowTop | FT -a | Out-String).Trim() Cyan $LogFile $ReportFile = "$ReportFolder\Summarize-FailureAudit_PerUserName_$TimeStamp.CSV" $Account | Export-Csv $ReportFile -NoTypeInformation Write-Log 'User Name summary exported to',$ReportFile Green,Cyan $LogFile } if ($All) { $ReportFile = "$ReportFolder\Summarize-FailureAudit_All_$TimeStamp.CSV" $FailureAuditData | Export-Csv $ReportFile -NoTypeInformation Write-Log 'All records exported to',$ReportFile Green,Cyan $LogFile } Write-Host ' ' Write-Log 'Latest',$ShowTop,'attempts:' Green,Cyan,Green $LogFile Write-Log ($FailureAuditData | select -Last $ShowTop | select EventId,ComputerName,LogName,Account,SourceIP,TimeCreated | sort TimeCreated -Descending | FT -a | Out-String).Trim() Cyan $LogFile } End { } } function Update-WindowsFirewall { <# .SYNOPSIS Function to create/update Windows firewall rule to block 1 or more IP addresses .DESCRIPTION Function to create/update Windows firewall rule to block 1 or more IP addresses .PARAMETER BlockIPList One or more IP addresses to block This can be a dotted decimal IPv4 address such as 123.45.67.89, or in CIDR notation such as 123.45.67.0/24 .PARAMETER AllowIPList One or more IP addresses to ensure are not blocked by this firewall rule This can be a dotted decimal IPv4 address such as 123.45.67.89, or in CIDR notation such as 123.45.67.0/24 This function is capable of recognizing and allowing an IP if its subnet is listed under this parameter. For example, if the BlockIPList parameter included '10.11.22.33' and the AllowIPList parameter included a subnet like 10.11.22.0/24 or 10.11.22.0/26, this function will recognize 10.11.22.33 as part of a subnet to be allowed, and as such it will not be blocked. Furthermore, if '10.11.22.33' already exists in this firewall rule, it will removed. .PARAMETER RuleName Name of the firewall rule to be created/updated. This defaults to 'BlockAttackers' .PARAMETER LogFile Path to a file where this function will log its console output .EXAMPLE Update-WindowsFirewall -BlockIPList '10.2.3.4' .EXAMPLE $BlockIPList = (Get-ChildItem -Path .\ -Filter Summarize-FailureAudit_All*.csv | foreach { Import-Csv $_.FullName }).SourceIP | select -Unique | sort $AllowIPList = @( '123.45.67.48/29' # My WAN subnet '10.0.1.0/16' # My LAN subnet (Resolve-DnsName -Name someallowedhost.domain.com).IPAddress '123.45.67.89' # Some known remote user IP ) $BlockedIPs = Update-WindowsFirewall -BlockIPList $BlockIPList -AllowIPList $AllowIPList -Verbose The first line of this example searches for CSV reports generated by the Summarize-FailureAudit function in the current folder, imports the SourceIP column, and deduplicates the IP List. The next line lists a bunch of allowed IPs and subnets. The last line uses the $BlockIPList and $AllowIPList as input to create/update a firewall rule to block the attacking IPs. Using the $AllowIPList ensures that ligitimate IPs are not blocked if they show up in the logs due to occasional failed logon. .OUTPUTS This cmdlet returns one or more Dotted Decimal string notations of the blocked IP addresses/subnets such as 185.209.0.20 185.209.0.68 185.231.71.184 185.56.90.90 186.202.178.2 186.91.191.103 186.95.172.116 .LINK https://superwidgets.wordpress.com/category/powershell/ .NOTES Function by Sam Boutros v0.1 - 17 April 2020 v0.2 - 18 April 2020 Added Exclude parameter Added accepting CIDR ranges in addition to individual IPs for IPAddress and Exclude paramters #> [CmdletBinding(ConfirmImpact='Low')] Param( [Parameter(Mandatory=$false)][Alias('IPAddress')][String[]]$BlockIPList, [Parameter(Mandatory=$false)][Alias('Exclude')][String[]]$AllowIPList, [Parameter(Mandatory=$false)][String]$RuleName = 'BlockAttackers', [Parameter(Mandatory=$false)][String]$LogFile = ".\Update-WindowsFirewall_$($env:COMPUTERNAME)_$(Get-Date -Format 'ddMMMMyyyy_hh-mm-ss_tt').txt" ) Begin { Write-Verbose "IPAddress: $($BlockIPList -join ', ')" Write-Verbose "Exclude : $($AllowIPList -join ', ')" Write-Verbose "RuleName : $RuleName" Write-Verbose "LogFile : $LogFile" # Validate IP addresses: $BlockIPList = $BlockIPList | where { $_ } # Remove blanks $IPList = foreach ($IP in $BlockIPList) { if ($IP -as [IPAddress]) { $IP } elseif ($CIDR = Get-IPv4Details -CIDRAddress $IP) { $CIDR.NetCIDR } } $ExcludeList = foreach ($IP in $AllowIPList) { if ( $IP -as [IPAddress] ) { $IP } elseif ($CIDR = Get-IPv4Details -CIDRAddress $IP) { $CIDR.NetCIDR 0..($CIDR.SubnetMaximumHosts-1) | foreach { Next-IP -IPAddress $CIDR.FirstSubnetIP -Increment $_ } # Expand CIDR } } } Process { if ($IPList) { $Description = "Rule to deny access to a list of IP addesses and subnets. " $Description += "This rule is set by Update-WindowsFirewall PS function of the AZSBTools PS Module " $Description += "which was last invoked on '$(Get-Date -Format 'dd MMMM yyyy, hh:mm:ss tt')' " $Description += "by '$($env:USERDOMAIN)\$($env:USERNAME)'" if ($BlockRule = Get-NetFirewallRule | where DisplayName -EQ $RuleName) { Write-Log 'Identified Block rule in Windows firewall:' Green $LogFile Write-Log ($BlockRule | FL DisplayName,Enabled,Profile,Direction,Action | Out-String).Trim() Cyan $LogFile if ($RemoteAddressList = $BlockRule | Get-NetFirewallAddressFilter) { Write-Log ' blocking',$RemoteAddressList.RemoteIP.Count,'address(es)' Green,Cyan,Green $LogFile $UpdatedList = @() $UpdatedList += $RemoteAddressList.RemoteIP $UpdatedList += $IPList $UpdatedList = $UpdatedList | select -Unique | sort $UpdatedList = foreach ($IP in $UpdatedList) { if ($IP -notin $ExcludeList) { $IP } } # Remove ExcludeList IPs Write-Log ' Updating IP list, now',$UpdatedList.Count,'address(es)' Green,Cyan,Green $LogFile $BlockRule | Set-NetFirewallRule -RemoteAddress $UpdatedList -NewDisplayName $RuleName -Enabled True -Profile Any -Direction Inbound -Action Block -Description $Description Write-Verbose 'Blocked IPs:' Write-Verbose ($UpdatedList|Out-String).trim() } else { $UpdatedList = foreach ($IP in $IPList) { if ($IP -notin $ExcludeList) { $IP } } # Remove ExcludeList IPs Write-Log ' Updating IP list, now',$UpdatedList.Count,'address(es)' Green,Cyan,Green $LogFile $BlockRule | Set-NetFirewallRule -RemoteAddress $UpdatedList -NewDisplayName $RuleName -Enabled True -Profile Any -Direction Inbound -Action Block -Description $Description Write-Verbose 'Blocked IPs:' Write-Verbose ($UpdatedList|Out-String).trim() } } else { $UpdatedList = foreach ($IP in $IPList) { if ($IP -notin $ExcludeList) { $IP } } # Remove ExcludeList IPs Write-Log 'No Block rule found in Windows firewall, adding',$UpdatedList.Count,'address(es)' Yellow,Cyan,Green $LogFile New-NetFirewallRule -RemoteAddress $UpdatedList -Name $RuleName -DisplayName $RuleName -Enabled True -Direction Inbound -Profile Any -Action Block -Description $Description Write-Verbose 'Blocked IPs:' Write-Verbose ($UpdatedList|Out-String).trim() } } else { Write-Log 'Update-WindowsFirewall: No IP addresses provided in input' Yellow $LogFile } } End { $UpdatedList } } function Block-FailedLogonIPs { <# .SYNOPSIS Function to automate blocking the IPs/subnets of failed Windows and SQL logon attempts .DESCRIPTION Function to automate blocking the IPs/subnets of failed Windows and SQL logon attempts Using the default parameter values, this function will: - Create Logs and Reports folders under its current location, with _Archive subfolder under each - Schedule itself to run hourly (under LocalSystem context) if not already scheduled - Read and parse Security and RDP (Microsoft-Windows-RemoteDesktopServices-RdpCoreTS/Operational) event logs for failed Windows logon events - Read and parse Application event log for failed SQL logon events - Summarize the data in 6 time-stamped CSV reports under the Reports folder - Combine and deduplicate the IP list from the above reports - Create/update a windows firewall rule to block these IPs, ensuring the IPs/subnets in the AllowIPList parameter are not blocked - Clear the Security, RDP, and Application event logs for faster processing next hour - Archive the Log and Report files under the corresponding _Archive folders .PARAMETER AllowIPList One or more IPs or subnets For example 123.45.67.89 or/and 10.20.30.0/24 This function adds the local LAN subnet(s) to this list .PARAMETER ScheduleHourly Optional switch parameter When set to True this function will schedule itself to run hourly .PARAMETER WorkFolder Optional parameter that defaults to current folder This function will create/validate the following folders under this folder: .\Logs .\Reports .\Logs\_Archive .\Reports\_Archive .PARAMETER ClearRdpCoreTSEventLog Optional switch parameter that defaults to True When set to True this function will clear the Microsoft-Windows-RemoteDesktopServices-RdpCoreTS/Operational Event Log after reading and analysing its events Before clearing the event log, this function will back it up under $WorkFolder\Logs .PARAMETER ClearSecurityEventLog Optional switch parameter that defaults to True When set to True this function will clear the Scruity Event Log after reading and analysing its events Before clearing the event log, this function will back it up under $WorkFolder\Logs .PARAMETER ClearApplicationEventLog Optional switch parameter that defaults to True When set to True this function will clear the Application Event Log after reading and analysing its events Before clearing the event log, this function will back it up under $WorkFolder\Logs .EXAMPLE $myScriptRoot = 'C:\Sandbox' # Change this line as needed New-Item $myScriptRoot -ItemType Directory -EA 0 | Out-Null # Create Script folder if not exist @' Block-FailedLogonIPs -WorkFolder $myScriptRoot -AllowIPList @( '22.33.44.55' # Trusted end point '10.1.2.0/24' # Trusted Local Subnet '123.45.67.48/29' # Trusted subnet 1 ) # -ScheduleHourly # Use this switch on the first run to schedule this script to run hourly '@ | Out-File "$myScriptRoot\Block-Attackers.ps1" ise "$myScriptRoot\Block-Attackers.ps1" # Review the file and invoke manually in ISE # & "$myScriptRoot\Block-Attackers.ps1" # Or invoke it now This example creates and invokes Block-Attackers.ps1 script which invokes this Block-FailedLogonIPs function abd self-schedules to run hourly. .LINK https://superwidgets.wordpress.com/category/powershell/ .NOTES Function by Sam Boutros v0.1 - 21 April 2020 v0.2 - 24 April 2020 - Added Verbose output, changed default value for switch ScheduleHourly to False v0.3 - 29 April 2020 - Added ClearRdpCoreTSEventLog, WorkFolder parameters v0.4 - 30 April 2020 - Added code to not archive empty Windows event logs #> [CmdletBinding(ConfirmImpact='Low')] Param( [Parameter(Mandatory=$false)][String[]]$AllowIPList, [Parameter(Mandatory=$false)][String]$WorkFolder = (Get-Location).Path, [Parameter(Mandatory=$false)][Switch]$ScheduleHourly, [Parameter(Mandatory=$false)][Switch]$ClearRdpCoreTSEventLog = $true, [Parameter(Mandatory=$false)][Switch]$ClearSecurityEventLog = $true, [Parameter(Mandatory=$false)][Switch]$ClearApplicationEventLog = $true ) Begin { if (-not $AllowIPList) { # Add local subnet(s) $AllowIPList = Get-NetIPAddress -AddressFamily IPv4 -PrefixOrigin Manual,DHCP | foreach { Write-Verbose "Adding local subnet ($($_.IPAddress + '/' + $_.PrefixLength)) to (AllowIPList)" $_.IPAddress + '/' + $_.PrefixLength } } $ThisFile = $MyInvocation.ScriptName # FullName $thisCommand = ($ThisFile | Split-Path -Leaf).Replace('.ps1','') $LogFile = "$WorkFolder\Logs\$thisCommand-$($env:COMPUTERNAME)-$(Get-Date -Format 'ddMMMMyyyy_hh-mm-ss_tt').txt" Write-Verbose "Block-FailedLogonIPs (AllowIPList): $($AllowIPList -join ', ')" Write-Verbose "Block-FailedLogonIPs (ScheduleHourly): $ScheduleHourly" Write-Verbose "Block-FailedLogonIPs (ClearSecurityEventLog): $ClearSecurityEventLog" Write-Verbose "Block-FailedLogonIPs (ClearApplicationEventLog): $ClearApplicationEventLog" Write-Verbose "Block-FailedLogonIPs (LogFile): $LogFile" function Backup-thisLog($LogName,$WorkFolder,$LogFile){ $EventSession = New-Object System.Diagnostics.Eventing.Reader.EventLogSession $LogInfo = $EventSession.GetLogInformation("$LogName",'LogName') if ($LogInfo.RecordCount -gt 1) { Backup-EventLog -EventLogName $LogName -BackupFolder "$WorkFolder\Logs" -LogFile $LogFile Clear-SBEventLog -EventLogName $LogName -LogFile $LogFile -Confirm:$false } else { Write-Log 'Windows event log',$LogName,'has',$LogInfo.RecordCount,'records, skipping..' Green,Cyan,Green,Cyan,Green $LogFile } } } Process { #region Initialize New-Item -Path "$WorkFolder\Logs" -ItemType Directory -Force -EA 0 | Out-Null New-Item -Path "$WorkFolder\Reports" -ItemType Directory -Force -EA 0 | Out-Null New-Item -Path "$WorkFolder\Logs\_Archive" -ItemType Directory -Force -EA 0 | Out-Null New-Item -Path "$WorkFolder\Reports\_Archive" -ItemType Directory -Force -EA 0 | Out-Null if ($ScheduleHourly) { $StartAt = (Get-Date).AddMinutes(50).Hour.ToString().PadLeft(2,'0') + ':' + (Get-Date).AddMinutes(50).Minute.ToString().PadLeft(2,'0') $Result = SCHTasks /Create /RU System /SC HOURLY /TN "PowerShell-$thisCommand" /TR "PowerShell $ThisFile" /ST $StartAt /RL HIGHEST /F if ($Result -match 'SUCCESS') { Write-Log $Result Cyan $LogFile } else { Write-Log $Result Yellow $LogFile } } #endregion #region Check logs, clear event logs, update firewall rules, archive logs/reports $EventList = Report-FailureAudit -LogFile $LogFile if ($EventList) { Summarize-FailureAudit -FailureAuditData $EventList -ReportFolder .\Reports -LogFile $LogFile } $BlockIPList = (Get-ChildItem -Path .\Reports\ -Filter Summarize-FailureAudit_All*.csv | foreach { Import-Csv $_.FullName }).SourceIP | select -Unique | sort $RuleIPList = Update-WindowsFirewall -BlockIPList $BlockIPList -AllowIPList $AllowIPList -LogFile $LogFile Write-Log ($RuleIPList|Out-String).Trim() Cyan $LogFile # Clear event logs and archive log files if ($ClearRdpCoreTSEventLog) { Backup-thisLog -LogName 'Microsoft-Windows-RemoteDesktopServices-RdpCoreTS/Operational' -WorkFolder $WorkFolder -LogFile $LogFile } if ($ClearSecurityEventLog) { Backup-thisLog -LogName Security -WorkFolder $WorkFolder -LogFile $LogFile } if ($ClearApplicationEventLog) { Backup-thisLog -LogName Application -WorkFolder $WorkFolder -LogFile $LogFile } Get-ChildItem -Path .\Logs -File | Move-Item -Destination .\Logs\_Archive -EA 0 Get-ChildItem -Path .\Reports -File | Move-Item -Destination .\Reports\_Archive -EA 0 #endregion } End { } } function New-Password { <# .SYNOPSIS Function to generate random password .DESCRIPTION Function to generate random password .PARAMETER Length Number between 2 and 256 Default is 25 .PARAMETER Include One or more of the following: UpperCase LowerCase Numbers SpecialCharacters Default is all 4 .PARAMETER CodeFriendly When set to True, this function excludes the following 4 characters from the 'SpecialCharacters' list of the password " ==> ASCII 34 $ ==> ASCII 36 ' ==> ASCII 39 ` ==> ASCII 96 .EXAMPLE New-Password .EXAMPLE New-Password -Length 10 -Include LowerCase,UpperCase,Numbers -Verbose .LINK https://superwidgets.wordpress.com/category/powershell/ .NOTES Function by Sam Boutros v0.1 - 27 July 2017 v0.2 - 3 May 2020 - included in AZSBTools PS module. v0.3 - 19 October 2020 - Added Switch to remove 4 code unfriendly characters. v0.4 - 4 October 2021 - Fixed bug to allow maximum password length past 94. #> [CmdletBinding(ConfirmImpact='Low')] Param( [Parameter(Mandatory=$false)][ValidateRange(2,256)][Int32]$Length = 25, [Parameter(Mandatory=$false)][ValidateSet('UpperCase','LowerCase','Numbers','SpecialCharacters')] [String[]]$Include = @('UpperCase','LowerCase','Numbers','SpecialCharacters'), [Parameter(Mandatory=$false)][Switch]$CodeFriendly ) Begin { } Process { Write-Verbose "Generate-Password: Input: Length = $Length" Write-Verbose "Generate-Password: Input: Include = $($Include -join ', ')" Remove-Variable MyRange -EA 0 $Include | foreach { if ($_ -eq 'UpperCase') { $MyRange += 65..90 Write-Verbose 'Generate-Password: MyRange: +UpperCase' } if ($_ -eq 'LowerCase') { $MyRange += 97..122 Write-Verbose 'Generate-Password: MyRange: +LowerCase' } if ($_ -eq 'Numbers') { $MyRange += 48..57 Write-Verbose 'Generate-Password: MyRange: +Numbers' } if ($_ -eq 'SpecialCharacters') { $MyRange += (33..47) + (58..64) + (91..96) + (123..126) Write-Verbose 'Generate-Password: MyRange: +SpecialCharacters' } } if ($CodeFriendly) { $MyRange = $MyRange | foreach { if ($_ -notin (34,36,39,96)) { $_ } } } # ($MyRange | Get-Random -Count $Length | foreach {[char]$_}) -join '' # This produces a maximum password length of the $MyRange count (94) (1..$Length | foreach { [Char]($MyRange | Get-Random) }) -join '' } End { } } function Get-StringHash { <# .SYNOPSIS Function to Hash a string .DESCRIPTION Function to Hash a string with one of 7 different hash algorithms .PARAMETER String The string to be hashed - required .PARAMETER Algorithm The algorithm used to hash the string. Available options are: SHA1 SHA256 SHA384 SHA512 MD5 RIPEMD160 MACTripleDES Default is SHA256 .EXAMPLE Get-StringHash 'hello' -Algorithm MD5 .OUTPUTS Hash value such as 5D41402ABC4B2A76B9719D911017C592 .LINK https://superwidgets.wordpress.com/category/powershell/ https://docs.microsoft.com/en-us/powershell/module/microsoft.powershell.utility/get-filehash .NOTES Function by Sam Boutros v0.1 - 25 May 2020 #> [CmdletBinding(ConfirmImpact='Low')] Param( [Parameter(Mandatory=$true)][String]$String, [Parameter(Mandatory=$false)][ValidateSet('SHA1','SHA256','SHA384','SHA512','MD5','RIPEMD160','MACTripleDES')][String]$Algorithm = 'SHA256' ) Begin { } Process { $stringAsStream = [System.IO.MemoryStream]::new() $Writer = [System.IO.StreamWriter]::new($stringAsStream) $Writer.write($String) $Writer.Flush() $stringAsStream.Position = 0 Get-FileHash -InputStream $stringAsStream -Algorithm $Algorithm | Select-Object -ExpandProperty Hash } End { } } function Invoke-2CowsAPI { <# .SYNOPSIS Function to Query 2Cows Domain Name Registrar API .DESCRIPTION Function to Query 2Cows Domain Name Registrar API This function stores API Key on disk in encrypted form - see Example to specify folder API call must originate from the WAN IP specified in your 2Cows Admin portal (Second Factor) .PARAMETER Cred This is a PSCredential object that includes: - Your 2Cows API reseller user name - see https://domains.opensrs.guide/docs - Your 2Cows 112-character API Key - See Example .PARAMETER Command PSCustomObject with the following properties/example: [PSCustomObject]@{ protocol = 'XCP' action = 'LOOKUP' object = 'DOMAIN' attributes = [PSCustomObject]@{ domain = 'google.com' } } See https://domains.opensrs.guide/docs for more details .EXAMPLE $myParameterSet = @{ Cred = Get-SBCredential -UserName 'my2CowsUser_Name' -CredPath C:\folderName Command = [PSCustomObject]@{ protocol = 'XCP' action = 'LOOKUP' object = 'DOMAIN' attributes = [PSCustomObject]@{ domain = 'google.com' } } } Invoke-2CowsAPI @myParameterSet This example will lookup the domain google.com .OUTPUTS PS Object containing the following properties/example: Domain : google.com Command : LOOKUP DOMAIN Response : Domain taken Code : 211 Success : True .LINK https://superwidgets.wordpress.com/category/powershell/ https://domains.opensrs.guide/docs .NOTES Function by Sam Boutros v0.1 - 25 May 2020 v0.2 - 26 May 2020 - Minor updates, Changed output property 'status' to 'success' as True/False #> [CmdletBinding(ConfirmImpact='Low')] Param( [Parameter(Mandatory=$true)][PSCredential]$Cred, [Parameter(Mandatory=$false)][PSCustomObject]$Command = [PSCustomObject][Ordered]@{ protocol = 'XCP' action = 'LOOKUP' object = 'DOMAIN' attributes = [PSCustomObject]@{ domain = 'google.com' } } ) Begin { } Process { $Query = [PSCustomObject][Ordered]@{ reseller_username = $Cred.UserName api_key = $Cred.GetNetworkCredential().Password api_host_port = 'https://rr-n1-tor.opensrs.net:55443' xml = @" <?xml version='1.0' encoding='UTF-8' standalone='no' ?> <!DOCTYPE OPS_envelope SYSTEM 'ops.dtd'> <OPS_envelope> <header> <version>0.9</version> </header> <body> <data_block> <dt_assoc> <item key="protocol">$($Command.protocol)</item> <item key="action">$($Command.action)</item> <item key="object">$($Command.object)</item> <item key="attributes"> <dt_assoc> <item key="domain">$($Command.attributes.domain)</item> </dt_assoc> </item> </dt_assoc> </data_block> </body> </OPS_envelope> "@.Trim() } $Hash1 = (Get-StringHash ($Query.xml + $Query.api_key).Trim() -Algorithm MD5).ToLower() $Hash2 = (Get-StringHash ($Hash1 + $Query.api_key).Trim() -Algorithm MD5).ToLower() $Headers = @{ 'Content-Type' = 'text/xml' 'X-Username' = $Query.reseller_username 'X-Signature' = $Hash2 } $ParameterSet = @{ Uri = $Query.api_host_port Headers = $Headers Method = 'Post' Body = $Query.xml } $Result = Invoke-WebRequest @ParameterSet Write-Verbose $Result.Content } End { [PSCustomObject][Ordered]@{ Domain = $Command.attributes.domain Command = $Command.action + ' ' + $Command.object Response = (([XML]$Result.Content).OPS_envelope.body.data_block.dt_assoc.ChildNodes | where key -EQ response_text).'#text' Code = (([XML]$Result.Content).OPS_envelope.body.data_block.dt_assoc.ChildNodes | where key -EQ response_code).'#text' Success = [Boolean](([XML]$Result.Content).OPS_envelope.body.data_block.dt_assoc.ChildNodes | where key -EQ is_success).'#text' } } } function Invoke2CowsAPI-GetDNSZone { <# .SYNOPSIS Function to Query DNS Zone in 2Cows Domain Name Registrar API .DESCRIPTION Function to Query DNS Zone in 2Cows Domain Name Registrar API This function stores API Key on disk in encrypted form - see Example to specify folder API call must originate from the WAN IP specified in your 2Cows Admin portal (Second Factor) Using the Verbose parameter will show the raw API XML returned data .PARAMETER Cred This is a PSCredential object that includes: - Your 2Cows API reseller user name - see https://domains.opensrs.guide/docs - Your 2Cows 112-character API Key - See Example .PARAMETER Domain This is your domain name registered with 2Cows .EXAMPLE $myParameterSet = @{ Cred = Get-SBCredential -UserName 'my2CowsUserName' -CredPath C:\folder domain = 'mydomain.com' } $Result = Invoke2CowsAPI-GetDNSZone @myParameterSet $Result | FT Domain,Command,Response,Code,Status -a $Result.DNSRecords | FT -a .OUTPUTS PS Object containing the following properties/example: Domain : mydomain.com Command : Get DNS Zone Response : Command Successful Code : 200 Success : True DNSRecords : {@{RecordType=A; IPAddress=....} $Result.DNSRecords would show the following properties/example: RecordType IPAddress Name hostname Priority text ---------- --------- ---- -------- -------- ---- A 11.22.33.44 jn41.mydomain.com A 22.33.44.55 x155.mydomain.com TXT mydomain.com v=spf1 a mx ptr ip4:33.44.55.66 include:somedomain.com ?all CNAME taxpilot.mydomain.com vhost66.mydomain.com CNAME mail.mydomain.com ghs.google.com MX mydomain.com aspmx4.googlemail.com 30 MX mydomain.com aspmx5.googlemail.com 30 MX mydomain.com aspmx.l.google.com 10 MX mydomain.com alt1.aspmx.l.google.com 20 MX mydomain.com alt2.aspmx.l.google.com 20 MX mydomain.com aspmx2.googlemail.com 30 MX mydomain.com aspmx3.googlemail.com 30 .LINK https://superwidgets.wordpress.com/category/powershell/ https://domains.opensrs.guide/docs/get_dns_zone .NOTES Function by Sam Boutros v0.1 - 25 May 2020 v0.2 - 26 May 2020 - Minor updates, Changed output property 'status' to 'success' as True/False #> [CmdletBinding(ConfirmImpact='Low')] Param( [Parameter(Mandatory=$true)][PSCredential]$Cred, [Parameter(Mandatory=$true)][String]$Domain ) Begin { } Process { $Query = [PSCustomObject][Ordered]@{ reseller_username = $Cred.UserName api_key = $Cred.GetNetworkCredential().Password api_host_port = 'https://rr-n1-tor.opensrs.net:55443' xml = @" <?xml version='1.0' encoding='UTF-8' standalone='no' ?> <!DOCTYPE OPS_envelope SYSTEM 'ops.dtd'> <OPS_envelope> <header> <version>0.9</version> </header> <body> <data_block> <dt_assoc> <item key="protocol">XCP</item> <item key="action">get_dns_zone</item> <item key="object">DOMAIN</item> <item key="attributes"> <dt_assoc> <item key="domain">$Domain</item> </dt_assoc> </item> </dt_assoc> </data_block> </body> </OPS_envelope> "@.Trim() } $Hash1 = (Get-StringHash ($Query.xml + $Query.api_key).Trim() -Algorithm MD5).ToLower() $Hash2 = (Get-StringHash ($Hash1 + $Query.api_key).Trim() -Algorithm MD5).ToLower() $Headers = @{ 'Content-Type' = 'text/xml' 'X-Username' = $Query.reseller_username 'X-Signature' = $Hash2 } $ParameterSet = @{ Uri = $Query.api_host_port Headers = $Headers Method = 'Post' Body = $Query.xml } $Result = Invoke-WebRequest @ParameterSet Write-Verbose $Result.Content } End { [PSCustomObject][Ordered]@{ Domain = $Domain Command = 'Get DNS Zone' Response = (([XML]$Result.Content).OPS_envelope.body.data_block.dt_assoc.ChildNodes | where key -EQ response_text).'#text' Code = (([XML]$Result.Content).OPS_envelope.body.data_block.dt_assoc.ChildNodes | where key -EQ response_code).'#text' Success = [Boolean](([XML]$Result.Content).OPS_envelope.body.data_block.dt_assoc.ChildNodes | where key -EQ is_success).'#text' DNSRecords = $( $List = ((([XML]$Result.Content).OPS_envelope.body.data_block.dt_assoc.ChildNodes | where key -EQ attributes).dt_assoc.ChildNodes | where key -EQ records).dt_assoc.ChildNodes foreach ($DNSRecordType in @('A','AAAA','CNAME','MX','SRV','TXT')) { ($List | where key -EQ $DNSRecordType).dt_array.ChildNodes | foreach { if ($Subdomain = ($_.dt_assoc.ChildNodes | where key -EQ subdomain).'#text') { $Subdomain += '.' } [PSCustomObject][Ordered]@{ RecordType = $DNSRecordType IPAddress = ($_.dt_assoc.ChildNodes | where key -EQ ip_address).'#text' Name = $Subdomain + $Domain hostname = ($_.dt_assoc.ChildNodes | where key -EQ hostname).'#text' Priority = ($_.dt_assoc.ChildNodes | where key -EQ priority).'#text' text = ($_.dt_assoc.ChildNodes | where key -EQ text).'#text' } } } ) } } } function New-Passphrase { <# .SYNOPSIS Function to generate random passphrase. .DESCRIPTION Function to generate random passphrase using English words. It takes about a minute for this function to execute. .PARAMETER PhraseCount Optional number that defaults to 1 This serves to produce several passphrases quickly by loading and filtering the word list once. .PARAMETER WordCount Optional number between 2 and 256 Default is 9 .PARAMETER MinLength This is the minimum word length Optional number between 3 and 99 Default is 12 .PARAMETER MaxLength This is the maximum word length Optional number between 2 and 15 Default is 6 .PARAMETER Delimiter Optional character that defaults to a space. Acceptable values are: ' ','-','_',',','#','!' .PARAMETER LettersOnly Optional switch the defaults to True. When set to Ture this parameter excludes words with dashes or dots. .EXAMPLE New-Passphrase This example generates a 9-word passphrase similar to: mordants tickings upsoars Pleurodira neurotomy Zizania tensioner emotionally sombreros .EXAMPLE New-Passphrase -PhraseCount 7 This example generates seven 9-word passphrases similar to: wowing Schiedam cystotome monadology neodiprion sourtop remedying millipede boucle Amagasaki hatbox nonsugars Navahos sindry curtalaxes outfitter fluidities pandour relapses westernizing asininities porporate allonym illest stalinists chaffer faultiness Valsaceae martyrology Atakapas pannus sandweed beyonds combatant groundspeed rugous kelpie arterialise undivinely Grindelia shrewly Connochaetes gagtooth limuloid cyprine hereat applotment schmeer caulicule masthead Rotameter stirrable unhooding anomies superoxide suretyship Petrarchism catfooted dermographic pidgins roughages cashews connections .EXAMPLE New-Passphrase -PhraseCount 15 -WordCount 2 -Delimiter '-' This example generates fifteen 2-word passphrases similar to: nigged-supples glycerize-chevalet umiaks-batlan carcajous-antinuke premen-siscowet piscatory-misrules hysteroid-calamistrum archines-figured seedmen-Girtin Maracaibo-vaster strigine-paperhangers titters-polygonic shunpiker-intonational Necturus-backstrokes spiritistic-Cogswellia .LINK https://superwidgets.wordpress.com/category/powershell/ .NOTES Function by Sam Boutros v0.1 - 21 October 2020 #> [CmdletBinding(ConfirmImpact='Low')] Param( [Parameter(Mandatory=$false)][Int32]$PhraseCount = 1, [Parameter(Mandatory=$false)][ValidateRange(2,256)][Int32]$WordCount = 9, [Parameter(Mandatory=$false)][ValidateRange(2,15)][Int32]$MinLength = 6, [Parameter(Mandatory=$false)][ValidateRange(3,99)][Int32]$MaxLength = 12, [Parameter(Mandatory=$false)][ValidateSet(' ','-','_',',','#','!')][String]$Delimiter = ' ', [Parameter(Mandatory=$false)][Switch]$LettersOnly = $true ) Begin { $WorkFolder = Split-Path -Path $PSCommandPath try { $WordList = Get-Content -Path "$WorkFolder\MerriamWordList.txt" -EA 1 } catch { Write-Log 'Failed to read dictionary file',"$WorkFolder\MerriamWordList.txt" Magenta,Yellow break } } Process { $DesiredWordList = $WordList | where { $_.Length -ge $MinLength -and $_.Length -le $MaxLength } if ($LettersOnly) { $DesiredWordList = $DesiredWordList | where { $_ -notmatch '-' -and $_ -notmatch '\.' } } 1..$PhraseCount | foreach { $OutList = 1..$WordCount | foreach { $DesiredWordList[(Get-Random -Maximum $DesiredWordList.Count)] } $OutList -join $Delimiter } } End { } } function Encrypt-String { <# .SYNOPSIS Function to encrypt a plain text string. .DESCRIPTION Function to encrypt a plain text string to an encrypted standard string, using the Advanced Encryption Standard (AES) encryption algorithm. .PARAMETER PlainTextString Required parameter. This is the string to be encrypted. .PARAMETER EncryptionKey Optional string representing 16, 24, or 32 Byte Array such as '76 33 170 234 30 100 129 180 79 200 12 14 172 254 34 158' If not provided this function will pick a random key. Key will be displayed to the console but not part of the (standard) output - see examples. This is also accepted as hex values .PARAMETER Base64 Optional switch. When set to True, this function will base64 encode the input string before encrypting it. .EXAMPLE $myEncryptedString = Encrypt-String -PlainTextString 'hello there' This example will encrypt the provided string using a random key. Console output will look like: Plain Text String: hello there Encryption Key: 218 132 75 11 9 221 124 243 70 120 9 85 188 12 213 104 246 145 133 102 2 157 167 17 3 176 167 37 55 88 144 154 Encrypted String: 76492d1116743f0423413b16050a5345MgB8AGcAVQA3AHMAWAB3ADEATAAxAHcAcABmAC8AVgBzADAAVwBWAGgAaQBHAHcAPQA9AHwAMABmADAAYQBkADMAZQBmAGYAMQA1AGYANgA0ADQAOQAwADIAMQA1ADgAYQA2AGIAOAAxADQAMQA0ADQAZgA5ADgAMwA2AGQANAA4ADMAZgA3ADMAZQAxADgAYQBmADAAYwAwAGUAZgA3AGQANAA0AGMAYQBhADAANgA0ADMAYQA= .EXAMPLE $myEncryptedString = Encrypt-String -PlainTextString 'hello there' -EncryptionKey '90 42 50 159 243 105 189 152 198 248 189 123 188 83 195 168' This example will encrypt the provided string using the provided key. Console output will look like: Plain Text String: hello there Encryption Key: 90 42 50 159 243 105 189 152 198 248 189 123 188 83 195 168 Encrypted String: 76492d1116743f0423413b16050a5345MgB8AE8AZgBZAHMAKwBZAGQAZgB3AHIANQBoAE8ANQBNAEEARQBuAEQAWgBLAGcAPQA9AHwANwBhAGMAOQA5ADcANQAzADkANQBhAGYAYQBhAGEAOQBjADYAZQBkADMANgA5ADEAYwBiADgANAAwAGIAOAAzADYAOQAyADgAMwAzAGYANgBkADkANgA3AGIAZABlADgAOQA5ADUAZgBlADUANgBhAGEAOAA1ADIAZQBhADkAYgA= .EXAMPLE $myEncryptedString = Encrypt-String -PlainTextString 'hello there' -EncryptionKey '90 42 50 159 243 105 189 152 198 248 189 123 188 83 195 168' -Base64 This example will base64-encode the provided string, then encrypt it using the provided key. Console output will look like: Plain Text String: hello there Encryption Key: 90 42 50 159 243 105 189 152 198 248 189 123 188 83 195 168 Base64 String: aABlAGwAbABvACAAdABoAGUAcgBlAA== Encrypted String: 76492d1116743f0423413b16050a5345MgB8AHoAYgBLAEoANABRAFUALwA0ADYAbgBzAHAAWABBAFQATwBTAFoAbgBoAFEAPQA9AHwAZgBiADIANwAyADQAYgAzAGQAYgA4AGIAMgBhAGYAOAA1ADkAMgAzAGYAYwAxAGIAYgBmADAAZgA3ADgAMwAxAGQAMABjADQAYQBiADQANABhADkAYgBhADcAMgAzAGIANwBjADcANAA3AGYAMgBkADEANgAyADEANQBlAGEAMwA0ADAANwBkADkAMgA4ADEAOAAyAGIAYgBlAGYAZABlADkAZQBhADIAYQA3ADQANwBhADIAZAA5ADAAYQBjADAAZgBhADUAMAA1ADAAMwAyAGMAZAA5ADEAZABlADcAZABhAGYAZgA3ADQAMwA5AGEAMgBhADcANQBjADUANwAyAGMANgBhAGEAYQBlAGQAZABlADUAYwBiAGEANgA0ADEAZgA2ADIAOQAzADEANgA0ADYAMAA2AGQAMQBmAGEAMwA= .OUTPUTS Encrypted string such as: 76492d1116743f0423413b16050a5345MgB8ACsAYwBrAGQAMQBJAEkAMQBLAE0AZABtAEcANABxADAASgBJADcAcgBnAHcAPQA9AHwANgBhADgAYwAwADkAZgA3ADYAZQA1AGQANAAzADEANgBjAGEAYgA2ADQAMwA3ADYAYQAwAGUAZgAzADEAYgA5AGQANAAyADkAZgBjADMAMwA5ADYAMAAyAGEAZABmAGYAYQA4AGUAMgA0ADcAOABkADEAYwA0ADYAYwBlADkAOQBkAGUAZQBmADEAYwBiADYAYgA4AGYAZAAxADcAOABkAGMAZgA3AGMAYgAxADgANgA3ADIAMQA2ADIANQAwAGUANgA3ADAANwAzAGQAOQA5ADgAOABlADgANQBmADIAZAAyADgAMQBjADcAZgA2ADMAYwA2ADAAMABhAGYANwAzADIAYQAwAGYANwBkAGQAMgAzADcAOAA3ADkAOAA4AGEANQAwADEAYgBkADYAZQAxADEAMwA4ADcAYwA1AGMAYwA= .LINK https://superwidgets.wordpress.com/category/powershell/ .NOTES Function by Sam Boutros v0.1 - 5 January 2021 v0.2 - 17 January 2021 - Added code to accept hex values as well as decimal values for the input EncryptionKey #> [CmdletBinding(ConfirmImpact='Low')] Param( [Parameter(Mandatory=$true)][String]$PlainTextString, [Parameter(Mandatory=$False, HelpMessage="String representing 16, 24, or 32 Byte Array such as '76 33 170 234 30 100 129 180 79 200 12 14 172 254 34 158'")] [String]$EncryptionKey = ((1..32 | foreach { Get-Random -Minimum 1 -Maximum 255 }) -join ' '), [Parameter(Mandatory=$False)][Switch]$Base64 ) Begin { $Rand = (1..32 | foreach { Get-Random -Minimum 1 -Maximum 255 }) -join ' ' Remove-Variable Key -EA 0 try { $Key = [Byte[]]($EncryptionKey -split ' ') } catch { } try { # Try hex input such as '0f fe e8 f4' $myEncKey = foreach ($Number in ($EncryptionKey -split ' ')) { [uint32]"0x$Number" } $Key = [Byte[]]$myEncKey } catch { } if (-not $Key) { Write-Log 'Encrypt-String Error:' Magenta Write-Log $_.Exception.Message Yellow Write-Log 'Received ''EncryptionKey'':',$EncryptionKey Magenta,Yellow Write-Log 'Expecting ''EncryptionKey'' parameter value to be a String representing 16, 24, or 32 Byte Array such as',$Rand,'(decimal or hex values accepted)' Green,Cyan,Green break } } Process { Try { if ($Base64) { $bytes = [System.Text.Encoding]::Unicode.GetBytes($PlainTextString) $EncodeMe = [Convert]::ToBase64String($bytes) } else { $EncodeMe = $PlainTextString } $SecureString = Convertto-SecureString $EncodeMe -AsPlainText -Force -EA 1 $EncryptedString = ConvertFrom-SecureString -SecureString $SecureString -Key $Key -EA 1 } Catch { Write-Log 'Encrypt-String Error:' Magenta Write-Log $_.Exception.Message Yellow Write-Log 'Expecting ''EncryptionKey'' parameter value to be a String representing 16, 24, or 32 Byte Array such as',$Rand Green,Cyan } } End { Write-Log 'Plain Text String:',$PlainTextString Green,Cyan Write-Log 'Encryption Key: ',($Key -join ' ') Green,Cyan if ($Base64) { Write-Log 'Base64 String: ',$EncodeMe Green,Cyan } Write-Log 'Encrypted String: ',$EncryptedString Green,Cyan $EncryptedString } } function Decrypt-String { <# .SYNOPSIS Function to decrypt a plain text string. .DESCRIPTION Function to decrypt a plain text string from an encrypted standard string, using the Advanced Encryption Standard (AES) decryption algorithm. .PARAMETER EncryptedString Required parameter. This is the string to be decrypted. .PARAMETER EncryptionKey Required string representing 16, 24, or 32 Byte Array such as '76 33 170 234 30 100 129 180 79 200 12 14 172 254 34 158' This is also accepted as hex values .EXAMPLE $myPlainTextString = Decrypt-String -EncryptedString '76492d1116743f0423413b16050a5345MgB8AEIAbQBDAC8AcwBWAGwAdgA5AHEAQwBtAHkAcwBFAEEANgAzAEEAWQBUAEEAPQA9AHwANQAxADgAZgA5AGUAOAA2ADMAMQBkADIAOAA0ADUAMQBjAGQANwAwADAAOQBmADkAZAAwAGYAOAA4ADMAYwAwADQAZQA1ADYAOQAxAGQAMwA1ADAAMAAxADYAOQAzAGEANABkADQAZgAxADQANgAwAGYAMgAxAGQANQBkADEAOQA=' -EncryptionKey '163 109 123 60 14 100 156 17 1 233 56 222 102 230 39 14 161 233 126 125 219 248 69 174 8 163 14 146 154 47 116 64' This example will decrypt the provided string using the provided key. Console output will look like: Encrypted String: 76492d1116743f0423413b16050a5345MgB8AEIAbQBDAC8AcwBWAGwAdgA5AHEAQwBtAHkAcwBFAEEANgAzAEEAWQBUAEEAPQA9AHwANQAxADgAZgA5AGUAOAA2ADMAMQBkADIAOAA0ADUAMQBjAGQANwAwADAAOQBmADkAZAAwAGYAOAA4ADMAYwAwADQAZQA1ADYAOQAxAGQAMwA1ADAAMAAxADYAOQAzAGEANABkADQAZgAxADQANgAwAGYAMgAxAGQANQBkADEAOQA= Encryption Key: 163 109 123 60 14 100 156 17 1 233 56 222 102 230 39 14 161 233 126 125 219 248 69 174 8 163 14 146 154 47 116 64 Plain Text String: hello there .EXAMPLE $myPlainTextString = Decrypt-String -EncryptedString '76492d1116743f0423413b16050a5345MgB8AGcASwA1AHQASABBAHgALwBvAHMAeQBBADcAbQBKAE0AbAB5AFIAWQBrAFEAPQA9AHwAZAA2AGIAOQBjAGUAYwAzADUAZQAzAGIAMwA2ADAAOQBiADcAYgA2AGEAOQAyAGMANQBlADQANQAzAGUAMgAxAGQAMABiADgAZQA0AGYAMwA0AGIAYQAwADAAMABkADcAYgBjADMANQBhADYAZQAzADkAZAA2AGIAYQBiAGUAYQBmADEANQA5AGEANQA4ADUAOABhAGIAZgBjAGYANwBjADAAOQA5AGEAOQBiAGEAMgA1AGMAMQA5ADMAMQA0ADQAOQA5ADYAMgAyADcAZQBmADgANAA3ADQAMQA5ADAANABmAGEAYQBmADgAMwAwADgAZQA2ADQAZgBjAGIANgAwAGEAZQA5ADcAMwAyADYAOABiADkAZQA2ADcAYQA1AGYAMgA0AGYANwBkADAAZAA5ADIAZgBkADEAYwA2AGEAMAA=' -EncryptionKey '90 42 50 159 243 105 189 152 198 248 189 123 188 83 195 168' This example will decrypt the provided string using the provided key, detect that the resulting string is base64-encoded, and decode the resulting base64 string. Console output will look like: Encrypted String: 76492d1116743f0423413b16050a5345MgB8AGcASwA1AHQASABBAHgALwBvAHMAeQBBADcAbQBKAE0AbAB5AFIAWQBrAFEAPQA9AHwAZAA2AGIAOQBjAGUAYwAzADUAZQAzAGIAMwA2ADAAOQBiADcAYgA2AGEAOQAyAGMANQBlADQANQAzAGUAMgAxAGQAMABiADgAZQA0AGYAMwA0AGIAYQAwADAAMABkADcAYgBjADMANQBhADYAZQAzADkAZAA2AGIAYQBiAGUAYQBmADEANQA5AGEANQA4ADUAOABhAGIAZgBjAGYANwBjADAAOQA5AGEAOQBiAGEAMgA1AGMAMQA5ADMAMQA0ADQAOQA5ADYAMgAyADcAZQBmADgANAA3ADQAMQA5ADAANABmAGEAYQBmADgAMwAwADgAZQA2ADQAZgBjAGIANgAwAGEAZQA5ADcAMwAyADYAOABiADkAZQA2ADcAYQA1AGYAMgA0AGYANwBkADAAZAA5ADIAZgBkADEAYwA2AGEAMAA= Encryption Key: 90 42 50 159 243 105 189 152 198 248 189 123 188 83 195 168 Base64 String: aABlAGwAbABvACAAdABoAGUAcgBlAA== Plain Text String: hello there .OUTPUTS Plain text string. .LINK https://superwidgets.wordpress.com/category/powershell/ .NOTES Function by Sam Boutros v0.1 - 5 January 2021 v0.2 - 17 January 2021 - Added code to accept hex values as well as decimal values for the input EncryptionKey #> [CmdletBinding(ConfirmImpact='Low')] Param( [Parameter(Mandatory=$true)][String]$EncryptedString, [Parameter(Mandatory=$true,HelpMessage="String representing 16, 24, or 32 Byte Array such as '76 33 170 234 30 100 129 180 79 200 12 14 172 254 34 158'")][String]$EncryptionKey ) Begin { $Rand = (1..32 | foreach { Get-Random -Minimum 1 -Maximum 255 }) -join ' ' Remove-Variable Key -EA 0 try { $Key = [Byte[]]($EncryptionKey -split ' ') } catch { } try { # Try hex input such as '0f fe e8 f4' $myEncKey = foreach ($Number in ($EncryptionKey -split ' ')) { [uint32]"0x$Number" } $Key = [Byte[]]$myEncKey } catch { } if (-not $Key) { Write-Log 'Encrypt-String Error:' Magenta Write-Log $_.Exception.Message Yellow Write-Log 'Received ''EncryptionKey'':',$EncryptionKey Magenta,Yellow Write-Log 'Expecting ''EncryptionKey'' parameter value to be a String representing 16, 24, or 32 Byte Array such as',$Rand,'(decimal or hex values accepted)' Green,Cyan,Green break } } Process { Try { $SecureString = ConvertTo-SecureString $EncryptedString -Key $Key $bstr = [Runtime.InteropServices.Marshal]::SecureStringToBSTR($SecureString) [string]$DecodeMe = [Runtime.InteropServices.Marshal]::PtrToStringAuto($bstr) [Runtime.InteropServices.Marshal]::ZeroFreeBSTR($bstr) if ($Base64 = try {[convert]::FromBase64String($DecodeMe)} catch {}) { $PlainTextString = ($Base64 | Foreach { if ($_ -ne 0) { [char]$_ } }) -join '' } else { $PlainTextString = $DecodeMe } } Catch { Write-Log 'Decrypt-String Error:' Magenta Write-Log $_.Exception.Message Yellow Write-Log 'Expecting ''EncryptionKey'' parameter value to be a String representing 16, 24, or 32 Byte Array such as',$Rand Green,Cyan } } End { Write-Log 'Encrypted String: ',$EncryptedString Green,Cyan Write-Log 'Encryption Key: ',($Key -join ' ') Green,Cyan if ($Base64) { Write-Log 'Base64 String: ',$DecodeMe Green,Cyan } Write-Log 'Plain Text String:',$PlainTextString Green,Cyan $PlainTextString } } function Report-WinEvent { <# .SYNOPSIS Function to gather information on a given Windows Event by Id across many computers .DESCRIPTION Function to gather information on a given Windows Event by Id across many computers This function uses PowerShell remorting to invoke parallel remote jobs for gathering event data. For example, when gathering events from all domain controllers in a given Active Directory domain. .PARAMETER EventId Windows Event Id such as 5829 This is currently limited to System event log event Id 5829. This function can be updated to handle additonal events from other event logs by updating the 'Receive Job data' region which parses a specifc event for relevent data. .PARAMETER LogName Windows event log name such as System This is currently limited to System event log event Id 5829. .PARAMETER ComputerList List of computers to query. This defaults to the list of the domain controllers of the current Active Directory domain .PARAMETER Cred PSCredential object that defaults to the current logged on user. This should have access/permission to PS-remote into the target computers. This can be obtained by Get-Credential or Get-SBCredential cmdlets .PARAMETER ReportFile Path to the Excel file where this function will write its Event List Excel report/output. .PARAMETER ComputerListFile Path to the Excel file where this function will write its Computer List Excel report/output. .PARAMETER LogFile Path to a file where this function will write its console output. .EXAMPLE Report-WinEvent This will report on event 5829 on all DCs of the current AD domain. .LINK https://superwidgets.wordpress.com/category/powershell/ .NOTES Function by Sam Boutros v0.1 - 26 January 2021 #> [CmdletBinding(ConfirmImpact='Low')] Param( [Parameter(Mandatory=$false)][ValidateSet(5829)][Int]$EventId = 5829, [Parameter(Mandatory=$false)][ValidateSet('System')][String]$LogName = 'System', [Parameter(Mandatory=$false)][String[]]$ComputerList = $thisDomainDCList, [Parameter(Mandatory=$false)][PSCredential]$Cred = (Get-SBCredential -UserName "$env:USERDOMAIN\$env:USERNAME"), [Parameter(Mandatory=$false)][String]$ReportFile = ".\Report-Event$EventId-$(Get-Date -Format 'ddMMMMyyyy_hh-mm-ss_tt').xlsx", [Parameter(Mandatory=$false)][String]$ComputerListFile = ".\Report-Event$EventId-ComputerList-$(Get-Date -Format 'ddMMMMyyyy_hh-mm-ss_tt').xlsx", [Parameter(Mandatory=$false)][String]$LogFile = ".\Report-Event$EventId-$(Get-Date -Format 'ddMMMMyyyy_hh-mm-ss_tt').log" ) Begin { } Process { $StartTime = Get-Date #region Get list of online computers $OnLineList = foreach ($Computer in $ComputerList) { Write-Log 'Checking if computer',($Computer).PadRight(35,' '),'is reachable:' Green,Cyan,Green $LogFile -NoNewLine if ($Result = Test-SBNetConnection -ComputerName $Computer -PortNumber 5985 -TimeoutSec 3 -WA 0) { [PSCustomObject]@{ Name = $Computer Port5985Open = $Result[0].TcpTestSucceeded } if ($Result[0].TcpTestSucceeded) { Write-Log 'PS Remoting port 5985 OK' DarkYellow $LogFile } else { Write-Log 'PS Remoting port 5985 unreachable' Magenta $LogFile } } } $ComputerCount = ($OnLineList | where { $_.Port5985Open }).Count if ($ComputerCount -lt 1) { Write-Log 'No reachable DCs found !?' Magenta $LogFile break } else { $OnLineList = $OnLineList | sort Name } #endregion #region Submit and wait for remote jobs Write-Log 'Gathering events for Event ID',$EventId,'from',$ComputerCount,'computers' Green,Cyan,Green,Cyan,Green $LogFile $Duration = Measure-Command { #region Submit remote jobs Get-Job | Remove-Job -Force foreach ($Computer in $OnLineList) { if ($Computer.Port5985Open) { # Remote Job Invoke-Command -AsJob -ComputerName $Computer.Name -JobName $Computer.Name -Credential $Cred -ScriptBlock { try { Get-EventLog -LogName $Using:LogName -InstanceId $Using:EventId -EA 1 } Catch { $_.Exception.Message } } } } #endregion #region Wait for jobs $JobMonitor = foreach ($JobStatus in (Get-Job)) { if ($JobStatus.State -eq 'Running') { $StatusColor = 'DarkYellow' } else { $StatusColor = 'Yellow' } Write-Log 'Remote Job',($JobStatus.Name).PadRight(35,' '),$JobStatus.State Green,Cyan,$StatusColor $LogFile New-Object -TypeName psobject -Property ([Ordered]@{ Name = $JobStatus.Name State = $JobStatus.state Changed = $false StartTime = Get-Date Duration = $null }) } Write-Log 'Monitoring Jobs'' status..' Green $LogFile $LiveStatus = Get-job while (($LiveStatus | where State -eq 'Running')) { foreach ($JobStatus in $LiveStatus) { $thisJobMonitor = $JobMonitor | where Name -EQ $JobStatus.Name if ($JobStatus.State -ne $thisJobMonitor.State -and -not $thisJobMonitor.Changed) { # Only display changed job status (once) $thisJobMonitor.Changed = $true $thisJobMonitor.Duration = New-TimeSpan -Start $thisJobMonitor.StartTime -End (Get-Date) # Record and display each DC job time if ($JobStatus.State -eq 'Running') { $StatusColor = 'DarkYellow' } else { $StatusColor = 'Yellow' } Write-Log 'Remote Job',($JobStatus.Name).PadRight(35,' '),"$($JobStatus.State) in" Green,Cyan,$StatusColor $LogFile -NoNewLine Write-Log "$($thisJobMonitor.Duration.Hours):$($thisJobMonitor.Duration.Minutes):$($thisJobMonitor.Duration.Seconds) (hh:mm:ss)" DarkYellow $LogFile } } Start-Sleep -Seconds 1 } #endregion } #endregion #region Receive Job data $Duration = Measure-Command { Write-Log 'Receiving job data..' Green $LogFile $rawCombinedEventList = foreach ($Job in (Get-Job | where { $_.HasMoreData })) { $Temp = Receive-Job -Name $Job.Name if ($Temp.InstanceID) { # Job returning expected data, accept it $Temp } elseif ($Temp -eq 'No matches found') { # Job returning no data Write-Log $Job.Name,'reports',$Temp,'for event ID',$EventId Green,Cyan,Green,Cyan,Yellow $LogFile } else { # Job not returning expected data, probably an error, display it Write-Log 'Job error',$Job.Name,$Temp Yellow,Magenta,Yellow $LogFile } } Get-Job | Remove-Job -Force $myCombinedEventList = foreach ($Event in $rawCombinedEventList) { New-Object -TypeName psobject -Property ([Ordered]@{ DCName = $Event.PSComputerName ClientName = $Event.ReplacementStrings[0] ClientOS = $Event.ReplacementStrings[3] Date = $Event.TimeGenerated }) } } Write-Log 'Received',$myCombinedEventList.Count,'events from',$ComputerCount,'DCs in', "$($Duration.Hours):$($Duration.Minutes):$($Duration.Seconds) (hh:mm:ss)" Green,Cyan,Green,Cyan,Green,DarkYellow $LogFile #endregion #region Export Excel reports if ($myCombinedEventList) { Write-Log 'Exporting events to',$ReportFile Green,Cyan $LogFile -NoNewLine $myCombinedEventList | Export-Excel $ReportFile -AutoSize -FreezeTopRowFirstColumn Write-Log 'done' Yellow $LogFile $Duration = Measure-Command { Write-Log 'Processing client computer list...' Green $LogFile -NoNewLine $ClientList = $myCombinedEventList | group ClientName | sort count -Descending $myClientList = foreach ($Client in $ClientList) { New-Object -TypeName psobject -Property ([Ordered]@{ ComputerName = $Client.Name IPv4Address = (Resolve-DnsName $Client.Name -Type A -EA 0).IPAddress -join ', ' EventCount = $Client.Count EventId = $EventId }) } } # Process Events Write-Log 'done in',"$($Duration.Hours):$($Duration.Minutes):$($Duration.Seconds) (hh:mm:ss)" Cyan,DarkYellow $LogFile Write-Log 'Exporting client computer list to',$ComputerListFile Green,Cyan $LogFile -NoNewLine $myClientList | Export-Excel $ComputerListFile -AutoSize -FreezeTopRowFirstColumn Write-Log 'done' Yellow $LogFile } else { Write-Log 'No events for event ID',$EventId,'found','(are you using the correct credential!?)' Green,Cyan,Green,Yellow $LogFile } $CombinedDuration = New-TimeSpan -Start $StartTime -End (Get-Date) Write-Log 'All done in',"$($CombinedDuration.Hours):$($CombinedDuration.Minutes):$($CombinedDuration.Seconds) (hh:mm:ss)" Cyan,DarkYellow $LogFile #endregion } End { } } function Disable-WindowsWeakProtocols { <# .SYNOPSIS Function to disable Windows weak protocols, hashes, and ciphers. .DESCRIPTION Function to disable Windows weak protocols, hashes, and ciphers. When a windows computer negotiates a secure connection, it may use a legacy insecure protocol, hash, or cipher if the other end of the connection requires it. Caution: Disabling Windows weak protocols, hashes, and ciphers will prevent this computer from establishing secure connections with computers that cannot meet the same requirements. For example, by default this function will prevent this computer from establishing an SSL or HTTPS connection to a site that can only use TLS 1.1. This function makes registry changes to prevent the use olf legacy weak protocols, hashes, and ciphers. This function requires elevation since it makes changes to the registry key HKLM:\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL .PARAMETER Protocol One or more protocols such as PCT 1.0 SSL 2.0 SSL 3.0 TLS 1.0 TLS 1.1 TLS 1.2 By default, this function will disable all these protcols except TLS 1.2 .PARAMETER Hash One or more hashes such as MD5 SHA (SHA 1 that is..) SHA 256 SHA 384 SHA 512 By default, this function will disable MD5 and SHA, leaving SHA 256. 384, and 512 .PARAMETER Protocol One or more ciphers such as DES 56/56 RC2 40/128 RC2 56/128 RC2 128/128 RC4 40/128 RC4 56/128 RC4 64/128 RC4 128/128 Triple DES 168 AES 128/128 AES 256/256 By default, this function will disable all these ciphers except AES 128/128 and AES 256/256 .EXAMPLE Disable-WindowsWeakProtocols .LINK https://superwidgets.wordpress.com/category/powershell/ .NOTES Function by Sam Boutros v0.1 - 13 March 2017 - Originally published as a script in the Technet gallery, which Microsoft retired in December 2020 without migrating community scripts to Github. v0.2 - 9 February 2021 - Rewrite v0.3 - 1 March 2021 - Added code to affirmatively enable protocols/hashes/ciphers not listed in this function's parameters. #> [CmdletBinding(ConfirmImpact='Low')] Param( [Parameter(Mandatory=$false)][ValidateSet('PCT 1.0','SSL 2.0','SSL 3.0','TLS 1.0','TLS 1.1','TLS 1.2')] [String[]]$Protocol = @('PCT 1.0','SSL 2.0','SSL 3.0','TLS 1.0','TLS 1.1'), # Leaving 'TLS 1.2' [Parameter(Mandatory=$false)][ValidateSet('MD5','SHA','SHA 256','SHA 384','SHA 512')] [String[]]$Hash = @('MD5','SHA'), # SHA here means SHA1, leaving 'SHA 256', 'SHA 384', and 'SHA 512' [Parameter(Mandatory=$false)][ValidateSet('DES 56/56','RC2 40/128','RC2 56/128','RC2 128/128','RC4 40/128','RC4 56/128','RC4 64/128','RC4 128/128','Triple DES 168','AES 128/128','AES 256/256')] [String[]]$Cipher = @('DES 56/56','RC2 40/128','RC2 56/128','RC2 128/128','RC4 40/128','RC4 56/128','RC4 64/128','RC4 128/128','Triple DES 168'), # Leaving 'AES 128/128' and 'AES 256/256' [Parameter(Mandatory=$false)][String]$LogFile = ".\Disable-WindowsWeakProtocols-$env:COMPUTERNAME-$(Get-Date -Format 'ddMMMMyyyy_hh-mm-ss_tt').log" ) Begin { $ProtocolList = @('PCT 1.0','SSL 2.0','SSL 3.0','TLS 1.0','TLS 1.1','TLS 1.2') $EnabledProtocolList = $ProtocolList| foreach { if ($_ -notin $Protocol) { $_ } } $HashList = @('MD5','SHA','SHA 256','SHA 384','SHA 512') $EnabledHashList = $HashList| foreach { if ($_ -notin $Hash) { $_ } } $CipherList = @('DES 56/56','RC2 40/128','RC2 56/128','RC2 128/128','RC4 40/128','RC4 56/128','RC4 64/128','RC4 128/128','Triple DES 168','AES 128/128','AES 256/256') $EnabledCipherList = $CipherList| foreach { if ($_ -notin $Cipher) { $_ } } } Process { $RegKey = 'HKLM:\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL' #region Disable Protocols $myError = @() foreach ($Entry in $Protocol) { Write-Log 'Disablig protocol',$Entry Green,Cyan $LogFile try { New-Item -Path "$RegKey\Protocols\$Entry" -Name 'Client' -ItemType directory -Force -EA 1 | Out-Null } catch { $myError += "Disabling protocol $Entry failed: $($_.Exception.Message)" } try { New-ItemProperty -Path "$RegKey\Protocols\$Entry\Client" -PropertyType DWORD -Name 'DisabledByDefault' -Value 1 -Force -EA 1 | Out-Null } catch { $myError += "Disabling protocol $Entry failed: $($_.Exception.Message)" } try { New-Item -Path "$RegKey\Protocols\$Entry" -Name 'Server' -ItemType directory -Force -EA 1 | Out-Null } catch { $myError += "Disabling protocol $Entry failed: $($_.Exception.Message)" } try { New-ItemProperty -Path "$RegKey\Protocols\$Entry\Server" -PropertyType DWORD -Name 'DisabledByDefault' -Value 1 -Force -EA 1 | Out-Null } catch { $myError += "Disabling protocol $Entry failed: $($_.Exception.Message)" } } foreach ($Entry in $EnabledProtocolList) { Write-Log 'Enabling protocol',$Entry Green,Cyan $LogFile try { New-Item -Path "$RegKey\Protocols\$Entry" -Name 'Client' -ItemType directory -Force -EA 1 | Out-Null } catch { $myError += "Enabling protocol $Entry failed: $($_.Exception.Message)" } try { New-ItemProperty -Path "$RegKey\Protocols\$Entry\Client" -PropertyType DWORD -Name 'DisabledByDefault' -Value 0 -Force -EA 1 | Out-Null } catch { $myError += "Enabling protocol $Entry failed: $($_.Exception.Message)" } try { New-Item -Path "$RegKey\Protocols\$Entry" -Name 'Server' -ItemType directory -Force -EA 1 | Out-Null } catch { $myError += "Enabling protocol $Entry failed: $($_.Exception.Message)" } try { New-ItemProperty -Path "$RegKey\Protocols\$Entry\Server" -PropertyType DWORD -Name 'DisabledByDefault' -Value 0 -Force -EA 1 | Out-Null } catch { $myError += "Enabling protocol $Entry failed: $($_.Exception.Message)" } } if ($myError) { Write-Log 'failed' Magenta $LogFile Write-Log ($myError | Out-String).Trim() Yellow $LogFile } else { Write-Log 'done' DarkYellow $LogFile } #endregion #region Disable Hashes $myError = @() foreach ($Entry in $Hash) { Write-Log 'Disablig hash',$Entry Green,Cyan $LogFile try { New-Item -Path "$RegKey\Hashes" -Name $Entry -ItemType directory -Force -EA 1 | Out-Null } catch { $myError += "Disabling hash $Entry failed: $($_.Exception.Message)" } try { New-ItemProperty -Path "$RegKey\Hashes\$Entry" -PropertyType DWORD -Name 'Enabled' -Value 0 -Force -EA 1 | Out-Null } catch { $myError += "Disabling hash $Entry failed: $($_.Exception.Message)" } } foreach ($Entry in $EnabledHashList) { Write-Log 'Enabling hash',$Entry Green,Cyan $LogFile try { New-Item -Path "$RegKey\Hashes" -Name $Entry -ItemType directory -Force -EA 1 | Out-Null } catch { $myError += "Enabling hash $Entry failed: $($_.Exception.Message)" } try { New-ItemProperty -Path "$RegKey\Hashes\$Entry" -PropertyType DWORD -Name 'Enabled' -Value 1 -Force -EA 1 | Out-Null } catch { $myError += "Enabling hash $Entry failed: $($_.Exception.Message)" } } if ($myError) { Write-Log 'failed' Magenta $LogFile Write-Log ($myError | Out-String).Trim() Yellow $LogFile } else { Write-Log 'done' DarkYellow $LogFile } #endregion #region Disable Ciphers $myError = @() foreach ($Entry in $Cipher) { if ($Entry -match '/') { $Name = "$($Entry.Split('/')[0])$([char]0x2215)$($Entry.Split('/')[1])" } else { $Name = $Entry } Write-Log 'Disablig Cipher',$Entry Green,Cyan $LogFile try { New-Item -Path "$RegKey\Ciphers" -Name $Name -ItemType directory -Force -EA 1 | Out-Null } catch { $myError += "Disabling Cipher $Entry failed: $($_.Exception.Message)" } try { New-ItemProperty -Path "$RegKey\Ciphers\$Name" -PropertyType DWORD -Name 'Enabled' -Value 0 -Force -EA 1 | Out-Null } catch { $myError += "Disabling Cipher $Entry failed: $($_.Exception.Message)" } } foreach ($Entry in $EnabledCipherList) { if ($Entry -match '/') { $Name = "$($Entry.Split('/')[0])$([char]0x2215)$($Entry.Split('/')[1])" } else { $Name = $Entry } Write-Log 'Enabling Cipher',$Entry Green,Cyan $LogFile try { New-Item -Path "$RegKey\Ciphers" -Name $Name -ItemType directory -Force -EA 1 | Out-Null } catch { $myError += "Enabling Cipher $Entry failed: $($_.Exception.Message)" } try { New-ItemProperty -Path "$RegKey\Ciphers\$Name" -PropertyType DWORD -Name 'Enabled' -Value 1 -Force -EA 1 | Out-Null } catch { $myError += "Enabling Cipher $Entry failed: $($_.Exception.Message)" } } if ($myError) { Write-Log 'failed' Magenta $LogFile Write-Log ($myError | Out-String).Trim() Yellow $LogFile } else { Write-Log 'done' DarkYellow $LogFile } #endregion } End { } } function Restrict-PointAndPrint { <# .SYNOPSIS Function to stop and disable the spooler service and ensure that only administrators can install printer drivers. .DESCRIPTION Function to stop and disable the spooler service and ensure that only administrators can install printer drivers. This function creates HKEY_LOCAL_MACHINE\Software\Policies\Microsoft\Windows NT\Printers\PointAndPrint DWord name - RestrictDriverInstallationToAdministrators Value data - 1 .PARAMETER LogFile Path to a file where this function logs its console output. .EXAMPLE Restrict-PointAndPrint .LINK https://superwidgets.wordpress.com/category/powershell/ https://cyber.dhs.gov/ed/21-04/ .NOTES Function by Sam Boutros v0.1 - 13 July 2021. #> [CmdletBinding(ConfirmImpact='Low')] Param( [Parameter(Mandatory=$false)][String]$LogFile = ".\Restrict-PointAndPrint-$(Get-Date -Format 'ddMMMMyyyy_hh-mm-ss_tt').log" ) Begin { } Process { #region Stop and disable the Print Spooler service if ($IsElevated) { Stop-Service Spooler -Force -Confirm:$false -NoWait -PassThru $Result = Set-Service Spooler -Status stopped -StartupType disabled -PassThru -Confirm:$false if ($Result.Status -eq 'Stopped' -and $Result.StartType -eq 'Disabled') { Write-Log 'Stopped and disabled the spooler service on',$env:COMPUTERNAME Green,Cyan $LogFile Write-Log ($Result | Select Name,Status,StartType | Out-String).Trim() Cyan $LogFile } else { Write-Log 'Failed to stop/disable the spooler service on',$env:COMPUTERNAME Magenta,Yellow $LogFile Write-Log ($Result | Select Name,Status,StartType | Out-String).Trim() Yellow $LogFile } } else { Write-Log 'Unable to stop/disable the spooler service','- need elevation' Magenta,Yellow $LogFile } #endregion #region Ensure that only administrators can install printer drivers $RegKey = 'HKLM:\Software\Policies\Microsoft\Windows NT\Printers\' if ($IsElevated) { try { New-Item -Path $RegKey -Name 'PointAndPrint' -ItemType directory -Force -EA 1 | Out-Null } catch { Write-Log $_.Exception.Message Magenta $LogFile } try { New-ItemProperty -Path "$RegKey\PointAndPrint" -PropertyType DWORD -Name 'RestrictDriverInstallationToAdministrators' -Value 1 -Force -EA 1 | Out-Null } catch { Write-Log $_.Exception.Message Magenta $LogFile } $Validation = Get-ItemProperty -Path "$RegKey\PointAndPrint" -EA 0 if ($Validation.RestrictDriverInstallationToAdministrators -eq 1) { Write-Log 'Ensured that only administrators can install print drivers' Green $LogFile } else { Write-Log 'Failed to ensure that only administrators can install print drivers' Magenta $LogFile } } else { Write-Log 'Unable to modify the registry to ensure that only administrators can install print drivers','- need elevation' Magenta,Yellow $LogFile } #endregion } End { } } function Invoke-ShodanAPI { <# .SYNOPSIS Function to query the Shodan API .DESCRIPTION Function to query the Shodan API It requires a Shodan API key - See https://developer.shodan.io/ Enterprise subscription level methods have not been implemented. shodan/query method optional parameters page, sort, and order have not been implemented. This function asks the user for API key and saves it securely to disk. To be implemented: Search, On-Demand Scanning, Network Alerts, and Notifiers methods. .PARAMETER Method Currently implemented methods are: 'api-info' 'account/profile' 'tools/httpheaders' 'dns/reverse' 'dns/resolve' 'dns/domain' 'org' 'shodan/query' 'shodan/query/search' 'shodan/query/tags' 'shodan/ports' 'shodan/protocols' 'shodan/scans' 'shodan/host' .PARAMETER Ips One or more IPv4 addresses. Needed with dns/reverse and shodan/host methods. Example: @('74.125.227.230','204.79.197.200') .PARAMETER Hostnames One or more hostnames. Needed with dns/resolve method. Example: @('google.com','bing.com') .PARAMETER Domain Domain name to lookup. Needed with dns/domain method. Example: 'cnn.com' .PARAMETER History Switch parameter. When set to $True, the API returns historical DNS data. Optional with dns/domain method. .PARAMETER Method .PARAMETER Type DNS type. Optional with dns/domain method. Valid values are: 'A','AAAA','CNAME','NS','SOA','MX','TXT' .PARAMETER Page The page number to page through results 100 at a time. Optional with dns/domain method. Defaults to 1. .PARAMETER Query What to search for in the directory of saved search queries. Needed with shodan/query/search method. Defaults to 'webcam' .PARAMETER Size The number of tags to return. Optional with shodan/query/tags method. Defaults to 99,999 .PARAMETER NewAPIKey Switch Parameter. When set to $True, the user is prompted to enter a new API key. .PARAMETER LogFile Path to a file where this function will save time-stamped entries similar to its console output. .EXAMPLE Invoke-ShodanAPI -Verbose -Method dns/reverse -Ips '8.8.8.8,1.1.1.1' .EXAMPLE Invoke-ShodanAPI -Verbose -Method dns/resolve -Hostnames 'google.com,bing.com' .EXAMPLE $PortList = invoke-shodanapi -Verbose -Method shodan/ports Returns a list of port numbers that the crawlers are looking for. .EXAMPLE $ProtocolList = invoke-shodanapi -Verbose -Method shodan/protocols Returns all the protocols that can be used when launching an Internet scan and their description. .EXAMPLE $ScanList = invoke-shodanapi -Verbose -Method shodan/scans Returns a listing of all the on-demand scans that are currently active on the account. .EXAMPLE $DomainInfo = Invoke-ShodanAPI -Verbose -Method dns/domain The dns/domain method requires the -Domain parameter, which defaults to 'CNN.Com' This is the same as Invoke-ShodanAPI -Verbose -Method dns/domain -Domain 'CNN.Com' $DomainInfo # Shows DNS data summary $DomainInfo.data # Shows DNS details $DomainInfo.data | where Type -eq 'A' # shows DNS A recors only $DomainInfo.data.subdomain | select -unique | sort # shows list of subdomains .EXAMPLE $DomainInfo = Invoke-ShodanAPI -Verbose -Method dns/domain -Domain 'shodan.io' -History $DomainInfo # Shows DNS data summary $DomainInfo.tags # Shows tag list $DomainInfo.data # Shows DNS details $DomainInfo.data | where Type -eq 'A' # shows DNS A recors only $DomainInfo.data.subdomain | select -unique | sort # shows list of subdomains ($DomainInfo.data | where subdomain -eq 'WWW').Ports | select -unique | sort # shows open TCP ports on the WWW subdomain .EXAMPLE $HostServiceList = invoke-shodanapi -Method shodan/host -Ips '8.8.8.8' -Verbose Will return output like: VERBOSE: GET https://api.shodan.io/shodan/host/8.8.8.8?key=9V1fZoOHMpuxrZ1qVlHB7YslfPM2G2s7 with 0-byte payload VERBOSE: received -1-byte response of content type application/json; charset=UTF-8 VERBOSE: StatusCode : 200 StatusDescription : OK Content : {"region_code": "CA", "ip": 134744072, "postal_code": null, "country_code": "US", "city": "Mountain View", "dma_code": null, "last_update": "2021-08-28T11:23:27.888451", "latitude": 37.4056, "tags": [... RawContent : HTTP/1.1 200 OK Transfer-Encoding: chunked Connection: keep-alive Vary: Accept-Encoding Access-Control-Allow-Origin: * X-Frame-Options: DENY X-Content-Type-Options: nosniff X-XSS-Protection: 1;... Forms : Headers : {[Transfer-Encoding, chunked], [Connection, keep-alive], [Vary, Accept-Encoding], [Access-Control-Allow-Origin, *]...} Images : {} InputFields : {} Links : {} ParsedHtml : RawContentLength : 1349 PS c:\> $HostServiceList Will return output like: region_code : CA ip : 134744072 postal_code : country_code : US city : Mountain View dma_code : last_update : 2021-08-28T11:23:27.888451 latitude : 37.4056 tags : {} area_code : country_name : United States hostnames : {dns.google} org : Google LLC data : {@{_shodan=; hash=-553166942; os=; opts=; timestamp=2021-08-28T11:23:27.888451; isp=Google LLC; port=53; hostnames=System.Object[]; location=; dns=; ip=134744072; domains=System.Object[]; org=Google LLC; data= Recursion: enabled; asn=AS15169; transport=udp; ip_str=8.8.8.8}} asn : AS15169 isp : Google LLC longitude : -122.0775 country_code3 : domains : {dns.google} ip_str : 8.8.8.8 os : ports : {53} PS c:\> $HostServiceList.data Will return output like: _shodan : @{id=148b3c6c-3f29-494f-9bcb-aa05ac534bac; options=; ptr=True; module=dns-udp; crawler=cdd92e2d835a37d2798fa6c7105171f4d214012f} hash : -553166942 os : opts : @{raw=34ef818200010000000000000776657273696f6e0462696e640000100003} timestamp : 2021-08-28T11:23:27.888451 isp : Google LLC port : 53 hostnames : {dns.google} location : @{city=Mountain View; region_code=CA; area_code=; longitude=-122.0775; country_code3=; country_name=United States; postal_code=; dma_code=; country_code=US; latitude=37.4056} dns : @{resolver_hostname=; recursive=True; resolver_id=; software=} ip : 134744072 domains : {dns.google} org : Google LLC data : Recursion: enabled asn : AS15169 transport : udp ip_str : 8.8.8.8 .EXAMPLE $SavedSearchQueries = Invoke-ShodanAPI -Verbose -Method shodan/query $SavedSearchQueries.matches # Shows saved search queries like: votes : 1 description : tags : {iis} timestamp : 2021-05-15T21:52:55.316000 title : Seagate.com query : Seagate.com votes : 1 description : tags : {} timestamp : 2021-05-14T15:17:22.864000 title : 80 query : net:193.110.3.0/24 votes : 2 description : Electronic highway message signs tags : {iot, signs} timestamp : 2021-05-13T16:34:00.023000 title : Saferoads Variable Message Signs query : Saferoads VMS votes : 3 description : tags : {adb, port 5555} timestamp : 2021-05-12T00:40:50.411000 title : ADB Remote Access query : Android Debug Bridge port:5555 votes : 1 description : shodan.io result tags : {} timestamp : 2021-05-11T11:36:34.190000 title : shodan query : intellicar.in votes : 1 description : tags : {} timestamp : 2021-05-08T07:46:02.973000 title : 高明区 query : title:"高明区" votes : 2 description : tags : {} timestamp : 2021-05-07T14:10:36.212000 title : crosslink query : net:3.214.40.103,3.236.72.167,54.175.33.251,54.173.230.130,3.236.12.118,34.233.129.30,44.192.123.74,52.202.154.236 votes : 2 description : Pfizer Inc (Pharma) Jabber clients across the world. tags : {pfizer, pharma, jabber} timestamp : 2021-05-07T13:50:03.890000 title : Pfizer Jabber Servers/Client query : org:"Pfizer Inc." port:"5222" votes : 1 description : fra shodan.io tags : {} timestamp : 2021-05-07T09:09:26.710000 title : JMA Internet exposure query : org:JMA country:DK votes : 1 description : tags : {} timestamp : 2021-05-06T22:47:42.599000 title : 208.83.148.0/26 query : net:"208.83.148.0/26" .EXAMPLE $ShodanQuery = Invoke-ShodanAPI -Verbose -Method shodan/query/search -Query 'voip' $ShodanQuery.matches # Shows query results like: votes : 1 description : title : voip timestamp : 2017-03-04T23:29:13.959000 tags : {} query : title:"Apache HTTP Server Test Page powered by CentOS" Content-Length: 4897 port:"80" votes : 8 description : voip title : Snom timestamp : 2010-09-12T17:09:08.891000 tags : {voip} query : snom embedded country:DE votes : 1 description : nec voip title : NEC Voip Phones timestamp : 2013-02-07T20:52:26.911000 tags : {nec, voip} query : title:"Web programming" chunked no-cache Transfer-Encoding votes : 4 description : MX VoIP title : MX VoIP timestamp : 2013-07-31T03:38:41.199000 tags : {3} query : MX VoIP votes : 10 description : 39 voip title : 39 voip timestamp : 2014-02-05T18:53:26.840000 tags : {39, voip} query : 39 voip votes : 3 description : title : Voip co timestamp : 2017-03-28T12:41:31.835000 tags : {} query : Voip votes : 2 description : Voip title : MyPBX Italy timestamp : 2014-02-20T22:26:56.784000 tags : {italy} query : mypbx country:IT votes : 3 description : sudanese voip online servers title : sudanese voip servers timestamp : 2012-02-01T17:59:56.557000 tags : {voip} query : country:SD port:5060 votes : 4 description : sagem voip phones and routers title : sagem timestamp : 2012-11-24T18:19:53.639000 tags : {voip} query : sagem votes : 1 description : voip title : Insped timestamp : 2012-08-10T15:51:36.098000 tags : {ornago} query : cisco-ios city:"Ornago" port:161 .EXAMPLE $ShodanTags = Invoke-ShodanAPI -Verbose -Method shodan/query/tags $ShodanTags.matches.Count # 2863 $ShodanTags.matches | sort Count -Desc | Select -First 10 # Shows top 10 tags count value ----- ----- 212 webcam 176 cam 166 camera 101 ip 93 router 91 scada 91 ftp 87 server 67 http 57 test .OUTPUTS This cmdlet returns the API data. .LINK https://superwidgets.wordpress.com/category/powershell/ .NOTES Function by Sam Boutros v0.1 - 25 August 2021 - Implemented api-info, tools/myip, tools/httpheaders, dns/resolve, dns/reverse, dns/domain/{domain}, account/profile, org, shodan/query, shodan/query/search, shodan/query/tags Not implemented put/delete: org/member/{user}, shodan/data, shodan/data/{dataset} v0.2 - 27 August 2021 - Added scanning methods: shodan/ports, shodan/protocols, shodan/scans Added search methods: /shodan/host/{ip} Not implemented: put: shodan/scan, put: shodan/scan/internet #> [CmdletBinding(ConfirmImpact='Low')] Param( [Parameter(Mandatory=$false)][ValidateSet( 'api-info', 'account/profile', 'tools/httpheaders', 'dns/reverse', 'dns/resolve', 'dns/domain', 'org', 'shodan/query', 'shodan/query/search', 'shodan/query/tags', 'shodan/ports', 'shodan/protocols', 'shodan/scans', 'shodan/host' )][String]$Method = 'api-info', [Parameter(Mandatory=$false)][IPAddress[]]$Ips = @('74.125.227.230','204.79.197.200'), [Parameter(Mandatory=$false)][String[]]$Hostnames = @('google.com','bing.com'), [Parameter(Mandatory=$false)][String]$Domain = 'cnn.com', [Parameter(Mandatory=$false)][Switch]$History, [Parameter(Mandatory=$false)][ValidateSet('A','AAAA','CNAME','NS','SOA','MX','TXT')][String]$Type, [Parameter(Mandatory=$false)][Int32]$Page = 1, [Parameter(Mandatory=$false)][String]$Query = 'webcam', [Parameter(Mandatory=$false)][Int32]$Size = 99999, [Parameter(Mandatory=$false)][Switch]$NewAPIKey, [Parameter(Mandatory=$false)][String]$LogFile = ".\Invoke-ShodanAPI_$Method_$(Get-Date -Format 'ddMMMMyyyy_hh-mm-ss_tt').log" ) Begin { $ShodanAPIKey = if ($NewAPIKey) { Get-SBCredential -UserName 'ShodanAPIKey' -Refresh } else { Get-SBCredential -UserName 'ShodanAPIKey' } if (-not $ShodanAPIKey) { Write-Log 'Shodan API key not provided, stopping..' Magenta $LogFile break } } Process { #region Validate method parameters, compile Uri $Method = $Method.ToLower() if ($Method -in $ShodanAPIMethodList) { $Uri = "$ShodanAPIBaseURL/$($Method)?key=$($ShodanAPIKey.GetNetworkCredential().password)" switch ($Method) { 'dns/resolve' { $Uri += "&hostnames=$($Hostnames -join ',')" } 'dns/reverse' { $Uri += "&ips=$($IPs.IPAddressToString -join ',')" } 'shodan/query/search' { $Uri += "&query=$($Query)" } 'shodan/query/tags' { $Uri += "&size=$($Size)" } 'dns/domain' { $Uri = "$ShodanAPIBaseURL/$($Method)/$($Domain)?key=$($ShodanAPIKey.GetNetworkCredential().password)&page=$($Page)" if ($History) { $Uri += '&history' } if ($Type) { $Uri += "&type=$($Type)" } } 'shodan/host' { if ($Ips.Count -eq 1) { $Uri = "$ShodanAPIBaseURL/$($Method)/$($Ips)?key=$($ShodanAPIKey.GetNetworkCredential().password)" } else { foreach ($IPAddress in $Ips) { Invoke-ShodanAPI -Method $Method -Ips $IPAddress -LogFile $LogFile } } } default { } } } else { Write-Log 'Invoke-ShodanAPI Error:','bad API method provided',$Method Magenta,Yellow,Magenta $LogFile Write-Log 'Known Shodan API methods (from https://developer.shodan.io/api):' Yellow $LogFile $ShodanAPIMethodList | foreach { Write-Log " $_" Cyan $LogFile } break } #endregion try { $Result = Invoke-WebRequest -Uri $Uri -UseBasicParsing -EA 1 Write-Verbose ($Result | Out-String).Trim() if ($Method -eq 'shodan/protocols') { $Obj = $Result.Content | ConvertFrom-Json foreach ($Prop in ($Obj | Get-Member -MemberType NoteProperty).Name) { New-Object -TypeName PSObject -Property ([Ordered]@{ Protocol = $Prop Description = $Obj.$Prop }) } } else { $Result.Content | ConvertFrom-Json } } catch { Write-Log $_.Exception.Message Magenta $LogFile Write-Log $_.ErrorDetails.Message Yellow $LogFile } } End { } } function Report-Kerberoasting { <# .SYNOPSIS Function to return information on AD user accounts in the current AD domain that have SPN's .DESCRIPTION Function to return information on AD user accounts in the current AD domain that have Service Principal Names and are subject to Kerberoasting attacks. Note that LastLogonTimeStamp may be off by up to 14 days. LogonCount notes: This attribute is not replicated and is maintained on each domain controller in the domain. To get an accurate value for the user's total number of successful logon attempts in the domain, each domain controller in the domain must be queried and the sum of the values should be used. Keep in mind that the attribute is not replicated, therefore domain controllers that are retired may have counted logons for the user as well, and these will be missing from the count. Due to compatibility with 16-bit versions of LAN Manager, the attribute has an upper limit of 65535. https://docs.microsoft.com/en-us/windows/win32/adschema/a-logoncount Notes on delegation: https://social.technet.microsoft.com/wiki/contents/articles/5392.active-directory-ldap-syntax-filters.aspx Accounts trusted for delegation (unconstrained delegation) (userAccountControl:1.2.840.113556.1.4.803:=524288) Accounts that are sensitive and not trusted for delegation (userAccountControl:1.2.840.113556.1.4.803:=1048576) http://eddiejackson.net/wp/?p=16387 1.2.840.113556.1.4.803 This is the bitwise AND operator (LDAP_MATCHING_RULE_BIT_AND). The rule is true only if all bits from the property match the value. 1.2.840.113556.1.4.804 This is the bitwise OR operator (LDAP_MATCHING_RULE_BIT_OR). The rule is true if any bits from the property match the value. TRUSTED_FOR_DELEGATION 0x80000 524288 When this flag is set, the service account (the user or computer account) under which a service runs is trusted for Kerberos delegation. Any such service can impersonate a client requesting the service. NOT_DELEGATED 0x100000 1048576 When this flag is set, the security context of the user is not delegated to a service even if the service account is set as trusted for Kerberos delegation. TRUSTED_TO_AUTH_FOR_DELEGATION 0x1000000 16777216 (Windows 2000/Windows Server 2003) The account is enabled for delegation. This is a security-sensitive setting. Accounts that have this option enabled should be tightly controlled. This setting lets a service that runs under the account assume a client’s identity and authenticate as that user to other remote servers on the network. .PARAMETER PropList Optional parameter that lists the attributes that this report should query for AD users. It defaults to 'Description','info','EmployeeId','EmailAddress','Enabled','userWorkstations','Created','LastLogonTimeStamp','PasswordLastSet','PasswordNeverExpires','ServicePrincipalNames','MemberOf' .PARAMETER SelectList Optional parameter that lists the attributes that this report should return on AD users. It defaults to 'Name','samAccountName','DistinguishedName','UserPrincipalName','Description','info','EmployeeId','EmailAddress','Enabled','userWorkstations','Created','LastLogonTimeStamp','PasswordLastSet','PasswordNeverExpires','ServicePrincipalNames','MemberOf' .PARAMETER IncludeBadSPNs Optional switch parameter. When set to True, this function will return all user accounts with SPNs, including those with bad SPNs. .PARAMETER LogFile Optional parameter that contains the name of a text file where this function will log its console output. When not provided, it defaults to a file in the current folder. .EXAMPLE Report-Kerberoasting .EXAMPLE $AccountList = Report-Kerberoasting $ReportFileName = ".\KerberoastingAccountList-$($thisDomainName)_$(Get-Date -Format 'ddMMMMyyyy_hh-mm-ss_tt').csv" $AccountList | Export-csv $ReportFileName -NoTypeInformation This example exports the resulting output to CSV file .OUTPUTS Progress output is displayed to the console and log file. Records similar to: Name : NDPSQLEXEC samAccountName : NDPSQLEXEC DistinguishedName : CN=NDPSQLEXEC,OU=someou,DC=domain,DC=com UserPrincipalName : NDPSQLEXEC@domain.com Description : some descr info : EmployeeId : EmailAddress : Enabled : True userWorkstations : Created : 10/15/2002 6:57:55 PM LastLogonTimeStamp : 1/22/2012 12:59:17 AM PasswordLastSet : 7/15/2011 10:11:06 AM PasswordNeverExpires : True ServicePrincipalNames : MSSQLSvc/servername:1433 [AES256-CTS-HMAC-SHA-1 (18)], MSSQLSvc/servername.domain.com:1433 [AES256-CTS-HMAC-SHA-1 (18)] UnconstrainedDelegation : False ConstrainedDelegation : False .LINK https://superwidgets.wordpress.com/category/powershell/ .NOTES Function by Sam Boutros v0.1 - 14 September 2021 v0.2 - 17 September 2021 Added SPN Kerberos Ticket Encryption Type Added IncludeBadSPNs Switch #> [CmdletBinding(ConfirmImpact='Low')] Param( [Parameter(Mandatory=$false)][String[]]$PropList = @('Description','info','EmployeeId','EmailAddress','Enabled','userWorkstations','Created','LastLogonTimeStamp','PasswordLastSet','PasswordNeverExpires','ServicePrincipalNames','MemberOf'), [Parameter(Mandatory=$false)][String[]]$SelectList = @('Name','samAccountName','DistinguishedName','UserPrincipalName','Description','info','EmployeeId','EmailAddress','Enabled','userWorkstations','Created','LastLogonTimeStamp','PasswordLastSet','PasswordNeverExpires','ServicePrincipalNames','MemberOf'), [Parameter(Mandatory=$false)][Switch]$IncludeBadSPNs, [Parameter(Mandatory=$false)][String]$LogFile = ".\Report-Kerberoasting_$(Get-Date -Format 'ddMMMMyyyy_hh-mm-ss_tt').log" ) Begin { $StartTime = Get-Date Write-Log 'Starting automation to report on AD accounts subject to Kerberoasting in the',$thisDomainName,'AD domain' Green,Cyan,Green $LogFile if ('LastLogonTimeStamp' -in $PropList) { Write-Log 'Please note that','LastLogonTimeStamp','may be off by up to 14 days.' Yellow,Cyan,Yellow $LogFile } } Process { #region Get AD accounts with SPN's Write-Log 'Retrieving AD accounts with SPN''s in the',$thisDomainName,'AD domain..' Green,Cyan,Green $LogFile -NoNewLine $Duration = Measure-Command { try { $AccountList = Get-ADUser -Filter "ServicePrincipalNames -like '*'" -Properties $PropList -EA 1 | select $SelectList } catch { Write-Log ' ' Write-Log 'Command',"Get-ADUser -Filter ""ServicePrincipalNames -like '*'"" -Properties $($PropList -join ',')",'failed' Magenta,Yellow,Magenta $LogFile Write-Log $_.Exception.Message Yellow $LogFile break } } if ($AccountList) { Write-Log 'identified',('{0:N0}' -f ($AccountList | measure -Sum -EA 0).Count),'account(s) with SPN''s in',"$($Duration.Hours):$($Duration.Minutes):$($Duration.Seconds)",'hh:mm:ss' Green,Cyan,Green,Cyan,Green $Logfile } else { Write-Log 'identified','NO','accounts with SPN''s in',"$($Duration.Hours):$($Duration.Minutes):$($Duration.Seconds)",'hh:mm:ss' Green,Cyan,Green,Cyan,Green $Logfile } #endregion #region Identify delegated accounts # 1.2.840.113556.1.4.803 This is the bitwise AND operator (LDAP_MATCHING_RULE_BIT_AND). The rule is true only if all bits from the property match the value. # TRUSTED_FOR_DELEGATION 0x80000 524288 When this flag is set, the service account (the user or computer account) under which a service runs is trusted for Kerberos delegation. # Any such service can impersonate a client requesting the service. $Duration = Measure-Command { Write-Log 'Checking for accounts','trusted for Kerberos delegation.','Any such service can impersonate a client requesting the service' Green,Yellow,Green $LogFile -NoNewLine $LDAPFilter = 'userAccountControl:1.2.840.113556.1.4.803:=524288' $DelegateList1 = Get-ADUser -LDAPFilter "($LDAPFilter)" -Properties $PropList | select $SelectList } if ($DelegateList1) { Write-Log 'found',$DelegateList1.Count,"in $($Duration.Hours):$($Duration.Minutes):$($Duration.Seconds)",'(hh:mm:ss)' Cyan,Yellow,Cyan,Green $LogFile } else { Write-Log 'None found in',"$($Duration.Hours):$($Duration.Minutes):$($Duration.Seconds) (hh:mm:ss)" Cyan,DarkYellow $LogFile } # TRUSTED_TO_AUTH_FOR_DELEGATION 0x1000000 16777216 (Windows 2000/Windows Server 2003) # The account is enabled for delegation. This is a security-sensitive setting. Accounts that have this option enabled should be tightly controlled. $Duration = Measure-Command { Write-Log 'Checking for accounts','enabled for delegation' Green,Yellow $LogFile -NoNewLine $LDAPFilter = 'userAccountControl:1.2.840.113556.1.4.803:=16777216' $DelegateList2 = Get-ADUser -LDAPFilter "($LDAPFilter)" -Properties $PropList | select $SelectList } if ($DelegateList2) { Write-Log 'found',$DelegateList2.Count,"in $($Duration.Hours):$($Duration.Minutes):$($Duration.Seconds)",'(hh:mm:ss)' Cyan,Yellow,Cyan,Green $LogFile } else { Write-Log 'None found in',"$($Duration.Hours):$($Duration.Minutes):$($Duration.Seconds) (hh:mm:ss)" Cyan,DarkYellow $LogFile } #endregion #region Identify SPN Ticket Encryption Type $Duration = Measure-Command { Write-Log 'Identifying' Green $LogFile -NoNewLine $SPNNameList = $AccountList.ServicePrincipalNames | select -Unique | sort Write-Log ('{0:N0}' -f ($SPNNameList | measure -Sum -EA 0).Count),'unique SPNs'' Encryption Type..' Cyan,Green $LogFile -NoNewLine $SPNList = foreach ($SPN in $SPNNameList) { New-Object -TypeName PSObject -Property @{ SPN = $SPN; EncType = Get-KTicketEncType $SPN } } } Write-Log 'identified' Green $LogFile -NoNewLine if ($FoundBadSPNs = $SPNList | where EncType -EQ 'Bad SPN') { Write-Log ('{0:N0}' -f ($FoundBadSPNs | measure -Sum -EA 0).Count),'bad SPNs, and' Yellow,Green $LogFile -NoNewLine } else { Write-Log 'No','bad SPNs, and' Cyan,Green $LogFile -NoNewLine } if ($FoundGoodSPNs = $SPNList | where EncType -NE 'Bad SPN') { Write-Log ('{0:N0}' -f ($FoundGoodSPNs | measure -Sum -EA 0).Count),'good SPNs' Cyan,Green $LogFile -NoNewLine } else { Write-Log 'No','good SPNs, and' Yellow,Green $LogFile -NoNewLine } Write-Log 'in',"$($Duration.Hours):$($Duration.Minutes):$($Duration.Seconds)",'(hh:mm:ss)' Green,Cyan,Green $LogFile #endregion #region Update SPN information, remove user accounts with bad SPNs $Duration = Measure-Command { Write-Log 'Updating',('{0:N0}' -f ($SPNList | measure -Sum -EA 0).Count),'unique SPNs'' information..' Green,Cyan,Green $LogFile -NoNewLine if ($IncludeBadSPNs) { foreach ($ADAccount in $AccountList) { $NameList = ($ADAccount.ServicePrincipalNames -split ',').Trim() $UpdatedNameList = foreach ($Name in $NameList) { # if ($FoundSPN = $SPNList | where SPN -EQ $Name) { "$Name [$($FoundSPN.EncType)]" } else { $Name } "$Name [$(($SPNList | where SPN -EQ $Name).EncType)]" } $ADAccount.ServicePrincipalNames = $UpdatedNameList -join ', ' } } else { $AccountList = foreach ($ADAccount in $AccountList) { $GoodSPN = $false $NameList = ($ADAccount.ServicePrincipalNames -split ',').Trim() $UpdatedNameList = foreach ($Name in $NameList) { $myEncType = ($SPNList | where SPN -EQ $Name).EncType if ($myEncType -ne 'Bad SPN') { $GoodSPN = $true } "$Name [$myEncType]" } $ADAccount.ServicePrincipalNames = $UpdatedNameList -join ', ' if ($GoodSPN) { $ADAccount } } } } Write-Log 'done in',"$($Duration.Hours):$($Duration.Minutes):$($Duration.Seconds)",'(hh:mm:ss)' Cyan,Yellow,Green $LogFile #endregion #region Add delegation information, normalize LastLogonTimeStamp, ServicePrincipalNames, MemberOf $Duration = Measure-Command { Write-Log 'Adding delegation information..' Green $LogFile -NoNewLine foreach ($ADAccount in $AccountList) { $ADAccount | Add-Member -MemberType NoteProperty -Name UnconstrainedDelegation -Value 0 -EA 0 $ADAccount.UnconstrainedDelegation = $false if ($FoundDelegate = $DelegateList1 | where DistinguishedName -EQ $ADAccount.DistinguishedName) { $ADAccount.UnconstrainedDelegation = $true } $ADAccount | Add-Member -MemberType NoteProperty -Name ConstrainedDelegation -Value 0 -EA 0 $ADAccount.ConstrainedDelegation = $false if ($FoundDelegate = $DelegateList2 | where DistinguishedName -EQ $ADAccount.DistinguishedName) { $ADAccount.ConstrainedDelegation = $true } if ('LastLogonTimeStamp' -in $PropList) { $ADAccount.LastLogonTimeStamp = $( try { $Temp1 = [datetime]::FromFileTime($($ADAccount.LastLogonTimeStamp) -as [int64]) if ($Temp1 -le [DateTime]'1/1/1900') { 'Never' } else { $Temp1 } } catch { 'Never' } ) } if ('ServicePrincipalNames' -in $PropList) { $ADAccount.ServicePrincipalNames = $ADAccount.ServicePrincipalNames -join ', ' } if ('MemberOf' -in $PropList) { $ADAccount.MemberOf = $ADAccount.MemberOf -join ', ' } } $AccountList = $AccountList | sort DistinguishedName } Write-Log 'Done in',"$($Duration.Hours):$($Duration.Minutes):$($Duration.Seconds) (hh:mm:ss)" Cyan,DarkYellow $LogFile #endregion } End { $Duration = New-TimeSpan -Start $StartTime -End (Get-Date) Write-Log 'All done in',"$($Duration.Hours):$($Duration.Minutes):$($Duration.Seconds)",'hh:mm:ss' Green,Cyan,Green $LogFile $AccountList } } function Get-KTicketEncType { <# .SYNOPSIS Function to return Encryption Type of a Kerberos Ticket of a given Service Principal Name .DESCRIPTION Function to return Encryption Type of a Kerberos Ticket of a given Service Principal Name This function obtains a Kerberos Ticket for a given SPN and returns its Encryption Type .PARAMETER SPN SPN Such as 'http/daserver' .EXAMPLE Get-KTicketEncType -SPN 'http/daserver' .OUTPUTS This cmdlet returns Encryption Type such as: AES256-CTS-HMAC-SHA-1 (18) .LINK https://superwidgets.wordpress.com/category/powershell/ .NOTES Function by Sam Boutros v0.1 - 17 September 2021 #> [CmdletBinding(ConfirmImpact='Low')] Param( [Parameter(Mandatory=$true)][String]$SPN ) Begin { } Process { Add-Type -AssemblyName System.IdentityModel Try { $Ticket = New-Object System.IdentityModel.Tokens.KerberosRequestorSecurityToken -ArgumentList $SPN -EA 1 $ByteStream = $Ticket.GetRequest() $HexStream = [System.BitConverter]::ToString($ByteStream) -replace '-' $eType = [Convert]::ToInt32(($HexStream -replace '.*A0030201')[0..1] -join '', 16) if ($FoundType = $EncType | where Id -EQ $eType) { "$($FoundType.Name) ($($FoundType.Id))" } else { "Unknown ($eType)" } } catch { Write-Verbose $_.Exception.InnerException.InnerException # serviceclass/host:port/servicename 'Bad SPN' } } End { } } function Encrypt-File { <# .SYNOPSIS Function to encrypt a file using AES CBC encryption. .DESCRIPTION Function to encrypt a file using using the Advanced Encryption Standard (AES) encryption algorithm, Cipher Block Chaining (CBC) mode for data confidentiality, 128 bit block size, and 256 bit key size. .PARAMETER FilePath Path to the file to be encrypted. .PARAMETER Key The key to be used for the aes encryption. .PARAMETER KeepOriginal Optional switch. When set to True, this function will not delete the original file. .PARAMETER DoNotWriteSecretsToLog Optional switch. When set to True, this function will not write the key to the log file. .PARAMETER LogFile Path to a file where this function will write its console output. .EXAMPLE Encrypt-File -FilePath .\Questions.txt -Key 'My secret key phrase here' This example, EAS-encrypts the provided file using the provided key, deletes the original file, and displays progress messages to the console and writes them to log file, including the key. The output file will be named the same as the input file + aes extension. If the output file exists, this function will over-write it. .EXAMPLE Encrypt-File -FilePath .\Questions.txt -Key 'My secret key phrase here' -KeepOriginal -DoNotWriteSecretsToLog This example, EAS-encrypts the provided file using the provided key, does NOT delete the original file, and displays progress messages to the console and writes them to log file, NOT including the key. The output file will be named the same as the input file + aes extension. If the output file exists, this function will over-write it. .OUTPUTS Console output, log file, and the encrypted .aes file. .LINK https://superwidgets.wordpress.com/category/powershell/ .NOTES Function by Sam Boutros v0.1 - 6 October 2021 #> [CmdletBinding(ConfirmImpact='Low')] Param( [Parameter(Mandatory=$true)][String]$FilePath, [Parameter(Mandatory=$true)][String]$Key, [Parameter(Mandatory=$false)][Switch]$KeepOriginal, [Parameter(Mandatory=$false)][Switch]$DoNotWriteSecretsToLog, [Parameter(Mandatory=$false)][String]$LogFile = ".\Encrypt-File_$(Get-Date -Format 'ddMMMMyyyy_hh-mm-ss_tt').log" ) Begin { $File = Get-Item -Path $FilePath -EA 0 if ($File.FullName) { $PlainBytes = [System.IO.File]::ReadAllBytes($File.FullName) } else { Write-Log 'File',$FilePath,'not found!' Magenta,Yellow,Magenta $LogFile Write-Log $_.Exception.Message Yellow $LogFile break } } Process { Write-Log 'Encrypting file',$File.FullName Green,Cyan $LogFile -NoNewLine if ($DoNotWriteSecretsToLog) { Write-host 'using key ' -ForegroundColor Yellow -NoNewline Write-host $Key -ForegroundColor Cyan } else { Write-Log 'using key',$Key Yellow,Cyan $LogFile } try { $aesManaged = New-Object System.Security.Cryptography.AesManaged $aesManaged.Mode = [System.Security.Cryptography.CipherMode]::CBC $aesManaged.Padding = [System.Security.Cryptography.PaddingMode]::Zeros $aesManaged.BlockSize = 128 $aesManaged.KeySize = 256 $shaManaged = New-Object System.Security.Cryptography.SHA256Managed $aesManaged.Key = $shaManaged.ComputeHash([System.Text.Encoding]::UTF8.GetBytes($Key)) $Encryptor = $aesManaged.CreateEncryptor() $EncryptedBytes = $aesManaged.IV + $Encryptor.TransformFinalBlock($PlainBytes, 0, $PlainBytes.Length) $FileAlreadyExists = Test-Path "$($File.FullName).aes" [System.IO.File]::WriteAllBytes("$($File.FullName).aes", $EncryptedBytes) if ($FileAlreadyExists) { Write-Log ' done,','over-writing exiting',"$($File.FullName).aes" Green,Yellow,Cyan $LogFile } else { Write-Log ' done,',"$($File.FullName).aes" Green,Cyan $LogFile } if (-not $KeepOriginal) { Write-Log ' deleting original file' Green $LogFile -NoNewLine try { Remove-Item -Path $File.FullName -Force -Confirm:$false -EA 1 Write-Log 'done' Cyan $LogFile } catch { Write-Log ' failed.' Magenta $LogFile Write-Log $_.Exception.Message Yellow $LogFile } } } catch { Write-Log ' failed.' Magenta $LogFile Write-Log $_.Exception.Message Yellow $LogFile } } End { $shaManaged.Dispose() $aesManaged.Dispose() } } function Decrypt-File { <# .SYNOPSIS Function to decrypt a file that was encrypted with the Encrypt-File function. .DESCRIPTION Function to decrypt a file that was encrypted with the Encrypt-File function. .PARAMETER FilePath Path to the file to be encrypted. .PARAMETER Key The key to be used for the AES-CBC encryption. .PARAMETER DoNotWriteSecretsToLog Optional switch. When set to True, this function will not write the key to the log file. .PARAMETER LogFile Path to a file where this function will write its console output. .EXAMPLE Decrypt-File -FilePath .\Questions.txt.aes -Key 'My secret key phrase here' This example decrypts the provided file using the provided key, and displays progress messages to the console and writes them to log file, including the key. The output file will be named the same as the input file less the aes extension. If the output file exists, this function will over-write it. .EXAMPLE Decrypt-File -FilePath .\Questions.txt -Key 'My secret key phrase here' -DoNotWriteSecretsToLog This example, decrypts the provided file using the provided key, and displays progress messages to the console and writes them to log file, NOT including the key. The output file will be named the same as the input file less aes extension. If the output file exists, this function will over-write it. .OUTPUTS Console output, log file, and the decrypted file. .LINK https://superwidgets.wordpress.com/category/powershell/ .NOTES Function by Sam Boutros v0.1 - 6 October 2021 #> [CmdletBinding(ConfirmImpact='Low')] Param( [Parameter(Mandatory=$true)][String]$FilePath, [Parameter(Mandatory=$true)][String]$Key, [Parameter(Mandatory=$false)][Switch]$DoNotWriteSecretsToLog, [Parameter(Mandatory=$false)][String]$LogFile = ".\Decrypt-File_$(Get-Date -Format 'ddMMMMyyyy_hh-mm-ss_tt').log" ) Begin { $File = Get-Item -Path $FilePath -EA 0 if ($File.FullName) { $CipherBytes = [System.IO.File]::ReadAllBytes($File.FullName) $OutFileName = if ($File.FullName.ToLower().EndsWith('.aes')) { $File.FullName -replace '.aes' } else { "$($File.FullName).Decrypted" } } else { Write-Log 'File',$FilePath,'not found!' Magenta,Yellow,Magenta $LogFile Write-Log $_.Exception.Message Yellow $LogFile break } } Process { Write-Log 'Decrypting file',$File.FullName Green,Cyan $LogFile -NoNewLine if ($DoNotWriteSecretsToLog) { Write-host 'using key ' -ForegroundColor Yellow -NoNewline Write-host $Key -ForegroundColor Cyan } else { Write-Log 'using key',$Key Yellow,Cyan $LogFile } try { $aesManaged = New-Object System.Security.Cryptography.AesManaged $aesManaged.Mode = [System.Security.Cryptography.CipherMode]::CBC $aesManaged.Padding = [System.Security.Cryptography.PaddingMode]::Zeros $aesManaged.BlockSize = 128 $aesManaged.KeySize = 256 $shaManaged = New-Object System.Security.Cryptography.SHA256Managed $aesManaged.Key = $shaManaged.ComputeHash([System.Text.Encoding]::UTF8.GetBytes($Key)) $aesManaged.IV = $CipherBytes[0..15] $Decryptor = $aesManaged.CreateDecryptor() $DecryptedBytes = $Decryptor.TransformFinalBlock($CipherBytes, 16, $CipherBytes.Length - 16) $FileAlreadyExists = Test-Path $OutFileName [System.IO.File]::WriteAllBytes($OutFileName, $DecryptedBytes) (Get-Item $OutFileName).LastWriteTime = $File.LastWriteTime if ($FileAlreadyExists) { Write-Log ' done,','over-writing exiting',$OutFileName Green,Yellow,Cyan $LogFile } else { Write-Log ' done,',$OutFileName Green,Cyan $LogFile } } catch { Write-Log ' failed.' Magenta $LogFile Write-Log $_.Exception.Message Yellow $LogFile } } End { $shaManaged.Dispose() $aesManaged.Dispose() } } #endregion Export-ModuleMember -Function * -Variable * -Alias * |