Public/Get-NSPIocSet.ps1
|
function Get-NSPIocSet { <# .SYNOPSIS Returns an indicator-of-compromise set: the app IDs, IP addresses, user agents and hunt window for one incident. .DESCRIPTION Sets ship in the module's IocSets\ folder, one .psd1 per incident, named by the file (IocSets\SkyKick.psd1 is 'SkyKick'). -Name may also be the path to a .psd1 of the same shape, so a new incident can be hunted before a module release. With no -Name, every shipped set is returned. -AdditionalIPs and -AdditionalAppIds add indicators a vendor published after the set was written, without editing it. .EXAMPLE Get-NSPIocSet .EXAMPLE Get-NSPIocSet -Name SkyKick -AdditionalIPs '203.0.113.7' .EXAMPLE Get-NSPIocSet -Name .\NextIncident.psd1 #> [CmdletBinding()] param( [Parameter(Position = 0)] [ArgumentCompleter({ Get-ChildItem -Path (Join-Path (Get-Module NSP.M365.Investigation).ModuleBase 'IocSets') -Filter "$($args[2])*.psd1" | ForEach-Object BaseName })] [string]$Name, [string[]]$AdditionalIPs = @(), [string[]]$AdditionalAppIds = @() ) $folder = Join-Path $script:ModuleRoot 'IocSets' if (-not $Name) { foreach ($file in Get-ChildItem -LiteralPath $folder -Filter '*.psd1' -File | Sort-Object Name) { Get-NSPIocSet -Name $file.FullName -AdditionalIPs $AdditionalIPs -AdditionalAppIds $AdditionalAppIds } return } $path = if ($Name -match '\.psd1$') { $Name } else { Join-Path $folder "$Name.psd1" } if (-not (Test-Path -LiteralPath $path)) { $available = @(Get-ChildItem -LiteralPath $folder -Filter '*.psd1' -File | ForEach-Object BaseName) -join ', ' throw "No IOC set '$Name'. Available: $available. -Name also takes the path to a .psd1." } $data = Import-PowerShellDataFile -LiteralPath $path $missing = @('Name', 'Title', 'Since', 'Apps', 'IPs' | Where-Object { -not $data.ContainsKey($_) }) if ($missing.Count) { throw "IOC set $path is missing: $($missing -join ', ')." } # Case-insensitive lookups whatever the file's casing. $apps = @{} foreach ($key in $data.Apps.Keys) { $apps[([string]$key).ToLowerInvariant()] = [string]$data.Apps[$key] } foreach ($appId in $AdditionalAppIds) { if (-not $apps.ContainsKey($appId.ToLowerInvariant())) { $apps[$appId.ToLowerInvariant()] = 'User-supplied IOC app' } } $context = @{} if ($data.ContainsKey('ContextIpPrefixes')) { foreach ($key in $data.ContextIpPrefixes.Keys) { $context[[string]$key] = [string]$data.ContextIpPrefixes[$key] } } $prefixes = @(if ($data.ContainsKey('NamePrefixes')) { $data.NamePrefixes }) $styles = [Globalization.DateTimeStyles]::AssumeUniversal -bor [Globalization.DateTimeStyles]::AdjustToUniversal [pscustomobject]@{ Name = [string]$data.Name Title = [string]$data.Title Updated = [string]$data.Updated Since = [datetime]::Parse([string]$data.Since, [Globalization.CultureInfo]::InvariantCulture, $styles) Apps = $apps OwnerTenantIds = @(if ($data.ContainsKey('OwnerTenantIds')) { $data.OwnerTenantIds }) NamePrefixes = $prefixes NameRegex = if ($prefixes.Count) { (@($prefixes | ForEach-Object { [regex]::Escape($_) }) -join '|') } else { $null } IPs = @(@($data.IPs) + @($AdditionalIPs) | Where-Object { $_ } | Select-Object -Unique) IpPrefixes = @(if ($data.ContainsKey('IpPrefixes')) { $data.IpPrefixes }) ContextIpPrefixes = $context UserAgents = @(if ($data.ContainsKey('UserAgents')) { $data.UserAgents }) Sources = @(if ($data.ContainsKey('Sources')) { $data.Sources }) Notes = @(if ($data.ContainsKey('Notes')) { $data.Notes }) Path = (Resolve-Path -LiteralPath $path).ProviderPath } } |