Skip to content

Instantly share code, notes, and snippets.

View JustinGrote's full-sized avatar

Justin Grote JustinGrote

View GitHub Profile
@JustinGrote
JustinGrote / Get-GraphGroupMember.ps1
Created August 5, 2026 05:30
Graph: Fetch Microsoft Graph Group Members Quickly
<#
.SYNOPSIS
Retrieves the members of several Microsoft Graph groups via batch processing.
.EXAMPLE
Get-GraphGroupMember -Id '00000000-0000-0000-0000-000000000000'
.EXAMPLE
Get-MgGroup -All | Get-GraphGroupMember
.EXAMPLE
Invoke-MgGraphRequest -Uri 'v1.0/groups' |% Value |% Get-GraphGroupMember
@JustinGrote
JustinGrote / Get-MgUserExchangeAttributes.ps1
Created August 5, 2026 01:29
Get ExchangeCustomAttribute via MS Graph
function Get-MgUserExchangeAttributes {
param(
[Parameter(Mandatory)]
[Guid]$UserId
)
$attributes = (1..5).ForEach{"extension_79c86e5137c54776975dce9deb389ed4_extensionAttribute$_"}
$result = Get-MgUser -UserId $UserId -Property $attributes -ErrorAction Stop
return [PSCustomObject]@{
UserId = $UserId
@JustinGrote
JustinGrote / Get-ExchangeDistributionGroupBatch.ps1
Created August 3, 2026 22:59
Exchange: Get a list of Distribution Groups
#This exploits the fact that you can have up to 500 filter parameters to a get-distributiongroup query (but I do 450 here to be conservative)
#This is much faster than trying to do it via runspaces.
function Get-DistributionGroupBatch {
param(
[string[]]$Name,
[int]$BatchSize = 450
)
for ($i, $i -lt $Name.Count; $i += $BatchSize) {
$batch = $Name[$i..([Math]::Min($i + $BatchSize - 1, $Name.Count - 1))]
@JustinGrote
JustinGrote / Get-AllMailboxFolderPermissions.ps1
Last active July 30, 2026 05:12
Using ExchangeOnlineManagement in Parallel Runspaces
#This is an example of doing exchange operations in parallel in runspaces.
#When Exchange PowerShell Module Connects, it saves token info in a global .NET object and then asks an API to generate a PowerShell
#Module when it downloads. That PowerShell module has the path for the key built-in, so all you have to do
#Is load the specified module into each runspace.
#requires -version 7.2
$ErrorActionPreference = 'stop'
$command = @(
'Get-ExoMailbox'
'Get-ExoMailboxFolderStatistics'
@JustinGrote
JustinGrote / ConvertFrom-ExchangeLog.ps1
Created July 16, 2026 21:36
Find Exchange SMTP Usage
<#[
.SYNOPSIS
Parses Exchange log files and returns records in descending date order.
.DESCRIPTION
Exchange logs store their column names in a '#Fields:' comment line. This
script reads that header, parses every data row, and sorts all records from
newest to oldest using the 'date-time' column.
.PARAMETER Path
@JustinGrote
JustinGrote / Find-MissingPSGalleryPackages.ps1
Created July 2, 2026 03:43
Search for packages not on the PowerShell Gallery
using namespace System.Management.Automation
using namespace System.Collections.Generic
using namespace System.Collections.Concurrent
$baseUri = 'https://www.powershellgallery.com/api/v2/Packages?$orderby=Published desc&$skip='
$maxPackages = 700000
$interval = 100
$statusDictionary = [ConcurrentDictionary[string, bool]]::new()
$existingpackagesjson = (iwr 'https://pwsh.gallery/sleet.packageindex.json').content -replace 'PSObject','__MODULEFASTIGNOREME' | convertfrom-json |% packages
@JustinGrote
JustinGrote / Get-KerberosRC4Events.ps1
Last active June 24, 2026 19:04
Get Kerberos RC4 usage events
using namespace System.Diagnostics.Eventing.Reader
[CmdletBinding(DefaultParameterSetName = 'ByCount')]
param(
#A list of domain controllers to query. Leave blank if running directly on a domain controller.
[string[]]$ComputerName,
#Maximum amount of events to retrieve. This is set to 1000 for a quick initial fetch. Note this is max events *investigated* and not max events returned.
[Parameter(ParameterSetName = 'ByCount')]
[int]$MaxEvents = 1000,
@JustinGrote
JustinGrote / Get-LdapUnsigned.ps1
Created June 23, 2026 19:02
Fetch LDAP Unsigned Events
[CmdletBinding(DefaultParameterSetName = 'ByCount')]
param(
#A list of domain controllers to query. Leave blank if running directly on a domain controller.
[string[]]$ComputerName,
#Maximum amount of events to retrieve. This is set to 1000 for a quick initial fetch. Note this is max events *investigated* and not max events returned.
[Parameter(ParameterSetName = 'ByCount')]
[int]$MaxEvents = 1000,
#How long back to search for events. Overrides MaxEvents if specified.
@JustinGrote
JustinGrote / Get-ExchangeSmtpSessions.ps1
Created May 20, 2026 23:21
Fetch and Parse Exchange Onprem Receive Connector Logs to find SMTP Senders
param(
[Parameter(Mandatory = $true)]
[string[]]$ExchangeServers,
[Parameter(Mandatory = $true)]
[PSCredential]$Credential,
[Parameter(Mandatory = $false)]
[datetime]$StartTime = (Get-Date).AddDays(-1),
@JustinGrote
JustinGrote / Find-CidrPolicyUpdates.kql
Last active April 22, 2026 00:22
Find the changes in an Azure Conditional Access Named Location when so many records exist it uses CompressedCidrIPRanges instead of CidrIPRanges
// The complete data we need gets split over multiple logs, so we have to recombine them. Export to CSV afterwards.
AuditLogs
| where OperationName == "Update policy"
| where isnotempty(CorrelationId) and isnotempty(AdditionalDetails)
| project TimeGenerated, CorrelationId, Identity, AdditionalDetails
| mv-apply d = AdditionalDetails on (
summarize
seq = tolong(take_anyif(tostring(d.value), tostring(coalesce(d.key, d.name)) == "seq")),
b = take_anyif(tostring(d.value), tostring(coalesce(d.key, d.name)) == "b")