Recent Discussions
Unleashing Parallelism in PowerShell
This blog on my site got a lot of possitive feedback. Hopefully it can help others as well hence I share it here. If you want to check more check my blog on: https://bartpasmans.tech/ or check my LinkedIn: https://www.linkedin.com/in/bart-pasmans-6533094b/ Unleashing Parallelism in PowerShell If you’ve been around PowerShell for a while, you know it’s great at looping through stuff. Need to process a list of files? Easy. Query a set of servers? Piece of cake. But sometimes… you hit a wall. The problem? Sequential execution. By default, your loops work in a single-file line, like shoppers at a small-town bakery. That’s fine for five people. Not so fine for five thousand. That’s where ForEach-Object -Parallel enters the chat. 🚀 This feature, introduced in PowerShell 7, lets you process multiple items at the same time, tapping into all those CPU cores just sitting there looking bored. Today, we’re going to walk through how it works, why it’s a game-changer, and where you should (and shouldn’t) use it. We’ll keep it hands-on, so keep PowerShell open! you’ll see the 🎬 icon whenever it’s time to get your hands dirty. Today’s Toolbelt Here’s the deal: ForEach-Object -Parallel is like the express checkout lane at the grocery store. Instead of every task waiting for the one ahead to finish, they all get their own lane. We’ll explore: Basic parallel loops – Using ForEach-Object -Parallel in the simplest form Passing variables – How to get data into your parallel script block Controlling thread counts – Because unlimited parallelism can get… messy Real-world scenarios – Places where it shines (and where it doesn’t) Why Parallel? Imagine you have 50 servers to check for a certain log file. Running them one after another takes… forever. With parallel processing, you can hit multiple servers at once, finishing in a fraction of the time. 📒 Under the hood: PowerShell spins up runspaces, lightweight, isolated environment, to execute each chunk of work simultaneously. It’s not “true” OS-level multithreading, but it’s incredibly efficient for I/O-bound tasks like network calls, file reads, or API requests. 1..5 | ForEach-Object -Parallel { Start-Sleep -Seconds 1 "Task $_ completed on thread $([System.Threading.Thread]::CurrentThread.ManagedThreadId)" } What’s happening: 1..5 gives us five items. Each item is processed in a parallel runspace. They all sleep for one second…. 🥁🥁🥁 but because they run in parallel, the whole thing finishes in just over a second, not five! My previous blog: https://bartpasmans.tech/start-scripting-like-a-pro-6-speeding-up-your-code/ I showed you how to make your own threads and consume them. Check it out! 😊 This blog sticks with PowerShell cmdlets natively. Passing Data Into Parallel Blocks One catch with ForEach-Object -Parallel: it runs in its own scope. Your outer variables aren’t magically available inside. 🎬 Here’s how to pass variables in: $prefix = "Server" 1..3 | ForEach-Object -Parallel { "$using:prefix-$($_)" } 📒 The magic word: $using: tells PowerShell to bring in a variable from outside the parallel block. Controlling the Chaos Yes, parallelism is powerful, but if you let 200 jobs spin up at once, you might as well be starting a tiny CPU apocalypse 💣. 🎬 Throttle it with 1..10 | ForEach-Object -Parallel { Start-Sleep -Seconds 2 "Processed $_" } -ThrottleLimit 3 Here, only three parallel tasks run at a time. As soon as one finishes, the next starts. Real-World Example: Network Ping 🎬 Checking multiple hosts in parallel: $servers = "server1","server2","server3","server4","server5" $servers | ForEach-Object -Parallel { $result = Test-Connection -ComputerName $_ -Count 1 -Quiet "$_ is " + ($(if ($result) { "online" } else { "offline" })) } -ThrottleLimit 2 The pings happen two at a time. Overall time drops drastically compared to running sequentially. When Not to Use It 📒 Parallelism is not a free lunch. If your task is super short and light (like adding numbers), spinning up runspaces is actually slower. For CPU-heavy operations, you might saturate your system quickly. Avoid it when order matters—parallel execution doesn’t guarantee output order unless you take extra steps. Summary Wrapping It Up! PowerShell Meets Parallelism 🎉 Today we saw how ForEach-Object -Parallel lets you take PowerShell’s already-great iteration abilities and put them into warp speed. We covered: The basics of parallel loops Passing variables with $using: Throttling to keep your system happy Real-world use cases like pinging servers The takeaway? When you’re faced with a big list of time-consuming tasks, don’t just wait your turn in the single checkout lane. Open more lanes with ForEach-Object -Parallel! Just remember that more isn’t always better. Got your own clever use for parallel loops? Share it, I love seeing how people bend PowerShell to their will. Until next time! Keep automating, keep experimenting, and keep pushing your scripts to the next level. 🚀☕🍰8Views0likes0CommentsBug: Invoke-MgGraphRequest not respecting ErrorAction.
Hi folks, This is a brief callout that Invoke-MgGraphRequest is not currently respecting the ErrorAction parameter. Rather, it's hardwired to throwing an exception as if ErrorAction:Stop had been provided. If you're like me and typically use ErrorAction:Stop in a try/catch block then you won't be impacted, but if use another value like Continue (the default) or SilentlyContinue, you may find this breaks your automation. Example Hopefully this is addressed in a future version of the Microsoft.Graph.Authentication module. Cheers, Lain29Views0likes0CommentsHow to: Finding large directories/recovering lost space.
Hi folks, Every once in a blue moon I need to figure out where a disk's free space has disappeared to. There's boatloads of free tools that do this via a GUI but I felt like a basic PowerShell solution I can use in other contexts. Here's the simple module I wrote as well as some basic examples on how it can be leveraged in a standalone context. Props to anyone who spots the Easter egg. Module: XTree.psm1 function Get-DirectorySize { [cmdletbinding()] param( [parameter(Mandatory=$true)][ValidateNotNull()][string] $Path ) Write-Verbose -Message "Parsing $Path"; $Summary = [PSCustomObject] @{ Path = $Path.ToLowerInvariant(); Count = 0; Size = 0; } [System.IO.Directory]::EnumerateFiles($Path) | ForEach-Object { [System.IO.FileInfo]::new($_) | ForEach-Object { $Summary.Count++; $Summary.Size += $_.Length; } } $Summary; } function Get-DirectoryTreeSize { [cmdletbinding()] param( [parameter(Mandatory=$true)][ValidateNotNull()][string] $Path ) # Reference: https://learn.microsoft.com/en-us/dotnet/api/system.io.fileattributes?view=netframework-4.8.1 New-Variable -Name "ReparsePoint" -Value ([System.IO.FileAttributes]::ReparsePoint.value__) -Option Constant; #region Create a new output object with default values. $Summary = [PSCustomObject] @{ Path = $Path.ToLowerInvariant(); Count = 0; Size = 0; TotalCount = 0; TotalSize = 0; } #endregion #region Make any recursive calls first. [System.IO.Directory]::EnumerateDirectories($Path) | ForEach-Object { # We do not want to process reparse points. if (0 -eq (([System.IO.DirectoryInfo]::new($_).Attributes.value__ -band $ReparsePoint))) { Get-DirectoryTreeSize -Path $_ | ForEach-Object { $Summary.TotalCount += $_.Count; $Summary.TotalSize += $_.Size; $_; } } } #endregion #region Now, process and output the current directory. $Stats = Get-DirectorySize -Path $Path; $Summary.Count = $Stats.Count; $Summary.Size = $Stats.Size; $Summary.TotalCount += $Stats.Count; $Summary.TotalSize += $Stats.Size; $Summary; #endregion } Export-ModuleMember -Function @( "Get-DirectorySize" , "Get-DirectoryTreeSize" ); Example 1: Selecting the top five consumers by TotalSize This sort method is most useful for getting a high-level overview of a large directory structure. Get-DirectoryTreeSize -Path "D:\Data\Temp\Edge\windows" | Sort-Object -Property TotalSize -Descending | Select-Object -First 5 | Format-Table -AutoSize -Property TotalSize, TotalCount, Path; Example 2: Selecting the top five consumers by Size This sort method is more useful where you're looking for large individual directories. Get-DirectoryTreeSize -Path "D:\Data\Temp\Edge\windows" | Sort-Object -Property Size -Descending | Select-Object -First 5 | Format-Table -AutoSize -Property Size, Count, Path; Example output from both examples Additional information We do not want to process reparse points because: If the reference points to within the structure then we end up counting the same files twice, which is misleading; If the reference points outside the structure then it shouldn't be counted as contributing within the structure. I've used the native .NET class [System.IO.Directory] in lieu of the PowerShell-native Get-ChildItem as it's more efficient in a few scenarios - both in execution and coding effort. Get-ChildItem also errors out on certain reparse points in Windows PowerShell, which you can test for yourself using: Get-ChildItem -Directory -Force -Path "$env:USERPROFILE\Application Data\"; Cheers, Lain61Views1like0CommentsGet DGs for a specific person from EXO
Having trouble with my script working right. Tried several scenarios. Looking for a GUI version. Attached are the two PS1's. I can connect to EXO but the GUI script won't produce anything ot just locks up Connect EXO: Connect-ExchangeOnLine This is straight forward and works but I cannot get it to work with the script below in an all in one script ------------------- <# Get-UserDGs-GUI.ps1 Author: You + ChatGPT Purpose: GUI to retrieve a user's Distribution Groups (member/owner) from Exchange Online. #> #region Preconditions if ($Host.Runspace.ApartmentState -ne 'STA') { Write-Warning "Re-running in STA mode..." Start-Process powershell.exe "-NoLogo -NoProfile -ExecutionPolicy Bypass -STA -File `"$PSCommandPath`"" -Verb RunAs exit } Add-Type -AssemblyName System.Windows.Forms Add-Type -AssemblyName System.Drawing [System.Windows.Forms.Application]::EnableVisualStyles() #endregion #region Helper Functions function Ensure-ExchangeModule { if (-not (Get-Module ExchangeOnlineManagement -ListAvailable)) { Write-Host "Installing ExchangeOnlineManagement..." Install-Module ExchangeOnlineManagement -Scope CurrentUser -Force -ErrorAction Stop } Import-Module ExchangeOnlineManagement -ErrorAction Stop } function Test-EXOConnection { try { # Fast no-op cmdlet to see if session works; adjust if needed Get-OrganizationConfig -ErrorAction Stop | Out-Null return $true } catch { return $false } } function Connect-EXO { param( [string]$AdminUpn ) if (Test-EXOConnection) { return $true } $connectParams = @{} if ($AdminUpn) { $connectParams.UserPrincipalName = $AdminUpn } try { Connect-ExchangeOnline @connectParams -ShowProgress $false -ErrorAction Stop | Out-Null return $true } catch { [System.Windows.Forms.MessageBox]::Show("Connection failed:`r`n$($_.Exception.Message)","Error", [System.Windows.Forms.MessageBoxButtons]::OK,[System.Windows.Forms.MessageBoxIcon]::Error) | Out-Null return $false } } function Disconnect-EXO-Safe { try { Disconnect-ExchangeOnline -Confirm:$false -ErrorAction SilentlyContinue | Out-Null } catch {} } function Get-UserDGData { param( [string]$TargetUpn, [bool]$IncludeOwner, [bool]$IncludeDynamic ) $resultTable = New-Object System.Data.DataTable "Groups" "DisplayName","PrimarySmtpAddress","ManagedBy","RecipientTypeDetails","MembershipType","IsDynamic" | ForEach-Object { [void]$resultTable.Columns.Add($_) } try { # DistinguishedName for membership filter $dn = (Get-User $TargetUpn -ErrorAction Stop).DistinguishedName # Member DGs $memberDgs = Get-DistributionGroup -ResultSize Unlimited -Filter "Members -eq '$dn'" -ErrorAction SilentlyContinue foreach ($dg in $memberDgs) { $managedBy = ($dg.ManagedBy | ForEach-Object { $_.Name }) -join '; ' $row = $resultTable.NewRow() $row.DisplayName = $dg.DisplayName $row.PrimarySmtpAddress = $dg.PrimarySmtpAddress $row.ManagedBy = $managedBy $row.RecipientTypeDetails = $dg.RecipientTypeDetails $row.MembershipType = "Member" $row.IsDynamic = if ($dg.RecipientTypeDetails -match 'Dynamic') {'Yes'} else {'No'} $resultTable.Rows.Add($row) } # Owner DGs if ($IncludeOwner) { $ownerDgs = Get-DistributionGroup -ResultSize Unlimited | Where-Object { ($_.ManagedBy -contains $dn) } foreach ($dg in $ownerDgs) { $managedBy = ($dg.ManagedBy | ForEach-Object { $_.Name }) -join '; ' $row = $resultTable.NewRow() $row.DisplayName = $dg.DisplayName $row.PrimarySmtpAddress = $dg.PrimarySmtpAddress $row.ManagedBy = $managedBy $row.RecipientTypeDetails = $dg.RecipientTypeDetails $row.MembershipType = "Owner" $row.IsDynamic = if ($dg.RecipientTypeDetails -match 'Dynamic') {'Yes'} else {'No'} $resultTable.Rows.Add($row) } } # Dynamic DG hit test (optional) if ($IncludeDynamic) { $dynamicHits = foreach ($ddg in Get-DynamicDistributionGroup -ResultSize Unlimited) { $filter = $ddg.RecipientFilter $ou = $ddg.RecipientContainer $match = Get-Recipient -ResultSize Unlimited -RecipientPreviewFilter $filter -OrganizationalUnit $ou | Where-Object { $_.PrimarySmtpAddress -ieq $TargetUpn } if ($match) { $ddg } } foreach ($dg in $dynamicHits) { # Avoid duplicates if already in table if (-not $resultTable.Select("PrimarySmtpAddress = '$($dg.PrimarySmtpAddress)' AND MembershipType='Member'").Count) { $row = $resultTable.NewRow() $row.DisplayName = $dg.DisplayName $row.PrimarySmtpAddress = $dg.PrimarySmtpAddress $row.ManagedBy = ($dg.ManagedBy | ForEach-Object { $_.Name }) -join '; ' $row.RecipientTypeDetails = $dg.RecipientTypeDetails $row.MembershipType = "Member (Dynamic Match)" $row.IsDynamic = "Yes" $resultTable.Rows.Add($row) } } } return $resultTable } catch { throw $_ } } #endregion #region GUI Build # Colors $colorBg = [System.Drawing.Color]::FromArgb(35,45,60) $colorPanel = [System.Drawing.Color]::FromArgb(50,60,80) $colorAccent = [System.Drawing.Color]::FromArgb(106,176,222) $colorText = [System.Drawing.Color]::White $fontMain = New-Object System.Drawing.Font("Segoe UI",10) $fontSmall = New-Object System.Drawing.Font("Segoe UI",7) $form = New-Object System.Windows.Forms.Form $form.Text = "Exchange Online - Distribution Groups Lookup" $form.StartPosition = "CenterScreen" $form.Size = New-Object System.Drawing.Size(1000,650) $form.BackColor = $colorBg $form.Font = $fontMain # Top panel $panelTop = New-Object System.Windows.Forms.Panel $panelTop.Dock = 'Top' $panelTop.Height = 120 $panelTop.BackColor = $colorPanel $form.Controls.Add($panelTop) # Labels / Inputs $lblAdminUpn = New-Object System.Windows.Forms.Label $lblAdminUpn.Text = "Admin UPN (for Connect):" $lblAdminUpn.ForeColor = $colorText $lblAdminUpn.Location = "20,15" $lblAdminUpn.AutoSize = $true $panelTop.Controls.Add($lblAdminUpn) $txtAdminUpn = New-Object System.Windows.Forms.TextBox $txtAdminUpn.Location = "220,12" $txtAdminUpn.Width = 250 $panelTop.Controls.Add($txtAdminUpn) $btnConnect = New-Object System.Windows.Forms.Button $btnConnect.Text = "Connect" $btnConnect.Location = "490,10" $btnConnect.Width = 100 $btnConnect.BackColor = $colorAccent $btnConnect.FlatStyle = 'Flat' $btnConnect.ForeColor = [System.Drawing.Color]::Black $panelTop.Controls.Add($btnConnect) $lblTargetUpn = New-Object System.Windows.Forms.Label $lblTargetUpn.Text = "Target User UPN:" $lblTargetUpn.ForeColor = $colorText $lblTargetUpn.Location = "20,50" $lblTargetUpn.AutoSize = $true $panelTop.Controls.Add($lblTargetUpn) $txtTargetUpn = New-Object System.Windows.Forms.TextBox $txtTargetUpn.Location = "220,47" $txtTargetUpn.Width = 250 $panelTop.Controls.Add($txtTargetUpn) $chkOwner = New-Object System.Windows.Forms.CheckBox $chkOwner.Text = "Include groups where user is OWNER" $chkOwner.ForeColor = $colorText $chkOwner.Location = "490,48" $chkOwner.Width = 260 $panelTop.Controls.Add($chkOwner) $chkDynamic = New-Object System.Windows.Forms.CheckBox $chkDynamic.Text = "Check Dynamic DG membership (slow)" $chkDynamic.ForeColor = $colorText $chkDynamic.Location = "490,70" $chkDynamic.Width = 260 $panelTop.Controls.Add($chkDynamic) $btnGet = New-Object System.Windows.Forms.Button $btnGet.Text = "Get Groups" $btnGet.Location = "770,44" $btnGet.Width = 160 $btnGet.Height = 40 $btnGet.BackColor = $colorAccent $btnGet.FlatStyle = 'Flat' $btnGet.ForeColor = [System.Drawing.Color]::Black $panelTop.Controls.Add($btnGet) # Grid $grid = New-Object System.Windows.Forms.DataGridView $grid.Dock = 'Fill' $grid.ReadOnly = $true $grid.AutoSizeColumnsMode = 'Fill' $grid.BackgroundColor = $colorBg $grid.ForeColor = [System.Drawing.Color]::Black $grid.EnableHeadersVisualStyles = $false $grid.ColumnHeadersDefaultCellStyle.BackColor = $colorAccent $grid.ColumnHeadersDefaultCellStyle.ForeColor = [System.Drawing.Color]::Black $grid.RowHeadersVisible = $false $form.Controls.Add($grid) # Bottom bar $panelBottom = New-Object System.Windows.Forms.Panel $panelBottom.Dock = 'Bottom' $panelBottom.Height = 70 $panelBottom.BackColor = $colorPanel $form.Controls.Add($panelBottom) $btnExport = New-Object System.Windows.Forms.Button $btnExport.Text = "Export CSV" $btnExport.Location = "20,15" $btnExport.Width = 110 $btnExport.BackColor = $colorAccent $btnExport.FlatStyle = 'Flat' $btnExport.ForeColor = [System.Drawing.Color]::Black $panelBottom.Controls.Add($btnExport) $btnCopy = New-Object System.Windows.Forms.Button $btnCopy.Text = "Copy to Clipboard" $btnCopy.Location = "140,15" $btnCopy.Width = 140 $btnCopy.BackColor = $colorAccent $btnCopy.FlatStyle = 'Flat' $btnCopy.ForeColor = [System.Drawing.Color]::Black $panelBottom.Controls.Add($btnCopy) $btnClear = New-Object System.Windows.Forms.Button $btnClear.Text = "Clear" $btnClear.Location = "290,15" $btnClear.Width = 90 $btnClear.BackColor = $colorAccent $btnClear.FlatStyle = 'Flat' $btnClear.ForeColor = [System.Drawing.Color]::Black $panelBottom.Controls.Add($btnClear) $btnDisconnect = New-Object System.Windows.Forms.Button $btnDisconnect.Text = "Disconnect" $btnDisconnect.Location = "390,15" $btnDisconnect.Width = 110 $btnDisconnect.BackColor = $colorAccent $btnDisconnect.FlatStyle = 'Flat' $btnDisconnect.ForeColor = [System.Drawing.Color]::Black $panelBottom.Controls.Add($btnDisconnect) $chkAutoDisc = New-Object System.Windows.Forms.CheckBox $chkAutoDisc.Text = "Auto-disconnect on close" $chkAutoDisc.ForeColor = $colorText $chkAutoDisc.Location = "520,20" $chkAutoDisc.Width = 180 $panelBottom.Controls.Add($chkAutoDisc) $statusLabel = New-Object System.Windows.Forms.Label $statusLabel.Text = "Ready." $statusLabel.ForeColor = $colorText $statusLabel.AutoSize = $true $statusLabel.Location = "720,22" $panelBottom.Controls.Add($statusLabel) # Footer $lblFooter = New-Object System.Windows.Forms.Label $lblFooter.Text = "Interactive Form Created By: Mark Snyder - All Rights Reserved!" $lblFooter.ForeColor = $colorText $lblFooter.Font = $fontSmall $lblFooter.AutoSize = $true $lblFooter.Location = New-Object System.Drawing.Point(20, $panelBottom.Top - 20) $form.Controls.Add($lblFooter) #endregion #region UI Logic $currentTable = $null function Set-Status { param([string]$msg) $statusLabel.Text = $msg [System.Windows.Forms.Application]::DoEvents() } $btnConnect.Add_Click({ Set-Status "Connecting..." Ensure-ExchangeModule if (Connect-EXO -AdminUpn $txtAdminUpn.Text) { Set-Status "Connected." } else { Set-Status "Not connected." } }) $btnGet.Add_Click({ if (-not $txtTargetUpn.Text.Trim()) { [System.Windows.Forms.MessageBox]::Show("Please enter the Target User UPN.","Missing Info", [System.Windows.Forms.MessageBoxButtons]::OK,[System.Windows.Forms.MessageBoxIcon]::Warning) | Out-Null return } Set-Status "Working..." $btnGet.Enabled = $false $btnGet.Text = "Working..." [System.Windows.Forms.Application]::DoEvents() Ensure-ExchangeModule if (-not (Test-EXOConnection)) { if (-not (Connect-EXO -AdminUpn $txtAdminUpn.Text)) { Set-Status "Connection failed." $btnGet.Enabled = $true $btnGet.Text = "Get Groups" return } } try { $table = Get-UserDGData -TargetUpn $txtTargetUpn.Text.Trim() -IncludeOwner $chkOwner.Checked -IncludeDynamic $chkDynamic.Checked $currentTable = $table $grid.DataSource = $currentTable Set-Status ("Retrieved {0} group(s)." -f $currentTable.Rows.Count) } catch { [System.Windows.Forms.MessageBox]::Show("Error retrieving data:`r`n$($_.Exception.Message)","Error", [System.Windows.Forms.MessageBoxButtons]::OK,[System.Windows.Forms.MessageBoxIcon]::Error) | Out-Null Set-Status "Error." } finally { $btnGet.Enabled = $true $btnGet.Text = "Get Groups" } }) $btnExport.Add_Click({ if (-not $currentTable -or $currentTable.Rows.Count -eq 0) { [System.Windows.Forms.MessageBox]::Show("Nothing to export.","Info",[System.Windows.Forms.MessageBoxButtons]::OK,[System.Windows.Forms.MessageBoxIcon]::Information) | Out-Null return } $sfd = New-Object System.Windows.Forms.SaveFileDialog $sfd.Filter = "CSV (*.csv)|*.csv" $sfd.FileName = "UserDGs.csv" if ($sfd.ShowDialog() -eq [System.Windows.Forms.DialogResult]::OK) { try { $currentTable | Export-Csv -NoTypeInformation -Path $sfd.FileName -Encoding UTF8 Set-Status "Saved to $($sfd.FileName)" } catch { [System.Windows.Forms.MessageBox]::Show("Export failed: $($_.Exception.Message)","Error", [System.Windows.Forms.MessageBoxButtons]::OK,[System.Windows.Forms.MessageBoxIcon]::Error) | Out-Null } } }) $btnCopy.Add_Click({ if (-not $currentTable -or $currentTable.Rows.Count -eq 0) { [System.Windows.Forms.MessageBox]::Show("Nothing to copy.","Info",[System.Windows.Forms.MessageBoxButtons]::OK,[System.Windows.Forms.MessageBoxIcon]::Information) | Out-Null return } $string = $currentTable | ConvertTo-Csv -NoTypeInformation | Out-String [System.Windows.Forms.Clipboard]::SetText($string) # Small toast-ish popup $popup = New-Object System.Windows.Forms.Form $popup.FormBorderStyle = 'None' $popup.StartPosition = 'Manual' $popup.BackColor = $colorAccent $popup.Size = New-Object System.Drawing.Size(200,60) $popup.TopMost = $true $popup.ShowInTaskbar = $false $popup.Location = New-Object System.Drawing.Point(($form.Location.X + $form.Width - 220), ($form.Location.Y + 40)) $lbl = New-Object System.Windows.Forms.Label $lbl.Text = "Copied to clipboard!" $lbl.AutoSize = $false $lbl.TextAlign = 'MiddleCenter' $lbl.Dock = 'Fill' $lbl.Font = New-Object System.Drawing.Font("Segoe UI",10,[System.Drawing.FontStyle]::Bold) $popup.Controls.Add($lbl) $popup.Show() $timer = New-Object System.Windows.Forms.Timer $timer.Interval = 1200 $timer.Add_Tick({ $timer.Stop(); $popup.Close(); $popup.Dispose() }) $timer.Start() }) $btnClear.Add_Click({ $grid.DataSource = $null $currentTable = $null Set-Status "Cleared." }) $btnDisconnect.Add_Click({ Disconnect-EXO-Safe Set-Status "Disconnected." }) $form.Add_FormClosing({ if ($chkAutoDisc.Checked) { Disconnect-EXO-Safe } else { $res = [System.Windows.Forms.MessageBox]::Show("Disconnect from Exchange Online now?","Disconnect?", [System.Windows.Forms.MessageBoxButtons]::YesNoCancel,[System.Windows.Forms.MessageBoxIcon]::Question) if ($res -eq [System.Windows.Forms.DialogResult]::Cancel) { $_.Cancel = $true } elseif ($res -eq [System.Windows.Forms.DialogResult]::Yes) { Disconnect-EXO-Safe } } }) #endregion [void]$form.ShowDialog()37Views0likes0CommentsInstall-Package - failed to be installed: End of Central Directory record could not be found.
Hi all, Since last week I've had multiple errors in my pipelines when trying to install NuGet packages: Install-Package -Name Microsoft.PowerBi.Api -Source MyNuGet -ProviderName NuGet -Scope CurrentUser -RequiredVersion 3.18.1 -SkipDependencies This seems to be affecting multiple packages: Install-Package : Package Newtonsoft.Json failed to be installed because: End of Central Directory record could not be found. Install-Package : Package Microsoft.Rest.ClientRuntime failed to be installed because: End of Central Directory record could not be found. Install-Package : Package Microsoft.PowerBI.Api failed to be installed because: End of Central Directory record could not be found. When downloading the package I don't see any errors using nuget verify. I get these errors in microsoft hosted agents in ADO pipelines, on my laptop, or any VM I use. Doesn't seem to be related to PS or OS version or any proxies/firewalls. Any ideas? Thank you345Views1like0CommentsPowerShell Not Creating Smartsheet Row as Expected
BACKGROUND: I created a PowerShell script that reads a Word document, extracts fields, and then creates a row on a Smartsheet with the data from that Word document...but the row created was blank, even though it showed success in PowerShell (ID's replaced with asterisks). What could I be missing? Best, Chris Hallo | email address removed for privacy reasons FROM POWERSHELL: Results: Post row to Smartsheet? (Y/N): Y Posting row to Smartsheet... ✅ Row added. Response: message : SUCCESS resultCode : 0 version : 13580 result : @{id=*; sheetId=*; rowNumber=1; expanded=True; locked=False; lockedForUser=False; createdAt=2025-07-16T19:07:35Z; modifiedAt=2025-07-16T19:07:35Z; cells=System.Object[]}23Views0likes0CommentsGetting Teams meeting transcripts using Powershell with Graph API
I have set up an Enterprise App in Entra with the following API permissions: Microsoft.Graph OnlineMeetings.Read (Delegated) OnlineMeetings.Read.All (Application) User.Read.All (Application) Admin consent has been granted for the Application types. Below is the code snippet for getting the meetings: $tenantId = "xxxxxx" $clientId = "xxxxxx" $clientSecret = "xxxxxx" $secureSecret = ConvertTo-SecureString $clientSecret -AsPlainText -Force $psCredential = New-Object System.Management.Automation.PSCredential ($clientId, $secureSecret) Connect-MgGraph -TenantId $tenantId -ClientSecretCredential $psCredential -NoWelcome $meetings = Get-MgUserOnlineMeeting -UserId "email address removed for privacy reasons" -All Connect-MgGraph is invoked without errors. I had verified this with Get-MgContext command. At line 10 I get this error: Status: 404 (NotFound) ErrorCode: UnknownError I don't know if this is means there was an error in the API call, or there were no records found (I do have Teams calls with transcripts though). I have tried changing the last line to (without -All): $meetings = Get-MgUserOnlineMeeting -UserId "my guid user id here" And I get this error: Status: 403 (Forbidden) ErrorCode: Forbidden Adding -All parameter results in this error: Filter expression expected - /onlineMeetings?$filter={ParameterName} eq '{id}'. I've done some searching but I haven't found any more information nor solution for this. I hope someone can point me in the right direction. Thanks in advance!115Views0likes0CommentsGui to deploy folder contents to multiple VMs
I am trying to improve imaging computers where I work. I need to create a gui for new hires since the imaging process is so complicated. I need the GUI to request necessary computer names that are being imaged and then copy files from a local workstation to the machines that are being imaged on the network that our technicians do not have physical access to. I have turned to Powershell for the solution in an attempt to improve on my knowledge which is basic really. Below is the code I have come up with so far. In this code I am getting the location of the file. I would rather copy the entire folder instead of the file but I couldnt find the code to do that. So, if that is possible please show me how. If not I figure I would have to save these imaging files to a ZIP file. Then I could maybe use this GUI I am working on to move the zip file to the remote computers. Add-Type -AssemblyName System.Windows.Forms # Create the form $form = New-Object System.Windows.Forms.Form $form.Text = "File and Network Location Collector" $form.Size = New-Object System.Drawing.Size(400, 200) # Create the label for file name $fileLabel = New-Object System.Windows.Forms.Label $fileLabel.Text = "File Name:" $fileLabel.Location = New-Object System.Drawing.Point(10, 20) $form.Controls.Add($fileLabel) # Create the text box for file name $fileTextBox = New-Object System.Windows.Forms.TextBox $fileTextBox.Location = New-Object System.Drawing.Point(100, 20) $fileTextBox.Size = New-Object System.Drawing.Size(250, 20) $form.Controls.Add($fileTextBox) # Create the label for network location $networkLabel = New-Object System.Windows.Forms.Label $networkLabel.Text = "Network Location:" $networkLabel.Location = New-Object System.Drawing.Point(10, 60) $form.Controls.Add($networkLabel) # Create the text box for network location $networkTextBox = New-Object System.Windows.Forms.TextBox $networkTextBox.Location = New-Object System.Drawing.Point(100, 60) $networkTextBox.Size = New-Object System.Drawing.Size(250, 20) $form.Controls.Add($networkTextBox) # Create the button to submit $submitButton = New-Object System.Windows.Forms.Button $submitButton.Text = "Submit" $submitButton.Location = New-Object System.Drawing.Point(150, 100) $form.Controls.Add($submitButton) # Add event handler for the button click $submitButton.Add_Click({ $fileName = $fileTextBox.Text $networkLocation = $networkTextBox.Text [System.Windows.Forms.MessageBox]::Show("File Name: $fileName`nNetwork Location: $networkLocation") }) # Show the form $form.ShowDialog() In this portion of the code it is copying from one source to many locations. Thank you for any assistance as this would help my organization a lot. We are getting several new hires who are very new to the industry. This would be a huge blessing. Pardon the change in font size. It did that for no reason, its my first time using the blog, and there appears to be no way to change the sizes lol. Forgive me. #Define the source folder and the list of target computers $sourceFolder = "C:\Path\To\SourceFolder" $destinationFolder = "C:\Path\To\DestinationFolder" $computers = @("Computer1", "Computer2", "Computer3") # Replace with actual computer names # Function to copy the folder function Copy-Folder { param ( [string]$source, [string]$destination ) Copy-Item -Path $source -Destination $destination -Recurse -Force } # Execute the copy operation on each computer foreach ($computer in $computers) { Invoke-Command -ComputerName $computer -ScriptBlock { param ($source, $destination) Copy-Folder -source $source -destination $destination } -ArgumentList $sourceFolder, $destinationFolder } Write-Host "Folder copied to all specified computers."53Views0likes0CommentsPurview -> Powershell
i need to export some users their data before their licenses are removed. It is about 60 users, so i would rather user powershell instead of the purview portal to automate the job. So i have been playing around with the commandlets, to get an idea to build the script. The strange thing is what i see in Powershell is not represented in the Purview portal. We had an older compliance case which was no longer used. I tried to remove the compliance case by the Purview portal, but nothing happens when clicking "delete case" or "close case". i then reverted back to PowerShell by using the Remove-ComplianceCase "$CaseName", where the compliance case was successfully removed. When running the Get-ComplianceCase, i can see that the old compliance case is indeed removed, however the removed compliance case is still present in the Purview portal even several hours after deleting the case with PowerShell. Then started to play around with a new compliance search New-ComplianceSearch -Name "$($TargetMailbox.displayName) License Cleanup" -ExchangeLocation "$($TargetMailbox.PrimarySmtpAddress)" -Case "License Cleanup" -SharePointlocation "$($PNPPersonalSite.url)" after refreshing a couple of times i could see the compliance search in the purview portal. Then started the compliance search by using the Start-ComplianceSeacrh commandlet and verified that the search status was completed: Get-compliancesearch "$($TargetMailbox.displayName) License Cleanup" | select status However in the Purview portal no statistics were shown (not available yet). Didn't spend to much attention as i already saw discrepancies between the purview portal and what i saw in Powershell, so continued exporting compliance search with a compliance search action to export the data in the process manager New-ComplianceSearchAction -SearchName ""$($TargetMailbox.displayName)" -Export Can successfully retrieve the compliancesearch action in Powershell and can see that the status is completed, but fail to retrieve the export in the purview portal. Get-ComplianceSearchAction -case "License CleanUp" -includecredential | fl Did not achieve a way in downloading the export results via PowerShell, but would already be pretty pleased if i could achieve the first two steps via PowerShell. But as i am unable to retrieve the export in the Purview portal, i am afraid that i am still stuck. I can create an export in the Purview portal from the compliance search i created in Powershell. Can anyone please explain me the issue with the discrepancies between what i see in PowerShell and the Purview Portal and is it possible to see the exports created in powershell in the purview portal? And is it feasible to download the export from Powershell as well (Start-Process)?216Views0likes0CommentsBeginners performance tip: Use pipelines.
Hi folks, In my role, I see a lot of poorly-written PowerShell code spanning a lot of different contexts. Without fail, the most common failing I see is that the script simply won't scale, meaning performance will decrease significantly when it's run against larger data sets. And within this context, one of the biggest reasons is the overuse of variables and the underutilisation of the PowerShell pipeline. If you're the investigative type, here's some official documentation and training on the pipeline: about_Pipelines - PowerShell | Microsoft Learn Understand the Windows PowerShell pipeline - Training | Microsoft Learn A short explanation is that piping occurs when the output from one command is automatically sent to and used by another command. As an example, let's say I want my first command to fetch all the files in my temporary directory (just the root in this case). I might run a command like the following: Get-ChildItem -Path $env:TEMP -File Which, as you'd expect, produces a list of files. Where PowerShell differs from the old command prompt (or DOS prompt, if you're old like me) is that it's not simply a bunch of text written to the screen. Instead, each of these files is an object. If you don't know what an object is, think of it as a school lunchbox for the time being, where that lunchbox contains a bunch of useful stuff (just data; nothing tasty, sadly) inside. Because this isn't just a bunch of useless text, we can take the individual lunchboxes (objects) produced from this first command and send those to another command. As the second command sees each lunchbox, it can choose to do something with it - or even just ignore it; the possibilities are endless! When the lunchboxes coming out of the first command travel to the second command, the pathway they travel along is the pipeline! It's what joins the two commands together. Continuing the example above, I now want to remove those lunchboxes - I mean files - from my temporary directory, which I'll do by piping the lunchboxes from the first command into a second command that will perform the deleting. Get-ChildItem -Path $env:TEMP -File | Remove-Item -Confirm:$false -ErrorAction:SilentlyContinue Now, there's no output to show for this command but it does pretty much what you'd expect: Deletes the files. Now, there's another way we could have achieved the same thing using variables and loops, which I'll demonstrate first before circling back to how this relates to performance and scalability. # Get all the files first and assign them to a variable. $MyTempFiles = Get-ChildItem -Path $env:TEMP -File; # Now we have a single variable holding all files, send all those files (objects) to the second command to delete them. $MyTempFiles | Remove-Item -Confirm:$false -ErrorAction:SilentlyContinue; This isn't the only way you you can go about it, and you can see I'm still using piping in the second command - but none of this is important. The important point - and this brings us back to the topic of performance - is that I've assigned all of the files to a variable instead of simply passing them over the pipeline. This means that all of these files consume memory and continue to do so until I get rid of the variable (named $MyTempFiles). Now, imagine that instead of dealing with a few hundred files in my temp directory, I'm dealing with 400,000 user objects from an Azure Active Directory tenant and I'll retrieving all attributes. The difference in memory usage is incomparable. And when Windows starts feeling memory pressure, this impacts disk caching performance and before you know it, the Event Log system starts throwing performance events everywhere. It's not a good outcome. So, the more objects you have, the more your performance decreases in a linear manner. Pipeline to the rescue! This conversation is deliberately simple and doesn't go into the internal mechanics of how many commands you might like using actually work, but only relevant part I want to focus on is something called paging. Let's say you use Get-MgBetaUser to pull down those 400,000 users from Azure Active Directory. Get-MgBetaUser (Microsoft.Graph.Beta.Users) | Microsoft Learn Internally, the command won't be pulling them down all at once. Instead, it will pull down a bite-sized chunk (i.e. a page, as evidenced by the ability to specify a value for the PageSize parameter that features in the above documentation) and push that out onto the pipeline. And if you are piping from Get-MgBetaUser to a second command, then that second command can read that set of users from the pipeline to do what it needs to do. And so on through any other commands until eventually there are no more commands left. At this stage - and this is where the memory efficiency comes in - that batch of users can be released from memory as they are no longer needed by anything. In pictures, and using a page size of 1,000, this looks like: Now, as anyone familiar with .NET can attest to, memory isn't actually released immediately by default. The .NET engine manages memory resourcing and monitoring internally but the key takeaway is by using the pipeline, we're allowing the early release of memory to occur. Conversely, when we store everything in a variable, we're preventing the .NET memory manager from releasing memory early. This, in turn, leads to the above-mentioned performance issues. In pictures, this looks like: Is there real benefit? Yes, absolutely. And it can be quite significant, too. In one case, I triaged a script that was causing system failure (Windows PowerShell has/had a default process limit of 2 GB) through storing Azure results in a variable. After some minor tweaks so that it used the pipeline, process memory fluctuated between 250 MB to 400 MB. Working with pipelines typically doesn't require any extra effort - and in some cases can actually condense your code. However, the performance and scalability benefits can potentially be quite significant - particularly on already-busy systems. Cheers, Lain173Views2likes0CommentsAd-Hoc Entra MFA using SMS
Error : Get MFA Client Access TokenDone. Send MFA challenge to the user Done. OTP sent to your phone. Please enter the OTP: Enter the OTP sent via SMS: 696632 Invoke-RestMethod: C:\Git_Repo\MFA_Test\MFATestWIthKyle\sms.ps1:54:28 Line | 54 | … ionResult = Invoke-RestMethod -Uri 'https://strongauthenticationservi … | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ | Service BODY { color: #000000; background-color: white; font-family: Verdana; margin-left: 0px; margin-top: 0px; | } #content { margin-left: 30px; font-size: .70em; padding-bottom: 2em; } A:link { color: #336699; font-weight: bold; | text-decoration: underline; } A:visited { color: #6699cc; font-weight: bold; text-decoration: underline; } A:active { color: | #336699; font-weight: bold; text-decoration: underline; } .heading1 { background-color: #003366; border-bottom: #336699 6px | solid; color: #ffffff; font-family: Tahoma; font-size: 26px; font-weight: normal;margin: 0em 0em 10px -20px; padding-bottom: | 8px; padding-left: 30px;padding-top: 16px;} pre { font-size:small; background-color: #e5e5cc; padding: 5px; font-family: | Courier New; margin-top: 0px; border: 1px #f0f0e0 solid; white-space: pre-wrap; white-space: -pre-wrap; word-wrap: break-word; | } table { border-collapse: collapse; border-spacing: 0px; font-family: Verdana;} table th { border-right: 2px white solid; | border-bottom: 2px white solid; font-weight: bold; background-color: #cecf9c;} table td { border-right: 2px white solid; | border-bottom: 2px white solid; background-color: #e5e5cc;} Service Endpoint not found. WARNING: Invalid OTP or validation failed. Below line causing the error $mfaValidationResult = Invoke-RestMethod -Uri 'https://strongauthenticationservice.auth.microsoft.com/StrongAuthenticationService.svc/Connector//ValidatePin' -Method POST -Headers $Headers -Body $XML -ContentType 'application/xml'84Views0likes0CommentsI want to create Sharepoint Verisoning Report on my Tenant
I have created a script, but dont seem to be able to find out how many file versions there are , Config Variables $TenantAdminURL = "https://admin.sharepoint.com" $CSVFilePath = "C:\Temp\RESTART.csv" #Get the Root Web #$Web = Get-PnpWeb #$versions = Get-SPOListItemVersion -ListItem $listItem #Get the Site Title Write-host -f Green $Web.Title #Connect to Admin Center using PnP Online Connect-PnPOnline -Url $TenantAdminURL -ClientId “cabf4-cc9b-4dcf-807b-8af94c3c4333" -Interactive -ForceAuthentication #Delete the Output Report, if exists if (Test-Path $CSVFilePath) { Remove-Item $CSVFilePath } #Get All Site collections - Exclude: Seach Center, Redirect site, Mysite Host, App Catalog, Content Type Hub, eDiscovery and Bot Sites $SiteCollections = Get-PnPTenantSite | Where { $.URL -like '/sites' -and $.Template -NotIn ("SRCHCEN#0", "REDIRECTSITE#0", "SPSMSITEHOST#0", "APPCATALOG#0", "POINTPUBLISHINGHUB#0", "EDISC#0", "STS#-1")} #Get All Large Lists from the Web - Exclude Hidden and certain lists $ExcludedLists = @("Form Templates","Site Assets", "Pages", "Site Pages", "Images", "Site Collection Documents", "Site Collection Images","Style Library") $SiteCounter = 1 #Loop through each site collection ForEach($Site in $SiteCollections) { #Display a Progress bar Write-Progress -id 1 -Activity "Processing Site Collections" -Status "Processing Site: $($Site.URL)' ($SiteCounter of $($SiteCollections.Count))" -PercentComplete (($SiteCounter / $SiteCollections.Count) * 100) #Connect to the site Connect-PnPOnline -Url $Site.URL -Interactive #Get all document libraries $DocumentLibraries = Get-PnPList | Where-Object {$_.BaseType -eq "DocumentLibrary" -and $_.Hidden -eq $False -and $_.Title -notin $ExcludedLists -and $_.ItemCount -gt 0} $ListCounter = 1 $ItemsColl = $List2.Items #Iterate through document libraries ForEach ($List in $DocumentLibraries) { $global:counter = 0 $FileData = @() Write-Progress -id 2 -ParentId 1 -Activity "Processing Document Libraries" -Status "Processing Document Library: $($List.Title)' ($ListCounter of $($DocumentLibraries.Count))" -PercentComplete (($ListCounter / $DocumentLibraries.Count) * 10) #Get All Files of the library with size > 100MB $Files = Get-PnPListItem -List $List -Fields FileLeafRef,FileRef,SMTotalFileStreamSize -PageSize 500 -ScriptBlock { Param($items) $global:counter += $items.Count; Write-Progress -Id 3 -parentId 2 -PercentComplete ($global:Counter / ($List.ItemCount) * 10) -Activity "Getting List Items of '$($List.Title)'" -Status "Processing Items $global:Counter to $($List.ItemCount)";} | Where {($_.FileSystemObjectType -eq "File") -and ($_.FieldValues.SMTotalFileStreamSize/1MB -gt 100)} #Collect data from each files ForEach ($File in $Files) { $FileData += [PSCustomObject][ordered]@{ Site = $Web.url Library = $List.Title FileName = $File.FieldValues.FileLeafRef URL = $File.FieldValues.FileRef Size = [math]::Round(($File.FieldValues.SMTotalFileStreamSize/1MB),2) } } #Export Files data to CSV File $FileData | Sort-object Size -Descending $FileData | Export-Csv -Path $CSVFilePath -NoTypeInformation -Append $ListCounter++ #Write-Progress -Activity "Completed Processing List $($List.Title)" -Completed -id 2 } $SiteCounter++ }42Views0likes0CommentsError PowerShell 30015-1015 (80)
Hello, using PowerShell for office installation, with ODT, it gives me the following error shown in the photo, or opening the console in any folder with the right mouse button "open the P.S. window here" gives an error: Missing termination character in the string: ". + CategoryInfo : ParserError: (:) [], ParentContainsErrorRecordException + FullyQualifiedErrorId : TerminatorExpectedAtEndOfString Or Set-Location : Impossibile trovare un parametro posizionale che accetta l'argomento 'Ripristino\Office\Office'. In riga:1 car:1 + Set-Location -literalPath D:\Ripristino\File Ripristino\Office\Office ... + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + CategoryInfo : InvalidArgument: (:) [Set-Location], ParameterBindingException + FullyQualifiedErrorId : PositionalParameterNotFound,Microsoft.PowerShell.Commands.SetLocationCommand While if I run the command on the desktop, the window opens normally! Thanks52Views0likes0CommentsError PowerShell 300-1015 (80)
Hello, using P.Shell for office installation, with ODT, it gives me the following error shown in the photo, or opening the console in any folder with the right mouse button "open the P.S. window here" gives an error: Missing termination character in the string: ". + CategoryInfo : ParserError: (:) [], ParentContainsErrorRecordException + FullyQualifiedErrorId : TerminatorExpectedAtEndOfString While if I run the command on the desktop, the window opens normally! Thanks57Views0likes0CommentsBest way to remove UseClientIntegration from each role definition (SharePoint Online)
I've created a PS script that removes Use Client integration from each permission level (role definition). This works, but as a side effect it gives the custom role definitions a new id. This can cause issues further down the line. Here is the part of the script which replaces the existing permission levels (role defs): #Install App to the Site Install-PnPApp -Identity $App.Id # Get all existing role definitions $roleDefinitions = Get-PnPRoleDefinition foreach ($role in $roleDefinitions) { # Create a new custom role definition by copying the existing one $newRoleName = "Custom_" + $role.Name # Clone the existing permission levels excluding Client Int.: Add-PnPRoleDefinition -RoleName $newRoleName -Clone $role -Exclude UseClientIntegration # Remove the original role definition Remove-PnPRoleDefinition -Identity $role.Name -Force } # Get the new role definitions: $newRoleDefinitions = Get-PnPRoleDefinition # Rename each permission to remove the "Custom_" foreach ($newRole in $newRoleDefinitions) { Set-PnPRoleDefinition -Identity $newRole.Name -NewRoleName $newRole.Name.TrimStart("Custom_") } # Remove the erroneously created permission levels: if($role.Name -eq "Custom_Limited Access" -or "Custom_Web-Only Limited Access" -or "Custom_Full Control") { Remove-PnPRoleDefinition -Identity "Custom_Limited Access" -Force Remove-PnPRoleDefinition -Identity "Custom_Web-Only Limited Access" -Force Remove-PnPRoleDefinition -Identity "Custom_Full Control" -Force Set-PnPRoleDefinition -Identity "ntribute" -NewRoleName "Contribute" #Not sure why earlier in the script it changes Contribute to "ntribute" but i'm having to rename it here. } I need a better way to do this, as you can see it's an amateur effort. I need someway to remove UserClientIntegration from each permission level but keep the original permission level role def id.69Views0likes0CommentsWebView2 HTML parsing
The code below embeds a web-scraping test URI into a Webview2 control. The button2 function returns the links for the site, but requires a redundant Invoke-WebRequest. I know Webview2 does not have an internal DOM capability and the $htmlContent = $WebView2.ExecuteScriptAsync() to return HTML is commented out as it does not appear to work. Is there a way to obtain the html without the Invoke-WebRequest()? function button1 { $title = 'Enter New URL to Navigate To' $msg = 'Please enter URL as https://[...some site...].com/.net/.org' $url = [Microsoft.VisualBasic.Interaction]::InputBox($msg, $title) $url if ($url -ne "") { try { $WebView2.Source = $url $WebView2.Refresh() } catch { $pop.popup("Invalid or Unuseable URL",2,"Error",4096) } } } function button2 { $links = ( Invoke-WebRequest -Uri $WebView2.Source).Links.Href | Get-Unique $regex = '/product*' $links | Select-String $regex | Select line | Out-Gridview -Title "Webpage Links for Products" -PassThru #$pop.popup("Code for Scraping Links",2,"Message",4096) } ######################################################################################################################## $pop = New-Object -ComObject wscript.shell New-Variable -Name 'data' -Value "$([Environment]::GetEnvironmentVariable('LOCALAPPDATA'))\Webview2" -Scope Global -Force $path=$PSScriptRoot # Get DLLs $WinForms = "$path\Microsoft.Web.WebView2.WinForms.dll" $Core = "$path\Microsoft.Web.WebView2.Core.dll" <# $loader = "$path\WebView2Loader.dll" $wpf = "$path\Microsoft.Web.WebView2.Wpf.dll" #> Add-Type -AssemblyName Microsoft.VisualBasic Add-Type -Path $WinForms #Add-Type -Path $Core Add-Type -AssemblyName System.Windows.Forms Add-Type -AssemblyName System.Drawing $Form = New-Object System.Windows.Forms.Form $button1 = New-Object System.Windows.Forms.Button $button2 = New-Object System.Windows.Forms.Button $cancelButton = New-Object System.Windows.Forms.Button # $button1.Location = New-Object System.Drawing.Point(23, 25) $button1.Name = "button1" $button1.Size = New-Object System.Drawing.Size(75, 23) $button1.TabIndex = 0 $button1.Text = "New URL" $button1.BackColor = "Green" $button1.ForeColor = "White" $button1.AutoSize = $true # $button2.Location = New-Object System.Drawing.Point(312, 25) $button2.Name = "button2" $button2.Size = New-Object System.Drawing.Size(75, 23) $button2.TabIndex = 1 $button2.Text = "Links" $button2.BackColor = "Green" $button2.ForeColor = "White" $button2.AutoSize = $true # $cancelButton.Location = New-Object System.Drawing.Point(684, 25) $cancelButton.Name = "button3" $cancelButton.Size = New-Object System.Drawing.Size(75, 23) $cancelButton.TabIndex = 2 $cancelButton.Text = "Close" $cancelButton.BackColor = "Red" $cancelButton.ForeColor = "White" $cancelButton.Text = 'Close Window' $cancelButton.AutoSize = $true $cancelButton.DialogResult = [System.Windows.Forms.DialogResult]::Cancel $Form.CancelButton = $cancelButton # $WebView2 = New-Object -TypeName Microsoft.Web.WebView2.WinForms.WebView2 $WebView2.CreationProperties = New-Object -TypeName 'Microsoft.Web.WebView2.WinForms.CoreWebView2CreationProperties' $WebView2.CreationProperties.UserDataFolder = $data #keeps it out of $PSScriptRoot $WebView2.Source = "https://www.scrapingcourse.com/ecommerce/" $Webview2.Location = New-Object System.Drawing.Point(23, 65) $Webview2.Size = New-Object System.Drawing.Size(749, 373) $WebView2.Anchor = 'top,right,bottom,left' <#navigation complete $WebView2_NavigationCompleted = { $htmlContent = $WebView2.ExecuteScriptAsync("window.chrome.webview.postMessage(document.documentElement.outerHTML;").Result #$htmlContent = $webView2.CoreWebView2.ExecuteScriptAsync("document.documentElement.outerHTML;").Result Write-Host $htmlContent } $WebView2.add_NavigationCompleted($WebView2_NavigationCompleted) $WebView2.add_WebMessageReceived({ param($WebView2, $message) $pop.popup($message.TryGetWebMessageAsString(),3,"Message",4096) }) #> $Form.ClientSize = New-Object System.Drawing.Size(800, 450) $Form.Controls.Add($Webview2) $Form.Controls.Add($cancelButton) $Form.Controls.Add($button2) $Form.Controls.Add($button1) $Form.Name = "Form" $Form.Text = "Webview Web Scraping Sample" $button1.add_click( { button1 }) $button2.add_click( { button2 }) $result=$Form.ShowDialog() #Terminate if Cancel button pressed if ($result -eq [System.Windows.Forms.DialogResult]::Cancel) { [System.GC]::Collect() [System.GC]::WaitForPendingFinalizers() $form.Dispose() Exit } ########################################################################################################################109Views0likes0CommentsAdding External Users in-bulk to: Microsoft Teams & Private Channel(s) within the Team
We have a customer who requires over 350 external users (their customers) to be added / invited into a Team which has been created. "Half" of the users need to be added into "private channel a", and the other "Half" need to be added into "private channel b". We have attempted to add the users via various PowerShell scripts, however none of these scripts that we have been provided with have worked for various reasons. I have been unable to locate any native methods for this within the MS 365 admin centre, therefore believe that the only way to achieve this is by PowerShell scripting. Example of the most recent script we have is as follows, omitting the creation of the private channel(s) as they have already been created - see below: We require assistance with the actual script itself to: Add users into the team from a CSV of their email addresses. Assign the users to the correct private channel. Note - users will be added in 2 batches - 1 per private channel, so we just require scripting that can be modified to achieve this. # Install the Microsoft Teams PowerShell Module Install-Module -Name PowerShellGet -Force -AllowClobber Install-Module -Name MicrosoftTeams -Force -AllowClobber # Connect to Microsoft Teams Connect-MicrosoftTeams # Define the team name and path to the CSV file $teamName = "Your Team Name" $csvPath = "C:\path\to\your\users.csv" # Get the GroupId of the team $team = Get-Team -DisplayName $teamName $groupId = $team.GroupId # Import users from the CSV file $users = Import-Csv $csvPath # Add external users to the team foreach ($user in $users) { Add-TeamUser -GroupId $groupId -User $user.Email } # Define the private channel name $privateChannelName = "Private Channel Name" # Create the private channel New-TeamChannel -GroupId $groupId -DisplayName $privateChannelName -MembershipType Private # Get the ChannelId of the private channel $channel = Get-TeamChannel -GroupId $groupId -DisplayName $privateChannelName $channelId = $channel.Id # Add users to the private channel foreach ($user in $users) { Add-TeamChannelUser -GroupId $groupId -User $user.Email -ChannelId $channelId }129Views0likes0CommentsAdd parent team in a shared channel with powershell
Hello, I'm trying to add the team, where my shared channel is, as a member oh this channel but I can't find the good command. The last one I used is : $TeamGrouId = "000-000-000-000" #ID de l'équipe concernée $Channel = "Mon canal partagé" $channelID = (Get-TeamChannel -GroupId $TeamGroupId | Where-Object { $_.DisplayName -Like $Channel}).Id $paramstest = @{ "@odata.type" = "microsoft.graph.aadUserConversationMember" roles = @("member") "*** Adresse électronique supprimée pour cause de confidentialité ***" = "https://graph.microsoft.com/v1.0/groups/$TeamGroupId" } Add-MgTeamChannelMember -TeamId $TeamGroupId -ChannelId $channelId -BodyParameter $paramstest The error message I'm encoutering is : Add-MgTeamChannelMember : Bind requests not expected for action payload. Status: 400 (BadRequest) ErrorCode: BadRequest If anyone have a solution :-) Best regards P.S. : I'm french so my english may be a little bad.63Views0likes0CommentsThreat Hunting with PowerShell - Security even with a small budget - there is no excuse!
Dear Threat Hunter, Lack of IT security is often excused by little or no available money. In my view, this is a very poor excuse. In this article I will try to give you a jump start on how to investigate threats with PowerShell. Is this a comprehensive and conclusive list of how you can find or detect threats/threats? NO, absolutely not. But it is meant to provide you with the support that you need to move forward on your own. Let's talk about the "general conditions": 1. If you use the PowerShell scripts I show/explain in this article, this is entirely your responsibility. I use the scripts in different situations, they are not dangerous, but you should already know what you are doing. 2. Written permission! If you are not sure if you are allowed to do an investigation, organize a written permission from your supervisor. 3. In the different scripts I sometimes (for this article deliberately) use standard search words like "malware", "malicious", "hacker" etc. Such search patterns/search words need to be customized, of course. These simply serve as an example. 4. The last part of the article examines some Microsoft cloud services. I am absolutely aware that there are a huge number of tools for hunting in the Microsoft cloud services. It starts with Azure Sentinel and continues with Cloud App Security. Since the focus is on a small budget, I'll leave those tools on the side. Introduction: So first, why should you use PowerShell for threat hunting? PowerShell is a useful threat hunting tool because it is a powerful scripting language and a platform for automating tools and accessing data across any Windows environment. It allows you to quickly gather information from various sources such as event logs, registries, files, and processes. Additionally, it can also be easily integrated with other tools and technologies making it a flexible and efficient tool for threat hunting. Some common use cases for PowerShell in the threat hunting environment include automated collection of log data, identification of unusual behavior anomalies in the system, the discovery of malware or malicious activity by known signatures or patterns or behaviors. These are just a few examples of how PowerShell can be used in a threat hunting capacity. Its versatility and ability to access and manipulate data from across the Windows environment make it a very valuable tool for any security professional. Threat Hunting in PowerShell - Use Cases: All right. So now that we understand where PowerShell can benefit an organization from a threat hunting perspective. Let's take a deeper look at some of the actual use cases you might encounter on a day to day basis, first being identify malicious processor files. So specifically, you can conduct raw file analysis to sift through different data shares to look for particular files in question whether that be a signature or even an extension of a certain file being able to quickly search and triage through files is an extreme benefit of using PowerShell for threat hunting. But how exactly do we start, what can we use as a guide? For example, the MITRE ATT&CK Framework. Here are a few examples: Indicator Removal: Clear Windows Event Logs https://attack.mitre.org/techniques/T1070/001/ Event Triggered Execution: Installer Packages https://attack.mitre.org/techniques/T1546/016/ Hide Artifacts: NTFS File Attributes https://attack.mitre.org/techniques/T1564/004/ Command and Scripting Interpreter: PowerShell https://attack.mitre.org/techniques/T1059/001/ Command and Scripting Interpreter: Windows Command Shell https://attack.mitre.org/techniques/T1059/003/ Event Triggered Execution: Windows Management Instrumentation Event Subscription https://attack.mitre.org/techniques/T1546/003/ Credentials from Password Stores: Windows Credential Manager https://attack.mitre.org/techniques/T1555/004/ Abuse Elevation Control Mechanism: Bypass User Account Control https://attack.mitre.org/techniques/T1548/002/ The MITRE ATT@CK framework provides a comprehensive and regularly updated overview of tactics, techniques, and procedures (TTPs) used by various threat actors. We can locate these TTPs using PowerShell, here are a few examples: Indicator Removal: Clear Windows Event Logs https://github.com/tomwechsler/Threat_Hunting_with_PowerShell/blob/main/Hunting_Active_Directory/06_Account_Events.ps1 Event Triggered Execution: Installer Packages https://github.com/tomwechsler/Threat_Hunting_with_PowerShell/blob/main/Tactics_Techniques_Procedures_(TTPs)/08_Get-ItemProperty_Software.ps1 Hide Artifacts: NTFS File Attributes https://github.com/tomwechsler/Threat_Hunting_with_PowerShell/blob/main/Tactics_Techniques_Procedures_(TTPs)/08_Get-ItemProperty_Software.ps1 Windows Installer Service is running https://github.com/tomwechsler/Threat_Hunting_with_PowerShell/blob/main/Tactics_Techniques_Procedures_(TTPs)/01_WIS_is_running.ps1 Search Alternate Data Streams on NTFS File Systems https://github.com/tomwechsler/Threat_Hunting_with_PowerShell/blob/main/Tactics_Techniques_Procedures_(TTPs)/02_Search_ADS_on_NTFS%20_(specific%20file).ps1 https://github.com/tomwechsler/Threat_Hunting_with_PowerShell/blob/main/Tactics_Techniques_Procedures_(TTPs)/03_Search_ADS_on_NTFS_file_systems.ps1 Read the Contents of a File https://github.com/tomwechsler/Threat_Hunting_with_PowerShell/blob/main/Tactics_Techniques_Procedures_(TTPs)/06_Read_the_contents_file.ps1 Locating Data Patterns within a File https://github.com/tomwechsler/Threat_Hunting_with_PowerShell/blob/main/Tactics_Techniques_Procedures_(TTPs)/05_locating_data_patterns_within_file.ps1 Search for Encoding with Regex https://github.com/tomwechsler/Threat_Hunting_with_PowerShell/blob/main/Tactics_Techniques_Procedures_(TTPs)/07_Search_encoding_with_regex.ps1 Search for Command and Scripting Interpreter: https://github.com/tomwechsler/Threat_Hunting_with_PowerShell/blob/main/Tactics_Techniques_Procedures_(TTPs)/04_Searching_for_PIDs.ps1 Threat hunting in different environments with PowerShell: Coming examples are about collecting information in very different environments. Also here a few examples as a kind => as first starting points: Hunt for Threats in Active Directory: https://github.com/tomwechsler/Threat_Hunting_with_PowerShell/blob/main/Hunting_Active_Directory/01_Resetting_Password_Unlocking_Accounts.ps1 https://github.com/tomwechsler/Threat_Hunting_with_PowerShell/blob/main/Hunting_Active_Directory/02_Search_stale_accounts.ps1 https://github.com/tomwechsler/Threat_Hunting_with_PowerShell/blob/main/Hunting_Active_Directory/03_Users_without_Manager.ps1 https://github.com/tomwechsler/Threat_Hunting_with_PowerShell/blob/main/Hunting_Active_Directory/04_Password_Expiration.ps1 https://github.com/tomwechsler/Threat_Hunting_with_PowerShell/blob/main/Hunting_Active_Directory/05_Group_Membership_Report.ps1 https://github.com/tomwechsler/Threat_Hunting_with_PowerShell/blob/main/Hunting_Active_Directory/06_Account_Events.ps1 https://github.com/tomwechsler/Active_Directory_Advanced_Threat_Hunting/blob/main/PowerShell/Tracking_the_Source_of_Account_Lock_Outs_and_Bad_Passwords.ps1 https://github.com/tomwechsler/Active_Directory_Advanced_Threat_Hunting/blob/main/PowerShell/Finding_Unused_Group_Policy_Objects.ps1 Some of the scripts are structured in such a way that they must be executed block by block/line by line. So do not execute the whole script at once. Pay attention to the different information that is collected. With some investigations in the Active Directory accounts can be indicated like "guest" or "krbtgt", there must be clear of course how this information is to be estimated. Depending on how and what information is searched. Hunt for Threats in Exchange Online: https://github.com/tomwechsler/Threat_Hunting_with_PowerShell/blob/main/Hunting_Exchange_Online/Exchange_Mailbox_LastLogin.ps1 Find mailboxes with the last login. Hunt for Threats in Azure: https://github.com/tomwechsler/Threat_Hunting_with_PowerShell/blob/main/Hunting_Azure/Collect_vms_subscription.ps1 We search Azure for all virtual machines in a subscription. https://github.com/tomwechsler/Threat_Hunting_with_PowerShell/blob/main/Hunting_Azure/02_Graph_Create_Time_Last_Password.ps1 When was the last password change and when were the accounts created? Hunt for Threats in SharePoint: https://github.com/tomwechsler/Threat_Hunting_with_PowerShell/blob/main/Hunting_SharePoint_Online/SharePoint_Online_specific_files.ps1 With this script we search for files with the extension .ps1 in a SharePoint Online page. Summary: Is this the best tactic to hunt for threats? No! There are many different tactics/techniques to search for threats. First of all, there are a huge number of different tools that can be used, for example SIEM/SOAR (Security Information and Event Management/Security Orchestration, Automation and Response). These tools are really great, sometimes cost a lot and often it takes a lot of knowledge to use such tools. But what is the use of such tools if the information generated by these tools cannot be understood properly, not very much. For this reason, I have tried in this article with simple tools to generate information that hopefully can be interpreted. Is finished here at this point. NO, the journey continues. The examples in this article are neither exhaustive nor complete, but they should give you a starting point. I hope you can build on this foundation. I hope that this information is helpful to you and that you have received a good "little" foundation. But I still hope that this information is helpful for you. Thank you for taking the time to read the article. Happy Hunting, Tom Wechsler P.S. All scripts (#PowerShell, Azure CLI, #Terraform, #ARM) that I use can be found on github! https://github.com/tomwechslerGenerate vCard for each user in Exchange online and attach to user's mail.
We are looking best method to create vCard for each user in Exchange online and attach vCard in user's outlook for outgoing email. Is there any script or tool to programmatically create a vCard, that can generate .vcf files with the desired contact information for each user and integrate to Exchange online to apply to corresponding user.55Views0likes0Comments