├── .gitignore ├── 2019-01-31 - Scottish PS UG - REST APIs ├── Code.ps1 └── README.md ├── 2019-04-29 - PowerShell Summit - Curl2PS Lightning Demo ├── Curl2PS_Prez.pptx └── Prez.ps1 ├── 2020-01-30 - MTX Portland - AD User Lifecycle ├── Demos │ ├── 00 - Lab Notes.ps1 │ ├── 01 - Creating Users from a Spreadsheet and Template.ps1 │ ├── 01 - UserUpdate.xlsx │ ├── 02 - Standardizing attributes.ps1 │ ├── 03 - Cleaning up Stale Accounts.ps1 │ ├── 04 - Finding Expiring Accounts and Notifying the Manager.ps1 │ └── 05 - Disabled and Retaining.ps1 └── MTX Portland - Automating AD User Lifecycle.pptx ├── 2020-01-30 - MTX Portland - REST APIs ├── Demos │ ├── 01 - Data.json │ ├── 01 - Working with json.ps1 │ ├── 02 - Invoke-RestMethod Syntax.ps1 │ ├── 03 - Interpreting Curl Syntax.ps1 │ └── 04 - Making API cmdlets.ps1 └── MTX Portland - REST APIs.pptx ├── 2020-02-10 - Mississippi PS UG - Module Building Modules ├── Demos │ ├── 01 - Plaster.ps1 │ ├── 02 - PlatyPS.ps1 │ ├── 03 - InvokeBuild.ps1 │ └── 04 - PSDeploy.ps1 ├── MSPSUG - Module Building Modules.pptx └── README.md ├── 2021-04-29 - PowerShell Summit 2021 - Practically Regexing ├── Demos │ ├── 1. 5 Minute PowerShell Regex Overview.ps1 │ ├── 2. AD DN.ps1 │ ├── 3. Log file example.ps1 │ ├── 4. VS Code Regex.ps1 │ ├── 5. Regex vs. Alternatives.ps1 │ ├── 6. What not to regex.ps1 │ ├── SampleLog.txt │ └── SanitizedLog.txt └── PowerShell Summit 2021 - Practically Regexing.pptx ├── 2022-01-05 - Research Triangle PS UG - Runway ├── README.md └── assets │ ├── action-chain.png │ ├── define-job.png │ ├── dl-job-results.png │ ├── job-w-connector.png │ ├── platformoverview.png │ └── results-chain.png ├── 2022-04-12 - Chicago PS UG - Runway ├── README.md └── assets │ ├── connector-examples.png │ ├── dl-job-results.png │ └── platformoverview.png ├── 2022-04-26 - aC the world! As Code for Anything ├── 00 configs │ └── README.md ├── 01 first example │ ├── MfpAccounts │ │ ├── 01 │ │ │ ├── Sites.txt │ │ │ ├── cicd.yaml │ │ │ └── script.ps1 │ │ └── 02 │ │ │ ├── Sites.txt │ │ │ ├── cicd.yaml │ │ │ ├── config.json │ │ │ └── script.ps1 │ └── README.md ├── 02 second example │ ├── DDLs │ │ ├── cicd.yaml │ │ ├── definitions │ │ │ ├── config.json │ │ │ └── regionbased.json │ │ └── script.ps1 │ └── README.md ├── PowerShell Summit 2022 - aC the World!.pptx └── README.md ├── 2022-04-28 - AutoRest The Module Generator ├── 01 setup │ └── README.md ├── 02 sample │ ├── README.md │ └── swagger.json ├── 03 build │ ├── README.md │ └── src │ │ └── README.md ├── 04 auth │ ├── README.md │ └── src │ │ ├── README.md │ │ └── custom │ │ └── Module.cs ├── 05 adding cmdlets │ ├── README.md │ └── src │ │ ├── README.md │ │ └── custom │ │ ├── Connect-Runway.ps1 │ │ ├── Get-RwConnectionByName.ps1 │ │ └── Module.cs ├── 06 directives │ ├── README.md │ └── src │ │ ├── README.md │ │ └── custom │ │ ├── Connect-Runway.ps1 │ │ ├── Get-RwConnectionByName.ps1 │ │ └── Module.cs ├── PowerShell Summit 2022 - AutoRest.pptx └── README.md ├── 2023-04-26 - Building a serverless Discord bot ├── 01 - Azure Function Workflow │ ├── 01 - Profile.ps1 │ ├── 02 - HTTPStart.ps1 │ └── 03 - Orchestrator.ps1 ├── 02 - Frostbite │ └── 01 - Monitor.ps1 ├── 03 - Function App │ ├── .funcignore │ ├── .gitignore │ ├── .vscode │ │ ├── extensions.json │ │ ├── launch.json │ │ ├── settings.json │ │ └── tasks.json │ ├── HttpStart │ │ ├── function.json │ │ └── run.ps1 │ ├── host.json │ ├── monitor │ │ ├── function.json │ │ └── run.ps1 │ ├── orchestrator │ │ ├── function.json │ │ └── run.ps1 │ ├── profile.ps1 │ └── requirements.psd1 ├── 04 - Manage Bot Commands │ └── 01 - Add Commands.ps1 ├── PowerShell Summit 2023 - Serverless Discord bot.pptx └── README.md ├── 2023-04-27 - Making Kubectl PowerShell friendly ├── 01 - Parsing │ ├── 01 - Sample.ps1 │ ├── 02 - k.ps1 │ └── 03 - Gotchas.ps1 ├── 02 - Formats │ ├── 01 - formats.json │ └── 02 - formatUpdater.ps1 ├── 03 - Filter Left │ └── 01 - Filter Left.ps1 ├── PowerShell Summit 2023 - Making Kubectl PowerShell friendly.pptx └── README.md ├── 2023-10-11 - Pacific PSUG - .NET Object in PowerShell ├── 00 - Understanding Types.ps1 ├── 01 - Discovery.ps1 ├── 02 - Importing.ps1 ├── 03 - Usage.ps1 ├── 04 - Using.ps1 ├── 05 - Generics.ps1 ├── 06 - Other Places.ps1 ├── Pacific PSUG - PowerShell and .NET.pptx └── README.md ├── 2024-04-10 - PSCustomObject[] vs Hashtables ├── 01 - Create │ ├── Hashtable.ps1 │ └── PSCustomObject.ps1 ├── 02 - Update │ ├── Hashtable.ps1 │ └── PSCustomObject.ps1 ├── 03 - Output │ ├── Hashtable.ps1 │ ├── PSCustomObject.ps1 │ └── SampleView.ps1xml ├── 04 - Iterate │ ├── Hashtable.ps1 │ └── PSCustomObject.ps1 ├── Case Study │ └── User Matching.ps1 ├── MOCK_DATA.json ├── PowerShell Summit 2024 - PSCustomObjects vs Hashtables.pptx └── README.md ├── 2025-01-08 - From Pets to Cattle Learning to let go and automate └── Summit 2025 - From Pets to Cattle.pptx ├── 2025-04-09 - Getting started with dev containers ├── 01 - Using a template │ └── README.md ├── 02 - Edited PowerShell only config │ └── .devcontainer │ │ └── devcontainer.json ├── 03 - Adding PowerShell to config │ ├── .devcontainer │ │ └── devcontainer.json │ ├── go.mod │ └── main.go ├── 04 - Integrating multiple containers │ └── networkSetup.ps1 └── Summit 2025 - Getting Started With Dev Containers.pptx ├── LICENSE.txt └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | ~$*.* -------------------------------------------------------------------------------- /2019-01-31 - Scottish PS UG - REST APIs/Code.ps1: -------------------------------------------------------------------------------- 1 | #region demo header 2 | Throw 'this is a demo' 3 | #endregion 4 | # cURL vs Invoke-RestMethod 5 | 6 | [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 7 | 8 | #region Airtable GET example 9 | Invoke-RestMethod -Uri https://api.airtable.com/v0/appBLvHFF78kERCvW/Payees -Headers @{ 10 | Authorization = "Bearer $AirTableKey" 11 | } 12 | # Get more info 13 | $resp = Invoke-RestMethod -Uri https://api.airtable.com/v0/appBLvHFF78kERCvW/Payees -Headers @{ 14 | Authorization = "Bearer $AirTableKey" 15 | } 16 | $resp.records 17 | #endregion 18 | 19 | #region PDF Generator GET Example 20 | $header = @{ 21 | 'X-Auth-Key' = $PDF.key 22 | 'X-Auth-Secret' = $PDF.secret 23 | 'X-Auth-Workspace' = $PDF.workspace 24 | 'Content-Type' = 'application/json' 25 | 'Accept' = 'application/json' 26 | } 27 | Invoke-RestMethod -Uri 'https://us1.pdfgeneratorapi.com/api/v3/templates' -Headers $header 28 | # Get more info 29 | $resp = Invoke-RestMethod -Uri 'https://us1.pdfgeneratorapi.com/api/v3/templates' -Headers $header 30 | $resp.response 31 | #endregion 32 | 33 | #region SherpaDesk GET example: 34 | $encodedAuth = [Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes("$($sd.WorkingOrganization)`-$($sd.WorkingInstance)`:$($sd.apikey)")) 35 | $header = @{ 36 | Authorization = "Basic $encodedAuth" 37 | Accept = 'application/json' 38 | } 39 | Invoke-RestMethod -Uri 'https://api.sherpadesk.com/tickets?status=open,onhold&role=user&limit=6&format=json' -Headers $header 40 | #endregion 41 | 42 | #region Airtable PATCH example 43 | # Get 44 | Invoke-RestMethod 'https://api.airtable.com/v0/appBLvHFF78kERCvW/Payees/recMvdJuoL6ivDA9I' -Method Get -Headers $headers 45 | $resp = Invoke-RestMethod 'https://api.airtable.com/v0/appBLvHFF78kERCvW/Payees/recMvdJuoL6ivDA9I' -Method Get -Headers $headers 46 | $resp.fields 47 | # Patch 48 | $headers = @{ 49 | Authorization = "Bearer $AirTableKey" 50 | 'Content-Type' = 'application/json' 51 | Accept = 'application/json' 52 | } 53 | $body = @{ 54 | fields = @{ 55 | Name = 'EWEB' 56 | } 57 | } | ConvertTo-Json 58 | Invoke-RestMethod 'https://api.airtable.com/v0/appBLvHFF78kERCvW/Payees/recMvdJuoL6ivDA9I' -Method Patch -Headers $headers -Body $body 59 | #endregion 60 | 61 | #region PDF Generator POST example 62 | $header = @{ 63 | 'X-Auth-Key' = $PDF.key 64 | 'X-Auth-Secret' = $PDF.secret 65 | 'X-Auth-Workspace' = $PDF.workspace 66 | "Content-Type" = "application/json" 67 | "Accept" = "application/json" 68 | } 69 | $body = @{ 70 | id = 304355781 71 | DocNumber = 15 72 | ShipAddr = @{ 73 | Line1 = "St Patrick Road 4" 74 | City = "London" 75 | Country = "United Kingdom" 76 | CountrySubDivisionCode = "UK12991" 77 | } 78 | CustomerInfo = @{ 79 | CompanyName = 'Acme Inc.' 80 | GivenName = "John" 81 | FamilyName = 'Smith' 82 | } 83 | CompanyInfo = @{ 84 | CompanyName = 'Your Fav Shipper!' 85 | } 86 | Line = @( 87 | @{ 88 | Name = "#1014A" 89 | Description = "Customer Notes" 90 | } 91 | ) 92 | } | ConvertTo-Json 93 | Invoke-RestMethod -Uri 'https://us1.pdfgeneratorapi.com/api/v3/templates/21661/output?format=pdf&output=base64' -Method Post -Headers $header -Body $Body 94 | 95 | #region Convert to file 96 | $resp = Invoke-RestMethod -Uri 'https://us1.pdfgeneratorapi.com/api/v3/templates/21661/output?format=pdf&output=base64' -Method Post -Headers $header -Body $Body 97 | $bytes = [Convert]::FromBase64String($resp.response) 98 | [IO.File]::WriteAllBytes('C:\tmp\PDFGenExample.pdf', $bytes) 99 | . 'C:\tmp\PDFGenExample.pdf' 100 | #endregion 101 | #endregion 102 | 103 | #region SherpaDesk Put example 104 | # Get 105 | (Invoke-RestMethod -Uri "https://api.sherpadesk.com/time" -Headers $header) | ?{$_.time_id -eq '2292188'} 106 | # Put 107 | $encodedAuth = [Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes("$($sd.WorkingOrganization)`-$($sd.WorkingInstance)`:$($sd.apikey)")) 108 | $header = @{ 109 | Authorization = "Basic $encodedAuth" 110 | Accept = 'application/json' 111 | 'Content-Type' = 'application/json' 112 | } 113 | $body = @{ 114 | account_id = '-1' 115 | hours = '2.00' 116 | is_project_log = 'false' 117 | note_text = 'test_30/01_31/01_BLAHBLAH' 118 | task_type_id = '94604' 119 | tech_id ='950330' 120 | } | ConvertTo-Json 121 | Invoke-RestMethod -Uri "https://api.sherpadesk.com/time/2292188?format=json" -Method Put -Headers $header -Body $body 122 | #endregion 123 | 124 | #region SherpaDesk retrieve API key 125 | $credential = Get-Credential 126 | $up = "$($credential.GetNetworkCredential().UserName)`:$($credential.GetNetworkCredential().Password)" 127 | $encodedUP = [Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes("$up")) 128 | $header = @{ 129 | Authorization = "Basic $encodedUP" 130 | Accept = 'application/json' 131 | } 132 | $resp = Invoke-RestMethod -Method Get -Uri 'https://api.sherpadesk.com/login' -Headers $header 133 | $Script:AuthConfig = @{ 134 | ApiKey = $resp.api_token 135 | WorkingOrganization = '' 136 | WorkingInstance = '' 137 | } 138 | #endregion 139 | 140 | #region SherpaDesk metadata 141 | $encodedAuth = [Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes("x:$($AuthConfig.ApiKey)")) 142 | $header = @{ 143 | Authorization = "Basic $encodedAuth" 144 | Accept = 'application/json' 145 | } 146 | $resp = Invoke-RestMethod -Uri 'https://api.sherpadesk.com/organizations/' -Method Get -Headers $header 147 | $Script:AuthConfig.WorkingOrganization = $resp[0].key 148 | $Script:AuthConfig.WorkingInstance = $resp[0].instances[0].key 149 | #endregion -------------------------------------------------------------------------------- /2019-01-31 - Scottish PS UG - REST APIs/README.md: -------------------------------------------------------------------------------- 1 | # Rest API Presentation 2 | 3 | ## cURL vs Invoke-RestMethod 4 | 5 | ### Getting Data 6 | 7 | #### Getting records from Airtable 8 | 9 | Retrieves info from a base called: 'appBLvHFF78kERCvW' and the 'Payees' Table: 10 | 11 | [API Docs](https://airtable.com/api) 12 | ```bash 13 | $ curl https://api.airtable.com/v0/appBLvHFF78kERCvW/Payees \ 14 | -H "Authorization: Bearer YOUR_API_KEY" 15 | ``` 16 | 17 | ```PowerShell 18 | Invoke-RestMethod -Uri https://api.airtable.com/v0/appBLvHFF78kERCvW/Payees -Header @{ 19 | Authorization = "Bearer YOUR_API_KEY" 20 | } 21 | ``` 22 | 23 | That -H is header stuff: 24 | 25 | #### Retrieving templates from the PDF Generator API: 26 | 27 | [API Docs](https://pdfgeneratorapi.com/docs#templates-get-all) 28 | ```bash 29 | curl -H "X-Auth-Key: 61e5f04ca1794253ed17e6bb986c1702" \ 30 | -H "X-Auth-Workspace: demo.example@actualreports.com" \ 31 | -H "X-Auth-Signature: " \ 32 | -H "Content-Type: application/json" \ 33 | -H "Accept: application/json" \ 34 | -X GET https://us1.pdfgeneratorapi.com/api/v3/templates 35 | ``` 36 | 37 | ```PowerShell 38 | Invoke-RestMethod -Uri 'https://us1.pdfgeneratorapi.com/api/v3/templates' -Header @{ 39 | 'X-Auth-Key' = '61e5f04ca1794253ed17e6bb986c1702' 40 | 'X-Auth-Secret' = '68db1902ad1bb26d34b3f597488b9b28' 41 | 'X-Auth-Workspace' = 'demo.example@actualreports.com' 42 | 'Content-Type' = 'application/json' 43 | 'Accept' = 'application/json' 44 | } 45 | ``` 46 | 47 | #### Using Parameters, Basic Authentication 48 | 49 | [API Docs](https://documenter.getpostman.com/view/4454237/apisherpadeskcom-playground/RW8AooQg#6a1f8cfa-8910-8c9f-2e68-bfaefb51920b) 50 | ```bash 51 | curl --request GET "https://ncg1in-8d1rag:5nuauzj5pkfftlz3fmyksmyhat6j35kf@api.sherpadesk.com/tickets?status=open,onhold&role=user&limit=6&format=json" \ 52 | --data "" 53 | ``` 54 | 55 | ```PowerShell 56 | # Convert to Base64 (watch encoding!) 57 | $encodedAuth = [Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes('ncg1in-8d1rag:5nuauzj5pkfftlz3fmyksmyhat6j35kf')) 58 | $header = @{ 59 | Authorization = "Basic $encodedAuth" 60 | Accept = 'application/json' 61 | } 62 | Invoke-RestMethod -Uri 'https://api.sherpadesk.com/tickets?status=open,onhold&role=user&limit=6&format=json' -Headers $header 63 | ``` 64 | 65 | ### Creating or Updating Data 66 | 67 | POST Creates 68 | 69 | PATCH Updates 70 | 71 | PUT Replaces 72 | 73 | #### Airtable API PATCH 74 | 75 | Note the resource uri. 76 | 77 | ```bash 78 | curl -v -XPATCH https://api.airtable.com/v0/appBLvHFF78kERCvW/Payees/recMvdJuoL6ivDA9I \ 79 | -H "Authorization: Bearer YOUR_API_KEY" \ 80 | -H "Content-Type: application/json" \ 81 | -d '{ 82 | "fields": { 83 | "Name": "Eugene Water and Electric Board" 84 | } 85 | }' 86 | ``` 87 | 88 | ```PowerShell 89 | $headers = @{ 90 | Authorization = "Bearer YOUR_API_KEY" 91 | 'Content-Type' = 'application/json' 92 | } 93 | $body = @{ 94 | fields = @{ 95 | Name = 'Eugene Water and Electric Board' 96 | } 97 | } | ConvertTo-Json 98 | Invoke-RestMethod 'https://api.airtable.com/v0/appBLvHFF78kERCvW/Payees/recMvdJuoL6ivDA9I' -Method Patch -Headers $headers -Body $body 99 | ``` 100 | 101 | #### PDF Generator API POST 102 | 103 | ```bash 104 | curl -H "X-Auth-Key: 61e5f04ca1794253ed17e6bb986c1702" \ 105 | -H "X-Auth-Workspace: demo.example@actualreports.com" \ 106 | -H "X-Auth-Signature: 423e1a765bb24d5139a7258545066d3fcb310998044ea3c000e393b75f5167d6" \ 107 | -H "Content-Type: application/json" \ 108 | -H "Accept: application/json" \ 109 | -X POST -d '{"id":304355781,"name":"#1014A","number":15,"note":"Customer Notes","shipping_address":{"name":"John Smith","address":"St Patrick Road 4","city":"London","country":"United Kingdom","zip":"UK12991"}}' \ 110 | 'https://us1.pdfgeneratorapi.com/api/v3/templates/21661/output?format=pdf&output=base64' 111 | ``` 112 | 113 | ```PowerShell 114 | $header = @{ 115 | "X-Auth-Key" =" 61e5f04ca1794253ed17e6bb986c1702" 116 | "X-Auth-Workspace" = "demo.example@actualreports.com" 117 | "X-Auth-Signature" = "423e1a765bb24d5139a7258545066d3fcb310998044ea3c000e393b75f5167d6" 118 | "Content-Type" = "application/json" 119 | "Accept" = "application/json" 120 | } 121 | $body = @{ 122 | id = 304355781 123 | name = "#1014A" 124 | number = 15 125 | note = "Customer Notes" 126 | shipping_address = @{ 127 | name = "John Smith" 128 | address = "St Patrick Road 4" 129 | city = "London" 130 | country = "United Kingdom" 131 | zip = "UK12991" 132 | } 133 | } | ConvertTo-Json 134 | Invoke-RestMethod -Uri 'https://us1.pdfgeneratorapi.com/api/v3/templates/21661/output?format=pdf&output=base64' -Method Post -Headers $header -Body $Body 135 | ``` 136 | 137 | #### SherpaDesk PUT 138 | 139 | ```bash 140 | curl --location --request PUT "https://ncg1in-8d1rag:5nuauzj5pkfftlz3fmyksmyhat6j35kf@api.sherpadesk.com/time/{{time_id}}?format=json" \ 141 | --form "account_id=-1" \ 142 | --form "hours=0.25" \ 143 | --form "is_project_log=true" \ 144 | --form "note_text=test_30/01_31/01" \ 145 | --form "task_type_id={{task_type_id}}" \ 146 | --form "tech_id={{user_id}}" 147 | ``` 148 | 149 | ```PowerShell 150 | $encodedAuth = [Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes('ncg1in-8d1rag:5nuauzj5pkfftlz3fmyksmyhat6j35kf')) 151 | $header = @{ 152 | Authorization = "Basic $encodedAuth" 153 | Accept = 'application/json' 154 | 'Content-Type' = 'application/json' 155 | } 156 | $body = @{ 157 | account_id = '-1' 158 | hours = '0.25' 159 | is_project_log = 'true' 160 | note_text = 'test_30/01_31/01' 161 | task_type_id = '{{task_type_id}}' 162 | tech_id ='{{user_id}}' 163 | } | ConvertTo-Json 164 | Invoke-RestMethod -Uri "https://api.sherpadesk.com/time/{{time_id}}?format=json" -Method Put -Headers $header -Body $body 165 | ``` 166 | 167 | # Standardizing API calls with an Invoke-APICall cmdlet 168 | 169 | ## One place for stuff like pagination 170 | 171 | [PSAirtable:PSAirtable.psm1](https://github.com/TechSnips/PSAirTable/blob/df02d8e1b5ac08d546a8f3ba804a99ec25c49b7f/PSAirTable.psm1#L459) 172 | ```PowerShell 173 | $response = Invoke-RestMethod @invRestParams 174 | 175 | if ('records' -in $response.PSObject.Properties.Name) { 176 | $baseId = $Uri.split('/')[4] 177 | $table = $Uri.split('/')[5] 178 | $response.records.foreach({ 179 | $output = $_.fields 180 | $output | Add-Member -MemberType NoteProperty -Name 'Record ID' -Value $_.id 181 | $output | Add-Member -MemberType NoteProperty -Name 'Base ID' -Value $baseId 182 | $output | Add-Member -MemberType NoteProperty -Name 'Table' -Value $table -PassThru 183 | }) 184 | 185 | while ('offset' -in $response.PSObject.Properties.Name) { 186 | $invParams = [hashtable]$PSBoundParameters 187 | if ($invParams['HttpBody'] -and $invParams['HttpBody'].ContainsKey('offset')) { 188 | $invParams['HttpBody'].offset = $response.offset 189 | } else { 190 | $invParams['HttpBody'] = $HttpBody + @{ offset = $response.offset } 191 | } 192 | 193 | InvokeAirTableApiCall @invParams | Tee-Object -Variable response 194 | } 195 | } elseif (... 196 | ``` 197 | 198 | ## One place to handle errors like retries 199 | 200 | [PSSlack:Send-SlackAPI.ps1](https://github.com/RamblingCookieMonster/PSSlack/blob/dcabbeedd50de32ec08bfe19875418334e917ae8/PSSlack/Public/Send-SlackAPI.ps1#L83) 201 | 202 | ```PowerShell 203 | if ($_.Exception.Response.StatusCode -eq 429) { 204 | 205 | # Get the time before we can try again. 206 | if( $_.Exception.Response.Headers -and $_.Exception.Response.Headers.Contains('Retry-After') ) { 207 | $RetryPeriod = $_.Exception.Response.Headers.GetValues('Retry-After') 208 | if($RetryPeriod -is [string[]]) { 209 | $RetryPeriod = [int]$RetryPeriod[0] 210 | } 211 | } 212 | else { 213 | $RetryPeriod = 2 214 | } 215 | Write-Verbose "Sleeping [$RetryPeriod] seconds due to Slack 429 response" 216 | Start-Sleep -Seconds $RetryPeriod 217 | Send-SlackApi @PSBoundParameters 218 | 219 | } 220 | ``` 221 | 222 | ## And it makes for easy cmdlet writing 223 | 224 | [PS_PDFGeneratorAPI:Get-PDFGenTemplates.ps1](https://github.com/ThePoShWolf/PS_PDFGeneratorAPI/blob/master/src/Public/Get-PDFGenTemplates.ps1) 225 | ```PowerShell 226 | Function Get-PDFGenTemplates { 227 | Param( 228 | [ValidateNotNullOrEmpty()] 229 | [string]$key = $AuthConfig.key, 230 | [ValidateNotNullOrEmpty()] 231 | [string]$secret = $AuthConfig.secret, 232 | [ValidateNotNullOrEmpty()] 233 | [string]$workspace = $AuthConfig.workspace 234 | ) 235 | 236 | (Invoke-PDFGeneratorAPICall -resource templates -method Get -key $key -secret $secret -workspace $workspace).response 237 | } 238 | ``` 239 | 240 | # Managing API credentials. 241 | 242 | ## Leave it to the user 243 | 244 | This is unwieldy and a bad idea. Makes for commands like these: 245 | 246 | ```PowerShell 247 | Get-SDTicket -Organization 'ncg1in' -Instance '8d1rag' -Key '5nuauzj5pkfftlz3fmyksmyhat6j35kf' 248 | # Or 249 | Get-Record -BaseIdentity 'Finances' -Table 'Payees' -ApiKey 'keyttau093ptSHauP' 250 | ``` 251 | 252 | And sure, you could just: 253 | 254 | ```PowerShell 255 | $PSDefaultParameterValues = @{ 256 | '*-SD*:Organization' = 'ncg1in' 257 | '*-SD*:Instance' = '8d1rag' 258 | '*-SD*:Key' = '5nuauzj5pkfftlz3fmyksmyhat6j35kf' 259 | } 260 | Get-SDTicket 261 | ``` 262 | 263 | Where's the fun in that? 264 | 265 | ## Module (script) scoped variable 266 | 267 | Inside your .psm1: 268 | ```PowerShell 269 | $script:ApiKey = 'key' 270 | ``` 271 | 272 | But you have to have it to be able to set it... 273 | 274 | ### SherpaDesk Example 275 | 276 | [PSSherpaDesk](https://github.com/theposhwolf/pssherpadesk) 277 | 278 | Retrieve the API key from the API using a user's email and password: 279 | 280 | ```PowerShell 281 | $credential = Get-Credential 282 | $up = "$($credential.GetNetworkCredential().UserName)`:$($credential.GetNetworkCredential().Password)" 283 | $encodedUP = [Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes("$up")) 284 | $header = @{ 285 | Authorization = "Basic $encodedUP" 286 | Accept = 'application/json' 287 | } 288 | $resp = Invoke-RestMethod -Method Get -Uri 'https://api.sherpadesk.com/login' -Headers $header 289 | $Script:AuthConfig = @{ 290 | ApiKey = $resp.api_token 291 | WorkingOrganization = '' 292 | WorkingInstance = '' 293 | } 294 | ``` 295 | 296 | This is soooo convenient. This turns into: 297 | 298 | ```PowerShell 299 | Get-SDApiKey -Email email@domain.com 300 | ``` 301 | 302 | Though as you saw with earlier API examples, you still need your Org and Instance, but there is a call for that: 303 | 304 | ```PowerShell 305 | $encodedAuth = [Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes("x:$ApiKey")) 306 | $header = @{ 307 | Authorization = "Basic $encodedAuth" 308 | Accept = 'application/json' 309 | } 310 | $resp = Invoke-RestMethod -Uri 'https://api.sherpadesk.com/organizations/' -Method Get -Headers $header 311 | $Script:AuthConfig.WorkingOrganization = $resp[0].key 312 | $Script:AuthConfig.WorkingInstance = $resp[0].instances[0].key 313 | ``` 314 | 315 | And now you have a module scope variable to use in your parameter blocks like: 316 | 317 | ```PowerShell 318 | Function Get-SDTicket{ 319 | [cmdletbinding()] 320 | Param( 321 | [parameter( 322 | ParameterSetName = 'ByKey' 323 | )] 324 | [string]$Key, 325 | [string]$Organization = $authConfig.WorkingOrganization, 326 | [string]$Instance = $authConfig.WorkingInstance, 327 | [string]$ApiKey = $authConfig.ApiKey 328 | ) 329 | ... 330 | } 331 | ``` 332 | 333 | ### PDF Generator API example 334 | 335 | [PS_PDFGeneratorAPI](https://github.com/theposhwolf/PS_PDFGeneratorAPI) 336 | 337 | For this API, and most, you have to download some auth info ahead of time. But you can still save it: 338 | 339 | ```PowerShell 340 | Function New-PDFGenAuthConfig { 341 | Param ( 342 | [ValidateNotNullOrEmpty()] 343 | [string]$key, 344 | [ValidateNotNullOrEmpty()] 345 | [string]$secret, 346 | [ValidateNotNullOrEmpty()] 347 | [string]$workspace 348 | ) 349 | $Script:AuthConfig = @{ 350 | key = $key 351 | secret = $secret 352 | workspace = $workspace 353 | } 354 | } 355 | ``` 356 | 357 | And of course, use these in your parameter blocks: 358 | 359 | ```PowerShell 360 | Function Get-PDFGenTemplates { 361 | Param( 362 | [ValidateNotNullOrEmpty()] 363 | [string]$key = $AuthConfig.key, 364 | [ValidateNotNullOrEmpty()] 365 | [string]$secret = $AuthConfig.secret, 366 | [ValidateNotNullOrEmpty()] 367 | [string]$workspace = $AuthConfig.workspace 368 | ) 369 | ... 370 | } 371 | ``` 372 | 373 | ## Store locally? 374 | 375 | Should you store your API keys in clear text? 376 | 377 | ![No](https://i.imgur.com/DKUR9Tk.png) 378 | 379 | ### But you can encrypt them using PowerShell! 380 | 381 | [PSAirtable](https://github.com/techsnips/psairtable) 382 | 383 | Credit to Adam Bertram 384 | 385 | He encrypts the API key using ConvertTo-SecureString: 386 | 387 | ```PowerShell 388 | function Save-AirTableApiKey { 389 | [CmdletBinding()] 390 | param ( 391 | [Parameter(Mandatory)] 392 | [ValidateNotNullOrEmpty()] 393 | [string]$ApiKey 394 | ) 395 | 396 | function encrypt([string]$TextToEncrypt) { 397 | $secure = ConvertTo-SecureString $TextToEncrypt -AsPlainText -Force 398 | $encrypted = $secure | ConvertFrom-SecureString 399 | return $encrypted 400 | } 401 | $config = Get-PSAirTableConfiguration 402 | $config.Application.ApiKey = encrypt($ApiKey) 403 | $config | ConvertTo-Json | Set-Content -Path "$WorkingDir\Configuration.json" 404 | } 405 | ``` 406 | 407 | I can't speak to the security of said secure string, but it _could_ be considered good enough for _most_ use cases. 408 | 409 | ### Another approach, also PS Core compatible 410 | 411 | Here's what I just implemented in both my SherpaDesk and PDFGen modules: 412 | 413 | ```PowerShell 414 | Function Get-SDSavePath { 415 | Param ( 416 | 417 | ) 418 | If($PSVersionTable.PSVersion.Major -ge 6){ 419 | # PS Core 420 | If($IsLinux){ 421 | $saveDir = $env:HOME 422 | }ElseIf($IsWindows){ 423 | $saveDir = $env:USERPROFILE 424 | } 425 | }Else{ 426 | # Windows PS 427 | $saveDir = $env:USERPROFILE 428 | } 429 | "$saveDir\.pssherpadesk" 430 | } 431 | Function Save-SDAuthConfig { 432 | Param( 433 | 434 | ) 435 | $dir = Get-SDSavePath 436 | If(-not(Test-Path $dir -PathType Container)){ 437 | New-Item $dir -ItemType Directory 438 | } 439 | If(-not(Test-Path $dir\credentials.json -PathType Leaf)){ 440 | New-Item $dir\credentials.json -ItemType File 441 | } 442 | $encryptedAuth = @{} 443 | ForEach($property in $AuthConfig.GetEnumerator()){ 444 | $encryptedAuth."$($property.Name)" = (ConvertFrom-SecureString (ConvertTo-SecureString $property.Value -AsPlainText -Force)) 445 | } 446 | $encryptedAuth | ConvertTo-Json | Set-Content $dir\credentials.json 447 | } 448 | ``` 449 | 450 | The only problem? ```ConvertFrom-SecureString``` isn't actually implemented in PS Core on Linux without specifying a key :( 451 | 452 | ```PowerShell 453 | PS> ConvertFrom-SecureString $ss 454 | 455 | ConvertFrom-SecureString : Unable to load shared library 'CRYPT32.dll' or one of its dependencies. In order to help diagnose loading problems, consider setting the LD_DEBUG environment variable: libCRYPT32.dll: cannot open shared object file: No such file or directory 456 | ``` 457 | 458 | And yep, its an issue: [PowerShell Issue 1654](https://github.com/PowerShell/PowerShell/issues/1654) -------------------------------------------------------------------------------- /2019-04-29 - PowerShell Summit - Curl2PS Lightning Demo/Curl2PS_Prez.pptx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ThePoShWolf/Sessions/ddaac2096083e091ba8175375560052451555120/2019-04-29 - PowerShell Summit - Curl2PS Lightning Demo/Curl2PS_Prez.pptx -------------------------------------------------------------------------------- /2019-04-29 - PowerShell Summit - Curl2PS Lightning Demo/Prez.ps1: -------------------------------------------------------------------------------- 1 | # Quick Curl2PS examples 2 | 3 | # Example docs 4 | 5 | # Here are some examples of API docs that use curl (non-exhaustive): 6 | 7 | <# 8 | - [Trello API](https://developers.trello.com/reference) 9 | - [PDF Generator API](https://docs.pdfgeneratorapi.com) 10 | - [Slack API](https://api.slack.com/web) 11 | - [Airtable](https://airtable.com/api) 12 | - [SherpaDesk](https://documenter.getpostman.com/view/4454237/apisherpadeskcom-playground/RW8AooQg) 13 | #> 14 | 15 | # Import Module 16 | Import-Module C:\users\Anthony\GIT\Curl2PS\build\Curl2PS\Curl2PS.psd1 17 | 18 | # Cmdlets 19 | Get-Command -Module Curl2PS 20 | 21 | # Example calls 22 | 23 | <# 24 | curl --request GET "https://ncg1in-8d1rag:5nuauzj5pkfftlz3fmyksmyhat6j35kf@api.sherpadesk.com/tickets?status=open,onhold&role=user&limit=6&format=json" 25 | #> 26 | 27 | $curlStr = 'curl --request GET "https://tkldsi-sgq3yh:pb5sd018hjapuzphcy6tc58g2ulqeque@api.sherpadesk.com/tickets?status=open,onhold&role=user&limit=6&format=json"' 28 | $irmStr = ConvertTo-IRM $curlStr 29 | $irmStr 30 | Invoke-Command ([scriptblock]::Create($irmStr)) 31 | 32 | # Terrible Output 33 | # Lets tell the API we want json 34 | 35 | <# 36 | curl --request GET -H "Accept: application/json" "https://ncg1in-8d1rag:5nuauzj5pkfftlz3fmyksmyhat6j35kf@api.sherpadesk.com/tickets?status=open,onhold&role=user&limit=6&format=json" 37 | #> 38 | 39 | $curlStr = 'curl --request GET -H "Accept: application/json" "https://ncg1in-8d1rag:5nuauzj5pkfftlz3fmyksmyhat6j35kf@api.sherpadesk.com/tickets?status=open,onhold&role=user&limit=6&format=json"' 40 | $irmStr = ConvertTo-IRM $curlStr 41 | $irmStr 42 | Invoke-Command ([scriptblock]::Create($irmStr)) -------------------------------------------------------------------------------- /2020-01-30 - MTX Portland - AD User Lifecycle/Demos/00 - Lab Notes.ps1: -------------------------------------------------------------------------------- 1 | Function New-LabSessions { 2 | Param ( 3 | [string[]]$Comps = 'DC01', 4 | [string]$Domain = 'techsnipsdemo.org', 5 | [string]$Password = 'Password2!' 6 | ) 7 | $global:Sessions = @() 8 | $securePassword = ConvertTo-SecureString $Password -AsPlainText -Force 9 | $Cred = [pscredential]::new("$Domain\administrator",$securePassword) 10 | ForEach($Comp in $Comps){ 11 | $global:Sessions += New-PSSession $comp -Credential $cred 12 | } 13 | } 14 | 15 | $PSVersionTable 16 | 17 | New-LabSessions 18 | Enter-PSSession $Sessions[0] -------------------------------------------------------------------------------- /2020-01-30 - MTX Portland - AD User Lifecycle/Demos/01 - Creating Users from a Spreadsheet and Template.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | Install-Module ImportExcel 3 | Import-Module ActiveDirectory 4 | #> 5 | 6 | #region prep work 7 | # Import the spreadsheet 8 | $SpreadSheet = '.\UserUpdate.xlsx' 9 | $Data = Import-Excel $SpreadSheet 10 | 11 | # Cleanup from practice run 12 | foreach ($user in $Data){ 13 | Remove-Aduser "$($user.'First Name').$($user.'Last Name')" -Confirm:$false 14 | } 15 | 16 | # Check the data 17 | $Data | Format-Table 18 | 19 | # Correlate fields 20 | $expectedProperties = @{ 21 | Name = 'Full Name' 22 | GivenName = 'First Name' 23 | SurName = 'Last Name' 24 | Title = 'Job Title' 25 | Department = 'Department' 26 | OfficePhone = 'Phone Number' 27 | } 28 | 29 | # Correlate 'Manager' field 30 | Help New-ADUser -Parameter Manager 31 | 32 | <# can be: 33 | SamAccountName 34 | DistinguishedName 35 | GUID 36 | SID 37 | #> 38 | 39 | Get-ADUser $Data[0].Manager 40 | 41 | Get-ADUser $Data[0].Manager.Replace(' ','.') 42 | 43 | # Manager 44 | $Data[0].Manager.Replace(' ','.') 45 | 46 | # SamAccountName 47 | "$($Data[0].'First Name').$($Data[0].'Last Name')" 48 | 49 | # Create a single user 50 | $user = $Data[0] 51 | $params = @{} 52 | ForEach($property in $expectedProperties.GetEnumerator()){ 53 | # If the new user has the property 54 | If($user."$($property.value)".Length -gt 0){ 55 | # Add it to the splat 56 | $params["$($property.Name)"] = $user."$($property.value)" 57 | } 58 | } 59 | # Deal with other values 60 | If($user.Manager.length -gt 0){ 61 | $params['Manager'] = $user.Manager.Replace(' ','.') 62 | } 63 | $params['SamAccountName'] = "$($user.$($expectedProperties['GivenName'])).$($user.$($expectedProperties['SurName']))" 64 | # Create the user 65 | New-ADUser @params 66 | 67 | Get-ADUser $params.SamAccountName 68 | 69 | #endregion 70 | 71 | #region Create a function 72 | Function Import-ADUsersFromSpreadsheet { 73 | [cmdletbinding()] 74 | Param( 75 | [ValidatePattern('.*\.xlsx$')] 76 | [ValidateNotNullOrEmpty()] 77 | [string]$PathToSpreadsheet 78 | ) 79 | # Hashtable to correlate properties 80 | $expectedProperties = @{ 81 | Name = 'Full Name' 82 | GivenName = 'First Name' 83 | SurName = 'Last Name' 84 | Title = 'Job Title' 85 | Department = 'Department' 86 | OfficePhone = 'Phone Number' 87 | } 88 | # Make sure the xlsx exists 89 | If(Test-Path $PathToSpreadsheet){ 90 | $data = Import-Excel $PathToSpreadsheet 91 | ForEach($user in $data){ 92 | # Build a splat 93 | $params = @{} 94 | ForEach($property in $expectedProperties.GetEnumerator()){ 95 | # If the new user has the property 96 | If($user."$($property.value)".Length -gt 0){ 97 | # Add it to the splat 98 | $params["$($property.Name)"] = $user."$($property.value)" 99 | } 100 | } 101 | # Deal with other values 102 | If($user.Manager.length -gt 0){ 103 | $params['Manager'] = $user.Manager.Replace(' ','.') 104 | } 105 | $params['SamAccountName'] = "$($user.$($expectedProperties['GivenName'])).$($user.$($expectedProperties['SurName']))" 106 | # Create the user 107 | New-ADUser @params 108 | } 109 | } 110 | } 111 | 112 | # Usage 113 | Import-ADUsersFromSpreadsheet -PathToSpreadsheet '.\UserUpdate.xlsx' 114 | 115 | # Verify 116 | ForEach($user in $data){ 117 | Get-ADUser "$($user.'First Name').$($user.'Last Name')" | Select-Object Name 118 | } 119 | #endregion 120 | 121 | #region Create a template user 122 | 123 | # Cleanup 124 | Remove-ADUser 'Template User' -Confirm:$false 125 | Remove-ADUser 'Walter White' -Confirm:$false 126 | foreach ($user in $Data){ 127 | Remove-Aduser "$($user.'First Name').$($user.'Last Name')" -Confirm:$false 128 | } 129 | 130 | # Create the template user 131 | New-ADUser -Name 'Template User' -Enabled $false 132 | 133 | # Set all your template properties 134 | Set-ADUser 'Template User' -StreetAddress '308 Negra Arroyo Lane' -City 'Albuquerque' -State 'New Mexico' -PostalCode '87104' 135 | 136 | # Add any groups 137 | $BaseGroups = 'EveryBody','Main File Share Access' 138 | ForEach($group in $BaseGroups){ 139 | Add-ADGroupMember $group -Members 'Template User' 140 | } 141 | 142 | # Verify 143 | Get-ADUser 'Template User' -Properties StreetAddress,City,State,PostalCode,MemberOf 144 | 145 | #endregion 146 | 147 | #region Creating users from the template 148 | # Retrieve the template user 149 | $user = Get-ADUser 'Template User' -Properties StreetAddress,City,State,PostalCode,MemberOf 150 | 151 | # Create a single user from that 152 | New-ADUser 'Walter White' -GivenName 'Walter' -Surname 'White' -Instance $user 153 | 154 | Get-ADUser 'Walter White' -Properties StreetAddress,City,State,PostalCode,MemberOf 155 | 156 | # Check Groups 157 | (Get-ADUser 'Walter White' -Properties MemberOf).MemberOf 158 | 159 | # Add that user to the same groups 160 | ForEach($group in $user.MemberOf){ 161 | Add-ADGroupMember $group -Members 'Walter White' 162 | } 163 | 164 | # Verify 165 | (Get-ADUser 'Walter White' -Properties MemberOf).MemberOf 166 | 167 | #endregion 168 | 169 | #region Function time! 170 | # Create your spreadsheet users using the template as well 171 | Function Import-ADUsersFromSpreadsheet { 172 | [cmdletbinding( 173 | DefaultParameterSetName = 'Plain' 174 | )] 175 | Param( 176 | [ValidatePattern('.*\.xlsx$')] 177 | [ValidateNotNullOrEmpty()] 178 | [Parameter( 179 | ParameterSetName = 'FromTemplate' 180 | )] 181 | [Parameter( 182 | ParameterSetName = 'Plain' 183 | )] 184 | [string]$PathToSpreadsheet, 185 | [Parameter( 186 | ParameterSetName = 'FromTemplate', 187 | Mandatory = $true 188 | )] 189 | [Microsoft.ActiveDirectory.Management.ADUser]$TemplateUser, 190 | [Parameter( 191 | ParameterSetName = 'FromTemplate' 192 | )] 193 | [ValidateNotNullOrEmpty()] 194 | [string[]]$Properties = @('StreetAddress','City','State','PostalCode') 195 | ) 196 | # Hashtable to correlate properties 197 | $expectedProperties = @{ 198 | Name = 'Full Name' 199 | GivenName = 'First Name' 200 | SurName = 'Last Name' 201 | Title = 'Job Title' 202 | Department = 'Department' 203 | OfficePhone = 'Phone Number' 204 | } 205 | # Make sure the xlsx exists 206 | If(Test-Path $PathToSpreadsheet){ 207 | $data = Import-Excel $PathToSpreadsheet 208 | ForEach($user in $data){ 209 | # Build a splat 210 | $params = @{} 211 | ForEach($property in $expectedProperties.GetEnumerator()){ 212 | # If the new user has the property 213 | If($user."$($property.value)".Length -gt 0){ 214 | # Add it to the splat 215 | $params["$($property.Name)"] = $user."$($property.value)" 216 | } 217 | } 218 | # Deal with other values 219 | If($user.Manager.length -gt 0){ 220 | $params['Manager'] = $user.Manager.Replace(' ','.') 221 | } 222 | $params['SamAccountName'] = "$($user.$($expectedProperties['GivenName'])).$($user.$($expectedProperties['SurName']))" 223 | # Create the user 224 | If($PSCmdlet.ParameterSetName -eq 'Plain'){ 225 | New-ADUser @params 226 | }ElseIf($PSCmdlet.ParameterSetName -eq 'FromTemplate'){ 227 | # Copy template properties 228 | $props = $Properties + 'MemberOf' 229 | $template = Get-ADUser $TemplateUser -Properties $props 230 | New-ADUser @params -Instance $template 231 | ForEach($group in $template.MemberOf){ 232 | Add-ADGroupMember $group -Members $params['samaccountname'] 233 | } 234 | } 235 | } 236 | } 237 | } 238 | 239 | # Cleanup 240 | foreach ($user in $Data){ 241 | Remove-Aduser "$($user.'First Name').$($user.'Last Name')" -Confirm:$false 242 | } 243 | 244 | # Usage 245 | Import-ADUsersFromSpreadsheet -PathToSpreadsheet '.\UserUpdate.xlsx' -TemplateUser 'Template User' 246 | 247 | # Verify 248 | $SpreadSheet = '.\UserUpdate.xlsx' 249 | $data = Import-Excel $SpreadSheet 250 | ForEach($user in $data){ 251 | Get-ADUser "$($user.'First Name').$($user.'Last Name')" -Properties StreetAddress,MemberOf | Select-Object Name,StreetAddress,MemberOf 252 | } 253 | 254 | #endregion -------------------------------------------------------------------------------- /2020-01-30 - MTX Portland - AD User Lifecycle/Demos/01 - UserUpdate.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ThePoShWolf/Sessions/ddaac2096083e091ba8175375560052451555120/2020-01-30 - MTX Portland - AD User Lifecycle/Demos/01 - UserUpdate.xlsx -------------------------------------------------------------------------------- /2020-01-30 - MTX Portland - AD User Lifecycle/Demos/02 - Standardizing attributes.ps1: -------------------------------------------------------------------------------- 1 | #region Filter for missing info 2 | #region normal filter 3 | # Define needed info 4 | $properties = 'Name','Department','Title','GivenName','SurName' 5 | 6 | # Example filter string 7 | Get-ADUser -Filter "Name -notlike '*'" 8 | 9 | # Build a filter 10 | $filterString = "($($properties[0]) -notlike '*')" 11 | For($x=1;$x -lt $properties.count; $x++){ 12 | $filterString += " -or ($($properties[$x]) -notlike '*')" 13 | } 14 | $filterString 15 | 16 | # Get those users 17 | Get-ADUser -Filter $filterString -Properties $properties | Format-Table $properties 18 | 19 | # Bonus 20 | Get-ADUser -Filter $filterString -Properties $properties | Select $properties | Export-Excel .\MissingProps.xlsx -TableName 'Props' 21 | #endregion 22 | 23 | #region Manager Property 24 | 25 | # Cleanup 26 | Remove-ADUser 'Jesse.Pinkman' -Confirm:$false 27 | 28 | # Prep 29 | New-ADUser 'Jesse.Pinkman' 30 | Set-ADUser 'Jesse.Pinkman' -Manager 'Walter White' 31 | 32 | # We can filter for specific managers 33 | Get-ADUser -Filter {Manager -eq 'Walter White'} 34 | 35 | # But not empty manager 36 | Get-ADUser -Filter {Manager -eq ''} 37 | Get-ADUser -Filter {Manager -notlike '*'} 38 | 39 | # Using an LDAPFilter 40 | Get-ADUser -LDAPFilter "(!manager=*)" -Properties Manager | Format-Table Name,Manager 41 | 42 | # Combine both into an LDAP filter 43 | $properties += 'Manager' 44 | $ldapFilter = "(|(!$($properties[0])=*)" 45 | For($x=1;$x -lt $properties.count; $x++){ 46 | $ldapFilter += "(!$($properties[$x])=*)" 47 | } 48 | $ldapFilter += ')' 49 | $ldapFilter 50 | 51 | Get-ADUser -LDAPFilter $ldapFilter -Properties $properties | Format-Table $properties 52 | 53 | # One issue with using an LDAP filter 54 | Get-ADUser -LDAPFilter '(!surname=*)' | Select-Object GivenName,SurName 55 | Get-ADUser -LDAPFilter '(!sn=*)' | Select-Object GivenName,SurName 56 | 57 | # Build a hashtable of values 58 | # https://stackoverflow.com/questions/41447372/is-there-a-complete-list-of-active-directory-attributes-and-a-mapping-to-ldap 59 | $ADAssembly = [Microsoft.ActiveDirectory.Management.ADEntity].Assembly 60 | $LDAPAttributes = $ADAssembly.GetType('Microsoft.ActiveDirectory.Management.Commands.LdapAttributes') 61 | $LDAPNameConstants = $LDAPAttributes.GetFields('Static,NonPublic') | Where-Object {$_.IsLiteral} 62 | $LDAPPropertyMap = @{} 63 | $LDAPNameConstants | ForEach-Object { 64 | $LDAPPropertyMap[$_.Name] = $_.GetRawConstantValue() 65 | } 66 | 67 | # Now 68 | $LDAPPropertyMap 69 | $LDAPPropertyMap['SurName'] 70 | 71 | # New filter 72 | $ldapFilter = "(|(!$($LDAPPropertyMap[$properties[0]])=*)" 73 | For($x=1;$x -lt $properties.count; $x++){ 74 | $ldapFilter += "(!$($LDAPPropertyMap[$properties[$x]])=*)" 75 | } 76 | $ldapFilter += ')' 77 | $ldapFilter 78 | 79 | Get-ADUser -LDAPFilter $ldapFilter -Properties $properties | Format-Table $properties 80 | #endregion 81 | 82 | #region Filter to the left 83 | # Compare that to Where-Object 84 | Measure-Command { 85 | Get-ADUser -LDAPFilter $ldapFilter -Properties $properties 86 | } 87 | 88 | Measure-Command { 89 | Get-ADUser -Filter * -Properties $properties | Where-Object { 90 | -not $_.Name -or 91 | -not $_.Department -or 92 | -not $_.Title -or 93 | -not $_.Manager -or 94 | -not $_.GivenName -or 95 | -not $_.SurName 96 | } 97 | } 98 | #endregion 99 | #endregion 100 | 101 | #region Make it into a function 102 | Function Get-ADUsersMissingInfo { 103 | [cmdletbinding()] 104 | Param ( 105 | [string[]]$Properties = @('Name','Department','Title','Manager','GivenName','SurName') 106 | ) 107 | # Build our property map 108 | $ADAssembly = [Microsoft.ActiveDirectory.Management.ADEntity].Assembly 109 | $LDAPAttributes = $ADAssembly.GetType('Microsoft.ActiveDirectory.Management.Commands.LdapAttributes') 110 | $LDAPNameConstants = $LDAPAttributes.GetFields('Static,NonPublic') | Where-Object {$_.IsLiteral} 111 | $LDAPPropertyMap = @{} 112 | $LDAPNameConstants | ForEach-Object { 113 | $LDAPPropertyMap[$_.Name] = $_.GetRawConstantValue() 114 | } 115 | 116 | # Find the users 117 | $ldapFilter = "(|(!$($LDAPPropertyMap[$properties[0]])=*)" 118 | For($x=1;$x -lt $properties.count; $x++){ 119 | $ldapFilter += "(!$($LDAPPropertyMap[$properties[$x]])=*)" 120 | } 121 | $ldapFilter += ')' 122 | Get-ADUser -LDAPFilter $ldapFilter -Properties $properties 123 | } 124 | 125 | # Usage 126 | Get-ADUsersMissingInfo 127 | 128 | # Custom 129 | Get-ADUsersMissingInfo -Properties OfficePhone | Select-Object Name,OfficePhone 130 | 131 | # Create an Excel sheet 132 | $exportExcelParams = @{ 133 | Autosize = $true 134 | TableName = 'Props' 135 | TableStyle = 'Light1' 136 | } 137 | Get-ADUsersMissingInfo | Select-Object $properties | 138 | Export-Excel .\MissingProperties.xlsx -Title 'Missing Properties' @exportExcelParams 139 | 140 | Import-Excel .\MissingProperties.xlsx -StartRow 2 | Format-Table 141 | 142 | # Copy spreadsheet to local to look at it 143 | Exit 144 | Copy-Item -FromSession $Sessions[0] -Path C:\Users\administrator\documents\MissingProperties.xlsx -Destination '.\2020-01-30 - MTX Portland - AD User Lifecycle\Demos\02 - MissingProperties.xlsx' 145 | Start '.\2020-01-30 - MTX Portland - AD User Lifecycle\Demos\02 - MissingProperties.xlsx' 146 | 147 | # Re-enter session 148 | Enter-PSSession $Sessions[0] 149 | #endregion -------------------------------------------------------------------------------- /2020-01-30 - MTX Portland - AD User Lifecycle/Demos/03 - Cleaning up Stale Accounts.ps1: -------------------------------------------------------------------------------- 1 | #region Define 'stale' 2 | <# Example definition: 3 | - Has not logged in in 90 days 4 | - Has never logged in and is older than 2 weeks 5 | #> 6 | # Using Search-ADAccount 7 | Search-ADAccount -AccountInactive -TimeSpan '90.00:00:00' -UsersOnly -ResultSetSize 10 8 | 9 | #region Using a filter 10 | # Info on the LastLogonTimeStamp: https://blogs.technet.microsoft.com/askds/2009/04/15/the-lastlogontimestamp-attribute-what-it-was-designed-for-and-how-it-works/ 11 | Get-ADUser administrator -Properties LastLogonTimeStamp | Select-Object Name,LastLogonTimeStamp 12 | 13 | # Convert from file time 14 | $admin = Get-ADUser administrator -Properties LastLogonTimeStamp | Select-Object Name,LastLogonTimeStamp 15 | [datetime]::FromFileTime($admin.LastLogonTimeStamp) 16 | 17 | # If it is older than $LogonDate 18 | $LogonDate = (Get-Date).AddDays(-90).ToFileTime() 19 | Get-ADUser -Filter {LastLogonTimeStamp -lt $LogonDate} 20 | 21 | # If it doesn't have value 22 | Get-ADUser -Filter {LastLogonTimeStamp -notlike "*"} -Properties LastLogonTimeStamp -ResultSetSize 10 | Select-Object Name,LastLogonTimeStamp 23 | 24 | # And if the account was created before $createdDate 25 | $createdDate = (Get-Date).AddDays(-14) 26 | Get-ADUser -Filter {Created -lt $createdDate} -Properties Created | Select-Object Name,Created 27 | 28 | # Add them all together: 29 | $filter = { 30 | ((LastLogonTimeStamp -lt $logonDate) -or (LastLogonTimeStamp -notlike "*")) 31 | -and (Created -lt $createdDate) 32 | } 33 | Get-ADuser -Filter $filter | Select-Object SamAccountName 34 | #endregion 35 | #endregion 36 | 37 | #region Functionize it 38 | Function Get-ADStaleUsers { 39 | [cmdletbinding()] 40 | Param ( 41 | [datetime]$NoLogonSince = (Get-Date).AddDays(-90), 42 | [datetime]$CreatedBefore = (Get-Date).AddDays(-14) 43 | ) 44 | $NoLogonString = $NoLogonSince.ToFileTime() 45 | $filter = { 46 | ((LastLogonTimeStamp -lt $NoLogonString) -or (LastLogonTimeStamp -notlike "*")) 47 | -and (Created -lt $createdBefore) 48 | } 49 | Write-Verbose $filter.ToString() 50 | Get-ADuser -Filter $filter 51 | } 52 | 53 | # Usage 54 | Get-ADStaleUsers 55 | 56 | # We can pipe 57 | Get-ADStaleUsers | Select Name,SamAccountName 58 | 59 | # Usage 60 | Get-ADStaleUsers -NoLogonSince (Get-Date).AddDays(-30) -CreatedBefore (Get-Date).AddDays(-7) | Remove-ADUser -WhatIf 61 | 62 | #endregion -------------------------------------------------------------------------------- /2020-01-30 - MTX Portland - AD User Lifecycle/Demos/04 - Finding Expiring Accounts and Notifying the Manager.ps1: -------------------------------------------------------------------------------- 1 | #region 2 | # Prep 3 | Get-ADUser -Filter * | %{Set-ADUser $_ -AccountExpirationDate $null} 4 | 5 | $count = 5 6 | $randomUsers = Get-ADUser -LDAPFilter "(manager=*)" 7 | for($x=0;$x-lt 5;$x++) { 8 | $rand = 1..13 | Get-Random 9 | $randomUsers | Get-Random | Set-ADUser -AccountExpirationDate (Get-Date).AddDays($rand) 10 | } 11 | 12 | # Finding accounts soon to expire 13 | $users = Search-ADAccount -AccountExpiring -TimeSpan '14.00:00:00' 14 | $users[0] 15 | 16 | # Adding the manager field 17 | $users = Search-ADAccount -AccountExpiring -TimeSpan '14.00:00:00' | Get-ADUser -Properties Manager,AccountExpirationDate 18 | $users[0] 19 | 20 | # Find their manager 21 | ForEach($user in $users){ 22 | Get-ADUser $user.Manager 23 | } 24 | #endregion 25 | 26 | #region Notify 27 | # Group the users by their manager 28 | $groupedUsers = $users | Group-Object Manager 29 | $groupedUsers 30 | 31 | # Format the users 32 | $tableInfo = '' 33 | ForEach($user in $groupedUsers[0].Group){ 34 | $ts = New-TimeSpan -Start (Get-Date) -End $user.AccountExpirationDate 35 | [string]$tableInfo += "$($user.Name)$($ts.Days)" 36 | } 37 | $tableInfo 38 | 39 | # Put them in HTML 40 | $header = @" 41 | 46 | "@ 47 | 48 | $htmlTemplate = @" 49 |

Hello {0},

50 |

You have minions with accounts that expire soon.

51 | 52 | 53 | 54 | 55 | 56 | {1} 57 |
NameDays till expiration
58 |

Thanks!

59 |

Your friendly, neighborhood PowerShell automation system.

60 | "@ 61 | 62 | #region Emails in PowerShell 63 | # Sending an email 64 | $exampleParams = @{ 65 | Subject = 'Email subject line' 66 | Body = '

Body

this is the paragraph

' 67 | BodyAsHtml = $true 68 | To = 'email@domain.com' 69 | From = 'email@domain.coum' 70 | SmtpServer = 'smtp.domain.com' 71 | UseSSL = $true 72 | } 73 | Send-MailMessage @params 74 | #endregion 75 | $manager = Get-ADUser $groupedUsers[0].Name -Properties EmailAddress 76 | $html = $header + ($htmlTemplate -f $manager.GivenName,$tableInfo) 77 | 78 | # To send the email 79 | $params['body'] = $html 80 | $param['To'] = $manager.EmailAddress 81 | Send-MailMessage @params 82 | 83 | # Copy the file to the local computer via PS Session 84 | $html | Out-File .\html.html 85 | exit 86 | Copy-Item -FromSession $Sessions[0] -Path C:\users\administrator\documents\html.html -Destination '.\2020-01-30 - MTX Portland - AD User Lifecycle\Demos\04 - html.html' 87 | start '.\2020-01-30 - MTX Portland - AD User Lifecycle\Demos\04 - html.html' 88 | 89 | # Jump back in the session 90 | Enter-PSSession $Sessions[0] 91 | 92 | #endregion 93 | 94 | #region Don't forget to functionize it! 95 | Function Send-ADAccountExpirations { 96 | param ( 97 | [int]$DaysTillExpiration, 98 | [pscredential]$EmailCredential = $cred, 99 | [string]$From 100 | ) 101 | $header = @" 102 | 107 | "@ 108 | $htmlTemplate = @" 109 |

Hello {0},

110 |

You have minions with accounts that expire soon.

111 | 112 | 113 | 114 | 115 | 116 | {1} 117 |
NameDays till expiration
118 |

Thanks!

119 |

Your friendly, neighborhood PowerShell automation system.

120 | "@ 121 | # Get the expiring users 122 | $users = Search-ADACcount -AccountExpiring -TimeSpan "$DaysTillExpiration.00:00:00" | Get-ADUser -Properties Manager,AccountExpirationDate 123 | # Group them 124 | $groupedUsers = $users | Group-Object Manager 125 | # Send the emails 126 | ForEach($group in $groupedUsers){ 127 | $tableInfo = $null 128 | ForEach($user in $group.Group){ 129 | $ts = New-TimeSpan -Start (Get-Date) -End $user.AccountExpirationDate 130 | [string]$tableInfo += "$($user.Name)$($ts.Days)" 131 | } 132 | $manager = Get-ADUser $user.Manager -Properties EmailAddress 133 | $html = $header + ($htmlTemplate -f $manager.GivenName,$tableInfo) 134 | $EmailParams = @{ 135 | To = $manager.EmailAddress 136 | From = $from 137 | Subject = 'Account Expiration Notification' 138 | Body = $html 139 | BodyAsHtml = $true 140 | UseSSL = $true 141 | SmtpServer = 'smtp.office365.com' 142 | Credential = $EmailCredential 143 | } 144 | Send-MailMessage @EmailParams 145 | } 146 | } 147 | 148 | # Usage 149 | Send-ADAccountExpirations -DaysTillExpiration 14 -From $cred.UserName 150 | 151 | #endregion -------------------------------------------------------------------------------- /2020-01-30 - MTX Portland - AD User Lifecycle/Demos/05 - Disabled and Retaining.ps1: -------------------------------------------------------------------------------- 1 | #region 2 | # Prep 3 | Set-ADAccountPassword 'Jesse.Pinkman' -NewPassword (ConvertTo-SecureString 'Password1234!' -AsPlainText -Force) -Reset 4 | Set-ADUser 'Jesse.Pinkman' -Description 'Partner' -Enabled $true 5 | 6 | # Disable a user account 7 | $user = Get-ADUser 'Jesse.Pinkman' -Properties Description 8 | $user.Enabled 9 | Disable-ADAccount $user 10 | 11 | # Tag the description 12 | Set-ADUser $user -Description "$($user.Description) - Disabled $((Get-Date).ToShortDateString())" 13 | (Get-ADUser 'Jesse.Pinkman' -Properties Description).Description 14 | 15 | # Retain it 16 | $disabledOU = 'OU=Disabled,OU=People,DC=techsnipsdemo,DC=org' 17 | Move-ADObject -Identity $user.DistinguishedName -TargetPath $disabledOU 18 | 19 | # Verify 20 | (Get-ADUser jesse.pinkman).DistinguishedName 21 | #endregion 22 | 23 | #region fix 24 | Set-aduser jesse.pinkman -Description 'Partner' -Enabled $true 25 | Move-ADObject -Identity (Get-ADUser jesse.pinkman).DistinguishedName -TargetPath 'OU=People,DC=techsnipsdemo,DC=org' 26 | #endregion 27 | 28 | #region Of course make that a function! 29 | Function Invoke-ADUserOffboarding { 30 | Param ( 31 | [Parameter( 32 | ValueFromPipeline = $true, 33 | Mandatory = $true 34 | )] 35 | [Microsoft.ActiveDirectory.Management.ADUser]$Identity, 36 | [string]$DisabledOU = 'OU=Disabled,OU=People,DC=techsnipsdemo,DC=org' 37 | ) 38 | # Grab the user 39 | $user = Get-ADUser $Identity -Properties Description 40 | # Disable the account 41 | Disable-ADAccount $user 42 | # Tag it 43 | Set-ADUser $user -Description "$($user.Description) - Disabled $((Get-Date).ToShortDateString())" 44 | # Move it to the disabled OU 45 | Move-ADObject -Identity $user.DistinguishedName -TargetPath $DisabledOU 46 | } 47 | 48 | # Usage 49 | Invoke-ADUserOffboarding jesse.pinkman 50 | 51 | # Verify 52 | Get-ADUser jesse.pinkman -Properties Description 53 | 54 | #endregion -------------------------------------------------------------------------------- /2020-01-30 - MTX Portland - AD User Lifecycle/MTX Portland - Automating AD User Lifecycle.pptx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ThePoShWolf/Sessions/ddaac2096083e091ba8175375560052451555120/2020-01-30 - MTX Portland - AD User Lifecycle/MTX Portland - Automating AD User Lifecycle.pptx -------------------------------------------------------------------------------- /2020-01-30 - MTX Portland - REST APIs/Demos/01 - Working with json.ps1: -------------------------------------------------------------------------------- 1 | #region Reading json 2 | # from a file 3 | & '.\2020-01-30 - MTX Portland - REST APIs\Demos\01 - Data.json' 4 | 5 | $jsonFile = '.\2020-01-30 - MTX Portland - REST APIs\Demos\01 - Data.json' 6 | 7 | ## Reading it into a psobject 8 | $json = Get-Content $jsonFile | ConvertFrom-Json 9 | 10 | $json 11 | $json.items 12 | $json.items[0] 13 | $json.items[0].owner 14 | 15 | # From a string 16 | $str = @' 17 | { 18 | "month":"1", 19 | "num":2257, 20 | "link":"", 21 | "year":"2020", 22 | "news":"", 23 | "safe_title":"Unsubscribe Message", 24 | "transcript":"", 25 | "alt":"A mix of the two is even worse: 'Thanks for unsubscribing and helping us pare this list down to reliable supporters.'", 26 | "img":"https://imgs.xkcd.com/comics/unsubscribe_message.png", 27 | "title":"Unsubscribe Message", 28 | "day":"20" 29 | } 30 | '@ 31 | 32 | $json = $str | ConvertFrom-Json 33 | 34 | $json 35 | #endregion 36 | 37 | #region Converting and writing json 38 | # From a PSObject 39 | Get-Process 40 | $process = Get-Process | Select -First 5 41 | 42 | $process.PSObject.TypeNames 43 | 44 | $process | ConvertTo-Json -Depth 10 45 | 46 | $process | ConvertTo-Json -Compress 47 | 48 | # From a hashtable 49 | $ht = @{ 50 | Key = 'Value' 51 | Key1 = 'Value1' 52 | Key2 = 'Value2' 53 | SubArray = @( 54 | @{ 55 | SubArrayKey = 'SubArrayValue1' 56 | }, 57 | @{ 58 | SubArrayKey = 'SubArrayValue2' 59 | } 60 | ) 61 | } 62 | 63 | $ht | ConvertTo-Json 64 | 65 | ## Converting from json to a hashtable, v6+ 66 | $str | ConvertFrom-Json -AsHashtable 67 | 68 | #endregion -------------------------------------------------------------------------------- /2020-01-30 - MTX Portland - REST APIs/Demos/02 - Invoke-RestMethod Syntax.ps1: -------------------------------------------------------------------------------- 1 | #region Common 2 | #region URI 3 | ## Uri breakdown 4 | 5 | <# 6 | 'https://api.howell-it.com:8080/projects?name=UrlStuff&count=2' 7 | 8 | https:// - protocol 9 | api - subdomain 10 | howell-it - domain 11 | com - top-level domain 12 | 8080 - port 13 | project - path (or resource) 14 | ? - what follows after this is the query 15 | name=UrlStuff&count=2 - parameters. 16 | #> 17 | 18 | ## Basic uri 19 | Invoke-RestMethod -Uri 'http://xkcd.com/info.0.json' 20 | 21 | ## Uri with query parameters 22 | Invoke-RestMethod -Uri 'https://api.stackexchange.com/2.2/answers?order=desc&sort=activity&site=stackoverflow' 23 | 24 | ### Breakdown 25 | 26 | $baseUri = 'https://api.stackexchange.com/2.2' 27 | 28 | $resource = 'answers' 29 | 30 | $queries = @( 31 | 'order=desc' 32 | 'sort=activity' 33 | 'site=stackoverflow' 34 | ) -join '&' 35 | 36 | $uri = "$baseUri/$resource`?$queries" 37 | 38 | $uri 39 | 40 | Invoke-RestMethod -Uri $uri 41 | #endregion 42 | 43 | # Method 44 | 45 | ## Basic: 46 | Invoke-RestMethod -Method 'Post' -Uri 'SomeUri' 47 | 48 | #region Common Headers 49 | 50 | ## v5.1 51 | $headers = @{ 52 | 'Content-Type' = 'application/json' 53 | 'Accept' = 'application/json' 54 | } 55 | 56 | Invoke-RestMethod -Headers $headers 57 | 58 | ## v6+ 59 | $headers = @{ 60 | 'Accept' = 'application/json' 61 | } 62 | Invoke-RestMethod -ContentType 'application/json' -Headers $headers 63 | 64 | ## Post a user to Octopus 65 | $apiKey = 'API-9M1UYY2H8ZRBJIN7CG4MNJLEA7A' 66 | #never save api tokens in plain text 67 | #this is for a demo server 68 | 69 | $baseUri = 'http://192.168.11.8/api' 70 | $resource = 'users' 71 | 72 | $htBody = @{ 73 | Username = 'NewUser1' 74 | DisplayName = 'New User' 75 | Password = 'Password1234!' 76 | } 77 | 78 | $jsonBody = $htBody | ConvertTo-Json 79 | 80 | $headers = @{ 81 | 'X-Octopus-ApiKey' = $apiKey 82 | 'Content-Type' = 'application/json' 83 | } 84 | 85 | Invoke-RestMethod -Uri $baseUri/$resource -Method Post -Headers $headers -Body $jsonBody 86 | 87 | ## Now get that user 88 | 89 | Invoke-RestMethod $baseUri/$resource -Method Get -Headers $headers 90 | 91 | (Invoke-RestMethod $baseUri/$resource -Method Get -Headers $headers).Items 92 | #endregion 93 | #endregion 94 | 95 | #region Authentication 96 | ## Basic Auth 97 | ## example source: https://docs.microsoft.com/en-us/rest/api/azure/devops/build/builds/list?view=azure-devops-rest-5.1 98 | 99 | ### v5.1 in the headers 100 | $apiKey = Get-Content C:\Users\AnthonyHowell\Documents\azdo.txt 101 | 102 | $base64Auth = [Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes(("username:$apiKey"))) 103 | 104 | $headers = @{ 105 | Authorization = "Basic $base64Auth" 106 | Accept = "application/json; api-version=5.1" 107 | 'Content-Type' = 'application/json' 108 | } 109 | 110 | Invoke-RestMethod -Uri 'https://dev.azure.com/theposhwolf/curl2ps/_apis/build/builds' -Headers $headers 111 | 112 | ### v6+ -Authentication 113 | $securePassword = ConvertTo-SecureString $apiKey -AsPlainText -Force 114 | $cred = [pscredential]::new('username',$securePassword) 115 | $params = @{ 116 | URI = 'https://dev.azure.com/theposhwolf/curl2ps/_apis/build/builds' 117 | Headers = $headers 118 | Authentication = 'Basic' 119 | Credential = $cred 120 | } 121 | Invoke-RestMethod @params 122 | 123 | ## OAuth (bearer token) 124 | 125 | ### v5.1 126 | $token = Get-Content C:\users\AnthonyHowell\Documents\at.txt 127 | $headers = @{ 128 | Authorization = "Bearer $token" 129 | Accept = 'application/json' 130 | } 131 | Invoke-RestMethod -Uri 'https://api.airtable.com/v0/appTczXUIAllL0x88/Work%20Items' -Headers $headers 132 | 133 | ### v6 134 | $token = Get-Content C:\users\AnthonyHowell\Documents\at.txt 135 | $secureToken = ConvertTo-SecureString $token -AsPlainText -Force 136 | $params = @{ 137 | Uri = 'https://api.airtable.com/v0/appTczXUIAllL0x88/Work%20Items' 138 | Authentication = 'OAuth' 139 | Token = $secureToken 140 | } 141 | Invoke-RestMethod @params 142 | 143 | #endregion -------------------------------------------------------------------------------- /2020-01-30 - MTX Portland - REST APIs/Demos/03 - Interpreting Curl Syntax.ps1: -------------------------------------------------------------------------------- 1 | #region Getting items from Airtable 2 | $token = Get-Content C:\users\AnthonyHowell\Documents\at.txt 3 | ## curl 4 | # link: https://airtable.com/appTczXUIAllL0x88/api/docs#curl/table:work%20items:list 5 | # this link is specific to an Airtable 'base'. This is using a template base. 6 | curl https://api.airtable.com/v0/appTczXUIAllL0x88/Work%20Items -H "Authorization: Bearer YOUR_API_KEY" 7 | 8 | ## PowerShell 9 | $headers = @{ 10 | Authorization = "Bearer $token" 11 | } 12 | Invoke-RestMethod -Uri 'https://api.airtable.com/v0/appTczXUIAllL0x88/Work%20Items' -Headers $headers 13 | #endregion 14 | 15 | #region Creating items in Airtable 16 | ## curl 17 | # link: https://airtable.com/appTczXUIAllL0x88/api/docs#curl/table:clients:create 18 | curl -v -X POST https://api.airtable.com/v0/appTczXUIAllL0x88/Clients \ 19 | -H "Authorization: Bearer YOUR_API_KEY" \ 20 | -H "Content-Type: application/json" \ 21 | --data '{ 22 | "records": [ 23 | { 24 | "fields": { 25 | "Work Items": [ 26 | "reczyGwJ6AiXVq9TA" 27 | ], 28 | "Name": "South Lake Store" 29 | } 30 | }, 31 | { 32 | "fields": { 33 | "Work Items": [ 34 | "reczwzBP3ygLPraYu", 35 | "recFFxoFVGrMPtbSE", 36 | "recGKApP4vqYZpeZI" 37 | ], 38 | "Name": "Marina Shop" 39 | } 40 | } 41 | ] 42 | }' 43 | 44 | ## PowerShell 45 | $headers = @{ 46 | Authorization = "Bearer $token" 47 | 'Content-Type' = 'application/json' 48 | } 49 | 50 | $body = @{ 51 | records = @( 52 | @{ 53 | fields = @{ 54 | workitems = @("reczwzBP3ygLPraYu","recFFxoFVGrMPtbSE","recGKApP4vqYZpeZI") 55 | Name = 'Marina Shop' 56 | } 57 | }, 58 | @{ 59 | fields = @{ 60 | workitems = @("reczyGwJ6AiXVq9TA") 61 | Name = 'South Lake Store' 62 | } 63 | } 64 | ) 65 | } | ConvertTo-Json -Depth 6 66 | 67 | Invoke-RestMethod -Method Post -Uri 'https://api.airtable.com/v0/appTczXUIAllL0x88/Clients' -Headers $headers -Body $body 68 | #endregion 69 | 70 | #region Basic Authentication 71 | # link: https://documenter.getpostman.com/view/4454237/apisherpadeskcom-playground/RW8AooQg?version=latest#6a1f8cfa-8910-8c9f-2e68-bfaefb51920b 72 | 73 | ## curl 74 | curl --request GET 'https://ncg1in-8d1rag:5nuauzj5pkfftlz3fmyksmyhat6j35kf@api.sherpadesk.com/tickets?status=open,onhold&role=user&limit=6&format=json' 75 | 76 | ## PowerShell 77 | $auth = Get-Content C:\users\AnthonyHowell\Documents\sd.txt | ConvertFrom-Json 78 | # using their values 79 | $encodedAuth = [Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes('ncg1in-8d1rag:5nuauzj5pkfftlz3fmyksmyhat6j35kf')) 80 | 81 | # using actual values 82 | $encodedAuth = [Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes("$($auth.username):$($auth.password)")) 83 | 84 | $headers = @{ 85 | Authorization = "Basic $encodedAuth" 86 | Accept = 'application/json' 87 | } 88 | Invoke-RestMethod -Uri 'https://api.sherpadesk.com/tickets?status=open,onhold&role=user&limit=6&format=json' -Headers $headers 89 | #endregion 90 | 91 | #region Curl2PS 92 | ## curl man pages: https://curl.haxx.se/docs/manpage.html 93 | 94 | ## Import dev version 95 | Import-Module ..\Curl2PS\build\Curl2PS 96 | 97 | ## Convert 98 | ConvertTo-IRM 'curl https://api.airtable.com/v0/appTczXUIAllL0x88/Work%20Items -H "Authorization: Bearer YOUR_API_KEY"' -String 99 | 100 | ConvertTo-IRM "curl --request GET https://ncg1in-8d1rag:5nuauzj5pkfftlz3fmyksmyhat6j35kf@api.sherpadesk.com/tickets?status=open,onhold&role=user&limit=6&format=json" -String 101 | #endregion -------------------------------------------------------------------------------- /2020-01-30 - MTX Portland - REST APIs/Demos/04 - Making API cmdlets.ps1: -------------------------------------------------------------------------------- 1 | # Using a Sherpa Desk example: 2 | # curl --request GET 'https://ncg1in-8d1rag:5nuauzj5pkfftlz3fmyksmyhat6j35kf@api.sherpadesk.com/tickets?status=open,onhold&role=user&limit=6&format=json' 3 | 4 | ## Name? 5 | Get-SherpaDeskTicket 6 | Get-SDTicket 7 | 8 | ## Parameters 9 | [string]$Organization 10 | [string]$Instance 11 | [string]$ApiKey # This could be a secure string: [System.Security.SecureString] 12 | [string[]]$Status 13 | [string]$Role 14 | [int]$Limit 15 | [string]$Format = 'json' 16 | 17 | ## Build the headers 18 | $encodedAuth = [Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes("$Organization-$Instance`:$ApiKey")) 19 | $headers = @{ 20 | Authorization = "Basic $encodedAuth" 21 | Accept = 'application/json' 22 | } 23 | 24 | ## Build the query string 25 | $queryParamNames = 'Status','Role','Limit','Format' 26 | $queryArr = foreach ($param in ($PSBoundParameters.Keys | Where-Object {$queryParamNames -contains $_})) { 27 | "$param=$($PSBoundParameters[$param] -join ',')" 28 | } 29 | 30 | ## Building the URI 31 | $baseUri = 'https://api.sherpadesk.com' 32 | $resource = 'tickets' 33 | $query = ($queryArr -join '&').ToLower() 34 | 35 | $uri = "$baseUri/$resource`?$query" 36 | 37 | ## Making the call 38 | Invoke-RestMethod -Uri $uri -Method Get 39 | 40 | ## Combine everything 41 | Function Get-SDTicket { 42 | [CmdletBinding()] 43 | param ( 44 | [string]$Organization, 45 | [string]$Instance, 46 | [string]$ApiKey, # This could be a secure string: [System.Security.SecureString] 47 | [string[]]$Status, 48 | [string]$Role, 49 | [int]$Limit, 50 | [string]$Format = 'json' 51 | ) 52 | ## Build the headers 53 | $encodedAuth = [Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes("$Organization-$Instance`:$ApiKey")) 54 | $headers = @{ 55 | Authorization = "Basic $encodedAuth" 56 | Accept = 'application/json' 57 | } 58 | 59 | ## Build the query string 60 | $queryParamNames = 'Status','Role','Limit','Format' 61 | $queryArr = foreach ($param in ($PSBoundParameters.Keys | Where-Object {$queryParamNames -contains $_})) { 62 | "$param=$($PSBoundParameters[$param] -join ',')" 63 | } 64 | 65 | ## Building the URI 66 | $baseUri = 'https://api.sherpadesk.com' 67 | $resource = 'tickets' 68 | $query = ($queryArr -join '&').ToLower() 69 | 70 | $uri = "$baseUri/$resource`?$query" 71 | 72 | ## Making the call 73 | Invoke-RestMethod -Uri $uri -Method Get -Headers $headers 74 | } 75 | 76 | # Run it 77 | $auth = Get-Content C:\users\AnthonyHowell\Documents\sd.txt | ConvertFrom-Json 78 | $sdTicketParams = @{ 79 | Organization = $auth.organization 80 | Instance = $auth.instance 81 | ApiKey = $auth.password 82 | Status = 'open','onhold' 83 | Role = 'user' 84 | Limit = 6 85 | Format = 'json' 86 | } 87 | Get-SDTicket @sdTicketParams -Verbose -------------------------------------------------------------------------------- /2020-01-30 - MTX Portland - REST APIs/MTX Portland - REST APIs.pptx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ThePoShWolf/Sessions/ddaac2096083e091ba8175375560052451555120/2020-01-30 - MTX Portland - REST APIs/MTX Portland - REST APIs.pptx -------------------------------------------------------------------------------- /2020-02-10 - Mississippi PS UG - Module Building Modules/Demos/01 - Plaster.ps1: -------------------------------------------------------------------------------- 1 | #region Create a template 2 | # Create a new manifest using New-PlasterManifest 3 | 4 | ## Make sure we are in the right spot 5 | Set-Location 'C:\users\anthony\git\sessions\2020-02-10 - Mississippi PS UG - Module Building Modules\Demos' 6 | function prompt{} 7 | 8 | ## Splat the required parameters 9 | ## Pulled from Mike F Robbins: https://mikefrobbins.com/2018/02/15/using-plaster-to-create-a-powershell-script-module-template/ 10 | $manifestProperties = @{ 11 | Path = '.\Template\PlasterManifest.xml' 12 | TemplateName = 'ScriptModuleTemplate' 13 | TemplateType = 'Project' # or Item. Modules are projects though 14 | Author = 'Anthony Howell' 15 | Description = 'Scaffolds the files required for a PowerShell script module' 16 | Tags = 'PowerShell, Module, ModuleManifest' 17 | } 18 | New-PlasterManifest @manifestProperties 19 | 20 | ## Now we can look at the file: 21 | .\Template\PlasterManifest.xml 22 | 23 | ## And we can test it: 24 | Invoke-Plaster -TemplatePath .\Template -DestinationPath .\TestModule 25 | 26 | ## And clean up 27 | Remove-Item .\TestModule 28 | 29 | #endregion 30 | 31 | #region Add stuff to the Plaster manifest 32 | # Add parameters 33 | @' 34 | 39 | 44 | 50 | 56 | 62 | 68 | '@ | clip 69 | 70 | # Add content 71 | 72 | @' 73 | 74 | Creating folder structure 75 | 76 | 80 | 84 | 88 | 92 | 96 | 100 | 104 | 105 | Deploying common files 106 | 107 | 111 | 115 | 119 | 123 | 124 | Creating Module Manifest 125 | 126 | 136 | '@ | clip 137 | 138 | # Copy template files 139 | # These files are also in my MyPlasterTemplates repo: https://github.com/ThePoShWolf/MyPlasterTemplates/tree/master/ScriptModule 140 | Get-ChildItem 'C:\users\anthony\git\MyPlasterTemplates\ScriptModule' -Filter *.ps1 | %{Copy-Item $_.FullName -Destination .\Template} 141 | 142 | #endregion 143 | 144 | # Test it 145 | Invoke-Plaster -TemplatePath .\Template -DestinationPath ..\..\..\DemoRepo 146 | 147 | # Verify 148 | Code ..\..\..\DemoRepo -------------------------------------------------------------------------------- /2020-02-10 - Mississippi PS UG - Module Building Modules/Demos/02 - PlatyPS.ps1: -------------------------------------------------------------------------------- 1 | # Demonstrate code 2 | 3 | #region Create the markdown files 4 | # Import the module 5 | Import-Module .\build\TestModule 6 | 7 | # Review the cmdlets 8 | Get-Command -Module TestModule 9 | 10 | # Review the help 11 | Help Get-InstalledSoftware 12 | Help Format-Bytes 13 | 14 | # Create the markdown help 15 | New-MarkdownHelp -Module TestModule -OutputFolder .\docs 16 | 17 | # Output the markdown help to an external PS help xml 18 | # This should go in your build script! (to be demonstrated) 19 | New-ExternalHelp .\docs -OutputPath ".\build\TestModule\EN-US" 20 | 21 | #endregion -------------------------------------------------------------------------------- /2020-02-10 - Mississippi PS UG - Module Building Modules/Demos/03 - InvokeBuild.ps1: -------------------------------------------------------------------------------- 1 | # Show the build file 2 | code .\TestModule.Build.ps1 3 | 4 | #region 5 | # Run a build 6 | Invoke-Build -Task ModuleBuild 7 | 8 | # Take a look at the results 9 | Import-Module .\build\TestModule 10 | 11 | Get-Command -Module TestModule 12 | 13 | # Add a cmdlet 14 | Copy-Item ..\Utilities\ActiveSessions\Get-ActiveSessions.ps1 -Destination .\src\public 15 | 16 | # Rebuild 17 | Invoke-Build -Task ModuleBuild 18 | 19 | # Verify 20 | Import-Module .\build\TestModule 21 | 22 | #endregion -------------------------------------------------------------------------------- /2020-02-10 - Mississippi PS UG - Module Building Modules/Demos/04 - PSDeploy.ps1: -------------------------------------------------------------------------------- 1 | # Show the PSDeploy file 2 | code .\TestModule.PSDeploy.ps1 3 | 4 | #region 5 | # List out the deployment: 6 | Get-PSDeployment -Path .\TestModule.PSDeploy.ps1 7 | 8 | # Make sure our build is up to date 9 | Invoke-Build -Task Publish 10 | 11 | # Deploy 12 | # Since we are in the root folder, no path is necessary 13 | # It finds all files *.psdeploy.ps1 14 | Invoke-PSDeploy -Force 15 | 16 | # Use our build script to do EVERYTHING 17 | Invoke-Build -Take Publish 18 | 19 | #endregion -------------------------------------------------------------------------------- /2020-02-10 - Mississippi PS UG - Module Building Modules/MSPSUG - Module Building Modules.pptx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ThePoShWolf/Sessions/ddaac2096083e091ba8175375560052451555120/2020-02-10 - Mississippi PS UG - Module Building Modules/MSPSUG - Module Building Modules.pptx -------------------------------------------------------------------------------- /2020-02-10 - Mississippi PS UG - Module Building Modules/README.md: -------------------------------------------------------------------------------- 1 | # Mississippi PowerShell User Group 2020-02-10 2 | 3 | - [Here is a link to the video recording](https://www.youtube.com/watch?v=_dvWeptT59A) 4 | - [Here is the demo repo used in the session](https://github.com/theposhwolf/demorepo) -------------------------------------------------------------------------------- /2021-04-29 - PowerShell Summit 2021 - Practically Regexing/Demos/1. 5 Minute PowerShell Regex Overview.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | How to user regular expressions in PowerShell 3 | #> 4 | 5 | #region -Match 6 | 7 | $emailAddress = 'anthony@howell-it.com' 8 | 9 | # pattern from: https://regular-expressions.mobi/email.html 10 | $pattern = "^[a-z0-9!#$%&'*+/=?^_‘{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_‘{|}~-]+)*@(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$" 11 | 12 | if ($emailAddress -match $pattern) { 13 | Write-Host 'You have an email address :)' 14 | } else { 15 | Write-Host 'That is not an email address :(' 16 | } 17 | 18 | #endregion 19 | 20 | #region -Replace 21 | 22 | $str = 'My phone number is: 1234567890' 23 | $str -replace '(\d\d\d)(\d\d\d)(\d\d\d\d)','($1)-$2-$3' 24 | 25 | # Advanced version 26 | $str -replace '(\(?\d{3}\)?)-?(\d{3})-?(\d{4})','($1)-$2-$3' 27 | 28 | #endregion 29 | 30 | #region Select-String 31 | 32 | Get-Content '.\2021-04-29 - PowerShell Summit 2021 - Practically Regexing\Demos\SampleLog.txt' | Select-String '\[error\]' 33 | 34 | #endregion 35 | 36 | #region switch -regex 37 | 38 | $str = 'My email is anthony@howell-it.com and my phone number is 1234567890' 39 | switch -regex ($str) { 40 | "[a-z0-9!#$%&'*+/=?^_‘{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_‘{|}~-]+)*@(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?" { 41 | Write-Host 'There is an email!' 42 | } 43 | '(\(?\d{3}\)?)-?(\d{3})-?(\d{4})' { 44 | Write-Host 'There is a phone number!' 45 | } 46 | } 47 | 48 | #endregion 49 | 50 | #region [ValidatePattern()] 51 | 52 | Function Get-EmailAddress { 53 | param ( 54 | [ValidatePattern( 55 | "^[a-z0-9!#$%&'*+/=?^_‘{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_‘{|}~-]+)*@(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$" 56 | )] 57 | [string]$EmailAddress 58 | ) 59 | Write-Output $EmailAddress 60 | } 61 | Get-EmailAddress 'blahdeblah' 62 | Get-EmailAddress 'anthony@howell-it.com' 63 | 64 | #endregion -------------------------------------------------------------------------------- /2021-04-29 - PowerShell Summit 2021 - Practically Regexing/Demos/2. AD DN.ps1: -------------------------------------------------------------------------------- 1 | # A sample Distinguished Name 2 | $sampleUserDn = 'CN=ThePoShWolf,OU=Oregon,OU=Users,DC=theposhwolf,DC=com' 3 | 4 | # Evaluate the DN definition: https://docs.microsoft.com/en-us/previous-versions/windows/desktop/ldap/distinguished-names 5 | 6 | #region User's cn: 7 | 8 | # The user DN should always start with CN= 9 | '^CN=' 10 | 11 | # Then, for the the name of the user, we'll use: 12 | # anything but a back slash or comma: 13 | '[^\,]' 14 | # OR a backslash followed by a character (including a back slash or comma): 15 | '\\.' 16 | 17 | # Combined with an plus sign to capture 1 or more: 18 | '([^\,]|\\.)+' 19 | 20 | # Combined with our start we get: 21 | $sampleUserDn -match '^CN=([^\,]|\\.)+,';$Matches 22 | 23 | # We can add a group to capture the cn of the user 24 | # And uncapture the last group: 25 | $sampleUserDn -match '^CN=(?(?:[^\,]|\\.)+),';$Matches 26 | $Matches.cn 27 | 28 | #endregion 29 | 30 | #region Path 31 | $userRegex = '^CN=(?(?:[^\,]|\\.)+),' 32 | 33 | # The user will be in an OU or container, so we'll need to account for either: 34 | '(OU|CN)' 35 | 36 | # And the name of said container or OU uses the same naming requirements as the user's CN, 37 | # so we'll use the same regex: 38 | '(OU|CN)=(?:[^\,]|\\.)+' 39 | 40 | # And the user could be in nested OUs or containers, so we'll account for 1 or more: 41 | '((OU|CN)=(?:[^\,]|\\.)+,)*' 42 | 43 | # Adding it to the userRegex: 44 | $sampleUserDn -match "$userRegex((OU|CN)=([^\,]|\\.)+,)*";$Matches 45 | 46 | # And then we'll clean up the groupings: 47 | $sampleUserDn -match "$userRegex(?(?:(?:OU|CN)=(?:[^\,]|\\.)+,)*)";$Matches 48 | 49 | #endregion 50 | 51 | #region Domain 52 | $pathRegex = '(?(?:(?:OU|CN)=(?:[^\,]|\\.)+,)*)' 53 | 54 | # We know the domain section starts with DC: 55 | 'DC=' 56 | 57 | # And for the domain name, we can use a stricter character set 58 | # Characters, digits, and hyphens, but it can't start or end with a hyphen 59 | # This is where look arounds can come into play: 60 | '(?!-)[a-zA-Z0-9-]+(?(?:DC=(?!-)[a-zA-Z0-9-]+(?(?:[^\,]|\\.)+),(?(?:(?:OU|CN)=(?:[^\,]|\\.)+,)*(?(?:DC=(?!-)[a-zA-Z0-9-]+(? 4 | 5 | #region Log sample 6 | 7 | SOURCE: { ADPID, FirstName LastName, newemail@theposhwolf.com } 8 | DEST : { email@theposhwolf.com, FirstName LastName (FirstName LastName) } 9 | emails[work].value is different: old= "oldemail@theposhwolf.com", new= "newemail@theposhwolf.com" 10 | userName is different: old= "oldemail@theposhwolf.com", new= "newemail@theposhwolf.com" 11 | ENT.department is different: old= "", new= "Department of Regexing" 12 | ENT.division is different: old= "", new= "PowerShell Division" 13 | ENT.employeeNumber is different: old= "", new= "empnumber" 14 | ENT.manager is different: old= "oldemail@theposhwolf.com", new= "newemail@theposhwolf.com" 15 | IAN.physicalDeliveryOfficeName is different: old= "Eugene", new= "Portland" 16 | newemail@theposhwolf.com: report only, no update 17 | 18 | # Source 19 | SOURCE: { ADPID, FirstName LastName, newemail@theposhwolf.com } 20 | 21 | # Dest 22 | DEST : { email@theposhwolf.com, FirstName LastName (FirstName LastName) } 23 | 24 | # Property 25 | emails[work].value is different: old= "oldemail@theposhwolf.com", new= "newemail@theposhwolf.com" 26 | 27 | #endregion 28 | 29 | #region Source 30 | 31 | $source = 'SOURCE: { ADPID, FirstName LastName, newemail@theposhwolf.com } ' 32 | 33 | # Start 34 | '^SOURCE: \{ \}' 35 | 36 | # Splitting by commas 37 | $source -match "^SOURCE: \{ \w+, [0-9a-zA-Z\-_' ]+, [^ ]+ \}";$Matches 38 | 39 | # Add groups 40 | $source -match "^SOURCE: \{ (?\w+), (?[0-9a-zA-Z\-_' ]+), (?[^ ]+) \}";$Matches 41 | 42 | #endregion 43 | 44 | #region Dest 45 | 46 | $dest = 'DEST : { email@theposhwolf.com, FirstName LastName (FirstName LastName) } ' 47 | 48 | # Start 49 | '^DEST : \{ \}' 50 | 51 | # Split by commas 52 | $dest -match '^DEST : \{ [^,]+, [^}]+ \}';$Matches 53 | 54 | # Add in the groups 55 | $dest -match '^DEST : \{ (?[^,]+), (?[^}]+) \}';$Matches 56 | 57 | #endregion 58 | 59 | #region Property 60 | 61 | $prop1 = 'ENT.employeeNumber is different: old= "", new= "empnumber" ' 62 | $prop2 = 'emails[work].value is different: old= "oldemail@theposhwolf.com", new= "newemail@theposhwolf.com" ' 63 | 64 | # Start 65 | '^\S+ is different: old= "[^"]*", new= "[^"]+"' 66 | ^ ^ 67 | 68 | $prop1 -match '^\S+ is different: old= "[^"]*", new= "[^"]+"';$Matches 69 | $prop2 -match '^\S+ is different: old= "[^"]*", new= "[^"]+"';$Matches 70 | 71 | # Add groups 72 | $prop1 -match '^(?\S+) is different: old= "(?[^"]*)", new= "(?[^"]+)"';$Matches 73 | $prop2 -match '^(?\S+) is different: old= "(?[^"]*)", new= "(?[^"]+)"';$Matches 74 | 75 | #endregion 76 | 77 | #region Parse the log! 78 | $logPath = '.\2021-04-29 - PowerShell Summit 2021 - Practically Regexing\Demos\SanitizedLog.txt' 79 | $content = Get-Content $logPath 80 | 81 | $sourceRegex = "^SOURCE: \{ (?\w+), (?[0-9a-zA-Z\-_' ]+), (?[^ ]+) \}" 82 | $destRegex = '^DEST : \{ (?[^,]+), (?[^}]+) \}' 83 | $propRegex = '^(?\S+) is different: old= "(?[^"]*)", new= "(?[^"]+)"' 84 | 85 | $x = 0 86 | $out = @{} 87 | while ($x -le $content.count) { 88 | $user = [ordered]@{} 89 | While ($content[$x] -notlike '---*' -and $x -le $content.Count) { 90 | if ($content[$x] -match $propRegex) { 91 | $user[$matches.prop] = @{ 92 | Old = $matches.old 93 | New = $matches.new 94 | } 95 | }elseif ($content[$x] -match $sourceRegex) { 96 | $user['Source'] = @{ 97 | AID = $matches.id 98 | Name = $matches.name 99 | Email = $matches.email 100 | } 101 | } elseif ($content[$x] -match $destRegex) { 102 | $user['Dest'] = @{ 103 | Name = $matches.name 104 | Email = $matches.email 105 | } 106 | } 107 | $x++ 108 | } 109 | $out[$user['Source']['AID']] = $user 110 | $x++ 111 | } 112 | 113 | $out['ADPID01'] | ConvertTo-Json 114 | 115 | #endregion -------------------------------------------------------------------------------- /2021-04-29 - PowerShell Summit 2021 - Practically Regexing/Demos/4. VS Code Regex.ps1: -------------------------------------------------------------------------------- 1 | #region Org to Server 2 | 3 | # regex 4 | 'Org(s)?' 5 | 6 | # replaces 7 | 'Server$1' 8 | 9 | # matches 10 | 'Org' 11 | 'Orgs' 12 | 13 | #endregion 14 | 15 | #region Noun prefix 16 | 17 | # regex 18 | '-ZCrm' 19 | 20 | # replace 21 | '-Demo' 22 | 23 | # matches 24 | '-ZCrm' 25 | 26 | #endregion 27 | 28 | #region Variable change 29 | 30 | # regex 31 | '(?<=\$)(script:)?config' 32 | 33 | # replace 34 | '$1moduleConfig' 35 | 36 | # matches (without replacing the $) 37 | $config 38 | $script:config 39 | 40 | #endregion -------------------------------------------------------------------------------- /2021-04-29 - PowerShell Summit 2021 - Practically Regexing/Demos/5. Regex vs. Alternatives.ps1: -------------------------------------------------------------------------------- 1 | #region AD DN 2 | 3 | $sampleUserDn = 'CN=ThePoShWolf,OU=Oregon,OU=Users,DC=theposhwolf,DC=com' 4 | $regex = '^CN=(?(?:[^\,]|\\.)+),(?(?:(?:OU|CN)=(?:[^\,]|\\.)+,)*(?(?:DC=(?!-)[a-zA-Z0-9-]+(?\w+), (?[0-9a-zA-Z\-_' ]+), (?[^ ]+) \}" 44 | $destRegex = '^DEST : \{ (?[^,]+), (?[^}]+) \}' 45 | $propRegex = '^(?\S+) is different: old= "(?[^"]*)", new= "(?[^"]+)"' 46 | 47 | $count = 100000 48 | 49 | Measure-Command { 50 | for($x=0; $x -lt $count; $x++) { 51 | $source -match $sourceRegex 52 | $dest -match $destRegex 53 | $prop1 -match $propRegex 54 | } 55 | } 56 | 57 | Measure-Command { 58 | for($x=0; $x -lt $count; $x++) { 59 | # Source 60 | $firstComma = $source.IndexOf(',') 61 | $secondComma = $source.IndexOf(',',$firstComma+1) 62 | @{ 63 | id = $source.Substring($source.IndexOf('{')+2,$firstComma-$source.IndexOf('{')-2) 64 | name = $source.Substring($firstComma+2,($secondComma)-$firstComma-2) 65 | email = $source.Substring($secondComma+2,$source.IndexOf('}')-$secondComma-2) 66 | } 67 | # Dest 68 | @{ 69 | email = $dest.Substring($dest.IndexOf('{')+2,$dest.IndexOf(',')-$dest.IndexOf('{')-2) 70 | name = $dest.Substring($dest.IndexOf(',')+2,$dest.IndexOf('}')-$dest.IndexOf(',')-2) 71 | } 72 | # Prop 73 | $firstQuote = $prop1.IndexOf('"') 74 | $secondQuote = $prop1.IndexOf('"',$firstQuote+1) 75 | $thirdQuote = $prop1.IndexOf('"',$secondQuote+1) 76 | $fourthQuote = $prop1.IndexOf('"',$thirdQuote+1) 77 | @{ 78 | prop = $prop1.Substring(0,$prop1.IndexOf(' ')) 79 | old = $prop1.Substring($firstQuote+1,$secondQuote-$firstQuote-1) 80 | new = $prop1.Substring($thirdQuote+1,$fourthQuote-$thirdQuote-1) 81 | } 82 | } 83 | } 84 | 85 | #endregion -------------------------------------------------------------------------------- /2021-04-29 - PowerShell Summit 2021 - Practically Regexing/Demos/6. What not to regex.ps1: -------------------------------------------------------------------------------- 1 | #region HTML 2 | 3 | # Why? 4 | 'Parser' 5 | 6 | # Good read: https://stackoverflow.com/questions/1732348/regex-match-open-tags-except-xhtml-self-contained-tags/1732454#1732454 7 | 8 | # Example (PsParseHtml): 9 | $table = @' 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 |
CompanyContactCountry
Alfreds FutterkisteMaria AndersGermany
Centro comercial MoctezumaFrancisco ChangMexico
Ernst HandelRoland MendelAustria
Island TradingHelen BennettUK
Laughing Bacchus WinecellarsYoshi TannamuriCanada
Magazzini Alimentari RiunitiGiovanni RovelliItaly
47 | '@ 48 | ConvertFrom-HtmlTable -Content $table 49 | 50 | #endregion 51 | 52 | #region XML 53 | 54 | # Why? 55 | 'Parser' 56 | 57 | # Example: 58 | [xml]$xml = @' 59 | 60 | Tove 61 | Jani 62 | Reminder 63 | Don't forget me this weekend! 64 | 65 | '@ 66 | $xml.note 67 | 68 | #endregion 69 | 70 | #region URL 71 | 72 | # Why? 73 | 'Parser' 74 | 75 | # Example 76 | $uri = 'https://www.bing.com/search?q=powershell' 77 | 78 | [System.Uri]::new($uri) 79 | 80 | #endregion 81 | 82 | #region Summary 83 | 84 | # Spend your time writing code that does the things that make you productive. 85 | 86 | #endregion -------------------------------------------------------------------------------- /2021-04-29 - PowerShell Summit 2021 - Practically Regexing/Demos/SampleLog.txt: -------------------------------------------------------------------------------- 1 | 1997-04-29T20:15:34 - [error] - Danger, Will Robinson! Danger! 2 | 1998-04-29T20:16:34 - [infor] - this will never be read, I don't think 3 | 1999-04-29T20:17:34 - [error] - PC LOAD LETTER -------------------------------------------------------------------------------- /2021-04-29 - PowerShell Summit 2021 - Practically Regexing/Demos/SanitizedLog.txt: -------------------------------------------------------------------------------- 1 | SOURCE: { ADPID01, FirstName LastName, email@theposhwolf.com } 2 | DEST : { email@theposhwolf.com, FirstName LastName (FirstName LastName) } 3 | emails[work].value is different: old= "email@theposhwolf.com", new= "email@theposhwolf.com" 4 | userName is different: old= "email@theposhwolf.com", new= "email@theposhwolf.com" 5 | ENT.department is different: old= "", new= "Department of Regexing" 6 | ENT.division is different: old= "", new= "PowerShell Division" 7 | ENT.employeeNumber is different: old= "", new= "empnumber" 8 | ENT.manager is different: old= "email@theposhwolf.com", new= "email@theposhwolf.com" 9 | IAN.physicalDeliveryOfficeName is different: old= "Eugene", new= "Portland" 10 | email@theposhwolf.com: report only, no update 11 | -------------------------------------------------------------------------------- 12 | SOURCE: { ADPID02, FirstName LastName, email@theposhwolf.com } 13 | DEST : { email@theposhwolf.com, FirstName LastName (FirstName LastName) } 14 | title is different: old= "Cool Person", new= "Cooler Person" 15 | ENT.division is different: old= "", new= "PowerShell Division" 16 | ENT.employeeNumber is different: old= "", new= "empnumber" 17 | IAN.physicalDeliveryOfficeName is different: old= "", new= "London" 18 | email@theposhwolf.com: report only, no update 19 | -------------------------------------------------------------------------------- 20 | SOURCE: { ADPID03, FirstName LastName, email@theposhwolf.com } 21 | DEST : { email@theposhwolf.com, FirstName LastName (FirstName LastName) } 22 | name.middleName is different: old= "", new= "Anthony" 23 | ENT.employeeNumber is different: old= "", new= "empnumber" 24 | IAN.physicalDeliveryOfficeName is different: old= "Portland", new= "Eugene" 25 | email@theposhwolf.com: report only, no update 26 | -------------------------------------------------------------------------------- 27 | SOURCE: { ADPID04, FirstName LastName, email@theposhwolf.com } 28 | DEST : { email@theposhwolf.com, FirstName LastName (FirstName LastName) } 29 | ENT.division is different: old= "", new= "North American Division" 30 | ENT.employeeNumber is different: old= "", new= "empnumber" 31 | IAN.physicalDeliveryOfficeName is different: old= "Eugene", new= "Seattle" 32 | email@theposhwolf.com: report only, no update 33 | -------------------------------------------------------------------------------- 34 | SOURCE: { ADPID05, FirstName LastName, email@theposhwolf.com } 35 | DEST : { email@theposhwolf.com, FirstName LastName (FirstName LastName) } 36 | name.middleName is different: old= "", new= "Grace" 37 | ENT.employeeNumber is different: old= "", new= "empnumber" 38 | email@theposhwolf.com: report only, no update 39 | -------------------------------------------------------------------------------- 40 | SOURCE: { ADPID06, FirstName LastName, email@theposhwolf.com } 41 | DEST : { email@theposhwolf.com, FirstName LastName (FirstName LastName) } 42 | ENT.division is different: old= "", new= "North American Division" 43 | ENT.employeeNumber is different: old= "", new= "empnumber" 44 | ENT.manager is different: old= "email@theposhwolf.com", new= "email@theposhwolf.com" 45 | IAN.physicalDeliveryOfficeName is different: old= "Seattle", new= "Eugene" 46 | email@theposhwolf.com: report only, no update 47 | -------------------------------------------------------------------------------- /2021-04-29 - PowerShell Summit 2021 - Practically Regexing/PowerShell Summit 2021 - Practically Regexing.pptx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ThePoShWolf/Sessions/ddaac2096083e091ba8175375560052451555120/2021-04-29 - PowerShell Summit 2021 - Practically Regexing/PowerShell Summit 2021 - Practically Regexing.pptx -------------------------------------------------------------------------------- /2022-01-05 - Research Triangle PS UG - Runway/README.md: -------------------------------------------------------------------------------- 1 | # Research Triangle PowerShell User Group 2022-01-05 2 | 3 | **Thank you for this opportunity to present Runway!** 4 | 5 | ## Disclaimer 6 | 7 | I work for Runway and I am not a salesperson. 8 | 9 | After getting a couple of demos of Runway, I asked if they were hiring because I thought it was so cool. So I ended up joining as a System Engineer, which means that I build actions, make recommendations to the dev team about features, I work with users to make them successful, I write all of the technical documentation, I support the PowerShell SDK, and I do demos for folks that I think would like Runway. So here I am. 10 | 11 | ## Marketing Spiel 12 | 13 | Runway is a new startup. The platform has been developed over the past 2 years led by a developer that has built other platforms from scratch. We haven't officially launched yet as we are still in stealth mode, but that just means that we aren't doing any big time marketing. There are no restrictions around discussing and sharing about Runway. 14 | 15 | We are new to dealing with users of our platform, so we would love to hear any and all feedback from PowerShell folks so that we can make Runway an effective tool for your toolbox. 16 | 17 | Runway has and will always have a free community version that only is limited by endpoint count (no feature limits). Currently the limit is 1000. 18 | 19 | ## Presentation 20 | 21 | Relevant links: 22 | - Runway website: https://runway.host 23 | - Runway Documentation: https://docs.runway.host 24 | - Runway Portal: https://portal.runway.host 25 | - Runway Actions repository: https://github.com/runway-software/actions 26 | - Runway PowerShell SDK: 27 | - Github: https://github.com/runway-software/runway-powershell 28 | - PowerShell Gallery: https://www.powershellgallery.com/packages/PsRunway 29 | 30 | ### What is Runway? 31 | 32 | Runway is a service orchestration and automation platform. At its core, it runs arbitrary code (scripts, executables, etc) on endpoints. 33 | 34 | Marketing description: Low-Code Automation and Connectivity for Hybrid Cloud Networks. 35 | 36 | Runway is: 37 | - An automation platform 38 | - REST API Driven 39 | - Web based Portal, compiled executable, PowerShell SDK 40 | 41 | Runway can: 42 | - Run arbitrary code on endpoints via installed agents (Runners) 43 | - Including PowerShell! Both Windows PowerShell and PowerShell, even if PowerShell isn't installed 44 | - Orchestrate Jobs 45 | - Securely move data between endpoints, no VPN required 46 | - Run asset discovery on networks 47 | - Self deploy in AD environments 48 | 49 | ![Platform Overview](assets/platformoverview.png) 50 | 51 | ## Enrolling a runner 52 | 53 | *Get a token from the Portal.* 54 | 55 | *Show Groups.* 56 | 57 | Or use the SDK: 58 | 59 | ```powershell 60 | # If you haven't already authenticated 61 | $s = Invoke-RwLoginAuthentication -Email -Password -Remember 62 | $env:RunwaySessionToken = $s.Session 63 | 64 | # If you don't already have the utility: 65 | $dls = Get-RwContentPublicDownload 66 | $w64 = $dls | ?{$_.Platform -eq 'Windows64'} 67 | Invoke-RwContentDownloadPublicFile -Id $w64.Id -OutFile .\runway.exe 68 | 69 | # Get a token, associate it to a specific group 70 | # Runway will default to your root group 71 | $group = (Get-RwGroup).Items | ?{$_.Name -eq 'Home'} 72 | $tokenSplat = @{ 73 | Expiration = (Get-Date).AddHours(1) 74 | IsOneTime = $true 75 | GroupId = $group.Id 76 | Type = 'EnrollPersistentRunner' 77 | } 78 | $token = New-RwEnrollmentSession @tokenSplat 79 | 80 | # Use the utility to install the Runner: 81 | .\runway install -t $token.Token 82 | ``` 83 | 84 | ## Running a job 85 | 86 | *Create a Job in the Portal.* 87 | 88 | *Note difference between a 'Job' and an 'Action'.* 89 | 90 | *Run the Job.* 91 | 92 | Here's the process that happens with a single Action job: 93 | 94 | ![Single Action Job Flow](assets/define-job.png) 95 | 96 | ## A note about results 97 | 98 | Results are whatever the Action wants them to be. Technically they are any files that are placed in .\results when the Action executes. 99 | 100 | Runway does not store Action results, they are zipped and cached on the Runner. 101 | 102 | Results can be routed with a Connector. 103 | 104 | ## Manually Retrieving results 105 | 106 | *Manually download results.* 107 | 108 | ![Manual job results download](assets/dl-job-results.png) 109 | 110 | ## Connectors 111 | 112 | Connectors are Actions that run on a dedicated Runner and run once for each Runner assigned the job. 113 | 114 | They are designed to do something with the Job's results. 115 | 116 | *Create Job with download:file connector* 117 | 118 | Here is what happens: 119 | 120 | ![Job with Connector](assets/job-w-connector.png) 121 | 122 | When a Job is assigned to multiple Runners, they each execute independently: 123 | 124 | ![Multiple Runner job](assets/action-chain.png) 125 | 126 | More details about how results are handled in Runway: 127 | 128 | ![Results chain](assets/results-chain.png) 129 | 130 | *Show the downloaded results* 131 | 132 | ## Custom Actions 133 | 134 | *Show the code for inventory:software* 135 | 136 | *Demonstrate the deployment process in the [Actions repository](https://github.com/Runway-Software/actions)* 137 | 138 | ## Custom Connectors 139 | 140 | *Show the code for download:file* 141 | 142 | ## SDK 143 | 144 | To demonstrate the SDK, I'll use my [Sample Scheduled Tasks repository](https://github.com/Runway-Software/sample-scheduled-tasks). 145 | 146 | The idea with this repository is twofold: 147 | 148 | 1. Demonstrate how Runway can be used to replace the Task Scheduler in Windows. 149 | 2. Demonstrate how Jobs and Actions can be stored in Git and synced to Runway using the PowerShell SDK. 150 | 151 | ## Upcoming features 152 | 153 | - MacOS support 154 | - Remote console -------------------------------------------------------------------------------- /2022-01-05 - Research Triangle PS UG - Runway/assets/action-chain.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ThePoShWolf/Sessions/ddaac2096083e091ba8175375560052451555120/2022-01-05 - Research Triangle PS UG - Runway/assets/action-chain.png -------------------------------------------------------------------------------- /2022-01-05 - Research Triangle PS UG - Runway/assets/define-job.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ThePoShWolf/Sessions/ddaac2096083e091ba8175375560052451555120/2022-01-05 - Research Triangle PS UG - Runway/assets/define-job.png -------------------------------------------------------------------------------- /2022-01-05 - Research Triangle PS UG - Runway/assets/dl-job-results.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ThePoShWolf/Sessions/ddaac2096083e091ba8175375560052451555120/2022-01-05 - Research Triangle PS UG - Runway/assets/dl-job-results.png -------------------------------------------------------------------------------- /2022-01-05 - Research Triangle PS UG - Runway/assets/job-w-connector.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ThePoShWolf/Sessions/ddaac2096083e091ba8175375560052451555120/2022-01-05 - Research Triangle PS UG - Runway/assets/job-w-connector.png -------------------------------------------------------------------------------- /2022-01-05 - Research Triangle PS UG - Runway/assets/platformoverview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ThePoShWolf/Sessions/ddaac2096083e091ba8175375560052451555120/2022-01-05 - Research Triangle PS UG - Runway/assets/platformoverview.png -------------------------------------------------------------------------------- /2022-01-05 - Research Triangle PS UG - Runway/assets/results-chain.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ThePoShWolf/Sessions/ddaac2096083e091ba8175375560052451555120/2022-01-05 - Research Triangle PS UG - Runway/assets/results-chain.png -------------------------------------------------------------------------------- /2022-04-12 - Chicago PS UG - Runway/README.md: -------------------------------------------------------------------------------- 1 | # Chicago PowerShell User Group 2022-04-12 2 | 3 | **Thank you for this opportunity to present Runway!** 4 | 5 | ## Disclaimer 6 | 7 | I work for Runway and I am not a salesperson. 8 | 9 | After getting a couple of demos of Runway, I asked if they were hiring because I thought it was so cool. So I ended up joining as a System Engineer, which means that I build actions, make recommendations to the dev team about features, I work with users to make them successful, I write all of the technical documentation, I support the PowerShell SDK, and I do demos for folks that I think would like Runway. So here I am. 10 | 11 | ## Marketing Spiel 12 | 13 | Runway is a new startup. The platform has been developed over the past 2 years led by a developer that has built other platforms from scratch. We haven't officially launched yet as we are still in stealth mode, but that just means that we aren't doing any big time marketing. There are no restrictions around discussing and sharing about Runway. 14 | 15 | We still don't have a lot of users, so we would love to hear any and all feedback from PowerShell folks so that we can make Runway an effective tool for your toolbox. 16 | 17 | Runway has and will always have a free community version that only is limited by endpoint count (no feature limits). Currently the limit is 100. 18 | 19 | ## Presentation 20 | 21 | Relevant links: 22 | - Runway website: https://runway.host 23 | - Runway Documentation: https://docs.runway.host 24 | - Runway Portal: https://portal.runway.host 25 | - Runway Actions repository: https://github.com/runway-software/actions 26 | - Runway PowerShell SDK: 27 | - Github: https://github.com/runway-software/runway-powershell 28 | - PowerShell Gallery: https://www.powershellgallery.com/packages/PsRunway 29 | 30 | ### What is Runway? 31 | 32 | Runway is an automation platform. At its core, it orchestrates arbitrary code (scripts, executables, etc) on endpoints providing a workflow engine, connectivity, and management. 33 | 34 | Marketing description: Automation Fabric for Hybrid Cloud Networks. 35 | 36 | Runway is: 37 | - An automation platform 38 | - REST API Driven 39 | - Web based Portal, compiled executable, PowerShell SDK 40 | 41 | Runway can: 42 | - Run arbitrary code on endpoints via installed agents (Runners) 43 | - Including PowerShell! Both Windows PowerShell and PowerShell, even if PowerShell isn't installed 44 | - Orchestrate Jobs 45 | - Securely move data between endpoints, no VPN required 46 | - Run asset discovery on networks 47 | - Self deploy in AD environments 48 | 49 | ![Platform Overview](assets/platformoverview.png) 50 | 51 | ## Enrolling a runner 52 | 53 | *Get a token from the Portal.* 54 | 55 | *Show Groups.* 56 | 57 | Or use the SDK: 58 | 59 | ```powershell 60 | # If you haven't already authenticated 61 | Connect-Runway -Email -Password 62 | 63 | # If you don't already have the utility: 64 | $dls = Get-RwContentPublicDownload 65 | $w64 = $dls | ?{$_.Platform -eq 'Windows64'} 66 | Invoke-RwDownloadContentPublicFile -Id $w64.Id -OutFile .\runway.exe 67 | 68 | # Get a token, associate it to a specific group 69 | # Runway will default to your root group 70 | $group = (Get-RwGroup).Items | ?{$_.Name -eq 'Customer 1'} 71 | $tokenSplat = @{ 72 | Expiration = (Get-Date).AddHours(1) 73 | IsOneTime = $true 74 | GroupId = $group.Id 75 | Type = 'EnrollPersistentRunner' 76 | } 77 | $token = New-RwEnrollmentSession @tokenSplat 78 | 79 | # Use the utility to install the Runner: 80 | .\runway install -t $token.Token 81 | ``` 82 | 83 | ## Running a job 84 | 85 | *Create a Job in the Portal.* 86 | 87 | *Note difference between a 'Job' and an 'Action'.* 88 | 89 | *Run the Job.* 90 | 91 | ## A note about results 92 | 93 | [Results](https://docs.runway.host/runway-documentation/action-developer-guides/components/results) are whatever the Action wants them to be. Technically they are any files that are placed in `.\results` when the Action executes. 94 | 95 | For example, getting a report of all of the local users: 96 | 97 | ```powershell 98 | Get-LocalUser | Export-Csv .\results\$($env:COMPUTERNAME)-users.csv -NoTypeInformation 99 | ``` 100 | 101 | Runway does not store Action results, they are zipped and cached on the Runner. 102 | 103 | Results can be routed with a Connector. 104 | 105 | ## Manually Retrieving results 106 | 107 | *Manually download results.* 108 | 109 | ![Manual job results download](assets/dl-job-results.png) 110 | 111 | ## Connectors 112 | 113 | Connectors are Actions that run on a Runner specified when the Job is created. 114 | 115 | They are designed to allow moving of data using Runway. 116 | 117 | *Show the three general use cases* 118 | 119 | Here's the high-level diagram: 120 | 121 | ![Connector examples](assets/connector-examples.png) 122 | 123 | Actions run on all assigned Runners and Connectors run only on the selected Connector. 124 | 125 | ## Custom Actions 126 | 127 | Simple Action: 128 | 129 | - Action Name 130 | - `manifest.txt` 131 | - `repository.json` 132 | - windows 133 | - `script.ps1` 134 | 135 | *Show the code for collect:file and deploy:file* 136 | 137 | *Demonstrate the deployment process in the [Actions repository](https://github.com/Runway-Software/actions)* 138 | 139 | ## Custom Connectors 140 | 141 | *Show the code for download:file* 142 | 143 | ## SDK 144 | 145 | ### Job Repo 146 | 147 | To demonstrate the SDK, I'll use my [Sample Scheduled Tasks repository](https://github.com/Runway-Software/sample-scheduled-tasks). 148 | 149 | The idea with this repository is twofold: 150 | 151 | 1. Demonstrate how Runway can be used to replace the Task Scheduler in Windows. 152 | 2. Demonstrate how Jobs and Actions can be stored in Git and synced to Runway using the PowerShell SDK. 153 | 154 | ### Invoke-RwPowerShellCommand 155 | 156 | *Demonstrate Invoke-RwPowerShellCommand* 157 | 158 | ## Upcoming features 159 | 160 | -------------------------------------------------------------------------------- /2022-04-12 - Chicago PS UG - Runway/assets/connector-examples.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ThePoShWolf/Sessions/ddaac2096083e091ba8175375560052451555120/2022-04-12 - Chicago PS UG - Runway/assets/connector-examples.png -------------------------------------------------------------------------------- /2022-04-12 - Chicago PS UG - Runway/assets/dl-job-results.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ThePoShWolf/Sessions/ddaac2096083e091ba8175375560052451555120/2022-04-12 - Chicago PS UG - Runway/assets/dl-job-results.png -------------------------------------------------------------------------------- /2022-04-12 - Chicago PS UG - Runway/assets/platformoverview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ThePoShWolf/Sessions/ddaac2096083e091ba8175375560052451555120/2022-04-12 - Chicago PS UG - Runway/assets/platformoverview.png -------------------------------------------------------------------------------- /2022-04-26 - aC the world! As Code for Anything/00 configs/README.md: -------------------------------------------------------------------------------- 1 | # Example Configurations (no code) 2 | 3 | These configs are designed to use resources (AD and Exchange) that I hope most folks are familiar with. 4 | 5 | ## AD Privileged Group Memberships 6 | 7 | Managing AD groups can be a pain, using a config we could lessen the load. 8 | 9 | ### Simple JSON 10 | 11 | ```json 12 | { 13 | "groups": { 14 | "Domain Admins": [ 15 | "admin.theposhwolf", 16 | "admin.jsnover", 17 | "admin.dabreakglass", 18 | ], 19 | "Schema Admins": [ 20 | "admin.djones", 21 | "admin.sabreakglass" 22 | ] 23 | } 24 | } 25 | ``` 26 | 27 | ### More complex JSON 28 | 29 | ```json 30 | { 31 | "groups": { 32 | "Domain Admins": { 33 | "members": [ 34 | "admin.theposhwolf", 35 | "admin.jsnover", 36 | "admin.dabreakglass", 37 | ], 38 | "location": "DC=com,DC=domain,OU=Privileged Groups", 39 | "restrictMembership": true, 40 | "monitor": true 41 | }, 42 | "Schema Admins": { 43 | "members": [ 44 | "admin.djones", 45 | "admin.sabreakglass" 46 | ], 47 | "location": "DC=com,DC=domain,ou=Privileged Groups", 48 | "restrictMembership": true, 49 | "monitor": true 50 | } 51 | } 52 | } 53 | ``` 54 | 55 | ### Array instead of object 56 | 57 | ```json 58 | { 59 | "groups": [ 60 | { 61 | "name": "Domain Admins", 62 | "members": [ 63 | "admin.theposhwolf", 64 | "admin.jsnover", 65 | "admin.dabreakglass", 66 | ], 67 | "location": "DC=com,DC=domain,OU=Privileged Groups", 68 | "restrictMembership": true, 69 | "monitor": true 70 | }, 71 | { 72 | "name": "Schema Admins", 73 | "members": [ 74 | "admin.djones", 75 | "admin.sabreakglass" 76 | ], 77 | "location": "DC=com,DC=domain,ou=Privileged Groups", 78 | "restrictMembership": true, 79 | "monitor": true 80 | } 81 | ] 82 | } 83 | ``` 84 | 85 | ### PowerShell notation 86 | 87 | ```powershell 88 | @{ 89 | groups = @{ 90 | "Domain Admins" = @{ 91 | members = @( 92 | "admin.theposhwolf", 93 | "admin.jsnover", 94 | "admin.dabreakglass", 95 | ) 96 | location = "DC=com,DC=domain,OU=Privileged Groups" 97 | restrictMembership = $true 98 | monitor = $true 99 | } 100 | "Schema Admins" = @{ 101 | members = @( 102 | "admin.djones", 103 | "admin.sabreakglass" 104 | ) 105 | location = "DC=com,DC=domain,ou=Privileged Groups" 106 | restrictMembership = $true 107 | monitor = $true 108 | } 109 | } 110 | } 111 | ``` 112 | 113 | ## Exchange Resource Mailboxes 114 | 115 | ```json 116 | { 117 | "Resources": { 118 | "ConferenceRoom1": { 119 | "email": "conferenceroom1@domain.com", 120 | "capacity": 20, 121 | "location": "Bldg3-Fl2", 122 | "booking": { 123 | "allowRepeating": true, 124 | "autoAccept": true, 125 | } 126 | } 127 | } 128 | } 129 | ``` 130 | 131 | -------------------------------------------------------------------------------- /2022-04-26 - aC the world! As Code for Anything/01 first example/MfpAccounts/01/Sites.txt: -------------------------------------------------------------------------------- 1 | 123-City-State 2 | 234-City-State -------------------------------------------------------------------------------- /2022-04-26 - aC the world! As Code for Anything/01 first example/MfpAccounts/01/cicd.yaml: -------------------------------------------------------------------------------- 1 | name: CICD 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | workflow_dispatch: 9 | 10 | jobs: 11 | Run: 12 | 13 | runs-on: ubuntu-latest 14 | 15 | steps: 16 | - name: Create variables for module cacher 17 | id: psmodulecache 18 | uses: potatoqualitee/psmodulecache@v3.5 19 | with: 20 | modules-to-cache: Microsoft.Graph.Identity.DirectoryManagement,Microsoft.Graph.Users.Actions,Microsoft.Graph.Groups,Microsoft.Graph.Users 21 | 22 | - name: Run module cacher action 23 | id: cacher 24 | uses: actions/cache@v2 25 | with: 26 | path: ${{ steps.psmodulecache.outputs.modulepath }} 27 | key: ${{ steps.psmodulecache.outputs.keygen }} 28 | 29 | - name: Install PowerShell modules 30 | if: steps.cacher.outputs.cache-hit != 'true' 31 | uses: potatoqualitee/psmodulecache@v3.5 32 | 33 | - uses: actions/checkout@v2 34 | with: 35 | fetch-depth: 0 36 | 37 | - name: Run script on PR 38 | if: ${{ github.event_name == 'pull_request' }} 39 | shell: pwsh 40 | run: .\script.ps1 -JSON_CERT '${{ secrets.JSON_CERT }}' -CERT_SECRET '${{ secrets.CERT_SECRET }}' -AAD_APP_ID '${{ secrets.AAD_APP_ID }}' -AAD_TENANT_ID '${{ secrets.AAD_TENANT_ID }}' -Test 41 | 42 | - name: Run script on push 43 | if: ${{ github.event_name == 'push' }} 44 | shell: pwsh 45 | run: .\script.ps1 -JSON_CERT '${{ secrets.JSON_CERT }}' -CERT_SECRET '${{ secrets.CERT_SECRET }}' -AAD_APP_ID '${{ secrets.AAD_APP_ID }}' -AAD_TENANT_ID '${{ secrets.AAD_TENANT_ID }}' -------------------------------------------------------------------------------- /2022-04-26 - aC the world! As Code for Anything/01 first example/MfpAccounts/01/script.ps1: -------------------------------------------------------------------------------- 1 | param ( 2 | [Parameter(Mandatory)] 3 | [string]$JSON_CERT, 4 | [Parameter(Mandatory)] 5 | [string]$CERT_SECRET, 6 | [Parameter(Mandatory)] 7 | [string]$AAD_APP_ID, 8 | [Parameter(Mandatory)] 9 | [string]$AAD_TENANT_ID, 10 | [switch]$Test 11 | ) 12 | 13 | #region Connect to Graph 14 | $certPath = "$PSScriptRoot\cert.pfx" 15 | [IO.File]::WriteAllBytes($certPath,($JSON_CERT | ConvertFrom-Json | %{[byte]$_})) 16 | 17 | $mgSplat = @{ 18 | Certificate = [System.Security.Cryptography.X509Certificates.X509Certificate2]::new($certPath,(ConvertTo-SecureString $CERT_SECRET -AsPlainText -Force)) 19 | ClientId = $AAD_APP_ID 20 | TenantId = $AAD_TENANT_ID 21 | } 22 | Connect-MgGraph @mgSplat | Out-Null 23 | #endregion 24 | 25 | "Loading sites from the repository..." 26 | 27 | # Load sites 28 | $sites = Get-Content $PSScriptRoot\Sites.txt 29 | 30 | # Initialize variables 31 | $mfaGroupName = 'MFA Exception' 32 | $saGroupName = 'Service Accounts' 33 | $accountDomain = 'domain.com' 34 | $skuPartNum = 'EXCHANGESTANDARD' 35 | 36 | # Import the module so we can work with certain types 37 | Import-Module Microsoft.Graph.Users 38 | 39 | "Retrieving Azure AD license information..." 40 | # Load license ID 41 | $subSku = Get-MgSubscribedSku | Where-Object {$_.SkuPartNumber -eq $skuPartNum} 42 | 43 | "Retrieving MFA group members..." 44 | # Load mfa group members 45 | $mfaGroup = Get-MgGroup -Filter "DisplayName eq '$mfaGroupName'" 46 | $mfaGroupMembers = Get-MgGroupMember -GroupId $mfaGroup.Id -All 47 | 48 | "Retrieving service account group members..." 49 | # Load service account group members 50 | $saGroup = Get-MgGroup -Filter "DisplayName eq '$saGroupName'" 51 | $saGroupMembers = Get-MgGroupMember -GroupId $saGroup.Id -All 52 | 53 | "Retrieving all MFP service accounts..." 54 | # Get all MFP accounts 55 | $existingSiteUsers = Get-MgUser -Filter "startswith(displayName,'svc.mfp')" -Select MailNickname,UserPrincipalName,Mail,UsageLocation,DisplayName,AccountEnabled,PasswordPolicies,AssignedLicenses,Id -All 56 | 57 | "Determining which sites from the repository already exist..." 58 | # Put them in a hashtable 59 | $existingSiteUsersHt = @{} 60 | foreach ($su in $existingSiteUsers) { 61 | $existingSiteUsersHt[$su.UserPrincipalName] = $su 62 | } 63 | 64 | foreach ($site in $sites) { 65 | $site 66 | $accountName = "svc.mfp.$($user.site)" 67 | $upn = "$accountName@$accountDomain" 68 | $mguserSplat = @{ 69 | AssignedLicenses = @(@{SkuId = $subSku.SkuId}) 70 | MailNickname = $accountName 71 | UserPrincipalName = $upn 72 | Mail = $upn 73 | UsageLocation = 'US' 74 | DisplayName = $accountName 75 | AccountEnabled = $true 76 | PasswordPolicies = 'DisablePasswordExpiration' 77 | } 78 | 79 | if ($existingSiteUsersHt.Keys -contains $upn) { 80 | "- Site MFP account already exists, updating" 81 | if (-not $Test.IsPresent) { 82 | Set-MgUser @mguserSplat -UserId $existingSiteUsersHt[$upn].Id 83 | } 84 | } else { 85 | "- Creating site account" 86 | if (-not $Test.IsPresent) { 87 | New-MgUser @mguserSplat 88 | } 89 | } 90 | 91 | $mgUser = Get-MgUser -UserId $upn 92 | 93 | if ($mfaGroupMembers.Id -notcontains $mgUser.Id) { 94 | "- User not a member of MFA Exception Group" 95 | if (-not $Test.IsPresent) { 96 | New-MgGroupMember -GroupId $mfaGroup.Id -DirectoryObjectId $mgUser.Id 97 | } 98 | } 99 | if ($saGroupMembers.Id -notcontains $mgUser.Id) { 100 | "- User not a member of Service Account Group" 101 | if (-not $Test.IsPresent) { 102 | New-MgGroupMember -GroupId $saGroup.Id -DirectoryObjectId $mgUser.Id 103 | } 104 | } 105 | } -------------------------------------------------------------------------------- /2022-04-26 - aC the world! As Code for Anything/01 first example/MfpAccounts/02/Sites.txt: -------------------------------------------------------------------------------- 1 | 123-City-State 2 | 234-City-State -------------------------------------------------------------------------------- /2022-04-26 - aC the world! As Code for Anything/01 first example/MfpAccounts/02/cicd.yaml: -------------------------------------------------------------------------------- 1 | name: CICD 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | workflow_dispatch: 9 | 10 | jobs: 11 | Run: 12 | 13 | runs-on: ubuntu-latest 14 | 15 | steps: 16 | - name: Create variables for module cacher 17 | id: psmodulecache 18 | uses: potatoqualitee/psmodulecache@v3.5 19 | with: 20 | modules-to-cache: Microsoft.Graph.Identity.DirectoryManagement,Microsoft.Graph.Users.Actions,Microsoft.Graph.Groups,Microsoft.Graph.Users 21 | 22 | - name: Run module cacher action 23 | id: cacher 24 | uses: actions/cache@v2 25 | with: 26 | path: ${{ steps.psmodulecache.outputs.modulepath }} 27 | key: ${{ steps.psmodulecache.outputs.keygen }} 28 | 29 | - name: Install PowerShell modules 30 | if: steps.cacher.outputs.cache-hit != 'true' 31 | uses: potatoqualitee/psmodulecache@v3.5 32 | 33 | - uses: actions/checkout@v2 34 | with: 35 | fetch-depth: 0 36 | 37 | - name: Run script on PR 38 | if: ${{ github.event_name == 'pull_request' }} 39 | shell: pwsh 40 | run: .\script.ps1 -JSON_CERT '${{ secrets.JSON_CERT }}' -CERT_SECRET '${{ secrets.CERT_SECRET }}' -AAD_APP_ID '${{ secrets.AAD_APP_ID }}' -AAD_TENANT_ID '${{ secrets.AAD_TENANT_ID }}' -Test 41 | 42 | - name: Run script on push 43 | if: ${{ github.event_name == 'push' }} 44 | shell: pwsh 45 | run: .\script.ps1 -JSON_CERT '${{ secrets.JSON_CERT }}' -CERT_SECRET '${{ secrets.CERT_SECRET }}' -AAD_APP_ID '${{ secrets.AAD_APP_ID }}' -AAD_TENANT_ID '${{ secrets.AAD_TENANT_ID }}' -------------------------------------------------------------------------------- /2022-04-26 - aC the world! As Code for Anything/01 first example/MfpAccounts/02/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "prefix": "svc.mfp", 3 | "aadLicenseSkuPartNumber": "EXCHANGESTANDARD", 4 | "groups": [ "MFA Exception", "Service Accounts" ], 5 | "accountDomain": "domain.com" 6 | } -------------------------------------------------------------------------------- /2022-04-26 - aC the world! As Code for Anything/01 first example/MfpAccounts/02/script.ps1: -------------------------------------------------------------------------------- 1 | param ( 2 | [Parameter(Mandatory)] 3 | [string]$JSON_CERT, 4 | [Parameter(Mandatory)] 5 | [string]$CERT_SECRET, 6 | [Parameter(Mandatory)] 7 | [string]$AAD_APP_ID, 8 | [Parameter(Mandatory)] 9 | [string]$AAD_TENANT_ID, 10 | [switch]$Test 11 | ) 12 | 13 | #region Connect to Graph 14 | $certPath = "$PSScriptRoot\cert.pfx" 15 | [IO.File]::WriteAllBytes($certPath,($JSON_CERT | ConvertFrom-Json | %{[byte]$_})) 16 | 17 | $mgSplat = @{ 18 | Certificate = [System.Security.Cryptography.X509Certificates.X509Certificate2]::new($certPath,(ConvertTo-SecureString $CERT_SECRET -AsPlainText -Force)) 19 | ClientId = $AAD_APP_ID 20 | TenantId = $AAD_TENANT_ID 21 | } 22 | Connect-MgGraph @mgSplat | Out-Null 23 | #endregion 24 | 25 | "Loading sites from the repository..." 26 | 27 | # Load sites 28 | $sites = Get-Content $PSScriptRoot\Sites.txt 29 | 30 | # Load Config 31 | $config = Get-Content $PSScriptRoot\config.json | ConvertFrom-Json -AsHashtable 32 | 33 | # Import the module so we can work with certain types 34 | Import-Module Microsoft.Graph.Users 35 | 36 | "Retrieving Azure AD license information..." 37 | # Load license ID 38 | $subSku = Get-MgSubscribedSku | Where-Object {$_.SkuPartNumber -eq $config['aadLicenseSkuPartNumber']} 39 | 40 | # Load groups 41 | $groups = @{} 42 | foreach ($group in $config['groups']) { 43 | "Retrieving members of '$group'" 44 | $groups[$group] = Get-MgGroup -Filter "DisplayName eq '$group'" 45 | $groups["members_$group"] = Get-MgGroupMember -GroupId $tmpGroup.Id -All 46 | } 47 | 48 | "Retrieving all MFP service accounts..." 49 | # Get all MFP accounts 50 | $existingSiteUsers = Get-MgUser -Filter "startswith(displayName,'$($config['prefix'])')" -Select MailNickname,UserPrincipalName,Mail,UsageLocation,DisplayName,AccountEnabled,PasswordPolicies,AssignedLicenses,Id -All 51 | 52 | "Determining which sites from the repository already exist..." 53 | # Put them in a hashtable 54 | $existingSiteUsersHt = @{} 55 | foreach ($su in $existingSiteUsers) { 56 | $existingSiteUsersHt[$su.UserPrincipalName] = $su 57 | } 58 | 59 | foreach ($site in $sites) { 60 | $site 61 | $accountName = "$($config['prefix']).$($user.site)" 62 | $upn = "$accountName@$($config['accountDomain'])" 63 | $mguserSplat = @{ 64 | AssignedLicenses = @(@{SkuId = $subSku.SkuId}) 65 | MailNickname = $accountName 66 | UserPrincipalName = $upn 67 | Mail = $upn 68 | UsageLocation = 'US' 69 | DisplayName = $accountName 70 | AccountEnabled = $true 71 | PasswordPolicies = 'DisablePasswordExpiration' 72 | } 73 | 74 | if ($existingSiteUsersHt.Keys -contains $upn) { 75 | "- Site MFP account already exists, updating" 76 | if (-not $Test.IsPresent) { 77 | Set-MgUser @mguserSplat 78 | } 79 | } else { 80 | "- Creating site account" 81 | if (-not $Test.IsPresent) { 82 | New-MgUser @mguserSplat 83 | } 84 | } 85 | 86 | $mgUser = Get-MgUser -UserId $upn 87 | 88 | foreach ($group in $config['groups']) { 89 | if ($groups["members_$group"].Id -notcontains $mgUser.Id) { 90 | "- User not a member of '$group'" 91 | if (-not $Test.IsPresent) { 92 | New-MgGroupMember -GroupId $groups[$group].Id -DirectoryObjectId $mguser.Id 93 | } 94 | } 95 | } 96 | } -------------------------------------------------------------------------------- /2022-04-26 - aC the world! As Code for Anything/01 first example/README.md: -------------------------------------------------------------------------------- 1 | # The Service Account Creator 2 | 3 | Security wants 1 MFP service account per site. 4 | 5 | ## Before aCing 6 | 7 | Someone wrote a detailed SOP on creating each account. 8 | 9 | ## After aCing 10 | 11 | I wrote a simple SOP on branching the repo and submitting a PR. -------------------------------------------------------------------------------- /2022-04-26 - aC the world! As Code for Anything/02 second example/DDLs/cicd.yaml: -------------------------------------------------------------------------------- 1 | name: CICD 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | workflow_dispatch: 10 | 11 | jobs: 12 | Sync: 13 | 14 | runs-on: windows-latest 15 | 16 | steps: 17 | - name: Create variables for module cacher 18 | id: psmodulecache 19 | uses: potatoqualitee/psmodulecache@v3.5 20 | with: 21 | modules-to-cache: ExchangeOnlineManagement 22 | 23 | - name: Run module cacher action 24 | id: cacher 25 | uses: actions/cache@v2 26 | with: 27 | path: ${{ steps.psmodulecache.outputs.modulepath }} 28 | key: ${{ steps.psmodulecache.outputs.keygen }} 29 | 30 | - name: Install PowerShell modules 31 | if: steps.cacher.outputs.cache-hit != 'true' 32 | uses: potatoqualitee/psmodulecache@v3.5 33 | 34 | - uses: actions/checkout@v2 35 | with: 36 | fetch-depth: 0 37 | 38 | - name: Run script 39 | if: ${{ github.event_name == 'pull_request' }} 40 | shell: pwsh 41 | run: .\script.ps1 -JSON_CERT '${{ secrets.JSON_CERT }}' -CERT_SECRET '${{ secrets.CERT_SECRET }}' -AAD_APP_ID '${{ secrets.AAD_APP_ID }}' -Test 42 | 43 | - name: Run script 44 | if: ${{ github.event_name == 'push' }} 45 | shell: pwsh 46 | run: .\script.ps1 -JSON_CERT '${{ secrets.JSON_CERT }}' -CERT_SECRET '${{ secrets.CERT_SECRET }}' -AAD_APP_ID '${{ secrets.AAD_APP_ID }}' -------------------------------------------------------------------------------- /2022-04-26 - aC the world! As Code for Anything/02 second example/DDLs/definitions/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "prefix": "sync", 3 | "regions": [ 4 | "Seattle", 5 | "Portland" 6 | ] 7 | } -------------------------------------------------------------------------------- /2022-04-26 - aC the world! As Code for Anything/02 second example/DDLs/definitions/regionbased.json: -------------------------------------------------------------------------------- 1 | { 2 | "All": { 3 | "Region": "all", 4 | "Name": "$Region-all", 5 | "Filter": "(CustomAttribute1 -eq '$Region')" 6 | }, 7 | "Provider": { 8 | "Region": "all", 9 | "Name":"$Region-Provider", 10 | "Filter": "((CustomAttribute1 -eq '$Region') -and (CustomAttribute2 -eq 'Provider'))" 11 | }, 12 | "Staff": { 13 | "Region": "all", 14 | "Name": "$Region-Staff", 15 | "Filter": "((CustomAttribute1 -eq '$Region') -and (CustomAttribute2 -eq 'Staff'))" 16 | }, 17 | "MAs": { 18 | "Region": [ "Seattle" ], 19 | "Name": "$Region-MA", 20 | "Filter": "((CustomAttribute1 -eq '$Region') -and (CustomAttribute2 -eq 'Staff')) -and ((Title -eq 'Clinical Support - MA') -or (Title -eq 'Practice Manager') -or (Title -eq 'Clinical Lead - MA'))" 21 | } 22 | } -------------------------------------------------------------------------------- /2022-04-26 - aC the world! As Code for Anything/02 second example/DDLs/script.ps1: -------------------------------------------------------------------------------- 1 | param ( 2 | [Parameter(Mandatory)] 3 | [string]$JSON_CERT, 4 | [Parameter(Mandatory)] 5 | [string]$CERT_SECRET, 6 | [Parameter(Mandatory)] 7 | [string]$AAD_APP_ID, 8 | [switch]$Test 9 | ) 10 | 11 | $certPath = "$PSScriptRoot\cert.pfx" 12 | [IO.File]::WriteAllBytes($certPath,($JSON_CERT | ConvertFrom-Json | %{[byte]$_})) 13 | 14 | $exoSplat = @{ 15 | CertificateFilePath = $certPath 16 | CertificatePassword = (ConvertTo-SecureString $CERT_SECRET -AsPlainText -Force) 17 | AppId = $AAD_APP_ID 18 | Organization = 'domain.onmicrosoft.com' 19 | } 20 | Connect-ExchangeOnline @exoSplat 21 | 22 | $config = Get-Content $PSScriptRoot\definitions\config.json | ConvertFrom-Json -AsHashtable 23 | $regionBased = Get-Content $PSScriptRoot\definitions\Regionbased.json | ConvertFrom-Json -AsHashtable 24 | 25 | $ddgs = Get-DynamicDistributionGroup 26 | $ddgHt = @{} 27 | foreach ($ddg in $ddgs) { 28 | $ddgHt[$ddg.name] = $ddg 29 | } 30 | 31 | foreach ($region in $config['Regions']) { 32 | $region 33 | 34 | foreach ($ddl in $regionBased.Keys | sort) { 35 | if ($regionBased[$ddl]['Regions'] -contains $region -or $regionBased[$ddl]['Regions'] -eq 'all') { 36 | $name = "$($config['prefix'])-" + ($regionBased[$ddl]['Name'] -replace ('\$Region',$region -replace ' ','')) 37 | "- $name" 38 | $filter = $regionBased[$ddl]['Filter'] -replace '\$Region',$region 39 | if ($ddgs.Name -contains $name) { 40 | "-- Update: $filter" 41 | if (-not $Test.IsPresent) { 42 | Set-DynamicDistributionGroup $name -RecipientFilter $filter -PrimarySmtpAddress "$name`@domain.com" 43 | } 44 | } else { 45 | "-- Create: $filter" 46 | if (-not $Test.IsPresent) { 47 | New-DynamicDistributionGroup $name -RecipientFilter $filter -PrimarySmtpAddress "$name`@domain.com" 48 | } 49 | } 50 | } 51 | } 52 | } -------------------------------------------------------------------------------- /2022-04-26 - aC the world! As Code for Anything/02 second example/README.md: -------------------------------------------------------------------------------- 1 | # Dynamically generated Dynamic Distribution Lists 2 | 3 | Company has 20+ regions, each of which need their own dynamic distribution lists based on region. The company is growing and the filters will occasionally change. 4 | 5 | ## Before aCing 6 | 7 | This was not reasonably possible without unnecessary finger grease. 8 | 9 | ## After aCing 10 | 11 | Adding in additional DDLs per region or tweaking existing DDLs requires a simple config update and a PR. -------------------------------------------------------------------------------- /2022-04-26 - aC the world! As Code for Anything/PowerShell Summit 2022 - aC the World!.pptx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ThePoShWolf/Sessions/ddaac2096083e091ba8175375560052451555120/2022-04-26 - aC the world! As Code for Anything/PowerShell Summit 2022 - aC the World!.pptx -------------------------------------------------------------------------------- /2022-04-26 - aC the world! As Code for Anything/README.md: -------------------------------------------------------------------------------- 1 | # Additional links 2 | 3 | - **Recording**: https://www.youtube.com/watch?v=s9Ty_WvDmZA 4 | 5 | - YAML example: https://github.com/Runway-Software/runway-powershell-yaml 6 | - Auth to Graph or Exchange in Github Workflow: https://theposhwolf.com/howtos/exchange-github-powershell/ -------------------------------------------------------------------------------- /2022-04-28 - AutoRest The Module Generator/01 setup/README.md: -------------------------------------------------------------------------------- 1 | # AutoRest Setup 2 | 3 | - node.js (12.19.x LTS preferred): https://github.com/coreybutler/nvm-windows 4 | - AutoRest: https://github.com/azure/autorest 5 | - `npm install -g autorest` 6 | - Dotnet SDK 2+ 7 | - `npm install -g dotnet-sdk-2.2` 8 | - AutoRest.PowerShell: https://github.com/azure/autorest.powershell 9 | - Installed by autorest 10 | - PowerShell 11 | - `npm install -g pwsh` -------------------------------------------------------------------------------- /2022-04-28 - AutoRest The Module Generator/02 sample/README.md: -------------------------------------------------------------------------------- 1 | # Basic REST call 2 | 3 | ## Parameters in Body 4 | 5 | Raw: 6 | 7 | ```plaintext 8 | POST https://portal.runway.host/api/v2/auth/login HTTP/1.1 9 | Host: portal.runway.host 10 | Authorization: Session 11 | Content-Type: application/json 12 | Content-Length: X 13 | 14 | { 15 | "email": "anthony@runway.host", 16 | "password": "", 17 | "remember": true 18 | } 19 | ``` 20 | 21 | PowerShell: 22 | 23 | ```powershell 24 | $splat = @{ 25 | Uri = 'https://portal.runway.host/api/v2/auth/login' 26 | Headers = @{ 27 | Authorization = 'Session ' 28 | 'Content-Type' = 'application/json' 29 | } 30 | Body = @{ 31 | email = "anthony@runway.host" 32 | password = "" 33 | remember = $true 34 | } | ConvertTo-Json 35 | } 36 | Invoke-RestMethod @splat 37 | ``` 38 | 39 | ## Parameters in Query 40 | 41 | Raw: 42 | 43 | ```plaintext 44 | POST https://portal.runway.host/api/v2/auth/login?email=anthony@runway.host&password=&remember=true HTTP/1.1 45 | Host: portal.runway.host 46 | Authorization: Session 47 | Content-Type: application/json 48 | Content-Length: X 49 | ``` 50 | 51 | PowerShell 52 | 53 | ```powershell 54 | $qParams = @( 55 | "email=anthony@runway.host" 56 | "password=" 57 | "remember=true" 58 | ) -join '&' 59 | $splat = @{ 60 | Uri = "https://portal.runway.host/api/v2/auth/login?$qParams" 61 | Headers = @{ 62 | Authorization = 'Session ' 63 | 'Content-Type' = 'application/json' 64 | } 65 | } 66 | Invoke-RestMethod @splat 67 | ``` 68 | 69 | ## Parameters in Path 70 | 71 | Raw: 72 | 73 | ```plaintext 74 | GET https://portal.runway.host/api/v2/accounts/{accountId} HTTP/1.1 75 | Host: portal.runway.host 76 | Authorization: Session 77 | Content-Type: application/json 78 | Content-Length: X 79 | ``` 80 | 81 | PowerShell: 82 | 83 | ```powershell 84 | $accountId = '' 85 | $splat = @{ 86 | Uri = "https://portal.runway.host/api/v2/accounts/$accountId" 87 | Headers = @{ 88 | Authorization = 'Session ' 89 | 'Content-Type' = 'application/json' 90 | } 91 | } 92 | Invoke-RestMethod @splat 93 | ``` -------------------------------------------------------------------------------- /2022-04-28 - AutoRest The Module Generator/03 build/README.md: -------------------------------------------------------------------------------- 1 | # Build 2 | 3 | ```powershell 4 | autorest README.md 5 | ``` -------------------------------------------------------------------------------- /2022-04-28 - AutoRest The Module Generator/03 build/src/README.md: -------------------------------------------------------------------------------- 1 | ### AutoRest Configuration 2 | 3 | ``` yaml 4 | use: "@autorest/powershell@3.0.471" 5 | input-file: ../../02 sample/swagger.json 6 | azure: false 7 | powershell: true 8 | output-folder: ./ 9 | clear-output-folder: true 10 | namespace: RunwaySdk.PowerShell 11 | title: Runway 12 | prefix: Rw 13 | module-version: 0.0.1 14 | metadata: 15 | authors: ThePoShWolf 16 | owners: Runway Software 17 | companyName: Runway Software 18 | description: "The PowerShell SDK for the Runway API" 19 | copyright: © Runway Software. All rights reserved. 20 | tags: Runway PowerShell 21 | requireLicenseAcceptance: false 22 | projectUri: https://github.com/runway-software/runway-powershell 23 | licenseUri: https://github.com/Runway-Software/runway-powershell/blob/main/license.txt 24 | ``` 25 | -------------------------------------------------------------------------------- /2022-04-28 - AutoRest The Module Generator/04 auth/README.md: -------------------------------------------------------------------------------- 1 | # Customize 2 | 3 | ```powershell 4 | autorest README.md 5 | ``` 6 | 7 | ## Results 8 | 9 | ```powershell 10 | $session = Invoke-RwLoginAuthentication -Email '' -Password '' 11 | $env:RunwaySessionToken = $session.Session 12 | ``` -------------------------------------------------------------------------------- /2022-04-28 - AutoRest The Module Generator/04 auth/src/README.md: -------------------------------------------------------------------------------- 1 | ### AutoRest Configuration 2 | 3 | ``` yaml 4 | use: "@autorest/powershell@3.0.471" 5 | input-file: ../../02 sample/swagger.json 6 | azure: false 7 | powershell: true 8 | output-folder: ./ 9 | clear-output-folder: true 10 | namespace: RunwaySdk.PowerShell 11 | title: Runway 12 | prefix: Rw 13 | module-version: 0.0.1 14 | metadata: 15 | authors: ThePoShWolf 16 | owners: Runway Software 17 | companyName: Runway Software 18 | description: "The PowerShell SDK for the Runway API" 19 | copyright: © Runway Software. All rights reserved. 20 | tags: Runway PowerShell 21 | requireLicenseAcceptance: false 22 | projectUri: https://github.com/runway-software/runway-powershell 23 | licenseUri: https://github.com/Runway-Software/runway-powershell/blob/main/license.txt 24 | ``` -------------------------------------------------------------------------------- /2022-04-28 - AutoRest The Module Generator/04 auth/src/custom/Module.cs: -------------------------------------------------------------------------------- 1 | namespace RunwaySdk.PowerShell 2 | { 3 | using Runtime; 4 | using System.Collections.Generic; 5 | using System.Net.Http; 6 | using System.Threading; 7 | using System.Threading.Tasks; 8 | 9 | 10 | /// A class that contains the module-common code and data. 11 | /// 12 | /// This class is where you can add things to modify the module. 13 | /// As long as it's in the 'private/custom' folder, it won't get deleted 14 | /// when you use --clear-output-folder in autorest. 15 | /// 16 | public partial class Module 17 | { 18 | partial void CustomInit() 19 | { 20 | // we need to add a step at the end of the pipeline 21 | // to attach the API key 22 | 23 | // once for the regular pipeline 24 | this._pipeline.Append(AddSessionToken); 25 | 26 | // once for the pipeline that supports a proxy 27 | this._pipelineWithProxy.Append(AddSessionToken); 28 | } 29 | 30 | protected async Task AddSessionToken(HttpRequestMessage request, IEventListener callback, ISendAsync next) 31 | { 32 | // does the request already have an authorization header? remove it 33 | if (request.Headers.Contains("Authorization")) request.Headers.Remove("Authorization"); 34 | 35 | // add in the auth header 36 | request.Headers.Add("Authorization", "Session " + System.Environment.GetEnvironmentVariable("RunwaySessionToken")); 37 | 38 | // let it go on. 39 | return await next.SendAsync(request, callback); 40 | } 41 | } 42 | } -------------------------------------------------------------------------------- /2022-04-28 - AutoRest The Module Generator/05 adding cmdlets/README.md: -------------------------------------------------------------------------------- 1 | # Adding cmdlets 2 | 3 | ```powershell 4 | autorest README.md 5 | ``` 6 | 7 | ## Results 8 | 9 | ```powershell 10 | Get-Command -Module Runway 11 | 12 | Connect-Runway -Email 'anthony@runway.host' 13 | 14 | Get-RwConnectionByName -Name 'File Server' 15 | ``` -------------------------------------------------------------------------------- /2022-04-28 - AutoRest The Module Generator/05 adding cmdlets/src/README.md: -------------------------------------------------------------------------------- 1 | ### AutoRest Configuration 2 | 3 | ``` yaml 4 | use: "@autorest/powershell@3.0.471" 5 | input-file: ../../02 sample/swagger.json 6 | azure: false 7 | powershell: true 8 | output-folder: ./ 9 | clear-output-folder: true 10 | namespace: RunwaySdk.PowerShell 11 | title: Runway 12 | prefix: Rw 13 | module-version: 0.0.1 14 | metadata: 15 | authors: ThePoShWolf 16 | owners: Runway Software 17 | companyName: Runway Software 18 | description: "The PowerShell SDK for the Runway API" 19 | copyright: © Runway Software. All rights reserved. 20 | tags: Runway PowerShell 21 | requireLicenseAcceptance: false 22 | projectUri: https://github.com/runway-software/runway-powershell 23 | licenseUri: https://github.com/Runway-Software/runway-powershell/blob/main/license.txt 24 | ``` -------------------------------------------------------------------------------- /2022-04-28 - AutoRest The Module Generator/05 adding cmdlets/src/custom/Connect-Runway.ps1: -------------------------------------------------------------------------------- 1 | Function Connect-Runway { 2 | [cmdletbinding()] 3 | [OutputType()] 4 | param ( 5 | [Parameter( 6 | Position = 0, 7 | Mandatory 8 | )] 9 | [string]$Email, 10 | [Parameter(Mandatory)] 11 | [SecureString]$Password 12 | ) 13 | $s = Invoke-RwLoginAuthentication -Email $Email -Password ([pscredential]::new('blah',$Password).GetNetworkCredential().Password) -Remember 14 | $env:RunwaySessionToken = $s.Session 15 | } -------------------------------------------------------------------------------- /2022-04-28 - AutoRest The Module Generator/05 adding cmdlets/src/custom/Get-RwConnectionByName.ps1: -------------------------------------------------------------------------------- 1 | Function Get-RwConnectionByName { 2 | [CmdletBinding( 3 | DefaultParameterSetName = 'ByName' 4 | )] 5 | param ( 6 | [Parameter( 7 | ParameterSetName = 'ByName', 8 | Position = 0 9 | )] 10 | [Alias('Name')] 11 | [string[]]$ConnectionName 12 | ) 13 | if ($ConnectionName.Count -gt 1) { 14 | $filterChildren = foreach ($name in $ConnectionName) { 15 | @{ 16 | Left = 'Name' 17 | Operator = '=' 18 | Right = $name 19 | } 20 | } 21 | $query = @{ 22 | includeSubgroups = $true 23 | skip = 0 24 | take = 100 25 | sortDirection = 0 26 | filter = @{ 27 | children = $filterChildren 28 | operator = 'OR' 29 | } 30 | } 31 | } else { 32 | $query = @{ 33 | includeSubgroups = $true 34 | skip = 0 35 | take = 100 36 | SortDirection = 0 37 | filter = @{ 38 | Left = 'Name' 39 | Operator = '=' 40 | Right = $ConnectionName[0] 41 | } 42 | } 43 | } 44 | (Invoke-RwQueryConnection -Query $query).Items 45 | } -------------------------------------------------------------------------------- /2022-04-28 - AutoRest The Module Generator/05 adding cmdlets/src/custom/Module.cs: -------------------------------------------------------------------------------- 1 | namespace RunwaySdk.PowerShell 2 | { 3 | using Runtime; 4 | using System.Collections.Generic; 5 | using System.Net.Http; 6 | using System.Threading; 7 | using System.Threading.Tasks; 8 | 9 | 10 | /// A class that contains the module-common code and data. 11 | /// 12 | /// This class is where you can add things to modify the module. 13 | /// As long as it's in the 'private/custom' folder, it won't get deleted 14 | /// when you use --clear-output-folder in autorest. 15 | /// 16 | public partial class Module 17 | { 18 | partial void CustomInit() 19 | { 20 | // we need to add a step at the end of the pipeline 21 | // to attach the API key 22 | 23 | // once for the regular pipeline 24 | this._pipeline.Append(AddSessionToken); 25 | 26 | // once for the pipeline that supports a proxy 27 | this._pipelineWithProxy.Append(AddSessionToken); 28 | } 29 | 30 | protected async Task AddSessionToken(HttpRequestMessage request, IEventListener callback, ISendAsync next) 31 | { 32 | // does the request already have an authorization header? remove it 33 | if (request.Headers.Contains("Authorization")) request.Headers.Remove("Authorization"); 34 | 35 | // add in the auth header 36 | request.Headers.Add("Authorization", "Session " + System.Environment.GetEnvironmentVariable("RunwaySessionToken")); 37 | 38 | // let it go on. 39 | return await next.SendAsync(request, callback); 40 | } 41 | } 42 | } -------------------------------------------------------------------------------- /2022-04-28 - AutoRest The Module Generator/06 directives/README.md: -------------------------------------------------------------------------------- 1 | # Directives 2 | 3 | ```powershell 4 | autorest README.md 5 | ``` 6 | 7 | ## Results 8 | 9 | ```powershell 10 | Get-Command -Module Runway 11 | ``` -------------------------------------------------------------------------------- /2022-04-28 - AutoRest The Module Generator/06 directives/src/README.md: -------------------------------------------------------------------------------- 1 | ### AutoRest Configuration 2 | 3 | ``` yaml 4 | use: "@autorest/powershell@3.0.471" 5 | input-file: ../../02 sample/swagger.json 6 | azure: false 7 | powershell: true 8 | output-folder: ./ 9 | clear-output-folder: true 10 | namespace: RunwaySdk.PowerShell 11 | title: Runway 12 | prefix: Rw 13 | module-version: 0.0.1 14 | metadata: 15 | authors: ThePoShWolf 16 | owners: Runway Software 17 | companyName: Runway Software 18 | description: "The PowerShell SDK for the Runway API" 19 | copyright: © Runway Software. All rights reserved. 20 | tags: Runway PowerShell 21 | requireLicenseAcceptance: false 22 | projectUri: https://github.com/runway-software/runway-powershell 23 | licenseUri: https://github.com/Runway-Software/runway-powershell/blob/main/license.txt 24 | ``` 25 | 26 | ### Directives 27 | 28 | ``` yaml 29 | directive: 30 | # They becomes Import-* due to how AutoRest correlates the *Load OperationId to a verb 31 | # https://github.com/Azure/autorest.powershell/blob/main/powershell/internal/name-inferrer.ts 32 | - where: 33 | verb: Import 34 | set: 35 | verb: Get 36 | # Convert invoke-counts to get-counts 37 | # i.e.: Invoke-RwCountRunner becomes Get-RwRunnerCount 38 | - where: 39 | verb: Invoke 40 | subject: Count([a-zA-Z]+)$ 41 | set: 42 | verb: Get 43 | subject: $1Count 44 | # Set the url to pull from the RunwayDomain environment variable 45 | # This makes it so we can configure the domain in the event that 46 | # we need to talk to staging or when Runway is customer hosted. 47 | - from: source-file-csharp 48 | where: $ 49 | transform: > 50 | if ($documentPath.match(/Runway.cs/gm)) { 51 | // line to match: 52 | // var _url = new global::System.Uri($"https://portal.runway.host{pathAndQuery}"); 53 | // replace portal.runway.host with environmental variable 54 | let urlRegex = /var _url = [^\r\n;]+portal\.runway\.host[^\r\n;]+;/gmi 55 | $ = $.replace(urlRegex,'var _url = new global::System.Uri($"https://{System.Environment.GetEnvironmentVariable("RunwayDomain")}{pathAndQuery}");'); 56 | 57 | return $; 58 | } else { 59 | return $; 60 | } 61 | ``` 62 | -------------------------------------------------------------------------------- /2022-04-28 - AutoRest The Module Generator/06 directives/src/custom/Connect-Runway.ps1: -------------------------------------------------------------------------------- 1 | Function Connect-Runway { 2 | [cmdletbinding()] 3 | [OutputType()] 4 | param ( 5 | [Parameter( 6 | Position = 0, 7 | Mandatory 8 | )] 9 | [string]$Email, 10 | [Parameter(Mandatory)] 11 | [SecureString]$Password, 12 | [string]$RunwayDomain = 'portal.runway.host' 13 | ) 14 | $env:RunwayDomain = $RunwayDomain 15 | 16 | $s = Invoke-RwLoginAuthentication -Email $Email -Password ([pscredential]::new('blah',$Password).GetNetworkCredential().Password) -Remember 17 | $env:RunwaySessionToken = $s.Session 18 | } -------------------------------------------------------------------------------- /2022-04-28 - AutoRest The Module Generator/06 directives/src/custom/Get-RwConnectionByName.ps1: -------------------------------------------------------------------------------- 1 | Function Get-RwConnectionByName { 2 | [CmdletBinding( 3 | DefaultParameterSetName = 'ByName' 4 | )] 5 | param ( 6 | [Parameter( 7 | ParameterSetName = 'ByName', 8 | Position = 0 9 | )] 10 | [Alias('Name')] 11 | [string[]]$ConnectionName 12 | ) 13 | if ($ConnectionName.Count -gt 1) { 14 | $filterChildren = foreach ($name in $ConnectionName) { 15 | @{ 16 | Left = 'Name' 17 | Operator = '=' 18 | Right = $name 19 | } 20 | } 21 | $query = @{ 22 | includeSubgroups = $true 23 | skip = 0 24 | take = 100 25 | sortDirection = 0 26 | filter = @{ 27 | children = $filterChildren 28 | operator = 'OR' 29 | } 30 | } 31 | } else { 32 | $query = @{ 33 | includeSubgroups = $true 34 | skip = 0 35 | take = 100 36 | SortDirection = 0 37 | filter = @{ 38 | Left = 'Name' 39 | Operator = '=' 40 | Right = $ConnectionName[0] 41 | } 42 | } 43 | } 44 | (Invoke-RwQueryConnection -Query $query).Items 45 | } -------------------------------------------------------------------------------- /2022-04-28 - AutoRest The Module Generator/06 directives/src/custom/Module.cs: -------------------------------------------------------------------------------- 1 | namespace RunwaySdk.PowerShell 2 | { 3 | using Runtime; 4 | using System.Collections.Generic; 5 | using System.Net.Http; 6 | using System.Threading; 7 | using System.Threading.Tasks; 8 | 9 | 10 | /// A class that contains the module-common code and data. 11 | /// 12 | /// This class is where you can add things to modify the module. 13 | /// As long as it's in the 'private/custom' folder, it won't get deleted 14 | /// when you use --clear-output-folder in autorest. 15 | /// 16 | public partial class Module 17 | { 18 | partial void CustomInit() 19 | { 20 | // we need to add a step at the end of the pipeline 21 | // to attach the API key 22 | 23 | // once for the regular pipeline 24 | this._pipeline.Append(AddSessionToken); 25 | 26 | // once for the pipeline that supports a proxy 27 | this._pipelineWithProxy.Append(AddSessionToken); 28 | } 29 | 30 | protected async Task AddSessionToken(HttpRequestMessage request, IEventListener callback, ISendAsync next) 31 | { 32 | // does the request already have an authorization header? remove it 33 | if (request.Headers.Contains("Authorization")) request.Headers.Remove("Authorization"); 34 | 35 | // add in the auth header 36 | request.Headers.Add("Authorization", "Session " + System.Environment.GetEnvironmentVariable("RunwaySessionToken")); 37 | 38 | // let it go on. 39 | return await next.SendAsync(request, callback); 40 | } 41 | } 42 | } -------------------------------------------------------------------------------- /2022-04-28 - AutoRest The Module Generator/PowerShell Summit 2022 - AutoRest.pptx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ThePoShWolf/Sessions/ddaac2096083e091ba8175375560052451555120/2022-04-28 - AutoRest The Module Generator/PowerShell Summit 2022 - AutoRest.pptx -------------------------------------------------------------------------------- /2022-04-28 - AutoRest The Module Generator/README.md: -------------------------------------------------------------------------------- 1 | # Additional links 2 | 3 | - **Recording**: https://www.youtube.com/watch?v=g6K7yvELCR8 -------------------------------------------------------------------------------- /2023-04-26 - Building a serverless Discord bot/01 - Azure Function Workflow/01 - Profile.ps1: -------------------------------------------------------------------------------- 1 | # Azure Functions profile.ps1 2 | # 3 | # This profile.ps1 will get executed every "cold start" of your Function App. 4 | # "cold start" occurs when: 5 | # 6 | # * A Function App starts up for the very first time 7 | # * A Function App starts up after being de-allocated due to inactivity 8 | # 9 | # You can define helper functions, run commands, or specify environment variables 10 | # NOTE: any variables defined that are not environment variables will get reset after the first execution 11 | 12 | # Authenticate with Azure PowerShell using MSI. 13 | # Remove this if you are not planning on using MSI or Azure PowerShell. 14 | <#if ($env:MSI_SECRET) { 15 | Disable-AzContextAutosave -Scope Process | Out-Null 16 | Connect-AzAccount -Identity 17 | }#> 18 | 19 | # Uncomment the next line to enable legacy AzureRm alias in Azure PowerShell. 20 | # Enable-AzureRmAlias 21 | 22 | # You can also define functions or aliases that can be referenced in any of your PowerShell functions. 23 | 24 | Function Send-DiscordFollowup { 25 | [cmdletbinding()] 26 | param ( 27 | [psobject]$RequestBody, 28 | [string]$Content 29 | ) 30 | $splat = @{ 31 | Uri = "https://discord.com/api/v8/webhooks/$($RequestBody.application_id)/$($RequestBody.token)/messages/@original" 32 | Method = 'Patch' 33 | ContentType = 'application/json' 34 | Body = ( 35 | @{ 36 | type = 4 37 | content = $Content 38 | } | ConvertTo-Json 39 | ) 40 | } 41 | Invoke-RestMethod @splat 42 | } -------------------------------------------------------------------------------- /2023-04-26 - Building a serverless Discord bot/01 - Azure Function Workflow/02 - HTTPStart.ps1: -------------------------------------------------------------------------------- 1 | using namespace System.Net 2 | 3 | # Ensure this matches the bindings in function.json 4 | param($Request, $TriggerMetadata) 5 | 6 | # check for monitor call 7 | if ($Request.Body.type -eq 'warmup') { 8 | $response = 'Warmed up' 9 | } else { 10 | 11 | # First ensure that the request has the correct data 12 | if ( 13 | [string]::IsNullOrEmpty($env:DISCORD_PUBLIC_KEY) -or ` 14 | [string]::IsNullOrEmpty($Request.Headers.'x-signature-ed25519') -or ` 15 | [string]::IsNullOrEmpty($Request.Headers.'x-signature-timestamp') -or ` 16 | [string]::IsNullOrEmpty($Request.RawBody) 17 | ) { 18 | Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{ 19 | StatusCode = [HttpStatusCode]::Unauthorized 20 | } 21 | ) 22 | Throw 'Invalid authentication context.' 23 | } 24 | 25 | # Then validate the signature using the Discord.Net.PowerShell Module 26 | $intSplat = @{ 27 | PublicKey = $env:DISCORD_PUBLIC_KEY 28 | Signature = $Request.Headers.'x-signature-ed25519' 29 | TimeStamp = $Request.Headers.'x-signature-timestamp' 30 | Body = $Request.RawBody 31 | } 32 | # Test-DiscordInteraction comes from Discord.NET.PowerShell 33 | # Make sure that is in your requirements.psd1 34 | if (-not (Test-DiscordInteraction @intSplat)) { 35 | Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{ 36 | StatusCode = [HttpStatusCode]::Unauthorized 37 | } 38 | ) 39 | throw 40 | } 41 | 42 | # If request type is ping, ack it with type 1 43 | if ($Request.Body.type -eq 1) { 44 | $response = @{ 45 | type = 1 46 | } 47 | Write-Host "ACKing ping" 48 | } elseif ($Request.Body.type -eq 2) { 49 | # If request type is 2, defer it with type 5 50 | # This must happen within 3 seconds 51 | Write-Host 'Type 2, submitting to queue and deferring...' 52 | $response = @{ 53 | type = 5 54 | content = "Pending" 55 | } 56 | 57 | # Submit the request to the orchestrator function 58 | # Times out after 1 second to finish within 3s for Discord 59 | $irmSplat = @{ 60 | Uri = "https://$($env:WEBSITE_HOSTNAME)/api/orchestrator?code=$($env:FUNCTIONKEY)" 61 | Method = 'Post' 62 | Body = ($Request.Body | ConvertTo-Json -Depth 10) 63 | Headers = @{ 64 | 'Content-Type' = 'application/json' 65 | Accept = 'application/json' 66 | } 67 | # This timeout value ensures that we can defer within 3s 68 | TimeoutSec = 1 69 | } 70 | 71 | # Since timing out generates an error, we'll try/catch it with no catch since it doesn't matter 72 | try { 73 | Invoke-RestMethod @irmSplat 74 | } catch {} 75 | } 76 | } 77 | 78 | # Push the output binding 79 | Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{ 80 | StatusCode = [HttpStatusCode]::OK 81 | Body = $response 82 | } 83 | ) -------------------------------------------------------------------------------- /2023-04-26 - Building a serverless Discord bot/01 - Azure Function Workflow/03 - Orchestrator.ps1: -------------------------------------------------------------------------------- 1 | using namespace System.Net 2 | 3 | # Ensure this matches the bindings in function.json 4 | param($Request, $TriggerMetadata) 5 | 6 | $Body = $Request.Body 7 | 8 | # remove color from output for easier reading in colorless terminals 9 | $PSStyle.OutputRendering = [System.Management.Automation.OutputRendering]::PlainText 10 | 11 | # switch on the command name 12 | switch ($Body.data.name) { 13 | # simple command 14 | 'hello' { 15 | Write-Host 'hello command...' 16 | $content = "Hello $($Body.member.user.username)" 17 | } 18 | # example with parameters 19 | 'start-server' { 20 | Write-Host 'start-server command...' 21 | if ($Body.data.keys -contains 'options') { 22 | $server = $Body.data['options'][0].value 23 | # code to start server... 24 | $content = "Started server '$server'" 25 | } else { 26 | $content = "Please specify a server with the server parameter." 27 | } 28 | } 29 | default { 30 | Write-Host "Unknown command: '$($Body.data.name)'" 31 | } 32 | } 33 | if (-not ($content.Length -gt 0)) { 34 | $content = 'failure' 35 | } 36 | Write-Host $content 37 | 38 | # send response to discord 39 | Send-DiscordFollowup -RequestBody $Body -Content $content 40 | 41 | # pushing the response to the output binding 42 | Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{ 43 | StatusCode = [HttpStatusCode]::OK 44 | Body = 'ok' 45 | } 46 | ) 47 | 48 | Write-Host 'End command' -------------------------------------------------------------------------------- /2023-04-26 - Building a serverless Discord bot/02 - Frostbite/01 - Monitor.ps1: -------------------------------------------------------------------------------- 1 | # Input bindings are passed in via param block. 2 | param($Monitor) 3 | 4 | # Get the current universal time in the default string format. 5 | $currentUTCtime = (Get-Date).ToUniversalTime() 6 | 7 | # The 'IsPastDue' property is 'true' when the current function invocation is later than scheduled. 8 | if ($Monitor.IsPastDue) { 9 | Write-Host "PowerShell timer is running late!" 10 | } 11 | 12 | #region keep chat bot warm 13 | $uri = "https://$($env:WEBSITE_HOSTNAME)/api/HttpStart?code=$($env:FUNCTIONKEY)" 14 | $headers = @{ 15 | 'Content-Type' = 'application/json' 16 | } 17 | Invoke-RestMethod -Method Post -Uri $uri -Headers $headers -Body $(@{ 18 | type = 'warmup' 19 | } | ConvertTo-Json -Compress 20 | ) 21 | #endregion 22 | 23 | # Write an information log with the current time. 24 | Write-Host "PowerShell timer trigger function ran! TIME: $currentUTCtime" 25 | -------------------------------------------------------------------------------- /2023-04-26 - Building a serverless Discord bot/03 - Function App/.funcignore: -------------------------------------------------------------------------------- 1 | .git* 2 | .vscode 3 | __azurite_db*__.json 4 | __blobstorage__ 5 | __queuestorage__ 6 | local.settings.json 7 | test -------------------------------------------------------------------------------- /2023-04-26 - Building a serverless Discord bot/03 - Function App/.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Azure Functions artifacts 3 | bin 4 | obj 5 | appsettings.json 6 | local.settings.json 7 | 8 | # Azurite artifacts 9 | __blobstorage__ 10 | __queuestorage__ 11 | __azurite_db*__.json -------------------------------------------------------------------------------- /2023-04-26 - Building a serverless Discord bot/03 - Function App/.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "ms-azuretools.vscode-azurefunctions", 4 | "ms-vscode.PowerShell" 5 | ] 6 | } -------------------------------------------------------------------------------- /2023-04-26 - Building a serverless Discord bot/03 - Function App/.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "name": "Attach to PowerShell Functions", 6 | "type": "PowerShell", 7 | "request": "attach", 8 | "customPipeName": "AzureFunctionsPSWorker", 9 | "runspaceId": 1, 10 | "preLaunchTask": "func: host start" 11 | } 12 | ] 13 | } -------------------------------------------------------------------------------- /2023-04-26 - Building a serverless Discord bot/03 - Function App/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "azureFunctions.deploySubpath": ".", 3 | "azureFunctions.projectLanguage": "PowerShell", 4 | "azureFunctions.projectRuntime": "~4", 5 | "debug.internalConsoleOptions": "neverOpen" 6 | } -------------------------------------------------------------------------------- /2023-04-26 - Building a serverless Discord bot/03 - Function App/.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "type": "func", 6 | "label": "func: host start", 7 | "command": "host start", 8 | "problemMatcher": "$func-powershell-watch", 9 | "isBackground": true 10 | } 11 | ] 12 | } -------------------------------------------------------------------------------- /2023-04-26 - Building a serverless Discord bot/03 - Function App/HttpStart/function.json: -------------------------------------------------------------------------------- 1 | { 2 | "bindings": [ 3 | { 4 | "authLevel": "function", 5 | "type": "httpTrigger", 6 | "direction": "in", 7 | "name": "Request", 8 | "methods": [ 9 | "get", 10 | "post" 11 | ] 12 | }, 13 | { 14 | "type": "http", 15 | "direction": "out", 16 | "name": "Response" 17 | } 18 | ] 19 | } -------------------------------------------------------------------------------- /2023-04-26 - Building a serverless Discord bot/03 - Function App/HttpStart/run.ps1: -------------------------------------------------------------------------------- 1 | using namespace System.Net 2 | 3 | # Ensure this matches the bindings in function.json 4 | param($Request, $TriggerMetadata) 5 | 6 | # check for monitor call 7 | if ($Request.Body.type -eq 'warmup') { 8 | $response = 'Warmed up' 9 | } else { 10 | 11 | # First ensure that the request has the correct data 12 | if ( 13 | [string]::IsNullOrEmpty($env:DISCORD_PUBLIC_KEY) -or ` 14 | [string]::IsNullOrEmpty($Request.Headers.'x-signature-ed25519') -or ` 15 | [string]::IsNullOrEmpty($Request.Headers.'x-signature-timestamp') -or ` 16 | [string]::IsNullOrEmpty($Request.RawBody) 17 | ) { 18 | Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{ 19 | StatusCode = [HttpStatusCode]::Unauthorized 20 | } 21 | ) 22 | Throw 'Invalid authentication context.' 23 | } 24 | 25 | # Then validate the signature using the Discord.Net.PowerShell Module 26 | $intSplat = @{ 27 | PublicKey = $env:DISCORD_PUBLIC_KEY 28 | Signature = $Request.Headers.'x-signature-ed25519' 29 | TimeStamp = $Request.Headers.'x-signature-timestamp' 30 | Body = $Request.RawBody 31 | } 32 | if (-not (Test-DiscordInteraction @intSplat)) { 33 | Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{ 34 | StatusCode = [HttpStatusCode]::Unauthorized 35 | } 36 | ) 37 | throw 38 | } 39 | 40 | # If request type is ping, ack it with type 1 41 | if ($Request.Body.type -eq 1) { 42 | $response = @{ 43 | type = 1 44 | } 45 | Write-Host "ACKing ping" 46 | } elseif ($Request.Body.type -eq 2) { 47 | # If request type is 2, defer it with type 5 48 | # This must happen within 3 seconds 49 | Write-Host 'Type 2, submitting to queue and deferring...' 50 | $response = @{ 51 | type = 5 52 | content = "Pending" 53 | } 54 | 55 | # Submit the request to the orchestrator function 56 | # Times out after 1 second to finish within 3s for Discord 57 | $irmSplat = @{ 58 | Uri = "https://$($env:WEBSITE_HOSTNAME)/api/orchestrator?code=$($env:FUNCTIONKEY)" 59 | Method = 'Post' 60 | Body = ($Request.Body | ConvertTo-Json -Depth 10) 61 | Headers = @{ 62 | 'Content-Type' = 'application/json' 63 | Accept = 'application/json' 64 | } 65 | # This timeout value ensures that we can defer within 3s 66 | TimeoutSec = 1 67 | } 68 | 69 | # Since timing out generates an error, we'll try/catch it with no catch since it doesn't matter 70 | try { 71 | Invoke-RestMethod @irmSplat 72 | } catch {} 73 | } 74 | } 75 | 76 | # Push the output binding 77 | Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{ 78 | StatusCode = [HttpStatusCode]::OK 79 | Body = $response 80 | } 81 | ) -------------------------------------------------------------------------------- /2023-04-26 - Building a serverless Discord bot/03 - Function App/host.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0", 3 | "logging": { 4 | "applicationInsights": { 5 | "samplingSettings": { 6 | "isEnabled": true, 7 | "excludedTypes": "Request" 8 | } 9 | } 10 | }, 11 | "extensionBundle": { 12 | "id": "Microsoft.Azure.Functions.ExtensionBundle", 13 | "version": "[3.*, 4.0.0)" 14 | }, 15 | "managedDependency": { 16 | "enabled": true 17 | } 18 | } -------------------------------------------------------------------------------- /2023-04-26 - Building a serverless Discord bot/03 - Function App/monitor/function.json: -------------------------------------------------------------------------------- 1 | { 2 | "bindings": [ 3 | { 4 | "name": "Monitor", 5 | "type": "timerTrigger", 6 | "direction": "in", 7 | "schedule": "0 */5 * * * *" 8 | } 9 | ] 10 | } -------------------------------------------------------------------------------- /2023-04-26 - Building a serverless Discord bot/03 - Function App/monitor/run.ps1: -------------------------------------------------------------------------------- 1 | # Input bindings are passed in via param block. 2 | param($Monitor) 3 | 4 | # Get the current universal time in the default string format. 5 | $currentUTCtime = (Get-Date).ToUniversalTime() 6 | 7 | # The 'IsPastDue' property is 'true' when the current function invocation is later than scheduled. 8 | if ($Monitor.IsPastDue) { 9 | Write-Host "PowerShell timer is running late!" 10 | } 11 | 12 | #region keep chat bot warm 13 | $uri = "https://$($env:WEBSITE_HOSTNAME)/api/HttpStart?code=$($env:FUNCTIONKEY)" 14 | $headers = @{ 15 | 'Content-Type' = 'application/json' 16 | } 17 | Invoke-RestMethod -Method Post -Uri $uri -Headers $headers -Body $(@{ 18 | type = 'warmup' 19 | } | ConvertTo-Json -Compress 20 | ) 21 | #endregion 22 | 23 | # Write an information log with the current time. 24 | Write-Host "PowerShell timer trigger function ran! TIME: $currentUTCtime" 25 | -------------------------------------------------------------------------------- /2023-04-26 - Building a serverless Discord bot/03 - Function App/orchestrator/function.json: -------------------------------------------------------------------------------- 1 | { 2 | "bindings": [ 3 | { 4 | "authLevel": "function", 5 | "type": "httpTrigger", 6 | "direction": "in", 7 | "name": "Request", 8 | "methods": [ 9 | "post" 10 | ] 11 | }, 12 | { 13 | "type": "http", 14 | "direction": "out", 15 | "name": "Response" 16 | } 17 | ] 18 | } -------------------------------------------------------------------------------- /2023-04-26 - Building a serverless Discord bot/03 - Function App/orchestrator/run.ps1: -------------------------------------------------------------------------------- 1 | using namespace System.Net 2 | 3 | # Ensure this matches the bindings in function.json 4 | param($Request, $TriggerMetadata) 5 | 6 | $Body = $Request.Body 7 | 8 | # remove color from output for easier reading in colorless terminals 9 | $PSStyle.OutputRendering = [System.Management.Automation.OutputRendering]::PlainText 10 | 11 | # switch on the command name 12 | switch ($Body.data.name) { 13 | # simple command 14 | 'hello' { 15 | Start-Sleep -Seconds 5 16 | Write-Host 'hello command...' 17 | $content = "Hello $($Body.member.user.username)" 18 | } 19 | # example with parameters 20 | 'start-server' { 21 | Start-Sleep -Seconds 5 22 | Write-Host 'start-server command...' 23 | if ($Body.data.keys -contains 'options') { 24 | $server = $Body.data['options'][0].value 25 | # code to start server... 26 | $content = "Started server '$server'" 27 | } else { 28 | $content = "Please specify a server with the server parameter." 29 | } 30 | } 31 | default { 32 | Write-Host "Unknown command: '$($Body.data.name)'" 33 | } 34 | } 35 | if (-not ($content.Length -gt 0)) { 36 | $content = 'failure' 37 | } 38 | Write-Host $content 39 | 40 | # send response to discord 41 | Send-DiscordFollowup -RequestBody $Body -Content $content 42 | 43 | # pushing the response to the output binding 44 | Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{ 45 | StatusCode = [HttpStatusCode]::OK 46 | Body = 'ok' 47 | } 48 | ) 49 | 50 | Write-Host 'End command' -------------------------------------------------------------------------------- /2023-04-26 - Building a serverless Discord bot/03 - Function App/profile.ps1: -------------------------------------------------------------------------------- 1 | # Azure Functions profile.ps1 2 | # 3 | # This profile.ps1 will get executed every "cold start" of your Function App. 4 | # "cold start" occurs when: 5 | # 6 | # * A Function App starts up for the very first time 7 | # * A Function App starts up after being de-allocated due to inactivity 8 | # 9 | # You can define helper functions, run commands, or specify environment variables 10 | # NOTE: any variables defined that are not environment variables will get reset after the first execution 11 | 12 | # Authenticate with Azure PowerShell using MSI. 13 | # Remove this if you are not planning on using MSI or Azure PowerShell. 14 | <#if ($env:MSI_SECRET) { 15 | Disable-AzContextAutosave -Scope Process | Out-Null 16 | Connect-AzAccount -Identity 17 | }#> 18 | 19 | # Uncomment the next line to enable legacy AzureRm alias in Azure PowerShell. 20 | # Enable-AzureRmAlias 21 | 22 | # You can also define functions or aliases that can be referenced in any of your PowerShell functions. 23 | 24 | Function Send-DiscordFollowup { 25 | [cmdletbinding()] 26 | param ( 27 | [psobject]$RequestBody, 28 | [string]$Content 29 | ) 30 | $splat = @{ 31 | Uri = "https://discord.com/api/v8/webhooks/$($RequestBody.application_id)/$($RequestBody.token)/messages/@original" 32 | Method = 'Patch' 33 | ContentType = 'application/json' 34 | Body = ( 35 | @{ 36 | type = 4 37 | content = $Content 38 | } | ConvertTo-Json 39 | ) 40 | } 41 | Invoke-RestMethod @splat 42 | } -------------------------------------------------------------------------------- /2023-04-26 - Building a serverless Discord bot/03 - Function App/requirements.psd1: -------------------------------------------------------------------------------- 1 | # This file enables modules to be automatically managed by the Functions service. 2 | # See https://aka.ms/functionsmanageddependency for additional information. 3 | # 4 | @{ 5 | # For latest supported version, go to 'https://www.powershellgallery.com/packages/Az'. 6 | # To use the Az module in your function app, please uncomment the line below. 7 | # 'Az' = '9.*' 8 | 'Discord.NET.PowerShell' = '0.0.1' 9 | } -------------------------------------------------------------------------------- /2023-04-26 - Building a serverless Discord bot/04 - Manage Bot Commands/01 - Add Commands.ps1: -------------------------------------------------------------------------------- 1 | # Auth to discord using bot token 2 | Connect-Discord -RestClient -TokenType Bot -Token (Get-Content C:\tmp\bt.txt) 3 | 4 | # Get the guild where you want the commands 5 | $guild = Get-DiscordGuild | Where-Object { $_.Name -eq 'Bot Testing' } 6 | $guild 7 | 8 | # Add a basic command 9 | New-DiscordGuildCommand -Name 'hello' -Description "Say hello to the bot" -Guild $guild -CommandBuilder ( 10 | New-DiscordSlashCommand -Name 'hello' -Description 'Say Hello to the bot' 11 | ) 12 | 13 | New-DiscordGuildCommand -Name 'bye' -Description "Say hello to the bot" -Guild $guild -CommandBuilder ( 14 | New-DiscordSlashCommand -Name 'by' -Description 'Say Hello to the bot' 15 | ) 16 | 17 | # Add a command with parameters 18 | New-DiscordGuildCommand -Name 'start-server' -Description 'Start a server' -Guild $guild -CommandBuilder ( 19 | New-DiscordSlashCommand -Name 'start-server' -Description 'Start a server' -Options @( 20 | (New-DiscordSlashCommandOption -Name 'server' -Description 'The selected server' -Type String) 21 | ) 22 | ) 23 | 24 | # Send a command to a channel 25 | $channel = Get-DiscordChannel | Where-Object { $_.Name -eq 'general' } 26 | Send-DiscordMessage -GuildId $guild.Id -ChannelId $channel.Id -MessageText 'Hello!' -------------------------------------------------------------------------------- /2023-04-26 - Building a serverless Discord bot/PowerShell Summit 2023 - Serverless Discord bot.pptx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ThePoShWolf/Sessions/ddaac2096083e091ba8175375560052451555120/2023-04-26 - Building a serverless Discord bot/PowerShell Summit 2023 - Serverless Discord bot.pptx -------------------------------------------------------------------------------- /2023-04-26 - Building a serverless Discord bot/README.md: -------------------------------------------------------------------------------- 1 | # Additional links 2 | 3 | - **Recording**: https://www.youtube.com/watch?v=ucAEzjV0yuA -------------------------------------------------------------------------------- /2023-04-27 - Making Kubectl PowerShell friendly/01 - Parsing/01 - Sample.ps1: -------------------------------------------------------------------------------- 1 | Import-Module specialK 2 | 3 | # If my local kubernetes cluster is working... 4 | $out = kubectl get pod 5 | $out 6 | $out.GetType() 7 | $out[0].GetType() 8 | $out.Count 9 | 10 | # If it isn't 11 | $out = @( 12 | 'NAME READY STATUS RESTARTS AGE' 13 | 'database-deployment-194a92db12-b2aat 1/1 Running 0 10d' 14 | 'web-deployment-98ad9380dd-8brg3 1/1 Running 0 2d7h' 15 | ) 16 | 17 | # headers 18 | $out[0] 19 | 20 | # if the output starts with the typical headers 21 | if ($out[0] -match '^(NAME |NAMESPACE |CURRENT |LAST SEEN )') { 22 | # locate all positions to place semicolons 23 | # we are using the headers since some values may be null in the data 24 | if ($null -ne $out) { 25 | $m = $out[0] | Select-String -Pattern ' \S' -AllMatches 26 | } 27 | 28 | # view matches 29 | $m 30 | $m.Matches | Format-Table 31 | 32 | # place semicolons, order descending 33 | $out = foreach ($line in $out) { 34 | foreach ($index in ($m.Matches.Index | Sort-Object -Descending)) { 35 | $line = $line.Insert($index + 2, ';') 36 | } 37 | $line 38 | } 39 | 40 | # view semicolons 41 | $out 42 | 43 | # objectCommands is the formats.json hashtable 44 | # will explain when we get to formats 45 | if ($objectCommands[$args[0]] -contains $args[1]) { 46 | # select the format type name 47 | # dynamically determined in the script 48 | $typeName = "$($args[0])-$($args[1])" 49 | # manually determined for demo 50 | $typeName = 'get-pod' 51 | # output 52 | $out -replace ' +;', ';' | ForEach-Object { $_.Trim() } | ConvertFrom-Csv -Delimiter ';' | ` 53 | ForEach-Object { $_.PSObject.TypeNames.Insert(0, $typeName); $_ } 54 | } else { 55 | $out -replace ' +;', ';' | ForEach-Object { $_.Trim() } | ConvertFrom-Csv -Delimiter ';' 56 | } 57 | } else { 58 | $out 59 | } -------------------------------------------------------------------------------- /2023-04-27 - Making Kubectl PowerShell friendly/01 - Parsing/02 - k.ps1: -------------------------------------------------------------------------------- 1 | # This version is from April 2023. 2 | # Please see https://github.com/theposhwolf/specialk for latest version 3 | $script:objectCommands = Get-Content '.\2023-04-27 - Making Kubectl PowerShell friendly\02 - Formats\01 - formats.json' | ConvertFrom-Json -AsHashtable 4 | function k { 5 | $skipArgs = @( 6 | 'exec', 'cp', 'scale', 'rollout', 'delete', 'logs' 7 | ) 8 | if ($skipArgs -contains $args[0]) { 9 | & kubectl $args 10 | } else { 11 | $out = (& kubectl $args) 12 | # if the output starts with the typical headers 13 | if ($out[0] -match '^(NAME |NAMESPACE |CURRENT |LAST SEEN )') { 14 | # locate all positions to place semicolons 15 | # we are using the headers since some values may be null in the data 16 | if ($null -ne $out) { 17 | $m = $out[0] | Select-String -Pattern ' \S' -AllMatches 18 | } 19 | 20 | # place semicolons 21 | $out = foreach ($line in $out) { 22 | foreach ($index in ($m.Matches.Index | Sort-Object -Descending)) { 23 | $line = $line.Insert($index + 2, ';') 24 | } 25 | $line 26 | } 27 | 28 | if ($objectCommands[$args[0]] -contains $args[1]) { 29 | # select the format type name 30 | $typeName = "$($args[0])-$($args[1])" 31 | $out -replace ' +;', ';' | ForEach-Object { $_.Trim() } | ConvertFrom-Csv -Delimiter ';' | ForEach-Object { $_.PSObject.TypeNames.Insert(0, $typeName); $_ } 32 | } else { 33 | # no format likely outputs as a list 34 | $out -replace ' +;', ';' | ForEach-Object { $_.Trim() } | ConvertFrom-Csv -Delimiter ';' 35 | } 36 | } else { 37 | $out 38 | } 39 | } 40 | } -------------------------------------------------------------------------------- /2023-04-27 - Making Kubectl PowerShell friendly/01 - Parsing/03 - Gotchas.ps1: -------------------------------------------------------------------------------- 1 | # Everything is a string 2 | $pods = k get pod 3 | $pods 4 | $pods[0].RESTARTS.GetType() 5 | 6 | # Even top 7 | $top = k top pod 8 | $top 9 | $top[0].'MEMORY(bytes)'.GetType() 10 | 11 | # But you can still compare some of the data, at least in PowerShell 7 12 | $pods | Where-Object { $_.RESTARTS -lt 3 } 13 | $pods | Where-Object { $_.RESTARTS -eq 2 } 14 | 15 | # top compare 16 | $top | Where-Object { $_.'MEMORY(bytes)' -gt 200 } 17 | 18 | # getting creative with top 19 | k top pod --sort-by=memory | Select-Object -First 1 20 | k top node --sort-by=cpu | Select-Object -First 1 21 | 22 | # Doesn't work well with 'LAST SEEN' in events 23 | $events = k get events 24 | $events | Format-Table 25 | $events | Where-Object { $_.'LAST SEEN' -gt (Get-Date).AddMinutes(-45) } | Format-Table 26 | $events | Where-Object { $_.'LAST SEEN' -lt '45m' } | Format-Table 27 | 28 | # You can get creative 29 | $events | Where-Object { $_.'LAST SEEN' -like '*m' -and $_.'LAST SEEN' -lt 45 } | Format-Table 30 | k get events --sort-by='lastTimestamp' | Select-Object -Last 5 | Format-Table 31 | 32 | # String -gt and -lt is sketchy 33 | # Be aware that in Windows PowerShell 5.1, this is $false: 34 | '0' -gt -1 35 | 36 | # and this is $true 37 | '0' -lt 1 -------------------------------------------------------------------------------- /2023-04-27 - Making Kubectl PowerShell friendly/02 - Formats/01 - formats.json: -------------------------------------------------------------------------------- 1 | { 2 | "get": [ 3 | "pod", 4 | "deployment", 5 | "node", 6 | "service" 7 | ], 8 | "top": [ 9 | "pod", 10 | "node" 11 | ], 12 | "config": [ 13 | "get-contexts", 14 | "get-clusters", 15 | "get-users" 16 | ] 17 | } -------------------------------------------------------------------------------- /2023-04-27 - Making Kubectl PowerShell friendly/02 - Formats/02 - formatUpdater.ps1: -------------------------------------------------------------------------------- 1 | param ( 2 | [string]$OutPath 3 | ) 4 | 5 | $commands = Get-Content '.\2023-04-27 - Making Kubectl PowerShell friendly\02 - Formats\01 - formats.json' | ConvertFrom-Json -AsHashtable 6 | 7 | # Creating the views 8 | $addViews = foreach ($command in $commands.Keys) { 9 | Write-Host $command 10 | # Foreach command in the formats.json 11 | foreach ($sub in $commands[$command]) { 12 | Write-Host "- $sub" 13 | # get the output of the command and subcommand 14 | # a bit of a circular dependency here 15 | $out = (k $command $sub) 16 | if ($null -ne $out) { 17 | $props = $out[0].psobject.properties.name 18 | } else { 19 | Write-Warning "Unable to generate format for 'kubectl $command $sub'. No objects were returned to examine." 20 | break 21 | } 22 | 23 | # foreach property name in the command-subcommand's output 24 | # create a format that matches the kubectl output 25 | 26 | # create the view 27 | ' ' 28 | ' {0}' -f "$command-$sub" 29 | ' ' 30 | ' {0}' -f "$command-$sub" 31 | ' ' 32 | ' ' 33 | ' ' 34 | # create the headers 35 | foreach ($header in $props) { 36 | ' ' -f $header 37 | } 38 | ' ' 39 | ' ' 40 | ' ' 41 | ' ' 42 | # create the column items 43 | foreach ($tci in $props) { 44 | ' {0}' -f $tci 45 | } 46 | ' ' 47 | ' ' 48 | ' ' 49 | ' ' 50 | ' ' 51 | } 52 | } 53 | 54 | @( 55 | '' 56 | ' ' 57 | $addViews 58 | ' ' 59 | '' 60 | ) | Out-File $OutPath -Force -------------------------------------------------------------------------------- /2023-04-27 - Making Kubectl PowerShell friendly/03 - Filter Left/01 - Filter Left.ps1: -------------------------------------------------------------------------------- 1 | #region Pods 2 | 3 | ## Get running pods 4 | ### Where-Object 5 | k get pod | Where-Object { $_.Status -eq 'Running' } 6 | 7 | ### Kubectl 8 | kubectl get pod --field-selector=status.phase=Running 9 | 10 | ### Even with specialK 11 | k get pod --field-selector=status.phase=Running 12 | 13 | 14 | ## Get non-running pods 15 | ### Where-Object 16 | k get pod | Where-Object { $_.Status -ne 'Running' } 17 | 18 | ### Kubectl 19 | kubectl get pod --field-selector=status.phase!=Running 20 | 21 | ### Even with specialK 22 | k get pod --field-selector=status.phase!=Running 23 | 24 | ## Finding field selector labels 25 | $pods = (k get pod -o json | ConvertFrom-Json).Items 26 | $pods[0].status 27 | $pods[0].status.phase 28 | 29 | 30 | ## Get pod by app 31 | ### Where-Object 32 | ### No reasonable way without -o json. Maybe filter on name? 33 | k get pod | Where-Object { $_.Name -like '*apache*' } 34 | 35 | ### Kubectl (-l, --selector='') 36 | kubectl get pod -l app=apache 37 | 38 | ### Works in specialK 39 | k get pod -l app=apache7 40 | 41 | ## Get pod by namespace 42 | ### Where-Object 43 | ### No reasonable way without -o json. 44 | 45 | ### kubectl 46 | kubectl get pod -n kube-system 47 | 48 | ### Note about specialK 49 | ### -n mynamespace adds another header, not currently supported 50 | k get pod -n kube-system 51 | 52 | ## Only return pod name 53 | ### specialK 54 | (k get pod -l app=apache --field-selector status.phase=Running)[0].name77 55 | 56 | ### kubectl 57 | kubectl get pod -l app=apache --field-selector status.phase=Running -o=jsonpath='{.items..metadata.name}' 58 | 59 | #endregion 60 | 61 | 62 | #region Nodes 63 | 64 | ## Get ready nodes 65 | ### Where-Object 66 | k get node | Where-Object { $_.Status -eq 'Ready' }7 67 | 68 | ### Kubectl 69 | ### I'm not aware of solution without awk/grep 70 | 71 | #endregion 72 | 73 | #region Services 74 | 75 | ## Get services by IP 76 | ### Where-Object 77 | k get service | Where-Object { $_.'CLUSTER-IP' -eq '10.96.0.1' } 78 | 79 | ### Kubectl` 80 | ### I'm not aware of solution without awk/grep 81 | $services = (k get service -o json | ConvertFrom-Json).items 82 | $services[0].spec.clusterIP 83 | 84 | kubectl get service --field-selector=spec.clusterIP=10.96.0.1 85 | 86 | #endregion -------------------------------------------------------------------------------- /2023-04-27 - Making Kubectl PowerShell friendly/PowerShell Summit 2023 - Making Kubectl PowerShell friendly.pptx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ThePoShWolf/Sessions/ddaac2096083e091ba8175375560052451555120/2023-04-27 - Making Kubectl PowerShell friendly/PowerShell Summit 2023 - Making Kubectl PowerShell friendly.pptx -------------------------------------------------------------------------------- /2023-04-27 - Making Kubectl PowerShell friendly/README.md: -------------------------------------------------------------------------------- 1 | # Additional links 2 | 3 | - **Recording**: https://www.youtube.com/watch?v=QnvQAafsIck -------------------------------------------------------------------------------- /2023-10-11 - Pacific PSUG - .NET Object in PowerShell/00 - Understanding Types.ps1: -------------------------------------------------------------------------------- 1 | # Examples of types: 2 | 3 | # string 4 | "string" 5 | "string".GetType() 6 | 7 | # integer (int32) 8 | 42 9 | $answer = 42 10 | $answer.GetType() 11 | 12 | # boolean 13 | $true 14 | $true.GetType() 15 | 16 | # DateTime 17 | Get-Date 18 | (Get-Date).GetType() 19 | 20 | # hashtable (dictionary) 21 | @{ 22 | key = "value" 23 | } 24 | @{ 25 | key = "value" 26 | }.GetType() 27 | 28 | # PSCustomObject 29 | [pscustomobject]@{ 30 | Property = "Value" 31 | } 32 | $obj = [pscustomobject]@{ 33 | Property = "Value" 34 | } 35 | $obj.GetType() 36 | 37 | # Example of a built-in .NET type 38 | (Get-Process)[0] 39 | (Get-Process)[0].GetType() 40 | (Get-Process)[0].GetType().FullName 41 | 42 | # How to get an object's type 43 | 44 | (Get-ChildItem)[0].GetType() 45 | (Get-ChildItem)[0].GetType().FullName 46 | 47 | # They can all use Get-Member 48 | 49 | "string" | Get-Member 50 | 42 | Get-Member 51 | $true | Get-Member 52 | Get-Date | Get-Member 53 | @{ 54 | key = "value" 55 | } | Get-Member 56 | [pscustomobject]@{ 57 | Property = "Value" 58 | } | Get-Member 59 | (Get-Process)[0] | Get-Member 60 | (Get-ChildItem)[0] | Get-Member -------------------------------------------------------------------------------- /2023-10-11 - Pacific PSUG - .NET Object in PowerShell/01 - Discovery.ps1: -------------------------------------------------------------------------------- 1 | # Creating an object using a .NET type 2 | 3 | # Using a cmdlet 4 | 5 | Get-Date 6 | (Get-Date).GetType().FullName 7 | 8 | Get-Item '.\2023-10-11 - Pacific PSUG - .NET Object in PowerShell' 9 | (Get-Item '.\2023-10-11 - Pacific PSUG - .NET Object in PowerShell').GetType() 10 | 11 | (Get-Process)[0] 12 | (Get-Process)[0].GetType() 13 | 14 | Get-Command Get-Command 15 | (Get-Command Get-Command).GetType() 16 | 17 | # Using New-Object 18 | 19 | New-Object -TypeName datetime -ArgumentList 2023, 10, 11 20 | 21 | New-Object -TypeName IO.DirectoryInfo -ArgumentList '.\2023-10-11 - Pacific PSUG - .NET Object in PowerShell' 22 | 23 | New-Object -TypeName System.Diagnostics.Process 24 | 25 | New-Object -TypeName System.Management.Automation.CommandInfo 26 | 27 | # .NET type syntax 28 | 29 | [Namespace.Type]::Method('parameters') 30 | 31 | # Using the constructor 32 | 33 | [datetime]::new(2023, 10, 11) 34 | 35 | [IO.DirectoryInfo]::new('.\2023-10-11 - Pacific PSUG - .NET Object in PowerShell') 36 | 37 | [System.Diagnostics.Process]::new() 38 | 39 | [System.Management.Automation.CommandInfo]::new() 40 | 41 | # How do we know if it has a constructor? 42 | 43 | [datetime].GetType() 44 | 45 | [datetime] | Get-Member 46 | 47 | [datetime].GetConstructors().Name 48 | 49 | [System.Management.Automation.CommandInfo].GetConstructors().Name 50 | 51 | # If it has a constructor, how do we know what to pass to it? Or what it outputs? 52 | 53 | [datetime]::new 54 | 55 | <# 56 | 57 | OverloadDefinitions 58 | ------------------- 59 | datetime new(long ticks) 60 | |------| |----------| 61 | Output Parameter(s) 62 | 63 | datetime new(long ticks, System.DateTimeKind kind) 64 | datetime new(int year, int month, int day) 65 | datetime new(int year, int month, int day, System.Globalization.Calendar calendar) 66 | datetime new(int year, int month, int day, int hour, int minute, int second, int millisecond, System.Globalization.Calendar calendar, System.DateTimeKind kind) 67 | datetime new(int year, int month, int day, int hour, int minute, int second) 68 | datetime new(int year, int month, int day, int hour, int minute, int second, System.DateTimeKind kind) 69 | datetime new(int year, int month, int day, int hour, int minute, int second, System.Globalization.Calendar calendar) 70 | datetime new(int year, int month, int day, int hour, int minute, int second, int millisecond) 71 | datetime new(int year, int month, int day, int hour, int minute, int second, int millisecond, System.DateTimeKind kind) 72 | datetime new(int year, int month, int day, int hour, int minute, int second, int millisecond, System.Globalization.Calendar calendar) 73 | datetime new(int year, int month, int day, int hour, int minute, int second, int millisecond, int microsecond) 74 | datetime new(int year, int month, int day, int hour, int minute, int second, int millisecond, int microsecond, System.DateTimeKind kind) 75 | datetime new(int year, int month, int day, int hour, int minute, int second, int millisecond, int microsecond, System.Globalization.Calendar calendar) 76 | datetime new(int year, int month, int day, int hour, int minute, int second, int millisecond, int microsecond, System.Globalization.Calendar calendar, System.DateTimeKind kind) 77 | #> 78 | 79 | # Discovering other methods and properties: 80 | 81 | [datetime]:: # tab, tab, tab, enter / Ctrl+Spacebar 82 | 83 | [datetime].GetMethods() | Format-Table Name 84 | 85 | [datetime]::Now 86 | 87 | # Another method 88 | 89 | [datetime]::DaysInMonth | gm 90 | 91 | [datetime]::DaysInMonth(2023, 10) 92 | 93 | # using reflection 94 | 95 | $method = [datetime]::DaysInMonth 96 | $method.Invoke(2023,10) 97 | 98 | # other static examples 99 | 100 | [DateTimeOffset]::Now.ToUnixTimeSeconds() 101 | 102 | [DateTimeOffset]::FromUnixTimeSeconds 103 | 104 | [DateTimeOffset]::FromUnixTimeSeconds(1697075821) 105 | 106 | # Getting enum values 107 | 108 | help Set-ExecutionPolicy -Parameter ExecutionPolicy 109 | 110 | [Microsoft.PowerShell.ExecutionPolicy] 111 | 112 | [Microsoft.PowerShell.ExecutionPolicy]:: # tab, tab, tab / Ctrl-Space 113 | 114 | [Microsoft.PowerShell.ExecutionPolicy].GetEnumNames() 115 | 116 | # One liner 117 | 118 | (Get-Command Set-ExecutionPolicy).Parameters['ExecutionPolicy'].ParameterType.GetEnumNames() -------------------------------------------------------------------------------- /2023-10-11 - Pacific PSUG - .NET Object in PowerShell/02 - Importing.ps1: -------------------------------------------------------------------------------- 1 | # Very simple 2 | 3 | Add-Type -Path "Fully qualified path to .dll" 4 | 5 | # For example, if you had the Discord.NET .dlls: 6 | 7 | Get-Item ..\Discordant\lib\*.dll | ForEach-Object { 8 | Add-Type $_.FullName 9 | } 10 | 11 | # Don't forget -Path 12 | 13 | Get-Item ..\Discordant\lib\*.dll | ForEach-Object { 14 | Add-Type -Path $_.FullName 15 | } 16 | 17 | # If you are importing into a module, you can instead use RequiredAssemblies in the manifest 18 | # Show Modrify build 19 | # https://stackoverflow.com/a/72509967/75772 20 | 21 | code ..\Modrify\build\Modrify\Modrify.psd1 22 | 23 | # Both ways make the .NET types available to the shell, not just module cmdlets 24 | 25 | # Finding types in a .dll 26 | $assembly = [System.Reflection.Assembly]::LoadFrom('C:\temp\mytestlib.dll') 27 | $assembly.GetTypes() -------------------------------------------------------------------------------- /2023-10-11 - Pacific PSUG - .NET Object in PowerShell/03 - Usage.ps1: -------------------------------------------------------------------------------- 1 | # If you are using an established .NET library, start with their documentation! 2 | # Discovery will only get you so far. 3 | # We are going to do some demos with the Discord.NET SDK 4 | # They have excellent docs, by the way 5 | 6 | # Need to find the fully qualified type name for DiscordRestClient 7 | 8 | [DiscordRestClient] # tab 9 | 10 | # Completes to: 11 | 12 | [Discord.Rest.DiscordRestClient] 13 | 14 | # Examine the constructor 15 | 16 | [Discord.Rest.DiscordRestClient]::new 17 | 18 | # What is the DiscordRestConfig? 19 | 20 | [Discord.Rest.DiscordRestConfig]::new 21 | 22 | # Lets make one 23 | 24 | $drc = [Discord.Rest.DiscordRestConfig]::new() 25 | $drc 26 | 27 | # Ok, probably don't need it. Lets make a client 28 | 29 | $client = [Discord.Rest.DiscordRestClient]::new() 30 | $client 31 | 32 | # How to connect? 33 | 34 | $client | Get-Member 35 | 36 | # Lets try LoginAsync 37 | 38 | $client.LoginAsync 39 | 40 | <# 41 | 42 | OverloadDefinitions 43 | ------------------- 44 | System.Threading.Tasks.Task LoginAsync(Discord.TokenType tokenType, string token, bool validateToken = True) 45 | |-------------------------| |--------| |----------------------------------------| |-----------------------| 46 | Output Method Required parameters Params with default values 47 | #> 48 | 49 | # What is token type? 50 | 51 | [Discord.TokenType] 52 | 53 | # What are the values? 54 | 55 | [Discord.TokenType].GetEnumNames() 56 | 57 | # Lets log in 58 | 59 | $task = $client.LoginAsync([Discord.TokenType]::Bot, (Get-Content C:\tmp\bottoken.txt)) 60 | $task 61 | $task | Get-Member 62 | $task.Wait() 63 | $task.Result 64 | 65 | # How do we know what happened? 66 | 67 | $client 68 | $client.LoginState 69 | 70 | # If we want to skip the async stuff, we can just do: 71 | 72 | $client.LoginAsync([Discord.TokenType]::Bot, (Get-Content C:\tmp\bottoken.txt)).Wait().Result 73 | 74 | # We can even shorten the enum: 75 | 76 | $client.LoginAsync("Bot", (Get-Content C:\tmp\bottoken.txt)).Wait().Result -------------------------------------------------------------------------------- /2023-10-11 - Pacific PSUG - .NET Object in PowerShell/04 - Using.ps1: -------------------------------------------------------------------------------- 1 | # Using using allows us to simplify type usage 2 | # C# example: https://github.com/discord-net/Discord.Net/blob/dev/src/Discord.Net.Rest/BaseDiscordClient.cs 3 | using namespace System.IO 4 | using namespace Discord.Rest 5 | 6 | # so instead of: 7 | [System.IO.DirectoryInfo]::new('.\2023-10-11 - Pacific PSUG - .NET Object in PowerShell') 8 | 9 | # we can use: 10 | [DirectoryInfo]::new('.\2023-10-11 - Pacific PSUG - .NET Object in PowerShell') 11 | 12 | #Link: https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_using?view=powershell-7.3 13 | 14 | # Discord example 15 | [DiscordRestClient]::new() -------------------------------------------------------------------------------- /2023-10-11 - Pacific PSUG - .NET Object in PowerShell/05 - Generics.ps1: -------------------------------------------------------------------------------- 1 | # Generics are types that are meant to work with any other type, or a specific set of types 2 | # Built in examples: 3 | 4 | # Array 5 | # https://learn.microsoft.com/en-us/dotnet/api/system.object?view=net-7.0 6 | 7 | @( 8 | 'string', 9 | 42, 10 | (Get-Date) 11 | ) 12 | $arr = @( 13 | 'string', 14 | 42, 15 | (Get-Date) 16 | ) 17 | $arr.GetType() 18 | $arr.GetType().FullName 19 | 20 | # Methods 21 | 22 | [System.Object[]]::new 23 | 24 | # Generic add 25 | $arr.Add 26 | 27 | # How are a string, integer, and datetime all System.Object? 28 | # Polymorphism: https://en.wikipedia.org/wiki/Polymorphism_(computer_science) 29 | 30 | $arr[0].GetType().BaseType 31 | $arr[1].GetType().BaseType 32 | $arr[2].GetType().BaseType 33 | 34 | $client.GetType().BaseType 35 | 36 | # Hashtable 37 | # https://learn.microsoft.com/en-us/dotnet/api/system.collections.hashtable?view=net-7.0 38 | 39 | $ht = @{ 40 | Key = 'value' 41 | } 42 | $ht.GetType() 43 | $ht.GetType().FullName 44 | 45 | # Methods 46 | 47 | [System.Collections.Hashtable]::new 48 | $ht.Add 49 | 50 | # Crazy example 51 | $ht.Add($client, $arr) 52 | 53 | # List 54 | # https://learn.microsoft.com/en-us/dotnet/api/system.collections.generic.list-1?view=net-7.0 55 | # List 56 | 57 | [System.Collections.Generic.List]::new 58 | 59 | # working lists 60 | 61 | $strList = [System.Collections.Generic.List[string]]::new() 62 | $strList.Add 63 | 64 | [System.Collections.Generic.List[int]]::new() 65 | [System.Collections.Generic.List[Discord.Rest.DiscordRestClient]]::new() 66 | 67 | # One list for anything 68 | 69 | $list = [System.Collections.Generic.List[System.Object]]::new() 70 | $list.Add 71 | $list.Add($ht) 72 | $list.Add($client) 73 | $list 74 | 75 | # old: 76 | [System.Collections.ArrayList] 77 | 78 | # new: 79 | [System.Collections.Generic.List[System.Object]] -------------------------------------------------------------------------------- /2023-10-11 - Pacific PSUG - .NET Object in PowerShell/06 - Other Places.ps1: -------------------------------------------------------------------------------- 1 | # Other places you can use imported .NET types 2 | 3 | # OutputType 4 | Function Get-DiscordGuild { 5 | [OutputType([Discord.Rest.RestGuild], ParameterSetName = 'byId')] 6 | [OutputType([Discord.Rest.RestGuild[]], ParameterSetName = 'all')] 7 | param(<#...#>) 8 | <#...#> 9 | } 10 | 11 | # Parameter type 12 | [Parameter( 13 | Mandatory, 14 | ParameterSetName = 'byObj' 15 | )] 16 | [ValidateNotNullOrEmpty()] 17 | [Discord.Rest.RestGuild]$Guild -------------------------------------------------------------------------------- /2023-10-11 - Pacific PSUG - .NET Object in PowerShell/Pacific PSUG - PowerShell and .NET.pptx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ThePoShWolf/Sessions/ddaac2096083e091ba8175375560052451555120/2023-10-11 - Pacific PSUG - .NET Object in PowerShell/Pacific PSUG - PowerShell and .NET.pptx -------------------------------------------------------------------------------- /2023-10-11 - Pacific PSUG - .NET Object in PowerShell/README.md: -------------------------------------------------------------------------------- 1 | # Additional links 2 | 3 | - **Recording**: https://www.youtube.com/watch?v=i6kJiDkqpIA -------------------------------------------------------------------------------- /2024-04-10 - PSCustomObject[] vs Hashtables/01 - Create/Hashtable.ps1: -------------------------------------------------------------------------------- 1 | # Empty hashtable 2 | $ht = New-Object -TypeName hashtable 3 | $ht 4 | 5 | # Alternate empty hashtable 6 | $ht = @{} 7 | $ht 8 | 9 | # Adding values 10 | $ht.Add('Event', 'PowerShell Summit 2024') 11 | $ht.Add('Date', '2024-04-10') 12 | $ht.Add('Location', 'Bellevue, WA') 13 | $ht 14 | 15 | # Easy way 16 | $ht = @{ 17 | Event = 'PowerShell Summit 2024' 18 | Date = '2024-04-10' 19 | Location = 'Bellevue, WA' 20 | } 21 | $ht 22 | 23 | # Or inline 24 | $ht = @{ Event = 'PowerShell Summit 2024'; Date = '2024-04-10'; Location = 'Bellevue, WA' } 25 | $ht 26 | 27 | # Sorting properties 28 | $ht | Sort-Object -Property Name -Descending 29 | $ht.GetEnumerator() | Sort-Object -Property Name -Descending 30 | 31 | # Ordered 32 | $orderedHt = [ordered]@{ 33 | Event = 'PowerShell Summit 2024' 34 | Date = '2024-04-10' 35 | Location = 'Bellevue, WA' 36 | } 37 | $orderedHt 38 | $orderedHt.GetType() 39 | 40 | # Nested 41 | $nestedHt = @{ 42 | Event = 'PowerShell Summit 2024' 43 | Date = @{ 44 | Year = 2024 45 | Month = 4 46 | Day = 10 47 | } 48 | Location = @{ 49 | City = 'Bellevue' 50 | State = 'WA' 51 | } 52 | } 53 | $nestedHt 54 | 55 | # Large(ish) hashtable 56 | $largeHt = @{ 57 | '2024-04-10' = @{ 58 | Event = 'PowerShell Summit 2024' 59 | Date = @{ 60 | Year = 2024 61 | Month = 4 62 | Day = 10 63 | } 64 | Location = @{ 65 | City = 'Bellevue' 66 | State = 'WA' 67 | } 68 | } 69 | '2024-04-11' = @{ 70 | Event = 'PowerShell Summit 2024' 71 | Date = @{ 72 | Year = 2024 73 | Month = 4 74 | Day = 11 75 | } 76 | Location = @{ 77 | City = 'Bellevue' 78 | State = 'WA' 79 | } 80 | } 81 | } 82 | $largeHt 83 | 84 | # As an array 85 | $arrayHt = @{ 86 | Events = @( 87 | @{ 88 | Event = 'PowerShell Summit 2024' 89 | Date = @{ 90 | Year = 2024 91 | Month = 4 92 | Day = 10 93 | } 94 | Location = @{ 95 | City = 'Bellevue' 96 | State = 'WA' 97 | } 98 | }, 99 | @{ 100 | Event = 'PowerShell Summit 2024' 101 | Date = @{ 102 | Year = 2024 103 | Month = 4 104 | Day = 11 105 | } 106 | Location = @{ 107 | City = 'Bellevue' 108 | State = 'WA' 109 | } 110 | } 111 | ) 112 | } 113 | $arrayHt 114 | $arrayHt.Events 115 | 116 | # Array of hashtables 117 | $arrayOfHts = 2024..2030 | ForEach-Object { 118 | @{ 119 | Event = "PowerShell Summit $_" 120 | Date = @{ 121 | Year = $_ 122 | Month = 4 123 | Day = 10 124 | } 125 | Location = @{ 126 | City = 'Bellevue' 127 | State = 'WA' 128 | } 129 | } 130 | } 131 | $arrayOfHts 132 | 133 | # Sorting an array of hashtables by a nested property 134 | $arrayOfHts | Sort-Object -Property @{ Expression = { $_.Date.Year } } -Descending 135 | 136 | # From JSON 137 | $data = Get-Content '.\2024-04-10 - PSCustomObject`[`] vs Hashtables\MOCK_DATA.json' | ConvertFrom-Json -AsHashtable 138 | $data[0] 139 | 140 | # Any object type as a key 141 | $validHt = @{ 142 | $true = 'True' 143 | $false = 'False' 144 | 1 = 'One' 145 | 'One' = 1 146 | [datetime]::Now = 'Now' 147 | (Get-Process)[0] = (Get-Service)[0] 148 | } 149 | $validHt -------------------------------------------------------------------------------- /2024-04-10 - PSCustomObject[] vs Hashtables/01 - Create/PSCustomObject.ps1: -------------------------------------------------------------------------------- 1 | # Empty PSObject 2 | $obj = New-Object -TypeName PSObject 3 | $obj 4 | 5 | # Adding values (the hard way) 6 | $obj | Add-Member -MemberType NoteProperty -Name Event -Value 'PowerShell Summit 2024' 7 | $obj | Add-Member -MemberType NoteProperty -Name Date -Value '2024-04-10' 8 | $obj | Add-Member -MemberType NoteProperty -Name Location -Value 'Bellevue, WA' 9 | $obj 10 | 11 | # The easy way with a hashtable 12 | $obj = New-Object -TypeName PSObject -Property @{ 13 | Event = 'PowerShell Summit 2024' 14 | Date = '2024-04-10' 15 | Location = 'Bellevue, WA' 16 | } 17 | $obj 18 | 19 | # The easiest way 20 | $obj = [pscustomobject]@{ 21 | Event = 'PowerShell Summit 2024' 22 | Date = '2024-04-10' 23 | Location = 'Bellevue, WA' 24 | } 25 | $obj 26 | 27 | # Or even inline 28 | $obj = [pscustomobject]@{ Event = 'PowerShell Summit 2024'; Date = '2024-04-10'; Location = 'Bellevue, WA' } 29 | $obj 30 | 31 | # An array of PSCustomObjects 32 | $objs = 2024..2030 | ForEach-Object { 33 | [pscustomobject]@{ 34 | Event = "PowerShell Summit $_" 35 | Date = "$_-04-10" 36 | Location = 'Bellevue, WA' 37 | } 38 | } 39 | $objs 40 | 41 | # Sorting 42 | $objs | Sort-Object -Property Date -Descending 43 | 44 | # Nested properties 45 | $nestedObj = [pscustomobject]@{ 46 | Event = 'PowerShell Summit 2024' 47 | Date = [pscustomobject]@{ 48 | Year = 2024 49 | Month = 4 50 | Day = 10 51 | } 52 | Location = [pscustomobject]@{ 53 | City = 'Bellevue' 54 | State = 'WA' 55 | } 56 | } 57 | $nestedObj 58 | 59 | # From JSON 60 | $data = Get-Content '.\2024-04-10 - PSCustomObject`[`] vs Hashtables\MOCK_DATA.json' | ConvertFrom-Json 61 | $data[0] 62 | 63 | # Faster than hashtables? 64 | # Hashtable 65 | Measure-Command { 66 | 1..10 | ForEach-Object { Get-Content '.\2024-04-10 - PSCustomObject`[`] vs Hashtables\MOCK_DATA.json' | ConvertFrom-Json -AsHashtable } 67 | } | Select-Object TotalMilliseconds 68 | # PSCustomObject 69 | Measure-Command { 70 | 1..10 | ForEach-Object { Get-Content '.\2024-04-10 - PSCustomObject`[`] vs Hashtables\MOCK_DATA.json' | ConvertFrom-Json } 71 | } | Select-Object TotalMilliseconds -------------------------------------------------------------------------------- /2024-04-10 - PSCustomObject[] vs Hashtables/02 - Update/Hashtable.ps1: -------------------------------------------------------------------------------- 1 | # Update property using hashtable notation 2 | $ht['Location'] = 'Redmond, WA' 3 | $ht 4 | 5 | # Update property using dot notation 6 | $ht.Location = 'Seattle, WA' 7 | $ht 8 | 9 | # Add property using hashtable notation 10 | $ht['MilitaryTime'] = '0900' 11 | $ht 12 | 13 | # Add property using dot notation 14 | $ht.Time = '9:00 AM' 15 | $ht 16 | 17 | # Add property using Add method 18 | $ht.Add('TimeZone', 'PST') 19 | $ht 20 | 21 | # Remove property 22 | $ht.Remove('TimeZone') 23 | $ht 24 | 25 | # Finding properties 26 | $ht.Keys 27 | 28 | # Testing for a property 29 | $ht.Contains('Location') 30 | $ht.Keys -contains 'Location' 31 | 32 | # Which is more efficient? 33 | Measure-Command { 34 | 0..10000 | ForEach-Object { $ht.Contains('Location') } 35 | } 36 | Measure-Command { 37 | 0..10000 | ForEach-Object { $ht.Keys -contains 'Location' } 38 | } 39 | 40 | # Accessing a property from a variable 41 | $property = 'Location' 42 | $ht.$property 43 | $ht[$property] -------------------------------------------------------------------------------- /2024-04-10 - PSCustomObject[] vs Hashtables/02 - Update/PSCustomObject.ps1: -------------------------------------------------------------------------------- 1 | # Update property using dot notation 2 | $obj.Location = 'Seattle, WA' 3 | $obj 4 | 5 | # Can't add a property using dot notation 6 | $obj.MilitaryTime = '0900' 7 | $obj 8 | 9 | # Add property using Add-Member 10 | $obj | Add-Member -MemberType NoteProperty -Name MilitaryTime -Value '0900' 11 | $obj 12 | 13 | # Add property using Add-Member with a hashtable 14 | $obj | Add-Member -NotePropertyMembers @{ 15 | TimeZone = 'PST' 16 | Time = '9:00 AM' 17 | } 18 | $obj 19 | 20 | # .PSObject has metadata about the object 21 | $obj.PSObject 22 | 23 | # Add property using Add() method 24 | $obj.PSObject.Properties.Add([PSNoteProperty]::new('Address', '123 Main st')) 25 | $obj 26 | 27 | # Remove property 28 | $obj.PSObject.Properties.Remove('Address') 29 | $obj 30 | 31 | # Finding properties 32 | $obj.PSObject.Properties.Name 33 | $obj | Get-Member -MemberType NoteProperty | Select-Object -ExpandProperty Name 34 | 35 | # Testing for a property 36 | $obj.PSObject.Properties.Name -contains 'Location' 37 | $obj.PSObject.Properties['Location'] 38 | 39 | # Accessing a property from a variable 40 | $property = 'Location' 41 | $obj.$property 42 | $obj[$property] -------------------------------------------------------------------------------- /2024-04-10 - PSCustomObject[] vs Hashtables/03 - Output/Hashtable.ps1: -------------------------------------------------------------------------------- 1 | # Select multiple keys 2 | $ht['Event', 'Location'] 3 | 4 | # But doesn't really look great with an array of hashtables 5 | $arrayOfHts 6 | 7 | # Even with formatters 8 | $arrayOfHts | Format-List 9 | $arrayOfHts | Format-Table 10 | 11 | # JSON is good though 12 | # make sure you use -Depth if needed 13 | $ht | ConvertTo-Json 14 | $arrayOfHts | ConvertTo-Json 15 | 16 | # Select works (but doesn't in 5.1) 17 | $arrayOfHts | Select-Object Event 18 | 19 | # Writing to files 20 | $arrayOfHts | ConvertTo-Json | Out-File -FilePath .\output.json 21 | $arrayOfHts | Export-Csv -Path .\output.csv -NoTypeInformation 22 | 23 | # Elegant 24 | $arrayOfHts | ForEach-Object { 25 | [PSCustomObject]$_ 26 | } 27 | 28 | # or 29 | $arrayOfHts | ForEach-Object { 30 | New-Object -TypeName PSObject -Property $_ 31 | } 32 | 33 | # Which is more efficient? 34 | Measure-Command { 35 | 1..10000 | ForEach-Object { $arrayOfHts | ForEach-Object { 36 | [PSCustomObject]$_ 37 | } 38 | } 39 | } 40 | 41 | Measure-Command { 42 | 1..10000 | ForEach-Object { $arrayOfHts | ForEach-Object { 43 | New-Object -TypeName PSObject -Property $_ 44 | } 45 | } 46 | } -------------------------------------------------------------------------------- /2024-04-10 - PSCustomObject[] vs Hashtables/03 - Output/PSCustomObject.ps1: -------------------------------------------------------------------------------- 1 | # Simple 2 | $obj 3 | $obj | Format-Table 4 | $obj | Format-List 5 | 6 | # Arrays 7 | $objs 8 | $objs | Format-List 9 | $objs | Format-Table 10 | 11 | # Using Select-Object and a hash table to rename properties 12 | $objs | Select-Object Event, @{Name = 'Date'; Expression = { [datetime]$_.Date } }, Location 13 | 14 | # Output to various types 15 | $objs | ConvertTo-Json | Out-File -FilePath .\output.json 16 | $objs | Export-Csv -Path .\output.csv -NoTypeInformation 17 | 18 | # Output formats 19 | Get-Process | Select-Object -first 2 20 | Get-Service | Select-Object -first 2 21 | 22 | # Fancily add a type to a non-typed object 23 | Update-FormatData -AppendPath '.\2024-04-10 - PSCustomObject`[`] vs Hashtables\03 - Output\SampleView.ps1xml' 24 | $obj.PSObject.TypeNames.Insert(0, 'MyType') 25 | $obj.PSTypeNames.Insert(0, 'MyType') 26 | $obj 27 | 28 | # Convert to hashtable 29 | $hashtable = @{} 30 | foreach ( $prop in $obj.PSObject.Properties.Name ) { 31 | $hashtable[$prop] = $obj.$prop 32 | } 33 | $hashtable -------------------------------------------------------------------------------- /2024-04-10 - PSCustomObject[] vs Hashtables/03 - Output/SampleView.ps1xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | MyTypeView 6 | 7 | MyType 8 | 9 | 10 | 11 | 12 | 13 | 25 14 | left 15 | 16 | 17 | 18 | 25 19 | left 20 | 21 | 22 | 23 | 24 | 25 | 26 | Date 27 | 28 | 29 | Location 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /2024-04-10 - PSCustomObject[] vs Hashtables/04 - Iterate/Hashtable.ps1: -------------------------------------------------------------------------------- 1 | # Iterate over values in a hashtable using GetEnumerator 2 | $ht.GetEnumerator() | ForEach-Object { 3 | "Key: $($_.Key)" 4 | "Value: $($_.Value)" 5 | } 6 | 7 | # Iterate over values in a hashtable using Keys 8 | $ht.Keys | ForEach-Object { 9 | "Key: $_" 10 | "Value: $($ht[$_])" 11 | } 12 | 13 | # Which is faster? 14 | Measure-Command { 15 | 1..10000 | ForEach-Object { $ht.GetEnumerator() | ForEach-Object { 16 | "Key: $($_.Key)" 17 | "Value: $($_.Value)" 18 | } 19 | } 20 | } 21 | 22 | Measure-Command { 23 | 1..10000 | ForEach-Object { $ht.Keys | ForEach-Object { 24 | "Key: $_" 25 | "Value: $($ht[$_])" 26 | } 27 | } 28 | } 29 | 30 | # You can also use foreach 31 | foreach ($key in $ht.Keys) { 32 | "Key: $key" 33 | "Value: $($ht[$key])" 34 | } 35 | 36 | # Or a for loop, maybe? 37 | for ($x = 0; $x -lt $ht.Count; $x++) { 38 | "Key: $($ht.Keys[$x])" 39 | "Value: $($ht[$ht.Keys[$x]])" 40 | } 41 | 42 | # Just don't try to modify the hashtable while iterating 43 | foreach ($key in $ht.Keys) { 44 | Write-Host "Removing key $key..." 45 | $ht.Remove($key) 46 | } 47 | $ht 48 | 49 | # Iterate over an array of hashtables 50 | foreach ($item in $arrayOfHts) { 51 | "Event: $($item.Event)" 52 | "Date: $($item.Date)" 53 | "Location: $($item.Location)" 54 | } 55 | 56 | # Nested hashtable 57 | $veryNestedHt = [ordered]@{ 58 | a = [ordered]@{ 59 | a1 = @{ 60 | a2 = 'a3' 61 | b2 = 'b3' 62 | c2 = 'c3' 63 | } 64 | b1 = @{ 65 | a2 = 'a3' 66 | b2 = 'b3' 67 | c2 = 'c3' 68 | } 69 | c1 = @{ 70 | a2 = 'a3' 71 | b2 = 'b3' 72 | c2 = 'c3' 73 | } 74 | } 75 | b = [ordered]@{ 76 | a1 = @{ 77 | a2 = 'a3' 78 | b2 = 'b3' 79 | c2 = 'c3' 80 | } 81 | b1 = @{ 82 | a2 = 'a3' 83 | b2 = 'b3' 84 | c2 = 'c3' 85 | } 86 | c1 = @{ 87 | a2 = 'a3' 88 | b2 = 'b3' 89 | c2 = 'c3' 90 | } 91 | } 92 | c = [ordered]@{ 93 | a1 = @{ 94 | a2 = 'a3' 95 | b2 = 'b3' 96 | c2 = 'c3' 97 | } 98 | b1 = @{ 99 | a2 = 'a3' 100 | b2 = 'b3' 101 | c2 = 'c3' 102 | } 103 | c1 = @{ 104 | a2 = 'a3' 105 | b2 = 'b3' 106 | c2 = 'c3' 107 | } 108 | } 109 | } 110 | 111 | # Iterate over a very nested hashtable with long notation 112 | foreach ($key in $veryNestedHt.Keys) { 113 | "Key: $key" 114 | foreach ($subKey in $veryNestedHt[$key].Keys) { 115 | "SubKey: $subKey" 116 | foreach ($subSubKey in $veryNestedHt[$key][$subKey].Keys) { 117 | "SubSubKey: $subSubKey" 118 | "SubSubValue: $($veryNestedHt[$key][$subKey][$subSubKey])" 119 | } 120 | } 121 | } 122 | 123 | # Iterate over a very nested hashtable with shorter notation 124 | foreach ($key in $veryNestedHt.Keys) { 125 | "Key: $key" 126 | $nestedHt = $veryNestedHt[$key] 127 | foreach ($nestedHtKey in $nestedHt.Keys) { 128 | "NestedHtKey: $nestedHtKey" 129 | $nestedHt1 = $nestedHt[$nestedHtKey] 130 | foreach ($nestedHt1Key in $nestedHt1.Keys) { 131 | "NestedHt1Key: $nestedHt1Key" 132 | "NestedHt1Value: $($nestedHt1[$nestedHt1Key])" 133 | } 134 | } 135 | } -------------------------------------------------------------------------------- /2024-04-10 - PSCustomObject[] vs Hashtables/04 - Iterate/PSCustomObject.ps1: -------------------------------------------------------------------------------- 1 | # Foreach 2 | foreach ($item in $objs) { 3 | "Event: $($item.Event)" 4 | "Date: $($item.Date)" 5 | "Location: $($item.Location)" 6 | } 7 | 8 | # ForEach-Object 9 | $objs | ForEach-Object { 10 | "Event: $($_.Event)" 11 | "Date: $($_.Date)" 12 | "Location: $($_.Location)" 13 | } 14 | 15 | # For 16 | for ($x = 0; $x -lt $objs.Count; $x++) { 17 | "Event: $($objs[$x].Event)" 18 | "Date: $($objs[$x].Date)" 19 | "Location: $($objs[$x].Location)" 20 | } 21 | 22 | # While 23 | $x = 0 24 | while ($x -lt $objs.Count) { 25 | "Event: $($objs[$x].Event)" 26 | "Date: $($objs[$x].Date)" 27 | "Location: $($objs[$x].Location)" 28 | $x++ 29 | } 30 | 31 | # Iterate through the properties 32 | foreach ($property in $obj.PSObject.Properties) { 33 | "$($property.Name): $($property.Value)" 34 | } -------------------------------------------------------------------------------- /2024-04-10 - PSCustomObject[] vs Hashtables/Case Study/User Matching.ps1: -------------------------------------------------------------------------------- 1 | # Users from platform 1 2 | $users1 = Get-Content '.\2024-04-10 - PSCustomObject`[`] vs Hashtables\MOCK_DATA.json' | ConvertFrom-Json | Select-Object -First 5000 3 | 4 | # Users from platform 2 5 | # reversing the ID order to simulate a different platform 6 | $users2 = Get-Content '.\2024-04-10 - PSCustomObject`[`] vs Hashtables\MOCK_DATA.json' | ConvertFrom-Json | ForEach-Object { 7 | $_.id = 10001 - $_.id 8 | $_ 9 | } 10 | 11 | # Find matching users using Where-Object 12 | # Takes about 2-3m on my laptop 13 | Measure-Command { 14 | $report = foreach ($user1 in $users1) { 15 | $user2 = $users2 | Where-Object email -eq $user1.email | Select-Object -First 1 16 | if ($user2) { 17 | [PSCustomObject]@{ 18 | id1 = $user1.id 19 | id2 = $user2.id 20 | } 21 | } 22 | } 23 | } 24 | 25 | # Find matching users using PSObjects 26 | # Takes about 20-22s on my laptop 27 | Measure-Command { 28 | $report = foreach ($user1 in $users1) { 29 | foreach ($user2 in $users2) { 30 | # match on email 31 | if ($user1.email -eq $user2.email) { 32 | [PSCustomObject]@{ 33 | id1 = $user1.id 34 | id2 = $user2.id 35 | } 36 | break 37 | } 38 | } 39 | } 40 | } 41 | 42 | # What if we used a list and removed each item from users2 as we go? 43 | [System.Collections.Generic.List[psobject]]$users2list = Get-Content '.\2024-04-10 - PSCustomObject`[`] vs Hashtables\MOCK_DATA.json' | ConvertFrom-Json | ForEach-Object { 44 | $_.id = 10001 - $_.id 45 | $_ 46 | } 47 | Measure-Command { 48 | $report = foreach ($user1 in $users1) { 49 | for ($x = 0; $x -lt $users2list.Count; $x++) { 50 | $user2 = $users2list[$x] 51 | # match on email 52 | if ($user1.email -eq $user2.email) { 53 | [PSCustomObject]@{ 54 | id1 = $user1.id 55 | id2 = $user2.id 56 | } 57 | $users2list.RemoveAt($x) 58 | break 59 | } 60 | } 61 | } 62 | } 63 | 64 | # What about hashtables? 65 | @{ 66 | 'dhankins0@php.net' = @{ 67 | "id" = 1001 68 | "first_name" = "Dasie" 69 | "last_name" = "Hankins" 70 | "email" = "dhankins0@php.net" 71 | "title" = "Assistant Manager" 72 | "department" = "Engineering" 73 | } 74 | #... 75 | } 76 | 77 | $users1ht = @{} 78 | foreach ($user in $users1) { 79 | $users1ht[$user.email] = $user 80 | } 81 | 82 | $users2ht = @{} 83 | foreach ($user in $users2) { 84 | $users2ht[$user.email] = $user 85 | } 86 | 87 | # Find matching users using hashtables 88 | Measure-Command { 89 | $report = foreach ($email in $users1ht.Keys) { 90 | if ($users2ht.Contains($email)) { 91 | [PSCustomObject]@{ 92 | id1 = $users1ht[$email].id 93 | id2 = $users2ht[$email].id 94 | } 95 | } 96 | } 97 | } 98 | 99 | # Is it faster if we also remove keys as we use them? 100 | # Find matching users using hashtables 101 | Measure-Command { 102 | $report = foreach ($email in $users1ht.Keys) { 103 | if ($users2ht.Contains($email)) { 104 | [PSCustomObject]@{ 105 | id1 = $users1ht[$email].id 106 | id2 = $users2ht[$email].id 107 | } 108 | $users2ht.Remove($email) 109 | } 110 | } 111 | } 112 | 113 | # Alternate method using -contains 114 | # ~2-3s 115 | Measure-Command { 116 | $report = foreach ($email in $users1ht.Keys) { 117 | if ($users2ht.Keys -contains $email) { 118 | [PSCustomObject]@{ 119 | id1 = $users1ht[$email].id 120 | id2 = $users2ht[$email].id 121 | } 122 | } 123 | } 124 | } -------------------------------------------------------------------------------- /2024-04-10 - PSCustomObject[] vs Hashtables/PowerShell Summit 2024 - PSCustomObjects vs Hashtables.pptx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ThePoShWolf/Sessions/ddaac2096083e091ba8175375560052451555120/2024-04-10 - PSCustomObject[] vs Hashtables/PowerShell Summit 2024 - PSCustomObjects vs Hashtables.pptx -------------------------------------------------------------------------------- /2024-04-10 - PSCustomObject[] vs Hashtables/README.md: -------------------------------------------------------------------------------- 1 | # Additional links 2 | 3 | - **Recording**: https://www.youtube.com/watch?v=DtMEI4xXC8w -------------------------------------------------------------------------------- /2025-01-08 - From Pets to Cattle Learning to let go and automate/Summit 2025 - From Pets to Cattle.pptx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ThePoShWolf/Sessions/ddaac2096083e091ba8175375560052451555120/2025-01-08 - From Pets to Cattle Learning to let go and automate/Summit 2025 - From Pets to Cattle.pptx -------------------------------------------------------------------------------- /2025-04-09 - Getting started with dev containers/01 - Using a template/README.md: -------------------------------------------------------------------------------- 1 | 1. Dev Containers: Add Dev Container Configuration Files... 2 | 2. Dev Containers: Reopen in Container -------------------------------------------------------------------------------- /2025-04-09 - Getting started with dev containers/02 - Edited PowerShell only config/.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | // For format details, see https://aka.ms/devcontainer.json. For config options, see the 2 | // README at: https://github.com/devcontainers/templates/tree/main/src/powershell 3 | { 4 | "name": "PowerShell", 5 | // Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile 6 | "image": "mcr.microsoft.com/powershell:debian-12", 7 | // https://containers.dev/features 8 | "features": { 9 | "ghcr.io/devcontainers/features/common-utils:2": {} 10 | }, 11 | "postCreateCommand": "pwsh -c \\$PSVersionTable", 12 | // Configure tool-specific properties. 13 | "customizations": { 14 | // Configure properties specific to VS Code. 15 | // https://containers.dev/supporting#visual-studio-code 16 | "vscode": { 17 | // Set *default* container specific settings.json values on container create. 18 | "settings": { 19 | "terminal.integrated.defaultProfile.linux": "pwsh" 20 | }, 21 | // Add the IDs of extensions you want installed when the container is created. 22 | "extensions": [ 23 | "ms-vscode.powershell" 24 | ] 25 | } 26 | } 27 | // Use 'forwardPorts' to make a list of ports inside the container available locally. 28 | // "forwardPorts": [], 29 | // Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root. 30 | // "remoteUser": "root" 31 | } -------------------------------------------------------------------------------- /2025-04-09 - Getting started with dev containers/03 - Adding PowerShell to config/.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | // For format details, see https://aka.ms/devcontainer.json. For config options, see the 2 | // README at: https://github.com/devcontainers/templates/tree/main/src/go 3 | { 4 | "name": "Go", 5 | // Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile 6 | "image": "mcr.microsoft.com/devcontainers/go:dev-1.23-bookworm", 7 | // Features to add to the dev container. More info: https://containers.dev/features. 8 | "features": { 9 | "ghcr.io/devcontainers/features/powershell:1": { 10 | "version": 7.5 11 | } 12 | }, 13 | // Configure tool-specific properties. 14 | // https://containers.dev/supporting#visual-studio-code 15 | "customizations": { 16 | "vscode": { 17 | "extensions": [ 18 | "golang.go" 19 | ], 20 | "settings": { 21 | "editor.formatOnSave": true, 22 | "terminal.integrated.defaultProfile.linux": "pwsh" 23 | } 24 | } 25 | }, 26 | // Use 'postCreateCommand' to run commands after the container is created. 27 | "postCreateCommand": "go version && pwsh -C \\$PSVersionTable" 28 | // Use 'forwardPorts' to make a list of ports inside the container available locally. 29 | // "forwardPorts": [9000], 30 | // Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root. 31 | // "remoteUser": "root" 32 | } -------------------------------------------------------------------------------- /2025-04-09 - Getting started with dev containers/03 - Adding PowerShell to config/go.mod: -------------------------------------------------------------------------------- 1 | module theposhwolf/helloWorld 2 | 3 | go 1.23.4 4 | -------------------------------------------------------------------------------- /2025-04-09 - Getting started with dev containers/03 - Adding PowerShell to config/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | ) 7 | 8 | func helloWorld(w http.ResponseWriter, req *http.Request) { 9 | fmt.Println("/helloWorld called") 10 | fmt.Fprintf(w, "Hello World\n") 11 | } 12 | 13 | func main() { 14 | http.HandleFunc("/helloWorld", helloWorld) 15 | fmt.Println("Server launching on port 8080...") 16 | http.ListenAndServe(":8080", nil) 17 | } 18 | -------------------------------------------------------------------------------- /2025-04-09 - Getting started with dev containers/04 - Integrating multiple containers/networkSetup.ps1: -------------------------------------------------------------------------------- 1 | # input the id or name of each container 2 | # can get values by running docker ps 3 | # or in the container by running hostname 4 | $apiContainer = '45b17ea9a67a' 5 | $psContainer = '367429cdd36a' 6 | $networkName = 'pssummit' 7 | 8 | # create the network 9 | docker network create $networkName 10 | 11 | # connect both containers to the network 12 | docker network connect $networkName $apiContainer 13 | docker network connect $networkName $psContainer 14 | 15 | # examine the network 16 | docker network inspect $networkName 17 | 18 | # from inside the container, make an API call 19 | Invoke-RestMethod "http://$apiContainer`:8080/helloWorld" -------------------------------------------------------------------------------- /2025-04-09 - Getting started with dev containers/Summit 2025 - Getting Started With Dev Containers.pptx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ThePoShWolf/Sessions/ddaac2096083e091ba8175375560052451555120/2025-04-09 - Getting started with dev containers/Summit 2025 - Getting Started With Dev Containers.pptx -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # The PoShWolf Sessions 2 | 3 | Hello! This is the collection of presentations that I've either given or am working on. If you found this repository it is likely because you followed a link you caught on a slide of mine. Welcome! 4 | 5 | ## License 6 | 7 | You are welcome to use anything you find in this repository! Just be sure to familiarize yourself with the [CC-BY-SA-4.0](https://choosealicense.com/licenses/cc-by-sa-4.0/) license. It doesn't matter to me what you use this content for, all you need to do is to license it with the same license and attribute me. 8 | 9 | ## Dates 10 | 11 | All dates are listed in [ISO 8601](https://en.wikipedia.org/wiki/ISO_8601) format. So 2020-01-03 is January 3, 2020. --------------------------------------------------------------------------------