├── .github ├── config.yml └── workflows │ └── main.yml ├── Tools ├── DashTools.psm1 ├── Get-EntraIdDevice.ps1 ├── ADMembership.ps1 ├── Remove-DuplicateModule.ps1 └── New-X509v3Certificate.ps1 ├── Demos ├── Zero-to-Hero-MANUAL.pdf ├── 3.00 LanguageParser.ps1 ├── Azure Storage Account Demo.ps1 ├── 3.00 ArgumentCompleter.ps1 ├── 1.09 ServiceTools SIMPLE.psm1 ├── 1.03 ForEach-Object LAB.ps1 ├── 2.05 String Conversion.ps1 ├── Azure VPN Certificates Demo.ps1 ├── 2.08 Workflows.ps1 ├── 2.02 COM Excel Application.ps1 ├── 3.02 Enums and flags.ps1 ├── 3.03 Module Publishing.ps1 ├── 2.02 WebRequest and RestMethod.ps1 ├── 2.03 Text Menus.ps1 ├── 1.08 Profile Script.ps1 ├── 3.01 Optimization.ps1 ├── Azure VM Demo.ps1 ├── 2.02 MailKit.ps1 ├── 1.08 Control Flow.ps1 ├── Zero-to-Hero-CLASSNOTES.ps1 └── Inside Certificates for PowerShell-DevOps Global Summit 2021.ps1 └── README.md /.github/config.yml: -------------------------------------------------------------------------------- 1 | todo: 2 | keyword: "TODO:" 3 | blobLines: 2 4 | caseSensitive: true 5 | -------------------------------------------------------------------------------- /Tools/DashTools.psm1: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DashTraining/PowerShell/HEAD/Tools/DashTools.psm1 -------------------------------------------------------------------------------- /Demos/Zero-to-Hero-MANUAL.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DashTraining/PowerShell/HEAD/Demos/Zero-to-Hero-MANUAL.pdf -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PowerShell 2 | 3 | ## Demos 4 | Scripts used in demos during online learning and classroom courses. To be referenced alongside training videos on http://pwsh.tv or during in-class training. 5 | File naming aligned with my internal course naming schema. 6 | 7 | ## Tools 8 | Useful functions in modules and scripts. 9 | Please use with caution. Read and understand the code before you execute! 10 | 11 | ### Copyright 12 | Copyright 2019-2025 Paul Dash (Paul Wojcicki-Jarocki) 13 | 14 | Permission is hereby granted to any person to access, use, copy, and modify the content published herein for private and educational purposes only. For commercial use, please contact the author. THIS CONTENT IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND. 15 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: Create issues from TODOs 2 | 3 | on: 4 | workflow_dispatch: 5 | inputs: 6 | importAll: 7 | default: false 8 | required: false 9 | type: boolean 10 | description: Enable, if you want to import all TODOs. Runs on checked out branch! Only use if you're sure what you are doing. 11 | push: 12 | branches: # do not set multiple branches, todos might be added and then get referenced by themselves in case of a merge 13 | - master 14 | 15 | permissions: 16 | issues: write 17 | repository-projects: read 18 | contents: read 19 | 20 | jobs: 21 | todos: 22 | runs-on: ubuntu-latest 23 | 24 | steps: 25 | - uses: actions/checkout@v3 26 | 27 | - name: Run Issue Bot 28 | uses: juulsn/todo-issue@main 29 | with: 30 | excludePattern: '^(node_modules/)' 31 | env: 32 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 33 | -------------------------------------------------------------------------------- /Demos/3.00 LanguageParser.ps1: -------------------------------------------------------------------------------- 1 | # 2 | # _| _ __|_ Script: '3.00 LanguageParser.ps1' 3 | # (_|(_|_)| ) . Author: Paul 'Dash' 4 | # t r a i n i n g Contact: paul@dash.training 5 | # Created: 2022-06-12 6 | # 7 | 8 | 9 | # LANGUAGE PARSER 10 | # Shows how PowerShell breaks down a command to try to understand it 11 | 12 | # What are tokens? Well, these identifiers: 13 | [enum]::GetValues([System.Management.Automation.Language.TokenKind]) | Measure-Object 14 | 15 | ########### RUN A COMMAND IN HERE 16 | # It doesn't matter whether it works or not. 17 | $APIPA = '169.*'; Get-NetIPAddress -AddressFamily IPv4 -AddressState Deprecated,'Duplicate',"Invalid" | 18 | Where {$_.IPAddress -NotLike $APIPA} 19 | 20 | 21 | # Gets the previously run command 22 | $ScriptBlock = (Get-History)[-1].CommandLine 23 | 24 | # Prepare arrays for the tokens and any errors 25 | $T = @() 26 | $E = @() 27 | 28 | # Let's analyze the script block 29 | [System.Management.Automation.Language.Parser]::ParseInput($ScriptBlock, [ref]$T, [ref]$E) 30 | 31 | # Show what was parsed 32 | $T 33 | 34 | # Make it nicer 35 | $T | Format-Table Text, Kind, TokenFlags -------------------------------------------------------------------------------- /Demos/Azure Storage Account Demo.ps1: -------------------------------------------------------------------------------- 1 | # _ _ 2 | # __| |____ ___| |__ 3 | # / _ |__ / __| '_ \ Script: 'Azure Storage Account Demo.ps1' 4 | # | (_| |(_| \__ \ | | | Author: Paul 'Dash' 5 | # \__,_\__,_|___/_| |_(_) E-mail: paul@dash.training 6 | # T R A I N I N G 7 | 8 | 9 | # Create a SA 10 | New-AzStorageAccount -ResourceGroupName $RGName ` 11 | -Location $Loc ` 12 | -Name 'gdcstoragedemo2' ` 13 | -SkuName 'Standard_LRS' ` 14 | -OutVariable sa 15 | 16 | $sa.Context 17 | 18 | # Create a BLOB Container in the SA 19 | New-AzStorageContainer -Name 'gallery' -Context $sa.Context -Permission blob 20 | 21 | # Upload some files 22 | Get-ChildItem C:\Users\Paul\OneDrive\Pictures\_MG*.jpg | 23 | ForEach-Object { 24 | Set-AzStorageBlobContent -Container 'gallery' ` 25 | -File $_.FullName ` 26 | -Blob $_.Name ` 27 | -Context $sa.Context ` 28 | -Properties @{"ContentType" = "image/jpg"} 29 | } 30 | 31 | # Retrieve links for access 32 | (Get-AzStorageBlob -Container 'gallery' -Context $sa.Context).ICloudBlob.uri.AbsoluteUri -------------------------------------------------------------------------------- /Demos/3.00 ArgumentCompleter.ps1: -------------------------------------------------------------------------------- 1 | # 2 | # _| _ __|_ Script: '3.00 ArgumentCompleter.ps1' 3 | # (_|(_|_)| ) . Author: Paul 'Dash' 4 | # t r a i n i n g Contact: paul@dash.training 5 | # Created: 2016-12-04 6 | # 7 | 8 | 9 | # ARGUMENT COMPLETER 10 | # for Intellisense and completion 11 | 12 | class LetterCompleter : System.Management.Automation.IArgumentCompleter { 13 | [System.Collections.Generic.IEnumerable[System.Management.Automation.CompletionResult]] CompleteArgument( 14 | [string]$commandName, 15 | [string]$parameterName, 16 | [string]$wordToComplete, 17 | [System.Management.Automation.Language.CommandAst]$commandAst, 18 | [System.Collections.IDictionary]$fakeBoundParameters 19 | ) { 20 | return [System.Management.Automation.CompletionResult[]]@( 21 | foreach ($l in [char[]]'ABCDEFGHIJKLMNOPQRSTUVQXYZ') { 22 | 23 | [System.Management.Automation.CompletionResult]::new( 24 | $l, 25 | $l, 26 | [System.Management.Automation.CompletionResultType]::ParameterValue, "The letter $l" 27 | ) # end ...CompletionResult 28 | } # end foreach 29 | ) # end return 30 | } # end CompleteArgument 31 | } # end class 32 | 33 | 34 | function Write-Letter { 35 | param ( 36 | [ArgumentCompleter([LetterCompleter])] 37 | [char]$Letter 38 | ) 39 | Write-Output $Letter 40 | } 41 | -------------------------------------------------------------------------------- /Demos/1.09 ServiceTools SIMPLE.psm1: -------------------------------------------------------------------------------- 1 | # _ _ 2 | # __| |____ ___| |__ 3 | # / _ |__ / __| '_ \ Script: '1.09 ServiceTools SIMPLE.psm1' 4 | # | (_| |(_| \__ \ | | | Author: Paul 'Dash' 5 | # \__,_\__,_|___/_| |_(_) E-mail: paul@dash.training 6 | # T R A I N I N G 7 | 8 | 9 | # MAKING MODULES 10 | ################################################# 11 | # This is a demo of creating a simple script module # Remember to save this file: # - in a Modules path (one of $env:PSModulePath) # - in a separate folder # - with a name like the name of the folder # - with the .psm1 extension 12 | 13 | 14 | function Get-ServiceStartInfo { 15 | <# 16 | .SYNOPSIS 17 | Gets information about how a service is being started. 18 | .DESCRIPTION 19 | More text here.... someday. 20 | .PARAMETER Name 21 | Defines the name of the service to retrieve. 22 | .EXAMPLE 23 | ServiceStartInfo.ps1 -Name spooler 24 | Gets information about the Print Spooler service from the local computer. 25 | #> 26 | 27 | [cmdletBinding()] 28 | param( 29 | [Parameter(Mandatory=$true)] 30 | [string]$Name, 31 | 32 | # The computer to retrieve services from. 33 | [string[]]$ComputerName = 'localhost' 34 | ) 35 | 36 | Write-Debug "Looking for service '$Name' on computer '$ComputerName'." 37 | 38 | Get-CimInstance -ClassName Win32_Service | 39 | Where-Object Name -eq $Name | 40 | Select-Object Name,DisplayName,State,StartMode,StartName 41 | } # END function -------------------------------------------------------------------------------- /Demos/1.03 ForEach-Object LAB.ps1: -------------------------------------------------------------------------------- 1 | # 1. Run this line to save a value to a variable. 2 | $ComputerName = 'localhost' 3 | 4 | # 2. Use Get-Member to inspect the System.String object that is now in the variable. 5 | # Notice the Chars parametrized property. 6 | # The index argument allows you to pick a letter from the string at that index. Indexing starts at 0. 7 | 8 | # 3. Use the Chars parametrized property to pick the first letter of the string in $ComputerName. 9 | # For example, this will show the third letter of the string: 10 | $ComputerName.Chars(2) 11 | 12 | 13 | 14 | # SCENARIO: You need to organize your installation files by vendor but first 15 | # organize vendors into folders by the first letter of their name. 16 | 17 | # 4. Run this line to save the list of software vendors to a variable. Add more if you feel like it. 18 | $Vendors = 'Adobe','Microsoft','Google','Autodesk' 19 | 20 | # 5. Using ForEach-Object, pick the first letter of each string in $Vendors. 21 | 22 | # 6. Read help on Select-Object on how it can select unique objects. 23 | # Use it to create a unique list of the first letters. 24 | 25 | # 7. Using ForEach-Object, create subdirectories named using the unique first letters of vendors. 26 | # You will need to use the New-Item cmdlet... a line to get you started is below: 27 | New-Item -ItemType Directory -Name A 28 | 29 | 30 | 31 | 32 | 33 | 34 | ################################################################################# 35 | # ANSWER 36 | # 37 | # ##### 38 | # 39 | # #### 40 | # 41 | # ### 42 | # 43 | # ## 44 | # 45 | # # 46 | # 47 | $Vendors | ForEach-Object {$_.Chars(0)} | Select-Object -Unique | 48 | ForEach-Object {New-Item -ItemType Directory -Name $_} 49 | 50 | 51 | -------------------------------------------------------------------------------- /Demos/2.05 String Conversion.ps1: -------------------------------------------------------------------------------- 1 | # 2 | # _| _ __|_ Script: '2.05 String Conversion.ps1' 3 | # (_|(_|_)| ) . Author: Paul 'Dash' 4 | # t r a i n i n g Contact: paul@dash.training 5 | # Created: 2020-02-12 6 | # 7 | 8 | 9 | # CONVERTING STRINGS TO OTHER STRINGS 10 | # The setup. Some strings written out to a file 11 | @" 12 | Johnathan.Doe@adatum.com 13 | Jane.Smith@contoso.com 14 | Ola.Nordmann@something.no 15 | Fake.Person@dash.training 16 | "@ | Out-File emails.txt 17 | 18 | # Let's see the strings that got produced: 19 | Get-Content .\emails.txt 20 | 21 | # We want to get just the first and last name from those e-mail addresses 22 | 23 | # First attempt 24 | Get-Content .\emails.txt | 25 | Convert-String -Example "First.Last@domain.com=Last,First" 26 | 27 | # Almost. This didn't find the last entry. 28 | # Possibly because it had a much longer domain extension. 29 | 30 | # Second attempt, with two "examples" for the command to learn from 31 | Get-Content .\emails.txt | 32 | Convert-String -Example "First.Last@domain.com=Last,First","First.Last@short.muchlonger=Last,First" | 33 | ConvertFrom-Csv -Header 'Last','First' 34 | 35 | 36 | 37 | # CONVERTING STRINGS TO OBJECTS 38 | # The setup: 39 | $netstat = netstat 40 | 41 | # Let's see just the data from that output. 42 | # We don't care about the first 4 lines and want to clean up the leading spaces. 43 | ($netstat[4..($netstat.Length-1)]).trim() 44 | 45 | # Now just have the system make sense of it by converting to objects. 46 | # You need to tell the cmdlet which "columns" of data it's seeing. 47 | ($netstat[4..($netstat.Length-1)]).trim() | 48 | ConvertFrom-String -PropertyNames Protocol,TO,FROM,State 49 | 50 | # ...or you can just use this, as Bror said ;) 51 | Get-NetTCPConnection -State Established,SynSent 52 | -------------------------------------------------------------------------------- /Tools/Get-EntraIdDevice.ps1: -------------------------------------------------------------------------------- 1 | # Paul Dash 2 | # November 2024 3 | 4 | # Shows how "OLD" API can still be used to access device information in EntraID 5 | # with regular user permissions, without providing any admin consent 6 | 7 | [cmdletBinding()] 8 | param() 9 | 10 | # Install the ADAL PowerShell module if not already installed 11 | # Uncomment the line below if you need to install the module 12 | # Install-Module -Name AzureAD.Standard.Preview -Force 13 | 14 | # Variables: Replace with your Azure AD tenant and API details 15 | $TenantId = 'f2864f9c-ee3d-4275-8137-960b00ab231f' 16 | $apiVersion = "1.6" # Azure AD Graph API version 17 | $resource = "https://graph.windows.net" # Azure AD Graph API resource 18 | 19 | Connect-AzAccount 20 | 21 | $authResult = Get-AzAccessToken -TenantId $tenantId -ResourceUrl $resource -Verbose 22 | Write-Verbose "Logged in as $($authResult.UserId)" 23 | $accessToken = $authResult.Token 24 | 25 | # API Request to get all devices 26 | $graphApiUrl = "$resource/$tenantId/devices?api-version=$apiVersion" 27 | $headers = @{ 28 | 'Authorization' = "Bearer $accessToken"; 29 | 'Content-Type' = "application/json" 30 | } 31 | 32 | # Fetch all devices 33 | Write-Verbose "Fetching devices from Azure AD Graph API..." 34 | $response = Invoke-RestMethod -Method Get -Uri $graphApiUrl -Headers $headers 35 | 36 | # Process and display the devices 37 | if ($response.value) { 38 | Write-Verbose "Devices found:" 39 | foreach ($device in $response.value) { 40 | [PSCustomObject]@{ 41 | Name = $device.displayName; 42 | DeviceID = $device.deviceId; 43 | OS = $device.deviceOSType; 44 | OSVersion = $device.deviceOSVersion; 45 | JoinType = $device.profileType; 46 | LastLogon = $device.approximateLastLogonTimestamp 47 | } 48 | } 49 | } else { 50 | throw "No devices found or insufficient permissions." 51 | } -------------------------------------------------------------------------------- /Demos/Azure VPN Certificates Demo.ps1: -------------------------------------------------------------------------------- 1 | # _ _ 2 | # __| |____ ___| |__ 3 | # / _ |__ / __| '_ \ Script: 'Azure VPN Certificates Demo.ps1' 4 | # | (_| |(_| \__ \ | | | Author: Paul 'Dash' 5 | # \__,_\__,_|___/_| |_(_) E-mail: paul@dash.training 6 | # T R A I N I N G 7 | 8 | # Creates a self-signed root certificate for signing client certificates 9 | $cert = New-SelfSignedCertificate -Type Custom -KeySpec Signature ` 10 | -Subject "CN=VPN-CA" ` 11 | -KeyExportPolicy Exportable ` 12 | -HashAlgorithm sha256 -KeyLength 2048 ` 13 | -CertStoreLocation "Cert:\CurrentUser\My" ` 14 | -KeyUsageProperty Sign -KeyUsage CertSign 15 | 16 | # Picks drive to save temporary file 17 | $DriveLetter = Get-Volume | 18 | Where-Object DriveLetter | 19 | Sort-Object DriveLetter | 20 | Select-Object -Last 1 -ExpandProperty DriveLetter 21 | 22 | # Displays above root certificate 23 | # Copy-and-paste this into the 'Point-to-site configuration' page of the 'Virtual network gateway' 24 | # in the section 'Root certificates' 25 | $cert.rawdata | ConvertTo-Base64 -NoLineBreak | Out-File "$DriveLetter:\vpn-CA.txt" 26 | notepad.exe "$DriveLetter:\vpn-CA.txt" 27 | 28 | # Creates a client certificate that is placed into the current user's Private certificate store 29 | New-SelfSignedCertificate -Type Custom -DnsName DemoVPN-Client -KeySpec Signature ` 30 | -Subject "CN=VPN-Client" -KeyExportPolicy Exportable ` 31 | -HashAlgorithm sha256 -KeyLength 2048 ` 32 | -CertStoreLocation "Cert:\CurrentUser\My" ` 33 | -Signer $cert -TextExtension @("2.5.29.37={text}1.3.6.1.5.5.7.3.2") -------------------------------------------------------------------------------- /Tools/ADMembership.ps1: -------------------------------------------------------------------------------- 1 | # _ _ 2 | # __| |____ ___| |__ 3 | # / _ |__ / __| '_ \ Script: 'ADMembership.ps1' 4 | # | (_| |(_| \__ \ | | | Author: Paul 'Dash' 5 | # \__,_\__,_|___/_| |_(_) E-mail: paul@dash.training 6 | # T R A I N I N G 7 | 8 | #Requires -Modules ActiveDirectory 9 | 10 | # .SYNOPSIS 11 | # Processes file with user names to produce User / Group pairs based on AD group membership. 12 | # .NOTES 13 | # Script for a friend. 14 | 15 | Param ( 16 | # Path to user list with a sAMAccountName or DistinguishedName or SID on each line. 17 | [Parameter(Mandatory=$true)] 18 | [ValidateNotNullOrEmpty()] 19 | [String] 20 | $Path 21 | ) 22 | 23 | # Read file in first so we know how many users to process 24 | $Users = Get-Content -Path $Path 25 | $Current = 0 26 | 27 | # Loop through the user list 28 | foreach ($SingleUser in $Users) { 29 | 30 | Write-Progress -Activity 'Getting group membership' -CurrentOperation $SingleUser -PercentComplete ($Current / $Users.Count) 31 | 32 | try { 33 | # Validate user DN 34 | $SingleDN = (Get-ADUser -Identity $SingleUser -ErrorAction Stop).DistinguishedName 35 | } catch { 36 | Write-Warning "Couldn't find user with identity '$SingleUser'." 37 | } 38 | 39 | # Proceed if user is valid 40 | if ($SingleDN) { 41 | try { 42 | # Try to get group membership 43 | # using LDAP_MATCHING_RULE_IN_CHAIN described in 44 | # https://docs.microsoft.com/en-us/windows/win32/adsi/search-filter-syntax?redirectedfrom=MSDN#operators 45 | $Groups = Get-ADGroup -LDAPFilter ("(member:1.2.840.113556.1.4.1941:={0})" -f $SingleDN) -ErrorAction Stop 46 | } catch { 47 | Write-Warning "Couldn't get group membership for '$SingleUser'." 48 | } 49 | 50 | # Loop through result to create User / Group pair for output 51 | foreach ($SingleGroup in $Groups) { 52 | New-Object -TypeName PSObject -Property @{User=$SingleUser;Group=$SingleGroup.Name} 53 | } 54 | 55 | # Cleanup before next loop iteration 56 | $SingleDN = $null 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /Demos/2.08 Workflows.ps1: -------------------------------------------------------------------------------- 1 | # 2 | # _| _ __|_ Script: '2.08 Workflows.ps1' 3 | # (_|(_|_)| ) . Author: Paul 'Dash' 4 | # t r a i n i n g Contact: paul@dash.training 5 | # Created: 2021-09-29 6 | # 7 | 8 | 9 | 10 | # PARALLELISM 11 | ################################################# 12 | workflow ParallelDemo { 13 | parallel { 14 | Get-Service 15 | Get-Process 16 | } 17 | } 18 | 19 | ParallelDemo # notice how objects will be returned in a mixed order 20 | 21 | 22 | # SUSPENDING 23 | ################################################# 24 | workflow SuspendDemo { 25 | sequence { 26 | Get-Service -Name spooler 27 | Suspend-Workflow 28 | Get-Process -Name spoolsv 29 | } 30 | } 31 | 32 | SuspendDemo # workflow suspends in middle of execution 33 | # so the Get-Service output is returned 34 | # along with an object that represents a JOB... 35 | # ...which you can list 36 | Get-Job -Newest 1 -OutVariable WorkflowJob 37 | # you can also resume running from the point where it was suspended 38 | Resume-Job -Job $WorkflowJob 39 | Get-Job -Id $WorkflowJob.Id 40 | # to get the result, you have to ask for it 41 | Receive-Job -Id $WorkflowJob.Id 42 | 43 | 44 | # NOT WITHOUT PROBLEMS 45 | ################################################# 46 | 47 | workflow ScopeDemo { 48 | $Name = 'Paul' 49 | Write-Host "Hello $Name" 50 | } 51 | ScopeDemo 52 | # PROBLEM: availability of certain commands 53 | # like the Write-Host which can't be used in WWF 54 | 55 | 56 | # SOLUTION: wrap them up in an InlineScript 57 | workflow ScopeDemo { 58 | $Name = 'Paul' 59 | InlineScript { 60 | Write-Host "Hello $Name" 61 | } 62 | } 63 | ScopeDemo 64 | # PROBLEM: InlineScript launches separate PowerShell process 65 | # which doesn't have access to the Name in WWF memory 66 | 67 | 68 | # SOLUTION: put the Name in the InlineScript or reference it using $Using: 69 | workflow ScopeDemo { 70 | $Name = 'Paul' 71 | InlineScript { 72 | Write-Host "Hello $Using:Name" 73 | } 74 | } 75 | ScopeDemo 76 | # But at this point you may just be better off running the whole thing as a remote PowerShell script 77 | -------------------------------------------------------------------------------- /Demos/2.02 COM Excel Application.ps1: -------------------------------------------------------------------------------- 1 | # 2 | # _| _ __|_ Script: '2.02 COM Excel Application.ps1' 3 | # (_|(_|_)| ) . Author: Paul 'Dash' 4 | # t r a i n i n g Contact: paul@dash.training 5 | # Created: 2021-02-09 6 | # 7 | 8 | 9 | # Demonstration of using complex COM interaction. 10 | # Runs the Excel application (must be installed) and enters some data into spreadsheet. 11 | # Documentation: 12 | # https://docs.microsoft.com/en-us/office/vba/api/overview/excel/object-model 13 | 14 | #Requires -Modules Pscx 15 | # Requires the Out-Clipboard cmdlet from the PowerShell Community Extensions 16 | # https://github.com/Pscx/Pscx 17 | 18 | 19 | # Create the Excel object. This launches the application in the background. 20 | $XLApp = New-Object -ComObject Excel.Application 21 | # Add a new document. 22 | $XLWorkbook = $XLApp.Workbooks.Add() 23 | # Choose the first worksheet (tab along the bottom). 24 | $XLSheet = $XLWorkbook.Worksheets.Item(1) 25 | # Enter a value into row X, column Y. 26 | $XLSheet.Cells.Item(1,1) = "Hello, Norway!" 27 | # Show the Excel application's window. 28 | $XLApp.Visible = $true 29 | 30 | # 31 | 32 | ## 33 | 34 | ### WE'RE NOT DONE YET! ### 35 | 36 | # Graphing values 37 | 38 | # Clear the value entered above 39 | $XLSheet.Cells.Item(1,1).ClearContents() | Out-Null 40 | 41 | # Grab some data about running processes 42 | $MemoryConsumers = Get-Process | 43 | Select-Object -Property ProcessName,WS | 44 | Sort-Object WS -Descending | 45 | Select-Object -First 5 46 | #Copy to clipboard 47 | if (Get-Module -ListAvailable pscx) { 48 | $MemoryConsumers | ConvertTo-CSV -NoTypeInformation -Delimiter "`t" | Out-Clipboard 49 | } else { 50 | 'Oops. You need the PSCX module to copy to the clipboard here.' 51 | exit 52 | } 53 | # Paste into Excel 54 | $XLSheet.Range("A1").Select() | Out-Null 55 | $XLSheet.Paste() 56 | 57 | # Define chart 58 | $Chart = $XLSheet.Shapes.AddChart().Chart 59 | $ChartData = $XLSheet.Range("A1:A6,B1:B6") 60 | $Chart.SetSourceData($ChartData) 61 | 62 | # Make chart look pretty 63 | $Chart.ChartTitle.Text = "Top Memory Consumers" 64 | $Chart.HasLegend = $false 65 | $Chart.ChartType = [Microsoft.Office.Interop.Excel.XLChartType]-4100 66 | 67 | # Save the chart as a file 68 | $Chart.Export('T:\MemChart.jpg') -------------------------------------------------------------------------------- /Tools/Remove-DuplicateModule.ps1: -------------------------------------------------------------------------------- 1 | # _ _ 2 | # __| |____ ___| |__ 3 | # / _ |__ / __| '_ \ Script: 'Remove-DuplicateModule.ps1' 4 | # | (_| |(_| \__ \ | | | Author: Paul 'Dash' 5 | # \__,_\__,_|___/_| |_(_) E-mail: paul@dash.training 6 | # T R A I N I N G 7 | 8 | 9 | # Removes duplicates of modules installed using the package manager 10 | # and leaves only the latest version. 11 | 12 | # Run as a cleanup task after using Update-Module! 13 | 14 | [cmdletbinding()] 15 | param( 16 | # Generates output object with Name and Version of removed module 17 | [switch]$OutputRemovedModule 18 | ) 19 | 20 | Write-Progress -Activity 'Removing duplicate modules' ` 21 | -CurrentOperation 'Getting list of installed modules' ` 22 | -PercentComplete 0 ` 23 | -Id 0 24 | 25 | Write-Verbose 'Getting list of installed modules...' 26 | 27 | $Modules = Get-Module -ListAvailable -Verbose:$false | 28 | Select-Object Name,Version | 29 | Sort-Object Name,Version 30 | $DuplicateModules = $Modules | 31 | Group-Object Name | 32 | Where-Object Count -GT 1 33 | 34 | Write-Verbose 'Starting module removal...' 35 | 36 | $i = 1 # counter for progress bar 37 | 38 | foreach ($m in $DuplicateModules) { 39 | Write-Progress -Activity 'Removing duplicate modules' ` 40 | -CurrentOperation "removal of $($m.Name)" ` 41 | -PercentComplete ($i++/$DuplicateModules.Count*100) ` 42 | -Id 0 43 | 44 | $Versions = $m.Group | Sort-Object Version 45 | for ($j = 0; $j -lt $Versions.Count -1; $j++) { 46 | try { 47 | Uninstall-Module -Name $Versions[$j].Name -RequiredVersion $Versions[$j].Version -Force -ErrorAction Stop | Out-Null 48 | if ($OutputRemovedModule) { # otherwise stay quiet 49 | New-Object PSCustomObject -Property @{ 'Name' = $Versions[$j].Name; 50 | 'Version' = $Versions[$j].Version } 51 | } 52 | Write-Verbose "Removed $($Versions[$j].Name) version $($Versions[$j].Version)." 53 | } catch { 54 | Write-Verbose "Could not remove $($Versions[$j].Name) version $($Versions[$j].Version)." 55 | } 56 | } # END for every $Version in group 57 | Write-Verbose "Newer version of $($Versions[-1].Name) is $($Versions[-1].Version)." 58 | } # END foreach $DuplicateModules group 59 | 60 | Write-Progress -Id 0 -Completed 61 | Write-Verbose "DONE." 62 | -------------------------------------------------------------------------------- /Demos/3.02 Enums and flags.ps1: -------------------------------------------------------------------------------- 1 | #Requires -Version 5 2 | 3 | # 4 | # _| _ __|_ Script: '3.02 Enums and flags.ps1' 5 | # (_|(_|_)| ) . Author: Paul 'Dash' 6 | # t r a i n i n g Contact: paul@dash.training 7 | # Created: 2021-03-25 8 | # 9 | 10 | # Demo of enumerations and bit flags based on a question about querying AD 11 | break 12 | 13 | 14 | 15 | # QUESTION 1: How do you find users in AD by something in their Canonical Name? 16 | 17 | # This filtering works 18 | Get-ADUser -Filter "Name -like 'Test*'" -Properties CanonicalName | 19 | Where-Object {$_.CanonicalName -like "*IT*"} 20 | 21 | # This filtering fails 22 | Get-ADUser -Filter "Name -like 'Test*' -and CanonicalName -like '*IT*'" -Properties CanonicalName 23 | 24 | 25 | 26 | # QUESTION 2: Why does the second way fail? 27 | 28 | # Let's digress: 29 | # ENUMERATIONS 30 | 31 | # This one exists 32 | [System.DayOfWeek]::Thursday 33 | [System.DayOfWeek]5 34 | 35 | # We can create enumerations ourselves 36 | enum DayOfWeekend { 37 | Saturday = 1; Sunday = 2 38 | } 39 | [DayOfWeekend]2 40 | 41 | 42 | 43 | # Inspecting the CanonicalName attribute in the Active Directory Schema 44 | $CanonicalNameFlags = Get-ADObject -Filter "Name -eq 'Canonical-Name'" ` 45 | -SearchBase 'CN=Schema,CN=Configuration,DC=ad,DC=graydaycafe,DC=com' ` 46 | -Properties * | 47 | Select-Object -ExpandProperty 'systemFlags' 48 | # Value is an integer 49 | $CanonicalNameFlags 50 | 51 | # Convert to Base2 to see the bits... each representing a bit flag (1 for ON, 0 for OFF) 52 | [System.Convert]::ToString($CanonicalNameFlags, 2) 53 | 54 | # Those flags are documented here 55 | # https://docs.microsoft.com/en-us/windows/win32/adschema/a-systemflags#remarks 56 | # https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-adts/1e38247d-8234-4273-9de3-bbf313548631 57 | 58 | # We can create an enumeration and view the bit values as [flags()] 59 | [flags()]enum SystemFlags { 60 | NONE = 0 61 | FLAG_ATTR_NOT_REPLICATED = 1 62 | FLAG_ATTR_REQ_PARTIAL_SET_MEMBER = 2 63 | FLAG_ATTR_IS_CONSTRUCTED = 4 64 | FLAG_ATTR_IS_OPERATIONAL = 8 65 | FLAG_SCHEMA_BASE_OBJECT = 16 66 | FLAG_DISALLOW_MOVE_ON_DELETE = 33554432 67 | FLAG_DOMAIN_DISALLOW_MOVE = 67108864 68 | FLAG_DOMAIN_DISALLOW_RENAME = 134217728 69 | } 70 | 71 | # Which flags does the CanonicalName attribute have? 72 | $CanonicalNameFlags -as [SystemFlags] 73 | 74 | # ANSWER: 75 | # Ah, so the Canonical Name is a constructed attribute, 76 | # so its value isn't really held in AD, but rather constructed on-the-go ! 77 | 78 | # That's why we can't search based on it. 79 | -------------------------------------------------------------------------------- /Demos/3.03 Module Publishing.ps1: -------------------------------------------------------------------------------- 1 | # _ _ 2 | # __| |____ ___| |__ 3 | # / _ |__ / __| '_ \ Script: '3.03 Module Publishing.ps1' 4 | # | (_| |(_| \__ \ | | | Author: Paul 'Dash' 5 | # \__,_\__,_|___/_| |_(_) E-mail: paul@dash.training 6 | # T R A I N I N G 7 | 8 | 9 | # Demo code for final stages of module creation. 10 | 11 | # To be used in advanced courses like the MOC 55039 or custom classes. 12 | # Makes sense only in combination with my ServiceTools module. 13 | break # STOP RUNNING 14 | 15 | 16 | 17 | # CREATING EXTERNAL HELP 18 | 19 | # How does Get-ChildItem hold its help content? 20 | Set-Location (Get-Command Get-ChildItem).Module.ModuleBase 21 | Get-Command Get-ChildItem | Select-Object -ExpandProperty HelpFile 22 | Invoke-Item (Get-Command Get-ChildItem | select -ExpandProperty HelpFile) 23 | 24 | # Prerequisite 25 | Install-Module Platyps -Scope AllUsers 26 | 27 | Set-Location (Get-Module ServiceTools -ListAvailable).ModuleBase 28 | New-Item -Path . -ItemType Directory -Name 'HelpSource' 29 | New-MarkdownHelp -Module ServiceTools -OutputFolder .\HelpSource -WithModulePage 30 | 31 | code .\HelpSource\Get-ServiceStartInfo.md 32 | code .\HelpSource\ServiceTools.md 33 | 34 | (Get-Culture).Name 35 | New-ExternalHelp -Path .\HelpSource -OutputPath ((Get-Culture).Name) 36 | 37 | 38 | 39 | # ANALYZING YOUR SCRIPT 40 | # This is a requirement for publishing into places like the PowerShell Gallery 41 | 42 | # Prerequisite 43 | Install-Module PSScriptAnalyzer -Scope AllUsers 44 | 45 | Get-ScriptAnalyzerRule 46 | Write-Host -f White -b DarkCyan "There are $((Get-ScriptAnalyzerRule).Count) rules analyzed" 47 | 48 | Set-Location (Get-Module ServiceTools -ListAvailable).ModuleBase 49 | Invoke-ScriptAnalyzer .\ServiceTools.psm1 -ReportSummary 50 | 51 | 52 | 53 | # PUBLISHING TO YOUR OWN PACKAGE PROVIDER 54 | 55 | # Create module manifest 56 | Set-Location (Get-Module ServiceTools -ListAvailable).ModuleBase 57 | New-ModuleManifest -Path .\ServiceTools.psd1 ` 58 | -Guid (New-Guid) ` 59 | -Author 'Paul Dash' ` 60 | -Description 'Tools for working with Windows system services.' ` 61 | -RootModule 'ServiceTools.psm1' ` 62 | -FunctionsToExport Get-ServiceStartInfo 63 | 64 | Test-ModuleManifest .\ServiceTools.psd1 65 | 66 | # Create repository and publish 67 | $lr = 'LocalRepository' 68 | 69 | New-Item -Path T:\ -Name $lr -ItemType Directory 70 | New-SmbShare -Name $lr -Path "T:\$lr" -FullAccess Administrators -ReadAccess Everyone 71 | 72 | Register-PSRepository -Name $lr -SourceLocation "\\DASH\$lr" -InstallationPolicy Trusted 73 | 74 | # REMEMBER to remove the HelpSource directory before publishing! 75 | Publish-Module -Path . -Repository $lr 76 | 77 | 78 | 79 | # DONE! 80 | 81 | ### Repository Cleanup 82 | Unregister-PSRepository -Name $lr 83 | Remove-SmbShare -Name $lr -Force 84 | Remove-Item "T:\$lr" -Recurse -Force 85 | -------------------------------------------------------------------------------- /Demos/2.02 WebRequest and RestMethod.ps1: -------------------------------------------------------------------------------- 1 | # 2 | # _| _ __|_ Script: '2.02 WebRequest and RestMethod.ps1' 3 | # (_|(_|_)| ) . Author: Paul 'Dash' 4 | # t r a i n i n g Contact: paul@dash.training 5 | # Created: 2016-03-23 6 | # Mod on: 2020-02-11 : new yr.no API 7 | # 8 | 9 | 10 | ################################################# 11 | # HTML 12 | # Getting a simple web page 13 | Invoke-WebRequest -Uri 'https://www.glasspaper.no' -Verbose 14 | 15 | 16 | 17 | ################################################# 18 | # XML 19 | # Getting the weather data 20 | 21 | $PostalCode = '5961' # 2061 Gardermoen 22 | 23 | # TODO: update from the old APIs 24 | # http://om.yr.no/verdata/xml/ 25 | $yrURI = "http://www.yr.no/sted/Norge/postnummer/$PostalCode/varsel.xml" 26 | 27 | $yrnoData = Invoke-WebRequest $yrURI 28 | 29 | # Save the result of the web request as XML object 30 | $WeatherResult = ([XML]$yrnoData.Content).weatherdata 31 | # Choose forecast for the near future 32 | $Forecast = $WeatherResult.forecast.tabular.time | Select-Object -First 1 33 | $Forecast | 34 | Select-Object @{N='Temp'; E={ [string]($_.temperature.value) + ' °C' }}, 35 | @{N='Wind'; E={ if ($_.windSpeed) { ($_.windDirection.code) + ' ' + ($_.windSpeed.mps) + ' mps' } 36 | else { 'no data' } }}, 37 | @{N='Pres'; E={ $_.pressure.value }} 38 | 39 | 40 | 41 | # Run this hidden ;) 42 | #region Extremes 43 | $ExtremeData = Invoke-RestMethod -Uri 'https://api.met.no/weatherapi/extremeswwc/1.3/' 44 | $ExtremeData.weatherdata.product.time.lowestTemperatures.location | 45 | Select-Object Name, @{Name='Temp';Expression={$_.lowestTemperature.value}} | 46 | Sort-Object -Property Temp | 47 | Select-Object -Last 1 | 48 | ForEach-Object {Write-Host "Which is not as bad as $($_.Temp) degrees in $($_.Name)!"} 49 | #endregion 50 | 51 | # MET.no has now changed this API and the information there requires an account: 52 | # https://frost.met.no/api.html#!/records/getRecords 53 | 54 | 55 | ################################################# 56 | # JSON 57 | # Validating the postal code 58 | 59 | $PostalCode = '1415' 60 | 61 | # https://developer.bring.com/api/postal-code/ 62 | $BringURI = 'https://api.bring.com/shippingguide/api/postalCode.json?clientUrl=dash.training&country=no&pnr=' 63 | 64 | # METHOD 1: get the HTML, look at the content, convert to JSON 65 | $BringData = Invoke-WebRequest ($BringURI + $PostalCode) 66 | $PostalCodeResult = $BringData.Content | ConvertFrom-Json 67 | 68 | # METHOD 2: get the result of a REST API 69 | $PostalCodeResult = Invoke-RestMethod ($BringURI + $PostalCode) 70 | 71 | # If the result is valid, print out its city 72 | if ($PostalCodeResult.valid) { 73 | Write-Host -F Black -B Gray "Postal code $PostalCode is valid for $($PostalCodeResult.result)." 74 | } 75 | 76 | 77 | $LocationForecastURI = 'https://api.met.no/weatherapi/locationforecast/2.0/compact?lat=59.90&lon=10.75' 78 | $AuthHead = @{'User-Agent' = 'Mozilla/5.0 (Windows NT 10.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/42.0.2311.135 Safari/537.36 Edge/12.10136'; 79 | } 80 | 81 | $ForecastResult = Invoke-RestMethod -Headers $AuthHead -Uri $LocationForecastURI -------------------------------------------------------------------------------- /Demos/2.03 Text Menus.ps1: -------------------------------------------------------------------------------- 1 | # 2 | # _| _ __|_ Script: '2.03 Text Menus.ps1' 3 | # (_|(_|_)| ) . Author: Paul 'Dash' 4 | # t r a i n i n g Contact: paul@dash.training 5 | # Created: 2021-11-08 6 | # 7 | 8 | 9 | 10 | # Simple text-based menu 11 | ################################################# 12 | 13 | # Doesn't work in ISE because of lack of implementation of ReadKey method 14 | # so please run this in the Console Host or Terminal 15 | if ($Host.Name.Contains('ISE')) { Exit } 16 | 17 | # Fancy function to draw the menu 18 | function DrawMenu { 19 | param([string]$Title, [string[]]$Items, [int]$Width = ($Host.UI.RawUI.BufferSize.Width)) 20 | 21 | $F = 'Cyan' 22 | $Horizontal = '-' * $Width 23 | $Sides = '|' + (' ' * ($Width-2)) + '|' 24 | 25 | Clear-Host 26 | Write-Host $Horizontal -F $F 27 | Write-Host $Sides -F $F 28 | Write-Host '| ' -NoNewline -F $F 29 | Write-Host $Title.ToUpper().PadRight($Width-3) -NoNewline 30 | Write-Host '|' -F $F 31 | Write-Host $Horizontal -F $F 32 | Write-Host $Sides -F $F 33 | foreach ($Item in ($Items -Split '\r?\n')) { 34 | Write-Host '| ' -NoNewline -F $F 35 | Write-Host $Item.PadRight($Width-3) -NoNewline 36 | Write-Host '|' -F $F 37 | } 38 | Write-Host $Sides -F $F 39 | Write-Host $Horizontal -F $F 40 | } 41 | 42 | # Items to include in the menu; one per line. 43 | $MenuItems = @' 44 | 1. Service info 45 | 2. Process info 46 | 3. Volume info 47 | Q. Quit 48 | '@ 49 | 50 | # Main loop to keep displaying the menu 51 | # The loop has two mechanisms for quitting. The value of $Choice can be... 52 | # [a] ...checked here in the While statement. 53 | while ($Choice -ne 'Q') { 54 | 55 | DrawMenu -Title 'Diagnostic tools' -Items $MenuItems -Width 40 56 | switch ($Choice) { 57 | '1' { Read-Host -Prompt 'Type service name' | Get-Service } 58 | '2' { Get-Process 'explorer' } 59 | '3' { Get-Volume | Where-Object {$_.DriveLetter} | Format-Table DriveLetter,Size,SizeRemaining } 60 | 'Q' { Clear-Host; Exit } # [b] ...or here within the Switch 61 | default {} 62 | } 63 | # Read the user input and react immediately without displaying character on the screen 64 | $Choice = $Host.UI.RawUI.ReadKey('NoEcho,IncludeKeyDown').Character 65 | } 66 | 67 | 68 | 69 | # A better menu system that works across different host applications 70 | ################################################# 71 | Clear-Host 72 | 73 | # Build the menu 74 | $MenuItems = @() 75 | 76 | $a = [System.Management.Automation.Host.ChoiceDescription]::new("&Service info") 77 | $a.HelpMessage = "Get running services" 78 | 79 | $b = [System.Management.Automation.Host.ChoiceDescription]::new("&Process info") 80 | $b.HelpMessage = "Get running processes" 81 | 82 | $c = [System.Management.Automation.Host.ChoiceDescription]::new("&Volume info") 83 | $c.HelpMessage = "Get disk volumes" 84 | 85 | $q = [System.Management.Automation.Host.ChoiceDescription]::new("&Quit") 86 | 87 | $Title = 'Diagnostic Tools' 88 | $Instruction = 'Select a task from the list below.' 89 | $MenuItems = $a,$b,$c,$q 90 | $DefaultItem = 3 91 | 92 | # Calling the universal PromptForChoice method 93 | $Choice = $Host.UI.PromptForChoice($Title, $Instruction, $MenuItems, $DefaultItem) 94 | 95 | # The rest of the logic can be implemented as in the above Foreach loop 96 | # based on the value that is stored in $Choice 97 | Write-Host "The output is $Choice" 98 | 99 | -------------------------------------------------------------------------------- /Demos/1.08 Profile Script.ps1: -------------------------------------------------------------------------------- 1 | # 2 | # _| _ __|_ Script: profile.ps1 3 | # (_|(_|_)| ) . Author: Paul 'Dash' 4 | # t r a i n i n g Contact: paul@pwsh.tv 5 | # 6 | 7 | # Created 2011-12-17 in Hjelmeland, Norway 8 | # Modified 2022-12-01 with new prompt function 9 | # 2024-02-01 version check around PredictionSource 10 | 11 | 12 | # Turn off command prediction 13 | if ((Get-PSReadLineOption).PredictionSource) { 14 | Set-PSReadLineOption -PredictionSource None 15 | } 16 | 17 | # Set up function for EDIT dependant on host 18 | function edit { 19 | switch -Wildcard ($host.Name) { 20 | "* ISE *" { ise $args } 21 | Default { code -r $args } 22 | } 23 | } 24 | 25 | # Set up function to go to parent item 26 | function .. { 27 | Set-Location -Path .. 28 | } 29 | 30 | # Set up function to check if running elevated 31 | function amiadmin { 32 | [Security.Principal.WindowsIdentity]::GetCurrent().Groups -contains 'S-1-5-32-544' 33 | } 34 | 35 | # Set up variables for Special Folders 36 | if (!(Test-Path Variable:\Desktop)) { 37 | $Desktop = [Environment]::GetFolderPath("Desktop") 38 | } 39 | if (!(Test-Path Variable:\Documents)) { 40 | $Documents = [Environment]::GetFolderPath("MyDocuments") 41 | } 42 | 43 | # Make a drive for running demos. 44 | $null = New-PSDrive -Name 'PS' -PSProvider FileSystem -Root "$Documents\..\Projects\Training\PowerShell" 45 | 46 | # Switch to PS: drive for demos. 47 | try { 48 | Set-Location PS:\Demo -ErrorAction Stop 49 | } catch [System.Management.Automation.DriveNotFoundException] { 50 | Write-Host "PS: drive not found. Current location is documents folder!" -f Red -b White 51 | Set-Location $Documents 52 | } 53 | 54 | 55 | # Have some fun with the 'kill' command to prevent misuse 56 | Remove-Item Alias:\kill 57 | function kill { 58 | Write-Host "`n`tKILL, v. To create a vacancy without nominating a successor." -ForegroundColor Cyan 59 | Write-Host "`t - Ambrose Bierce in `"The Devil's Dictionary`", published 1906`n" -ForegroundColor DarkCyan 60 | ### Write-Warning "Try to be more professional and use Stop-Process." -WarningAction Stop 61 | } 62 | 63 | # Declare colors and the console prompt 64 | # but running in the ISE we need to do coloring differently 65 | if ($host.Name -like "*ISE*") { 66 | $hostBg = 'Black' # $psISE.Options.ConsolePaneBackgroundColor 67 | $hostFg = 'White' # $psISE.Options.ConsolePaneForegroundColor 68 | $PromptFirstChar = [char]0xE0B0 69 | } else { 70 | $hostBg = $Host.UI.RawUI.BackgroundColor 71 | $hostFg = 'White' # 'DarkGray' 72 | $PromptFirstChar = [char]0xE0B0 73 | } 74 | # Change the prompt 75 | function prompt { 76 | if (Test-Path Variable:/PSDebugContext) { 77 | Write-Host -f $hostFg -b $hostBg '[DBG] ' -NoNewline } 78 | $CurrentLocation = $executionContext.SessionState.Path.CurrentLocation.Path.Split('\') 79 | if (($CurrentLocation.Count -gt 3)){ 80 | $PromptPath = $($CurrentLocation[0], [char]0x2026, $CurrentLocation[-2], $CurrentLocation[-1] -join ('\')) 81 | } else { 82 | $PromptPath = $($pwd.path) 83 | } 84 | Write-Host -f $hostBg -b $hostFg "$PromptFirstChar $PromptPath " -NoNewline 85 | Write-Host -f $hostFg -b $hostBg "$([string]($PromptFirstChar) * ($NestedPromptLevel + 1))" -NoNewline 86 | return " " 87 | } 88 | 89 | # Don't run subsequent commands if in a Visual Studio Host 90 | if ($host.Name -like "*Visual Studio*") { 91 | break 92 | } 93 | 94 | # Set up some colors 95 | # TODO: These should be based on current background color 96 | $VisibleFG = 'DarkYellow' 97 | # Do this only if running in Console and a FullLanguage session. 98 | # See about_Language_Modes for a detailed explanation. 99 | if (($host.Name -eq 'ConsoleHost') -and ($ExecutionContext.SessionState.LanguageMode -eq 'FullLanguage')) { 100 | # Make errors more legible 101 | $host.PrivateData.ErrorBackgroundColor = 'white' 102 | # Better color for strings 103 | switch ((Get-Module PSReadline).Version.Major) { 104 | 1 { Set-PSReadlineOption -TokenKind String -ForegroundColor Cyan } 105 | 2 { Set-PSReadLineOption -Colors @{String = "Cyan"; Parameter = "Gray"; Operator = "Magenta"}} 106 | } 107 | $VisibleFG = 'Yellow' 108 | } 109 | 110 | 111 | # Set host application window title to indicate when running elevated 112 | $host.ui.rawui.WindowTitle = "PowerShell {0}.{1}" -f $PSVersionTable.PSVersion.ToString().Split('.')[0..1] 113 | $CurrentUser = [System.Security.Principal.WindowsIdentity]::GetCurrent() 114 | $principal = new-object System.Security.principal.windowsprincipal($CurrentUser) 115 | if ($principal.IsInRole("Administrators")) { 116 | $host.ui.rawui.WindowTitle += ' [ELEVATED]' 117 | } 118 | 119 | Clear-Host 120 | if ($host.Name -eq 'ConsoleHost') { 121 | Write-Host -ForegroundColor DarkGray @' 122 | 123 | :::::::-. :::. .::::::. :: .: 124 | ;;, `';, ;;`;; ;;;` ` ,;; ;;, 125 | `[[ [[ ,[[ '[[, '[==/[[[[,,[[[,,,[[[ 126 | $$, $$c$$$cc$$$c ''' $"$$$"""$$$ 127 | 888_,o8P' 88A 8U8,88L dP 888 "88o d8b 128 | MMMMP"` YMM ""` "YMmMY" MMM YMM YMP 129 | 130 | 131 | '@ 132 | } 133 | Write-Host -BackgroundColor DarkGray -ForegroundColor White (" session launched at $(Get-Date -Format t)").PadRight((Get-Host).UI.RawUI.WindowSize.Width) 134 | -------------------------------------------------------------------------------- /Demos/3.01 Optimization.ps1: -------------------------------------------------------------------------------- 1 | #Requires -Version 5 2 | 3 | # 4 | # _| _ __|_ Script: '3.01 Optimization.ps1' 5 | # (_|(_|_)| ) . Author: Paul 'Dash' 6 | # t r a i n i n g Contact: paul@dash.training 7 | # Created: 2016-12-04 8 | # 9 | 10 | break 11 | <# 12 | TALKING POINTS - Before trying to optimize, ask yourself: 13 | 14 | 0. Running it once? Do you need to optimize? 15 | 0A. Do you have time to optimize? 16 | 0B. Will optimizing make it unreadable? 17 | 0C. Has somebody done it before? 18 | 1. Use filtering as soon as possible 19 | 2. Try to not repeat "expensive" operations 20 | 3. Use the right collection type (array, arraylist, hash table) 21 | 4. Search text using REGEX or Convert-String / ConvertFrom-String 22 | 5. Do Start-Sleep to let other threads execute 23 | 24 | #> 25 | 26 | 27 | 28 | ######################################## Piping Inefficiency 29 | ######################################## 30 | ######################################## 31 | ######################################## Looping 32 | Clear-Host 33 | (Measure-Command { 34 | 35 | 1..10000 | ForEach-Object { Write-Output $_ } 36 | 37 | }).TotalMilliseconds 38 | 39 | (Measure-Command { 40 | 41 | foreach ($number in (1..10000)) { Write-Output $number } 42 | 43 | }).TotalMilliseconds 44 | 45 | 46 | 47 | ######################################## Cmdlet Parameters 48 | Clear-Host 49 | (Measure-Command { 50 | 51 | 1..1000000 | Get-Random 52 | 53 | }).TotalMilliseconds 54 | 55 | (Measure-Command { 56 | 57 | Get-Random -Minimum 1 -Maximum 1000001 58 | 59 | }).TotalMilliseconds 60 | 61 | 62 | ######################################## but... piping allows you to quit early 63 | Clear-Host 64 | (Measure-Command { 65 | 66 | 1..1000000 | Select-Object -First 1 67 | 68 | }).TotalMilliseconds 69 | 70 | (Measure-Command { 71 | 72 | (1..1000000)[0] 73 | 74 | }).TotalMilliseconds 75 | 76 | 77 | 78 | ######################################## Filtering 79 | ######################################## 80 | ######################################## 81 | ######################################## Cmdlet vs. Method 82 | Clear-Host 83 | (Measure-Command { 84 | 1..100 | foreach { 85 | 86 | Get-Service | Where-Object {$_.Status -EQ 'Running'} 87 | 88 | } 89 | }).TotalMilliseconds 90 | 91 | (Measure-Command { 92 | 1..100 | foreach { 93 | 94 | (Get-Service).where({$_.Status -EQ 'Running'}) 95 | 96 | } 97 | }).TotalMilliseconds 98 | 99 | 100 | 101 | ######################################## Array vs. Allocated Array vs. ArrayList 102 | ######################################## 103 | ######################################## 104 | Clear-Host 105 | $size = 10000 106 | 107 | (Measure-Command { 108 | $array = @() 109 | for ($i = 0; $i -lt $size; $i++) { 110 | $array += $i 111 | } 112 | }).TotalMilliseconds 113 | 114 | (Measure-Command { 115 | $array = New-Object Int32[] $size 116 | for ($i = 0; $i -lt $size; $i++) { 117 | $array[$i] = $i 118 | } 119 | }).TotalMilliseconds 120 | 121 | (Measure-Command { 122 | $array = New-Object System.Collections.ArrayList 123 | for ($i = 0; $i -lt $size; $i++) { 124 | $array.Add($i) 125 | } 126 | }).TotalMilliseconds 127 | 128 | (Measure-Command { 129 | $array = [System.Collections.Generic.List[Int32]]::new() 130 | for ($i = 0; $i -lt $size; $i++) { 131 | $array.Add($i) 132 | } 133 | }).TotalMilliseconds 134 | 135 | 136 | 137 | ######################################## No Strings Attached 138 | ######################################## 139 | ######################################## 140 | 141 | Clear-Host 142 | (Measure-Command { 143 | 144 | [string]$string = '' 145 | for ($i = 1; $i -le 10000; $i++) { 146 | $string += 'na' 147 | } 148 | 149 | }).TotalMilliseconds 150 | 151 | (Measure-Command { 152 | 153 | $stringBuilder = New-Object System.Text.StringBuilder 154 | for ($i = 1; $i -le 10000; $i++) { 155 | $stringBuilder.Append('na') 156 | } 157 | 158 | }).TotalMilliseconds 159 | 160 | break 161 | # Oh, reading the result requires you to: 162 | $stringBuilder.ToString() 163 | 164 | 165 | 166 | ######################################## Stream IO 167 | ######################################## 168 | ######################################## 169 | Clear-Host 170 | $file = (Read-Host -Prompt "Full path to a large file") 171 | 172 | (Measure-Command { 173 | 174 | $content1 = Get-Content $file 175 | 176 | }).TotalMilliseconds 177 | 178 | (Measure-Command { 179 | 180 | $content2 = ([System.IO.StreamReader] $file).ReadToEnd() 181 | # You can also use this to read line-by-line, 182 | # doing other processing in between, 183 | # and decreasing memory usage. 184 | 185 | }).TotalMilliseconds 186 | 187 | 188 | 189 | ######################################## Different types of nothing 190 | ######################################## "Into the Void" 191 | ######################################## 192 | 193 | Clear-Host 194 | $data = 1..10 195 | $data # This will redirect the object to Out-Default 196 | break 197 | 198 | Clear-Host 199 | $data = 1..1000000 200 | 201 | (Measure-Command { 202 | 203 | $data | Out-Null 204 | 205 | }).TotalMilliseconds 206 | 207 | (Measure-Command { 208 | 209 | $data > $null 210 | 211 | }).TotalMilliseconds 212 | 213 | (Measure-Command { 214 | 215 | $null = $data 216 | 217 | }).TotalMilliseconds 218 | 219 | (Measure-Command { 220 | 221 | [void]$data 222 | 223 | }).TotalMilliseconds 224 | -------------------------------------------------------------------------------- /Demos/Azure VM Demo.ps1: -------------------------------------------------------------------------------- 1 | # _ _ 2 | # __| |____ ___| |__ 3 | # / _ |__ / __| '_ \ Script: 'Azure VM Demo.ps1' 4 | # | (_| |(_| \__ \ | | | Author: Paul 'Dash' 5 | # \__,_\__,_|___/_| |_(_) E-mail: paul@dash.training 6 | # T R A I N I N G 7 | 8 | 9 | Install-Module Az 10 | 11 | # Connect to Azure Tenant 12 | Connect-AzAccount -Subscription '' 13 | Get-AzSubscription 14 | 15 | # Validate connection 16 | Get-AzContext | Select-Object Account,Name 17 | 18 | # Create Resource Group to hold objects 19 | Get-AzLocation | Select-Object DisplayName,Location 20 | 21 | $Loc = 'westeurope' 22 | $RGName = 'rg-Demo' 23 | New-AzResourceGroup -Name $RGName -Location $Loc 24 | 25 | 26 | # Find available SIZEs for VM 27 | Get-AzVMSize -Location $Loc 28 | 29 | # Find available IMAGEs for VM 30 | Get-AzVMImagePublisher -Location $Loc | 31 | Where-Object {$_.PublisherName -like "Microsoft*" -and $_.PublisherName -notlike "*.Azure.*"} 32 | 33 | Get-AzVMImageOffer -Location $Loc -PublisherName 'MicrosoftWindowsServer' 34 | 35 | Get-AzVMImageSKU -Location $Loc -PublisherName 'MicrosoftWindowsServer' -Offer 'WindowsServer' 36 | 37 | 38 | # The proper way to create a VM: 39 | #region CreateTheVM 40 | # Create networking for the VM 41 | # (this relies on an existing 'Demo' v-net, 'FrontEnd' subnet, and 'nsg-gdc-common' Network Security Group) 42 | $DemoVnet = Get-AzVirtualNetwork -ResourceGroupName 'rg-GDC-INFRA-Networking' 43 | $FrontEndSubnet = $DemoVnet.Subnets | Where-Object Name -eq 'FrontEnd' 44 | $CommonNSG = Get-AzNetworkSecurityGroup -ResourceGroupName 'rg-GDC-INFRA-Networking' -Name 'nsg-GDC-HQ-WestEurope-FrontEnd' 45 | 46 | $VMAddress = New-AzPublicIpAddress -ResourceGroupName $RGName ` 47 | -Location $Loc ` 48 | -Name 'ip-Demo-Win-2' ` 49 | -Sku 'Basic' -AllocationMethod Dynamic 50 | $VMInterface = New-AzNetworkInterface -ResourceGroupName $RGName ` 51 | -Location $Loc ` 52 | -Name 'nic-Demo-Win-2' ` 53 | -SubnetId $FrontEndSubnet.Id ` 54 | -PublicIpAddressId $VMAddress.Id ` 55 | -NetworkSecurityGroupId $CommonNSG.Id 56 | 57 | # Create a VM configuration 58 | $VMConfig = New-AzVMConfig -VMName 'vm-Demo-Win-2' -VMSize 'Basic_A0' 59 | $VMConfig = Set-AzVMOperatingSystem -VM $VMConfig -Windows -ComputerName 'vm-Demo-Win-2' -Credential (Get-Credential) 60 | $VMConfig = Add-AzVMNetworkInterface -VM $VMConfig -Id $VMInterface.Id 61 | $VMConfig = Set-AzVMSourceImage -VM $VMConfig ` 62 | -PublisherName 'MicrosoftWindowsServer' ` 63 | -Offer 'WindowsServer' ` 64 | -Skus '2016-Datacenter' ` 65 | -Version latest 66 | 67 | # Finally... create the VM! 68 | New-AzVM -ResourceGroupName $RGName -Location $Loc -VM $VMConfig -OutVariable vm 69 | #endregion 70 | 71 | # The quicker way to create a VM: 72 | #region SimpleParameterSet 73 | New-AzVM -ResourceGroupName 'rg-Demo-2' ` 74 | -Location $Loc ` 75 | -Name 'vm-Demo-3' ` 76 | -Size 'Basic_A0' ` 77 | -Image 'Win2016Datacenter' ` 78 | -Credential (Get-Credential) ` 79 | -VirtualNetworkName 'vnet-GDC-HQ-WestEurope' ` 80 | -SubnetName 'FrontEnd' ` 81 | -SecurityGroupName 'nsg-GDC-HQ-WestEurope-FrontEnd' 82 | 83 | #-OutVariable vm 84 | #endregion 85 | 86 | 87 | # The creation with an ARM Template: 88 | #region ARM 89 | New-AzResourceGroupDeployment -ResourceGroupName $RGName ` 90 | -Mode Incremental ` 91 | -TemplateFile .\vm-Demo2-template.json ` 92 | -TemplateParameterFile .\vm-Demo2-parameters.json 93 | #endregion 94 | 95 | # Show information about the VM 96 | Get-AzVM 97 | Get-AzVM | Get-Member 98 | Get-AzVM -Status | Get-Member 99 | Get-AzVM -Status | Select-Object Name, PowerState 100 | 101 | # Connect to VM 102 | Get-AzPublicIpAddress -ResourceGroupName $RGName | Select-Object IpAddress 103 | #mstsc /v: 104 | #ssh 105 | 106 | # Resize VM 107 | $vm.HardwareProfile.VmSize = "Basic_A1" 108 | Update-AzVM -ResourceGroupName $RGName -VM $vm[0] 109 | 110 | # Add managed disk 111 | $vmDiskConfig = New-AzDiskConfig -Location $Loc -CreateOption Empty -DiskSizeGB 4 112 | $vmDataDisk = New-AzDisk -ResourceGroupName $RGName -DiskName 'Demo-Win-2_DataDisk' -Disk $vmDiskConfig 113 | #$vm = This variable should have been populated during VM creation 114 | Add-AzVMDataDisk -VM $vm[0] -Name 'Demo-Win-2_DataDisk' -CreateOption Attach -ManagedDiskId $vmDataDisk.Id -Lun 1 115 | Update-AzVM -ResourceGroupName $RGName -VM $vm[0] 116 | 117 | # Check uptime of VMs using Log Analytics 118 | Get-AzOperationalInsightsWorkspace 119 | 120 | $KustoQuery = @' 121 | Heartbeat 122 | | where TimeGenerated > ago(30d) 123 | | summarize heartbeat_count = count() by bin(TimeGenerated, 1h), Computer 124 | | summarize HoursUptime = count() by Computer 125 | '@ 126 | 127 | $UptimeQuery = Invoke-AzOperationalInsightsQuery -Query $KustoQuery ` 128 | -WorkspaceId ((Get-AzOperationalInsightsWorkspace).CustomerId)[0] 129 | $UptimeQuery.Results 130 | 131 | 132 | # Clean up 133 | Remove-AzResourceGroup -Name $RGName -Force 134 | -------------------------------------------------------------------------------- /Demos/2.02 MailKit.ps1: -------------------------------------------------------------------------------- 1 | # 2 | # _| _ __|_ Script: '2.02 Send-Mail.ps1' 3 | # (_|(_|_)| ) . Author: Paul 'Dash' 4 | # t r a i n i n g Contact: paul@dash.training 5 | # Created: 2021-02-11 6 | # 7 | 8 | 9 | # E-MAILING 10 | # Uses the MailKit library that replaces System.Net.Mail 11 | 12 | # Read the Send-Mail function, the setup requirements, the data structure for 13 | # mail server configuration and examples of function invocation. 14 | 15 | 16 | 17 | # SETUP 18 | 19 | # The right assemblies need to be in place for the right Namespaces to be available. 20 | # NuGet in Windows PowerShell seems to have issues. Try the below line with PowerShell 7. 21 | Find-Package -Name 'MimeKit','MailKit','System.Buffers' ` 22 | -Source 'https://www.nuget.org/api/v2' | 23 | Install-Package -Verbose 24 | # Also needs the Unprotect-SecureString function from my DashTools module 25 | # or from http://blogs.msdn.com/b/besidethepoint/archive/2010/09/21/decrypt-secure-strings-in-powershell.aspx 26 | 27 | 28 | # SERVER DATA 29 | 30 | # Store connection parameters in an object like below. It may be wise to save this in your $PROFILE. 31 | # The password is encrypted using your Windows session key. It's safe! 32 | $MailConfig = [PSCustomObject][ordered]@{Server='smtp.gmail.com';Port=587;Address='';User='you@gmail.com';Password=''} 33 | $MailConfig.Address = $MailConfig.User 34 | $MailConfig.Password = Read-Host -Prompt "Password for user $($MailConfig.User) on server $($MailConfig.Server)" -AsSecureString | 35 | ConvertFrom-SecureString 36 | 37 | 38 | 39 | # THE FUNCTION 40 | 41 | # Put this in a module. 42 | # Looking for the right assemblies needs fixing. Here the paths are hard-coded 43 | # and appropriate for Windows Powershell. Other paths required for Core. 44 | 45 | function Send-Mail { 46 | <# 47 | .SYNOPSIS 48 | Sends an e-mail message. 49 | .DESCRIPTION 50 | Theis cmdlet uses the MailKit library to send an e-mail message 51 | to the specified recipient with the provided title and body. 52 | .PARAMETER To 53 | Specifies the addresses to which the mail is sent. 54 | .PARAMETER Body 55 | Specifies the body (text content only) of the e-mail message. 56 | .PARAMETER Subject 57 | Specifies the subject of the e-mail message. The default value is set to the name of the previously executed command. 58 | #> 59 | 60 | [CmdletBinding()] 61 | param( 62 | [parameter(Mandatory=$true)] 63 | [string[]]$To, 64 | 65 | [parameter(Mandatory=$false)] 66 | [string]$Subject = "Send-Mail RESULT of $^", 67 | 68 | [parameter(Mandatory=$true,ValueFromPipeline=$true)] 69 | [string[]]$Body 70 | ) 71 | 72 | BEGIN { 73 | # Load required assemblies 74 | $RequiredAssemblies = 'MailKit','MimeKit','System.Buffers' 75 | foreach ($Assembly in $RequiredAssemblies) { 76 | (Get-Package -ProviderName NuGet -Name $Assembly).Source 77 | } 78 | 79 | # TODO: Change so that paths don't need to be hard-coded 80 | # TODO: Consider different environment versions 81 | $PathMailKit = 'C:\Program Files\PackageManagement\NuGet\Packages\MailKit.3.3.0\lib\net48\MailKit.dll' 82 | $PathMimeKit = 'C:\Program Files\PackageManagement\NuGet\Packages\MimeKit.3.4.0\lib\net48\MimeKit.dll' 83 | $PathBuffers = 'C:\Program Files\PackageManagement\NuGet\Packages\System.Buffers.4.5.1\lib\net461\System.Buffers.dll' 84 | [System.Reflection.Assembly]::LoadFile($PathMailKit) > $null 85 | [System.Reflection.Assembly]::LoadFile($PathMimeKit) > $null 86 | [System.Reflection.Assembly]::LoadFile($PathBuffers) > $null 87 | # LoadFile doesn't work on its own. Neither does Add-Type on its own. 88 | # Doing both works. Why? 89 | Add-Type -Path $PathMailKit,$PathMailKit,$PathBuffers 90 | 91 | # Create message object 92 | $Message = New-Object -TypeName MimeKit.MimeMessage 93 | # Define From, To, and Subject 94 | $Message.From.Add($MailConfig.Address) 95 | foreach ($MailRecipient in $To) { 96 | $Message.To.Add($MailRecipient) 97 | } 98 | $Message.Subject = $Subject 99 | 100 | # Start constructing message body 101 | $MessageBody = New-Object MimeKit.BodyBuilder 102 | } 103 | 104 | PROCESS { 105 | foreach ($Line in $Body) { 106 | # Add single line to message body 107 | $MessageBody.TextBody += ($Line + "`n") 108 | } 109 | } 110 | 111 | END { 112 | # Save the message body 113 | $Message.Body = $MessageBody.ToMessageBody() 114 | 115 | # Create mail client 116 | $SMTP = New-Object MailKit.Net.Smtp.SmtpClient 117 | # Set TLS to automatically negotiate security 118 | $SSLAuto = [MailKit.Security.SecureSocketOptions]::Auto 119 | 120 | try { 121 | # Connect and send 122 | $SMTP.Connect($MailConfig.Server, $MailConfig.Port, $SSLAuto) 123 | $SMTP.Authenticate($MailConfig.User, ($MailConfig.Password | ConvertTo-SecureString | Unprotect-SecureString)) 124 | $SMTP.Send($Message) 125 | } catch { 126 | Write-Output "There was a problem sending the e-mail." 127 | } 128 | 129 | # Cleanup 130 | $SMTP.Disconnect($true) 131 | $SMTP.Dispose() 132 | } # END END 133 | } # END function 134 | 135 | 136 | 137 | # EXAMPLES 138 | break 139 | 140 | 141 | -------------------------------------------------------------------------------- /Demos/1.08 Control Flow.ps1: -------------------------------------------------------------------------------- 1 | # 2 | # _| _ __|_ Script: '1.08 Control Flow.ps1' 3 | # (_|(_|_)| ) . Author: Paul 'Dash' 4 | # t r a i n i n g Contact: paul@dash.training 5 | # Created: 2019-09-20 6 | # Mod on: 2020-03-03 : commented code 7 | # 8 | 9 | 10 | # CONTROL FLOW EXAMPLES 11 | 12 | ## LOOPS 13 | 14 | ### FOREACH 15 | # In cmdlet form, this typically takes input from the pipeline. 16 | # The special placeholder variable ($_ or $PSItem) is used to represent each individual passed object. 17 | Get-Service X* | ForEach-Object { Write-Host -f White -b DarkCyan $_.DisplayName } 18 | 19 | # In statement form, a new variable and a collection to iterate are enclosed parenthesis, 20 | # and the new variable is used in the code block to represent each individual passed object. 21 | # If passed through a variable, traditionally the collection is named as a plural and 22 | # the new variable representing each object is named as singular. 23 | $ManyServices = Get-Service X* 24 | foreach ($OneService in $ManyServices) { 25 | Write-Host -f White -b DarkCyan $OneService.DisplayName 26 | } 27 | 28 | 29 | ### FOR 30 | # Like in many other languages, PowerShell has a 'For loop'. 31 | # The parenthesis hold the: initial counter value, 32 | # condition for running the loop, 33 | # operation to be performed on the counter each time the loop runs. 34 | # Traditionally, counters are named i, j, k. 35 | # This example counts from 1 to 10: 36 | for ($i = 1; $i -lt 11; $i++) { 37 | Write-Host -f White -b DarkCyan $i 38 | } 39 | 40 | 41 | ## WHILE AND UNTIL 42 | # Constructs that are similar to each other and loop, checking a condition to quit. 43 | # Difference is in WHEN the condition is checked and whether we expect a value of TRUE or FALSE. 44 | 45 | # Checks condition at the beginning. Condition has to be FALSE to quit. 46 | Clear-Host 47 | while ((Read-Host -Prompt 'Type "Q" to quit') -ne 'Q') { 48 | Write-Host -f White -b DarkCyan 'Running code in ScriptBlock' 49 | } 50 | 51 | # Checks condition after first execution of script block. Condition has to be FALSE to quit. 52 | Clear-Host 53 | do { 54 | Write-Host -f White -b DarkCyan 'Running code in ScriptBlock' 55 | } while ((Read-Host -Prompt 'Type "Q" to quit') -ne 'Q') 56 | 57 | # Checks condition after first execution of script block. Condition has to be TRUE to quit. 58 | Clear-Host 59 | do { 60 | Write-Host -f White -b DarkCyan 'Running code in ScriptBlock' 61 | } until ((Read-Host -Prompt 'Type "Q" to quit') -eq 'Q') 62 | 63 | 64 | ## CONDITIONAL STATEMENTS 65 | 66 | ### IF 67 | # Like in other languages, processes a script block if the condition is true 68 | cls 69 | $ComputerName = Read-Host -Prompt 'Type a computer name' 70 | 71 | if ($ComputerName -eq 'LON-DC1') { 72 | 'The DC in London' 73 | } 74 | 75 | # Combined with ELSE, to run a script block when the condition is false 76 | cls 77 | $ComputerName = Read-Host -Prompt 'Type a computer name' 78 | 79 | if ($ComputerName -eq 'LON-DC1') { 80 | 'The DC in London' 81 | } else { 82 | 'Unknown computer' 83 | } 84 | 85 | 86 | # Can combine multiple (even different) conditions with 'elseif'. 87 | # Make sure the most specific case is checked first. Only single match is made. 88 | cls 89 | $ComputerName = Read-Host -Prompt 'Type a computer name' 90 | 91 | if ($ComputerName -eq 'LON-DC1') { 92 | 'The DC in London' 93 | } elseif ((Get-Date).Hour -lt 7) { # unrelated comparison, but possible to do within the IF 94 | 'This is too early for me' 95 | } elseif ($ComputerName -like "*DC?") { 96 | 'Some other DC, but not the London ONE' 97 | } elseif ($ComputerName -like "*CL?" -or $ComputerName -like "*CL??") { 98 | 'The client' 99 | } else { 100 | 'Unknown computer' 101 | } 102 | 103 | 104 | # You can do multiple comparisons in one IF clause... 105 | Clear-Host 106 | $ComputerName = Read-Host -Prompt 'Type a computer name' 107 | if ($ComputerName -eq 'LON-DC1' -or $ComputerName -eq 'LON-DC2' -or $ComputerName -eq 'LON-DC3') { 108 | 'This is a known DC in London' 109 | } else { 110 | 'Unknown computer' 111 | } 112 | 113 | # ...but think of ways to shorten the comparisons 114 | Clear-Host 115 | $ComputerName = Read-Host -Prompt 'Type a computer name' 116 | if ($ComputerName -in 'LON-DC1','LON-DC2','LON-DC3') { 117 | 'This is a known DC in London' 118 | } else { 119 | 'Unknown computer' 120 | } 121 | 122 | 123 | # just LEFT side of -AND being checked in the IF 124 | Set-StrictMode -Version Latest 125 | Clear-Host 126 | if ($false -and ($ComputerName.RandomAccessMemorySize -gt 16GB)) { 127 | 'Will not be reached' 128 | } else { 129 | 'Will be reached without running right side' 130 | } 131 | 132 | Clear-Host 133 | if ($true -or ($ComputerName.StorageDiskCapacity -gt 100GB)) { 134 | 'Will be reached without running right side' 135 | } 136 | 137 | 138 | # in PowerShell 7 you can also do: 139 | ($ComputerName -LIKE "*DC?") ? 'This is a DC in London' : 'Unknown Computer' 140 | 141 | 142 | # SWITCH (also called CASE) STATEMENT 143 | # Compares input from parenthesis against values on subsequent lines. 144 | # Multiple matches are allowed, so a 'break' statement can be used to exit the switch. 145 | # Does exact comparisons by default, but can also use wildcards or Regular Expressions. 146 | cls 147 | switch -Wildcard (Read-Host -Prompt 'Type a computer name') { 148 | '*DC*' { 'The DOMAIN CONTROLLER'} 149 | '*CL*' { 'The CLIENT'} 150 | '*LON*' { 'in London'; BREAK } 151 | '*OSL*' { 'in Oslo' } 152 | '*WDC*' { 'in Washington, District of Columbia' } 153 | default { 'UNKNOWN' } 154 | } 155 | 156 | 157 | # Break vs Continue 158 | # Thanks Jiri for asking this ;) 159 | # TODO: improve this example 160 | cls 161 | for ($i = 1; $i -lt 5; $i++) { 162 | if (-not ($i % 2)) { continue } 163 | $i 164 | } 165 | 166 | cls 167 | for ($i = 1; $i -lt 5; $i++) { 168 | if (-not ($i % 2)) { break } 169 | $i 170 | } 171 | -------------------------------------------------------------------------------- /Tools/New-X509v3Certificate.ps1: -------------------------------------------------------------------------------- 1 | # _ _ 2 | # __| |____ ___| |__ 3 | # / _ |__ / __| '_ \ 4 | # | (_| |(_| \__ \ | | | 5 | # \__,_\__,_|___/_| |_(_) 6 | # T R A I N I N G 7 | 8 | # Script: 'New-X509v3Certificate.ps1' 9 | 10 | # Original author: Adam Conkle - Microsoft Corporation 11 | # Original source: http://social.technet.microsoft.com/wiki/contents/articles/4714.how-to-generate-a-self-signed-certificate-using-powershell.aspx 12 | 13 | # Adopted by: Paul Wojcicki-Jarocki - Paul Dash (paul@pauldash.com) 14 | # Adopted because: * added more options for intended usage (EKU) and Subject fields 15 | # * commented throughout 16 | # * change to more secure sha256 17 | # * certificate validity period is corrected 18 | # * generated certificate is added to correct stores 19 | # * added export of .CER file to user-defined path 20 | # * added checks for existing certificates and successful export/import 21 | 22 | 23 | 24 | Write-Host "This script will generate a self-signed certificates with an exportable private key.`n" 25 | 26 | $ContextAnswer = Read-Host "Store certificate in the User or Computer store? [U/C]" 27 | if ($ContextAnswer -eq "U") { 28 | $machineContext = 0 29 | $initContext = 1 30 | $CertStoreLocation = 'CurrentUser' 31 | } elseif ($ContextAnswer -eq "C") { 32 | $machineContext = 1 33 | $initContext = 2 34 | $CertStoreLocation = 'LocalMachine' 35 | } else { 36 | Write-Host "Invalid selection. Exiting." 37 | Exit 38 | } 39 | 40 | # Set certificate Subject name based on user input 41 | $SubjectCN = Read-Host "Subject name of the certificate " 42 | $SubjectE = Read-Host "Subject E-mail address " 43 | 44 | # Dangerous things about to happen ;) 45 | $ErrorActionPreference = 'Stop' 46 | 47 | if (Get-ChildItem "Cert:\$CertStoreLocation\My\" | Where-Object {$_.Subject -like "*CN=$Subject*"}) { 48 | Write-Warning "Other certificates for that subject exist." 49 | } 50 | 51 | $DistinguishedName = New-Object -ComObject "X509Enrollment.CX500DistinguishedName.1" 52 | if ($SubjectE) { 53 | $DistinguishedName.Encode("CN=$SubjectCN, E=$SubjectE", 0) 54 | } else { 55 | $DistinguishedName.Encode("CN=$SubjectCN", 0) 56 | } 57 | 58 | # Generate private key 59 | $key = New-Object -ComObject 'X509Enrollment.CX509PrivateKey.1' 60 | $key.ProviderName = 'Microsoft RSA SChannel Cryptographic Provider' 61 | #$key.ProviderName = 'Microsoft Base Smart Card Crypto Provider' # from CryptoAPI 62 | #$key.ProviderName = 'Microsoft Smart Card Key Storage Provider' # from CNG 63 | $key.KeySpec = 1 # for other purposes: 3 64 | $key.Length = 2048 65 | $key.SecurityDescriptor = "D:PAI(A;;0xd01f01ff;;;SY)(A;;0xd01f01ff;;;BA)(A;;0x80120089;;;NS)" 66 | $key.MachineContext = $machineContext 67 | $key.ExportPolicy = 1 # 0 for non-exportable Private Key 68 | $key.Create() 69 | 70 | # Create OID for intended usage 71 | ### TODO: check out the .NET type System.Security.Cryptography.X509Certificates.X509EnhancedKeyUsageExtension 72 | $codesigningoid = New-Object -ComObject 'X509Enrollment.CObjectId.1' 73 | ### TODO: put this in a SWITCH 74 | $codesigningoid.InitializeFromValue("1.3.6.1.5.5.7.3.3") # Code Signing 75 | #$codesigningoid.InitializeFromValue("1.3.6.1.5.5.7.3.1") # Server Authentication 76 | #$codesigningoid.InitializeFromValue("1.3.6.1.5.5.7.3.2") # Client Authentication 77 | #$codesigningoid.InitializeFromValue("1.3.6.1.5.5.7.3.4") # Secure Email 78 | #$codesigningoid.InitializeFromValue("1.3.6.1.5.5.7.3.8") # Time Stamping 79 | #$codesigningoid.InitializeFromValue("1.3.6.1.4.1.311.10.3.12") # Document Signing 80 | 81 | # Add OID to list 82 | $ekuoids = New-Object -ComObject "X509Enrollment.CObjectIds.1" 83 | $ekuoids.Add($codesigningoid) 84 | 85 | 86 | ################## 87 | <# 88 | # Add OID to list 89 | $ekuoids = New-Object -ComObject "X509Enrollment.CObjectIds.1" 90 | 91 | @' 92 | 1. Code Signing 93 | 2. Server Authentication 94 | 3. Client Authentication 95 | 4. Secure Email 96 | 5. Time Stamping 97 | 6. Document Signing 98 | '@ 99 | $ExitSelectingOID = $false 100 | do { 101 | $ekuOID = New-Object -ComObject "X509Enrollment.CObjectId.1" 102 | 103 | switch (Read-Host -Prompt 'Type choice of Enhanced Key Usage or [D]one') 104 | { 105 | '1' { $ekuOID.InitializeFromValue("1.3.6.1.5.5.7.3.3") } 106 | '2' { $ekuOID.InitializeFromValue("1.3.6.1.5.5.7.3.1") } 107 | '3' { $ekuOID.InitializeFromValue("1.3.6.1.5.5.7.3.2") } 108 | '4' { $ekuOID.InitializeFromValue("1.3.6.1.5.5.7.3.4") } 109 | '5' { $ekuOID.InitializeFromValue("1.3.6.1.5.5.7.3.8") } 110 | '6' { $ekuOID.InitializeFromValue("1.3.6.1.4.1.311.10.3.12") } 111 | 'D' { $ExitSelectingOID = $true } 112 | Default { Write-Warning 'Unsupported key usage!' } 113 | } 114 | 115 | if ($ekuOID.Value -and ($ekuOID -notin $ekuoids)) { 116 | $ekuoids.Add($ekuOID) 117 | } 118 | 119 | } until ($ExitSelectingOID) 120 | #> 121 | 122 | 123 | 124 | # Add list of OIDs to extensions 125 | $ekuext = New-Object -ComObject 'X509Enrollment.CX509ExtensionEnhancedKeyUsage.1' 126 | $ekuext.InitializeEncode($ekuoids) 127 | 128 | # Create certificate request 129 | $CertReq = New-Object -ComObject 'X509Enrollment.CX509CertificateRequestCertificate.1' 130 | $CertReq.InitializeFromPrivateKey($initContext, $key, "") 131 | ### $CertReq.CriticalExtensions.Remove(0) # Remove 'Key Usage' 132 | $CertReq.Subject = $DistinguishedName 133 | ### $CertReq.Issuer = $CertReq.Subject # Self-signed, so Issuer generated automatically 134 | $CertReq.NotBefore = (Get-Date).ToUniversalTime().Date 135 | $CertReq.NotAfter = $CertReq.NotBefore.AddYears(1) 136 | 137 | # Set signing algorithm to SHA-2 256-bit 138 | [string]$SigAlgorithmName = 'sha256' 139 | $SigAlgorithmOID = New-Object -ComObject X509Enrollment.CObjectId 140 | $SigAlgorithmOID.InitializeFromValue(([Security.Cryptography.Oid]$SigAlgorithmName).Value) 141 | $CertReq.HashAlgorithm = $SigAlgorithmOID 142 | 143 | # Add list of extensions to request 144 | $CertReq.X509Extensions.Add($ekuext) 145 | 146 | # Generate request 147 | $CertReq.Encode() 148 | 149 | # Send request 150 | $Enrollment = New-Object -ComObject 'X509Enrollment.CX509Enrollment.1' 151 | $Enrollment.InitializeFromRequest($CertReq) 152 | # Receive requested certificate in DER-encoded format 153 | $CertBASE64 = $Enrollment.CreateRequest(0) 154 | 155 | Write-Host "Certificate creation: $($Enrollment.Status.ErrorText)" -ForegroundColor Green 156 | 157 | # Install certificate in store 158 | # https://docs.microsoft.com/en-us/windows/win32/api/certenroll/nf-certenroll-ix509enrollment-installresponse 159 | # Restrictions = 4 (AllowUntrustedRoot) 160 | # Encoding = 0 (XCN_CRYPT_STRING_BASE64HEADER) 161 | $Enrollment.InstallResponse(4, $CertBASE64, 0, "") 162 | 163 | # Export certificate to a file 164 | $FilePath = Read-Host "Directory path to store the .CER file" 165 | if (Test-Path -Path $FilePath -PathType Container) { 166 | # Encoding = 12 (XCN_CRYPT_STRING_HEXRAW) 167 | $SignedCert = Get-ChildItem "Cert:\$CertStoreLocation\My\" | 168 | Where-Object {$_.SerialNumber -eq $CertReq.SerialNumber(12).Trim()} 169 | $ExportedCertPath = Export-Certificate -Cert $SignedCert -FilePath (Join-Path -Path $FilePath -ChildPath "$Subject.cer") 170 | } else { 171 | Write-Warning "Certificate export: Could not write to path $FilePath. Will not save to the CA and publishers stores." 172 | exit 173 | } 174 | # Verify export 175 | if (Test-Path -Path $ExportedCertPath -PathType Leaf) { 176 | Write-Host "Certificate export: Completed successfully." -ForegroundColor Green 177 | } else { 178 | Write-Warning "Certificate export: File export failed. Will not save to the CA and publishers stores." 179 | exit 180 | } 181 | 182 | if ($SignedCert.EnhancedKeyUsageList.FriendlyName -contains 'Code Signing') { 183 | 184 | # Import certificate into Root CA (or Intermediate Certification Authorities) and Trusted Publishers 185 | Import-Certificate -FilePath $ExportedCertPath -CertStoreLocation "Cert:\$CertStoreLocation\Root" | Out-Null 186 | Import-Certificate -FilePath $ExportedCertPath -CertStoreLocation "Cert:\$CertStoreLocation\TrustedPublisher" | Out-Null 187 | # Verify import 188 | if ((Get-ChildItem "Cert:\$CertStoreLocation\Root\$($SignedCert.Thumbprint)") -and 189 | (Get-ChildItem "Cert:\$CertStoreLocation\TrustedPublisher\$($SignedCert.Thumbprint)")) { 190 | Write-Host "Certificate install: Completed successfully." -Fore Green 191 | } 192 | 193 | Write-Host "`nUse this path for Set-AuthenticodeSignature:`nCert:\$CertStoreLocation\My\$($SignedCert.Thumbprint)" 194 | } 195 | -------------------------------------------------------------------------------- /Demos/Zero-to-Hero-CLASSNOTES.ps1: -------------------------------------------------------------------------------- 1 | # 2 | # _| _ __|_ Script: 'Zero-to-Hero-CLASSNOTES.ps1' 3 | # (_|(_|_)| ) . Author: Paul 'Dash' 4 | # t r a i n i n g Contact: paul@dash.training 5 | # Created: 2019-03-13 6 | # 7 | 8 | 9 | # CLASS NOTES 10 | # from my "Zero to Hero" 1-day training 11 | 12 | break # so whole thing won't run if you press F5 on this file! 13 | 14 | 15 | ########### FINDING CMDLETS 16 | 17 | Get-Command -Name *Item* 18 | Get-Command -Noun Item 19 | Get-Command -Verb Stop 20 | Get-Command -Module Hyper-V 21 | 22 | 23 | ########### GETTING HELP 24 | 25 | Update-Help 26 | 27 | Get-Help dir 28 | # then use -Detailed OR -Full OR better yet: 29 | Get-Help dir -ShowWindow 30 | # SYNTAX section shows: 31 | # Get-ChildItem [[-Path] ] 32 | dir 33 | dir -Path C:\ 34 | dir C:\ 35 | dir -Path C:\, S:\, . 36 | 37 | Get-Help about_* 38 | Get-Help about_CommonParameters -ShowWindow 39 | del X:\Windows\explorer.exe -WhatIf 40 | 41 | 42 | ########### MODULES 43 | 44 | Get-Module 45 | Get-Module -ListAvailable 46 | 47 | Find-Module -Name *WSUS* # looks at the PowerShellGallery.com repository 48 | #Install-Module PoshWSUS # running this will install a module on your system 49 | 50 | 51 | ########### Review of Lab Exercise 1 52 | 53 | get-help Get-EventLog 54 | 55 | Set-Service -? 56 | 57 | Get-NetFirewallRule -? 58 | get-help Get-NetFirewallRule -ShowWindow 59 | Get-NetFirewallRule -Enabled true 60 | 61 | Get-WindowsEdition -Online 62 | 63 | Get-Module 64 | Get-Volume 65 | Get-Module 66 | 67 | 68 | ########### PIPELINE 69 | 70 | dir C:\Windows | more # this needs to be run in the console 71 | 72 | Get-Process | Get-Member 73 | Get-Process | Sort-Object -Property WS -Descending | Select-Object -First 3 74 | 75 | Get-ChildItem | Get-Member 76 | Get-ChildItem | Select-Object -Property Name,Length,LastAccessTime 77 | 78 | # Creating your own property 79 | Get-ChildItem | Select-Object -Property Name, @{Name='LengthInKB';Expression={$_.Length / 1KB}}, Length 80 | Get-ChildItem | Select-Object -Property Name, @{Name='LengthInKB';Expression={$_.Length / 1KB}}, Length | Get-Member 81 | 82 | 83 | ########### FILTERING 84 | 85 | | Where-Object PROPERTY -OPERATOR VALUE # basic syntax of the Where-Object filter 86 | 87 | Get-Help about_Comparison_Operators -ShowWindow 88 | 89 | # Comparing numbers 90 | 2 -gt 3 91 | 4 -lt 777 92 | 93 | # Comparing strings 94 | 'hello' -eq 'HELLO' # case-insensitive 95 | 'hello' -Ceq 'HELLO' # case-sensitive 96 | 'hello' -like "*LL*" # true 97 | 'hello' -like "*LLL*" # false 98 | 99 | Get-Service -Running # no built-in filtering 100 | Get-Service | Get-Member 101 | Get-Service | Where-Object Status -EQ 'Running' # displays only the RUNNING services 102 | 103 | 104 | ########### FORMATING 105 | 106 | dir | format-wide -AutoSize -Property LastAccessTime 107 | 108 | dir | Format-List -Property Name, LastAccessTime 109 | dir | Format-List -Property * 110 | 111 | dir | Format-Table -Property Name, LastAccessTime | gm # produces format information 112 | dir | select-object -Property Name, LastAccessTime | gm # retains the original objects 113 | 114 | dir | Format-Table -AutoSize -HideTableHeaders -Wrap 115 | 116 | 117 | ########### REDIRECTING OUTPUT 118 | dir # 119 | dir | Out-Default # 120 | dir | Out-Host # these 3 are the same! 121 | 122 | dir | Out-File -FilePath T:\dir.txt # output to a text file 123 | dir | Export-Csv -Path T:\dir.csv -Delimiter ';' 124 | Invoke-Item T:\dir.csv 125 | Import-Csv T:\dir.csv | gm 126 | dir | export-clixml -Path T:\dir.xml # export maintaining object structure 127 | Invoke-Item T:\dir.xml 128 | Import-Clixml T:\dir.xml | gm 129 | 130 | dir | Out-Printer -Name PDF 131 | dir | Out-Null 132 | dir | Out-GridView 133 | 134 | Get-Process | select-object Name | Out-GridView -OutputMode Single | Stop-Process -WhatIf 135 | 136 | "It's lunch time!" | Out-Voice 137 | 138 | 139 | ########### Review of Lab Exercise 2 140 | 141 | get-help Get-EventLog -ShowWindow 142 | get-eventlog -LogName Security | gm 143 | 144 | Get-EventLog -LogName Security -InstanceId 4616 | Select-Object -First 10 # works, but is slow 145 | 146 | # we can compare speed of commands and see that using the cmdlet with just parameters is faster! 147 | Measure-Command { 148 | Get-EventLog -LogName Security | Where-Object InstanceID -eq 4616 | Select-Object -First 10 } 149 | 150 | Measure-Command { 151 | Get-EventLog -LogName Security -InstanceId 4616 -Newest 10 } 152 | 153 | Set-Location Cert: 154 | Get-ChildItem -Recurse | Get-Member 155 | Get-ChildItem -Recurse | Where-Object HasPrivateKey -eq $true 156 | # be careful here with data types: "false" is still _true_ 157 | # use the boolean variable: $false if you want false 158 | 159 | 160 | ########### VARIABLES 161 | 162 | # bad name 163 | $a = 'anything here' 164 | # good name, because it tells you what is inside 165 | $AllServices = Get-Service 166 | 167 | New-Variable -Name c -Value 'something' -Option Constant 168 | $c = 'and now something else' # this will fail! 169 | 170 | # automatic data types 171 | $a = 'something' 172 | $a | Get-Member 173 | $a.GetType() 174 | $b = '4' 175 | $x = 3 176 | $pi = 3.141592 177 | $y = 2 178 | 179 | # but be careful... 180 | $y + $b 181 | $b + $y 182 | 183 | # setting and changing object type 184 | [int]$z = '7' 185 | $x -as [string] 186 | ($x -as [string]).GetType() 187 | $pi -as [int] 188 | 189 | # also remember variable scope 190 | # sample file included in ZIP to be placed in current directory 191 | .\scope.ps1 192 | # what is the value of A now? 193 | $a 194 | 195 | 196 | ########### COLLECTIONS 197 | 198 | $array = 1, 2, 34, 555, 678, 9000 199 | $array 200 | $array[0] # 1st element 201 | $array[2] # 3rd element 202 | $array.Count # every collection has a Count property. 203 | $array[$array.Count -1] 204 | $array[-2] # Cool! You can index from end. 205 | $array[20] # NOT cool. No out-of-bounds errors. 206 | 207 | # collections typically show up as the type of object that is inside 208 | $array | Get-Member 209 | # you can see that this is an array by using a method 210 | $array.GetType() 211 | 212 | # easy way to loop through elements of an array 213 | $array | ForEach-Object { Write-Host -f Yellow -b Black "The number $_" } 214 | # side-note: be careful with variables in strings - PowerShell is not greedy 215 | $services | ForEach-Object { Write-Host -f Yellow -b Black "The service name $_.DisplayName" } 216 | # the fix: 217 | $services | ForEach-Object { Write-Host -f Yellow -b Black "The service name $($_.DisplayName)" } 218 | 219 | # if you need to loop through an array to look for an element, use these operators instead 220 | 3 -in $array # false in our case 221 | $array -contains 34 # true 222 | 223 | $emptyArray = @() 224 | 225 | $twoDimensional = @('A1','A2'),@('B1','B2') 226 | $twoDimensional[1][0] 227 | 228 | # expensive memory-wise 229 | $array = $array + 100000 230 | # because it creates a copy of the data in memory 231 | # as an array is of a fixed size 232 | $array.IsFixedSize 233 | 234 | # uses less memory but requires more work. 235 | $list = New-Object System.Collections.ArrayList 236 | $list.IsFixedSize # no, it's flexible 237 | $list.Add(1) 238 | $list.Add(2) 239 | $list.Add(34) 240 | # etc... 241 | $list 242 | $list[0] 243 | 244 | # hash tables 245 | $hashTable = @{ 246 | 'Shape' = 'circle'; 247 | 'Color' = 'blue' 248 | } 249 | # are indexed by Key, not order 250 | $hashTable['color'] 251 | 252 | $emptyHashTable = @{} 253 | 254 | 255 | ########### FLOW CONTROL 256 | 257 | # FOR loop to count 1 to 10 258 | for ($i = 1; $i -le 10; $i++) { 259 | $i 260 | } 261 | 262 | # same thing with a DO..WHILE loop 263 | $i = 1 264 | do { 265 | $i 266 | $i++ 267 | } while ($i -le 10) 268 | 269 | # but while loops don't tend to use counters 270 | # here instead, Read-Host can be used to gather user input 271 | do { 272 | Write-Host 'Stuff happens here...' 273 | } while ((Read-Host -Prompt 'Type Q to quit') -ne 'Q') 274 | 275 | # conditional IF statements 276 | if ($Light -eq 'Green') { 277 | # WALK 278 | } elseif ($Light.Blinking -eq $true) { 279 | # finish crossing 280 | } else { 281 | # DON'T WALK 282 | } 283 | 284 | 285 | ########### REPOSITORY 286 | 287 | # WMI and WinRM cmdlets both do the same thing using different protocols 288 | Get-Command -Noun WMI*, CIM* 289 | 290 | Get-Help Get-WmiObject -ShowWindow 291 | 292 | # search for classes 293 | Get-WmiObject -List -Class Win32*BIOS* 294 | # get instance of that class 295 | Get-WmiObject -Class Win32_BIOS 296 | # get instance and see all its properties 297 | # remember that PowerShell typically hides things from you 298 | Get-WmiObject -Class Win32_BIOS | fl -Property * 299 | 300 | 301 | ########### REMOTING 302 | 303 | # getting credentials 304 | $creds = Get-Credential -Message 'Credentials for GDC-DC' 305 | $creds 306 | 307 | # Remote WMI command 308 | Get-WmiObject -Class Win32_Service -ComputerName 10.11.12.13 -Credential $creds 309 | 310 | # remote WinRM command, first setting up a reusable session 311 | New-CimSession -ComputerName 10.11.12.13 -Credential $creds 312 | Get-CimInstance -ClassName Win32_Service -CimSession (Get-CimSession -Id 1) 313 | 314 | # PowerShell Remoting to send ANY command to another system 315 | Enter-PSSession -ComputerName 10.11.12.13 -Credential $creds 316 | Invoke-Command { Get-WindowsEdition -online } -computerName 10.11.12.13 -Credential $creds 317 | 318 | 319 | ########### SCRIPT SECURITY 320 | 321 | Get-ExecutionPolicy 322 | Get-ExecutionPolicy -List 323 | Set-ExecutionPolicy -ExecutionPolicy AllSigned -WhatIf 324 | # sample file included in ZIP to be placed in current directory 325 | Get-AuthenticodeSignature .\SampleSignedScript.ps1 326 | 327 | # to sign a script, first find a code-signing certificate 328 | Get-ChildItem cert:\ -Recurse -CodeSigningCert 329 | -------------------------------------------------------------------------------- /Demos/Inside Certificates for PowerShell-DevOps Global Summit 2021.ps1: -------------------------------------------------------------------------------- 1 | # 2 | # _| _ __|_ Script: 'Inside Certificates.ps1' 3 | # (_|(_|_)| ) . Author: Paul 'Dash' Wojcicki-Jarocki 4 | # t r a i n i n g Contact: paul@dash.training 5 | # Created: 2021-03-31 6 | # 7 | ############################################################################### 8 | 9 | # created for the PowerShell + DevOps Global Summit 2021 10 | # session entitled Inside Certificates 11 | 12 | break # Don't run top-to-bottom! 13 | 14 | 15 | 16 | # DATA-IN-USE 17 | ############################################################################### 18 | # besides the well-recognized Data-at-Rest and Data-in-Motion 19 | 20 | 'My secret is good coffee' | ConvertTo-SecureString -AsPlainText 21 | # the cmdlet doesn't like the fact that the string 22 | # is already in memory in an unencrypted format 23 | 24 | 25 | 26 | # CLASSIC CIPHERS 27 | ############################################################################### 28 | # Ceaser Cipher, a simple substitution by shifting the alphabet 29 | 30 | function CeaserCipherAlgorithm { 31 | param($Plaintext, $Key) 32 | 33 | [char[]]$Alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ' 34 | [string]$Ciphertext = '' 35 | 36 | foreach ($char in $Plaintext) { 37 | if ($char -in $Alphabet) { 38 | $Newindex = (($Alphabet.IndexOf($char) + $Key) % $Alphabet.Count) 39 | $Ciphertext += $Alphabet[$Newindex] 40 | } else { 41 | $Ciphertext += $char 42 | } 43 | } # foreach 44 | 45 | Write-Output $Ciphertext 46 | } # function 47 | 48 | cls 49 | [int]$Key1 = 3 50 | [char[]]$Plaintext = 'COFFEE CAME TO EUROPE IN THE SIXTEENTH CENTURY' 51 | 52 | $Ciphertext = CeaserCipherAlgorithm $Plaintext $Key1 53 | $Ciphertext 54 | 55 | 56 | 57 | # MODERN CIPHERS 58 | ############################################################################### 59 | # symmetrical encryption shown here with the .NET AES abstract class 60 | 61 | cls 62 | [char[]]$Key2 = 'Where have you BEAN all my life?' 63 | $Plaintext = "It's hard to ESPRESSO my feelings for you. I like you a LATTE." 64 | 65 | # Choose the algorithm and set the key 66 | $AESAlgorithm = [System.Security.Cryptography.Aes]::Create() 67 | $AESAlgorithm 68 | $AESAlgorithm.Key = $Key2 69 | 70 | # Convert input 71 | $PlainBytesIn = [System.Text.Encoding]::UTF8.GetBytes($Plaintext) 72 | 73 | # Encrypt 74 | $Encryptor = $AESAlgorithm.CreateEncryptor() 75 | $CipherBytes = $Encryptor.TransformFinalBlock($PlainBytesIn, 0, $PlainBytesIn.Length); 76 | $CipherText = [System.Convert]::ToBase64String($CipherBytes) 77 | 78 | $Ciphertext 79 | 80 | # Decrypt 81 | $Decryptor = $AESAlgorithm.CreateDecryptor(); 82 | $PlainBytesOut = $Decryptor.TransformFinalBlock($CipherBytes, 0, $CipherBytes.Length) 83 | 84 | [System.Text.Encoding]::UTF8.GetString($PlainBytesOut) 85 | 86 | 87 | 88 | # CALCULATING ONE-WAY FUNCTION 89 | ############################################################################### 90 | # using the 256-bit version of SHA-2 91 | 92 | cls 93 | [string]$Plaintext = (Get-Content S:\CoffeeRecipe.txt -Raw) 94 | 95 | [byte[]]$PlainBytesIn = $Plaintext.ToCharArray() 96 | 97 | $SHAAlgorithm = [System.Security.Cryptography.SHA256]::Create() 98 | $HashBytes = $SHAAlgorithm.ComputeHash($PlainBytesIn) 99 | 100 | $HashBytes.Length * 8 # always 256 bits output for SHA256 101 | 102 | $Hash = ([System.BitConverter]::ToString($HashBytes)).Replace('-','') 103 | $Hash 104 | 105 | # Above will work for any data, but for files 106 | # a hash can also be calculated quickly using: 107 | (Get-FileHash -Path S:\CoffeeRecipe.txt).Hash 108 | 109 | # useful for confirming files were properly downloaded 110 | # against hashes published on developer's website 111 | 112 | 113 | 114 | # INSIDE A CERTIFICATE 115 | ############################################################################### 116 | # navigating the PSDrive for Certificates 117 | 118 | Get-ChildItem Cert:\ 119 | Get-ChildItem Cert:\CurrentUser\ 120 | Get-ChildItem Cert:\CurrentUser\My\ 121 | 122 | cls 123 | # Here's my own self-issued e-mail signing certificate 124 | # (change the filter to load a certificate you own) 125 | $Cert1 = Get-ChildItem Cert:\CurrentUser\My\* | 126 | Where-Object Subject -like "CN=Paul Dash*" 127 | 128 | $Cert1 | 129 | Select-Object Subject, 130 | @{N='PublicKey'; 131 | E={([BitConverter]::ToString( 132 | $_.PublicKey.EncodedKeyValue.RawData)).Replace('-','')}}, 133 | NotBefore, NotAfter, 134 | @{N='Extensions'; 135 | E={$_.Extensions.Oid}}, 136 | Version, SerialNumber, Issuer 137 | 138 | 139 | 140 | # EXTENSIONS: POLICIES 141 | ############################################################################### 142 | # Microsoft's Certificate Policy and Certification Practice Statement: 143 | # https://www.microsoft.com/pki/mscorp/cps/default.htm 144 | 145 | 146 | 147 | # EXPLORING OIDs 148 | ############################################################################### 149 | # standardized ways of describing algorithms, key usage, policies... 150 | 151 | $Cert1.Extensions.Oid 152 | 153 | # Here we can translate an OID into its friendly name 154 | [System.Security.Cryptography.Oid]'2.5.29.37' 155 | [System.Security.Cryptography.Oid]'1.3.6.1.5.5.7.3.1' 156 | [System.Security.Cryptography.Oid]'Code Signing' 157 | 158 | # Companies can also use these data structures to hold useful information 159 | # Private Enterprise Number can be requested on https://pen.iana.org/pen/PenApplication.page 160 | # Good OID search http://www.oid-info.com/basic-search.htm 161 | $OID = New-Object System.Security.Cryptography.Oid('1.3.6.1.4.1.53698', 'Gray Day Cafe') 162 | 163 | 164 | 165 | # SUBJECT NAMING 166 | ############################################################################### 167 | # many forms of names used inside the certificate 168 | 169 | $Cert2 = Get-ChildItem Cert:\LocalMachine\ADDRESSBOOK | 170 | Where-Object Subject -like "*.microsoft.com*" 171 | 172 | # an easy way to do this #Requires -Version 7.1 173 | Get-ChildItem -Path Cert:\ -DnsName microsoft.com -Recurse 174 | 175 | cls 176 | # X500 Distinguished Name format 177 | $Cert2.SubjectName.Name -split ', ' | Out-String 178 | 179 | # If this extension is present, we can list IPs, DNS Names, URIs, e-mail addresses 180 | $Cert2.Extensions['2.5.29.17'].Oid 181 | # But we have to decode it from Abstract Syntax Notation One (ASN.1) 182 | # Format method argument TRUE for multi-line output 183 | ([System.Security.Cryptography.AsnEncodedData]::new( 184 | $Cert2.Extensions['2.5.29.17'].Oid, 185 | $Cert2.Extensions['2.5.29.17'].RawData)).Format($true) 186 | 187 | # blank by default, this can be set by a user after the certificate has been issued 188 | $Cert2.FriendlyName 189 | $Cert2.FriendlyName = 'Microsoft Websites DEMO' 190 | 191 | 192 | # SELF-SIGNED CERTIFICATES part 1 193 | ############################################################################### 194 | # using the COM object is powerfull but tedious 195 | 196 | cls 197 | # Prepare the private key 198 | $Key3 = New-Object -ComObject 'X509Enrollment.CX509PrivateKey.1' 199 | $Key3.MachineContext = 1 200 | $Key3.Create() 201 | 202 | # Specify the subject 203 | $Subject = 'GrayDayCafe.test' 204 | $DistinguishedName = New-Object -ComObject "X509Enrollment.CX500DistinguishedName.1" 205 | $DistinguishedName.Encode("CN=$Subject", 0) 206 | 207 | # Create the certificate request (that would get sent to a CA) 208 | $CertReq = New-Object -ComObject 'X509Enrollment.CX509CertificateRequestCertificate.1' 209 | $CertReq.InitializeFromPrivateKey(2, $Key3, "") # context 2 = COMPUTER 210 | $CertReq.Subject = $DistinguishedName 211 | $CertReq.Encode() 212 | 213 | # Send request 214 | $Enrollment = New-Object -ComObject 'X509Enrollment.CX509Enrollment.1' 215 | $Enrollment.InitializeFromRequest($CertReq) 216 | 217 | # Check if certificate enrollment succeeded 218 | $Enrollment.Status.ErrorText 219 | # Receive requested certificate in DER-encoded format 220 | $Cert3 = $Enrollment.CreateRequest(0) 221 | $Cert3 222 | 223 | $Enrollment.CertificateFriendlyName = 'GDC Test 3' 224 | 225 | # Install into certificate store 226 | # https://docs.microsoft.com/en-us/windows/win32/api/certenroll/nf-certenroll-ix509enrollment-installresponse 227 | # Restrictions = 4 (AllowUntrustedRoot) 228 | # Encoding = 0 (XCN_CRYPT_STRING_BASE64HEADER) 229 | $Enrollment.InstallResponse(4, $Cert3, 0, "") 230 | 231 | 232 | 233 | # SELF-SIGNED CERTIFICATES part 2 234 | ############################################################################### 235 | # using the cmdlet introduced in Server 2012/Windows 8 236 | 237 | cls 238 | # Quickest way to create a certificate 239 | # as SSLServerAuthentication is the default Type 240 | # and Subject will get derived from DnsName 241 | New-SelfSignedCertificate -DnsName 'GrayDayCafe.test' ` 242 | -FriendlyName 'GDC Test 4' 243 | # That was easy! 244 | 245 | # You gain more control by defining your own extensions: 246 | # - Subject Alternative Name including server IP 247 | # - Certificate Policy 248 | # - Enhanced Key Usage 249 | $Extensions = @('2.5.29.17={text}IPAddress=10.11.12.13&DNS=GrayDayCafe.test&DNS=10.11.12.13', 250 | '2.5.29.32={text}OID=1.3.6.1.4.1.53698.1&Notice=You found another Easter Egg!', 251 | '2.5.29.37={text}1.3.6.1.5.5.7.3.1') 252 | 253 | # ...and with lots of parameters 254 | $Cert5Params = @{ Subject = 'CN=GrayDayCafe.test,O=Gray Day Cafe'; 255 | FriendlyName = 'GDC Test 5'; 256 | TextExtension = $Extensions; # from above 257 | Provider = 'Microsoft Software Key Storage Provider'; 258 | HashAlgorithm = 'SHA256'; 259 | KeyAlgorithm = 'RSA'; 260 | KeyLength = 2048; 261 | KeyExportPolicy = 'Exportable'; 262 | KeyProtection = 'None' } 263 | # NotBefore and NotAfter should also be set to your liking 264 | # Trend now is to use short expiration dates of 1~3 months 265 | 266 | $Cert5 = New-SelfSignedCertificate @Cert5Params 267 | $Cert5 268 | 269 | 270 | 271 | # PSREMOTING OVER WINRM WITH HTTPS 272 | ############################################################################### 273 | #region ON_REMOTE 274 | 275 | # Explaining the problem 276 | # Across untrusted domains (or in workgroups), Negotiate authentication 277 | # will choose NTLM, which does not guarantee server identity 278 | # DEFAULT VALUES: 279 | Get-ChildItem WSMan:\localhost\Service\Auth # Kerberos and Negotiate are ON 280 | Get-ChildItem WSMan:\localhost\Listener # HTTP only 281 | 282 | # Add HTTPS Listener 283 | New-Item -Path WSMan:\localhost\Listener ` 284 | -Transport HTTPS ` 285 | -Address 'IP:10.11.12.13' ` 286 | -CertificateThumbPrint $Cert5.Thumbprint ` 287 | -Force 288 | 289 | # Allow traffic on TCP 5986 290 | New-NetFirewallRule -DisplayName 'Windows Remote Management (HTTPS-In)' ` 291 | -Name 'Windows Remote Management (HTTPS-In)' ` 292 | -Description 'DEMO of WSMan over HTTPS' ` 293 | -Direction Inbound ` 294 | -Protocol TCP ` 295 | -LocalPort 5986 ` 296 | -Action Allow ` 297 | -Profile Any # consider using on Private profile only 298 | 299 | 300 | # Block TCP 5985 traffic and disable HTTP Listener 301 | Disable-NetFirewallRule -DisplayName "Windows Remote Management (HTTP-In)" 302 | Set-WSManInstance -ResourceURI winrm/config/listener ` 303 | -SelectorSet @{Address='*';Transport='HTTP'} ` 304 | -ValueSet @{Enabled='false'} 305 | Restart-Service WinRM 306 | 307 | #endregion 308 | 309 | #region ON_LOCAL 310 | cls 311 | $RemotingParams = @{ ComputerName = '10.11.12.13'; 312 | Credential = (Get-Credential) } 313 | 314 | # Test if the connectivity is going to work 315 | Test-WSMan @RemotingParams -UseSSL -Authentication Negotiate 316 | 317 | # To prevent error about unknown certificate authority 318 | $PSSessionOption = New-PSSessionOption -SkipCACheck 319 | Enter-PSSession @RemotingParams -UseSSL -SessionOption $PSSessionOption 320 | 321 | # NOTE: Starting PowerShell 6, you can do PSRemoting over SSH 322 | # to Linux systems and use certificates for authentication 323 | #endregion 324 | 325 | 326 | 327 | # SSL CERTIFICATES IN IIS 328 | ############################################################################### 329 | # using regular certificates (not SNI) in Windows certificate storage 330 | 331 | # Server Name Indication (SNI) is an extension to the TLS protocol. 332 | # It allows a client or browser to indicate which hostname it is trying to connect to at the start of the TLS handshake. 333 | # This allows the server to present multiple certificates on the same IP address and port number. 334 | 335 | 336 | # certificates for IIS need to be in this Certificate Store 337 | Get-ChildItem Cert:\LocalMachine\WebHosting 338 | Copy-Item -Path "Cert:\LocalMachine\My\$($Cert5.Thumbprint)" ` 339 | -Destination 'Cert:\LocalMachine\WebHosting' 340 | # Copy-Item not supported by this PSProvider, although Move-Item is! 341 | 342 | # source is our certificate 343 | $IISCert = Get-Childitem cert:\LocalMachine\My | 344 | Where-Object { $_.Subject -like '*GrayDayCafe.test*' } 345 | # destination Certificate Store 346 | $DestStore = New-Object -TypeName System.Security.Cryptography.X509Certificates.X509Store ` 347 | -ArgumentList 'WebHosting','LocalMachine' 348 | $DestStore.Open([System.Security.Cryptography.X509Certificates.OpenFlags]::ReadWrite) 349 | $DestStore.Add($IISCert) 350 | $DestStore.Close() 351 | 352 | # We need to bind the HTTPS protocol to the website and assign the right certificate 353 | New-WebBinding -Name "Default Web Site" -IPAddress "*" -Port 443 -HostHeader "GrayDayCafe.test" -Protocol "https" 354 | (Get-WebBinding -Port 443 -Protocol 'HTTPS').AddSslCertificate($IISCert.Thumbprint, "WebHosting") 355 | Get-WebBinding 356 | 357 | # We could also store .PFX certificate files in a Centralized Certificate Store 358 | # on a network share and have IIS automatically select the right one. 359 | 360 | 361 | 362 | # ACME CERTIFICATE REQUEST 363 | # FOR AZURE APPSERVICE 364 | ############################################################################### 365 | # Let's Encrypt, a non-profit Certification Authority has a list of 366 | # ACME (RFC8555) protocol clients https://letsencrypt.org/docs/client-options/ 367 | # including PowerShell modules 368 | 369 | Install-Module -Name Posh-ACME -Scope AllUsers 370 | 371 | # SHORT DIGRESSION ABOUT TLS VERSIONS 372 | ############################################################ 373 | # up to version 1.1 have been deprecated, 374 | # so check what you're using: 375 | [System.Net.ServicePointManager]::SecurityProtocol 376 | 377 | # setup of Azure permissions is described in the plugin documentation 378 | # https://github.com/rmbolger/Posh-ACME/blob/main/Posh-ACME/Plugins/Azure-Readme.md 379 | 380 | # I already connected to my Azure account 381 | Get-AzContext 382 | 383 | $LetsEncryptParams = @{ Domain = 'test.GrayDayCafe.com'; 384 | Contact = 'paul@dash.training'; 385 | AcceptTOS = $true; # Terms of Service 386 | Plugin = 'Azure'; 387 | PluginArgs = @{ AZSubscriptionId = (Get-AzContext).Subscription.Id; 388 | AZAccessToken = (Get-AzAccessToken).Token } } 389 | # Verify domain ownership and 390 | # retrieve the certificate 391 | $Cert6 = New-PACertificate @LetsEncryptParams 392 | 393 | # Show the interesting properties 394 | $Cert6 | Select-Object @{N='Domain';E={($_.AllSANs)[0]}},Thumbprint,PfxFullChain,PfxPass | Format-List 395 | $Key6Password = ([pscredential]::new('PFX',$Cert6.PfxPass).GetNetworkCredential().Password) 396 | 397 | # Nothing left to look at in DNS! 398 | # The record used to validate domain ownership has been cleaned up for us 399 | Get-AzDnsRecordSet -ResourceGroupName Demo -ZoneName test.graydaycafe.com 400 | 401 | # Add binding to Custom Domain Name 402 | New-AzWebAppSSLBinding -WebAppName 'graydaycafe' ` 403 | -ResourceGroupName 'gdc-rg' ` 404 | -Name ($Cert6.AllSANs)[0] ` 405 | -CertificateFilePath $Cert6.PfxFullChain ` 406 | -CertificatePassword $Key6Password ` 407 | -SslState SniEnabled 408 | 409 | 410 | 411 | # CERTIFICATE VALIDATION 412 | ############################################################################### 413 | # Using the excellent free tool by PKI Solutions: 414 | # GitHub: https://github.com/PKISolutions/SSLVerifier.WPF 415 | # Info: https://www.pkisolutions.com/ssl-certificate-verifier-tool-v1-5-4-update/ 416 | 417 | cls 418 | # This may #Requires -RunAsAdministrator 419 | Add-Type -Path 'C:\Program Files\PKI Solutions\SSL Verifier\SSLVerifier.Core.dll' 420 | 421 | $WebSiteCert = New-Object SSLVerifier.Core.Default.ServerEntry 'test.GrayDayCafe.com' 422 | 423 | $SSLVerifierConfig = New-Object SSLVerifier.Core.Default.CertProcessorConfig 424 | $SSLVerifierConfig 425 | $SSLVerifierConfig.SslProtocolsToUse += [System.Security.Authentication.SslProtocols]::Tls13 426 | 427 | $SSLVerifier = New-Object SSLVerifier.Core.Processor.CertProcessor $SSLVerifierConfig 428 | $SSLVerifier.StartScan($WebSiteCert) 429 | 430 | # properties scanner will fill in: ItemStatus, SAN, ChainStatus, Certificate and Tree 431 | $WebSiteCert.ItemStatus # should show Valid 432 | $WebSiteCert.ChainStatus # should show NoError 433 | 434 | 435 | 436 | # SERVER CRYPTOGRAPHY SUITES 437 | ############################################################################### 438 | # A best practice is to remove protocols, ciphers, hashes and key exchange algorithms 439 | # that are seen as outdated or compromised. This tool can help. 440 | # No PowerShell, but there's a CLI version: 441 | # https://www.nartac.com/Products/IISCrypto/ 442 | 443 | 444 | 445 | # CODE SIGNING 446 | ############################################################################### 447 | # signing may be required under your Execution Policy 448 | 449 | cls 450 | $Cert7 = Get-ChildItem Cert:\ -Recurse -CodeSigningCert | 451 | Where-Object NotAfter -gt (Get-Date) 452 | 453 | # Important settings of a code-signing certificate 454 | $Cert7 | 455 | Select-Object Subject, 456 | @{N='KeyUsage'; 457 | E={([System.Security.Cryptography.AsnEncodedData]::new( 458 | $_.Extensions['2.5.29.15'].Oid, 459 | $_.Extensions['2.5.29.15'].RawData)).Format($false)}}, 460 | EnhancedKeyUsageList, HasPrivateKey, Thumbprint 461 | 462 | # Same certificate in multiple Certificate Stores for it to be trusted 463 | Get-ChildItem -Path Cert:\ -Recurse | 464 | Where-Object Thumbprint -eq $Cert7.Thumbprint | 465 | Select-Object Subject,PSParentPath 466 | 467 | Set-AuthenticodeSignature -Certificate $Cert7 ` 468 | -FilePath S:\CafeScript.ps1 ` 469 | -TimestampServer 'http://tsa.starfieldtech.com' 470 | # Pretty recent list of Time Stamp Authorities: 471 | # https://kbpdfstudio.qoppa.com/list-of-timestamp-servers-for-signing-pdf/ 472 | 473 | ############################################################################### 474 | # Th-th-th-that's all folks! 475 | --------------------------------------------------------------------------------