ThreatHunting/Get-ThreatIntelligence.ps1
|
function Get-ThreatIntelligence { <# .SYNOPSIS Matches system artifacts against threat intelligence indicators .DESCRIPTION Checks running processes, network connections, files, and registry against known Indicators of Compromise (IOCs) from threat intelligence feeds. IP indicators are exact-matched against the live TCP connection table. Domain/URL-host indicators cannot be matched against literal IP addresses, so they are correlated against real hostname telemetry: the DNS client cache (Get-DnsClientCache) and Sysmon DNS query events (ID 22) when available. Hash indicators are matched against Get-FileHash results for the image paths of running processes (algorithms are derived from the stored hash lengths). The SourcesChecked property of the result lists exactly which correlation sources were queried on this system. .PARAMETER CheckProcesses Check running processes against IOCs .PARAMETER CheckNetwork Check network connections against IOCs .PARAMETER CheckFiles Check files against IOC patterns .PARAMETER CheckRegistry Check registry against IOCs .PARAMETER IOCFile Path to custom IOC file (JSON format) .PARAMETER UpdateFeeds Download or refresh live threat intelligence feeds (URLhaus, Feodo Tracker, optional AbuseIPDB) .PARAMETER FeedCachePath Directory used to cache downloaded feed data (default: $env:LOCALAPPDATA\WindowsSecurityAudit\ThreatIntel) .PARAMETER FeedMaxAgeHours Maximum age in hours before a cached feed is downloaded again with -UpdateFeeds (default: 24) .PARAMETER MaxFeedEntries Maximum number of feed-derived indicators merged into the IOC database (default: 5000) .PARAMETER AbuseIPDBApiKey Optional AbuseIPDB API key; enables the AbuseIPDB blacklist feed (sent only as a request header) .PARAMETER MISPUrl Base URL of a MISP instance (requires -MISPApiKey); attributes are pulled via attributes/restSearch (types: ip-dst, domain, hostname, url, uri, md5, sha1, sha256, email-src, email-dst, filename, mutex) .PARAMETER MISPApiKey MISP API auth key (requires -MISPUrl); sent only as the Authorization request header and never logged .PARAMETER TaxiiServerUrl Base URL of a TAXII 2.x server (requires -TaxiiCollection); indicator objects are pulled from the collection .PARAMETER TaxiiCollection TAXII collection ID or title to pull STIX indicators from (requires -TaxiiServerUrl); paginated envelopes are followed automatically via the 'more'/'next' pattern (up to 20 pages) .PARAMETER TaxiiUsername Optional TAXII username; sent only as a Basic auth request header together with -TaxiiPassword .PARAMETER TaxiiPassword Optional TAXII password (SecureString); sent only as a Basic auth request header together with -TaxiiUsername .PARAMETER StixFilePath Path to a local STIX 2.x JSON bundle file; indicators are imported fully offline .EXAMPLE Get-ThreatIntelligence -CheckProcesses -CheckNetwork Get-ThreatIntelligence -CheckProcesses -CheckNetwork -CheckFiles -IOCFile "C:\iocs.json" Get-ThreatIntelligence -CheckNetwork -UpdateFeeds -FeedMaxAgeHours 12 Get-ThreatIntelligence -CheckNetwork -UpdateFeeds -MISPUrl "https://misp.example.com" -MISPApiKey "your-api-key" Get-ThreatIntelligence -CheckNetwork -UpdateFeeds -TaxiiServerUrl "https://taxii.example.com" -TaxiiCollection "12345678-1234-1234-1234-123456789012" Get-ThreatIntelligence -CheckNetwork -StixFilePath "C:\intel\bundle.json" .OUTPUTS PSCustomObject with IOC match results #> [CmdletBinding()] param( [Parameter()] [switch]$CheckProcesses, [Parameter()] [switch]$CheckNetwork, [Parameter()] [switch]$CheckFiles, [Parameter()] [switch]$CheckRegistry, [Parameter()] [string]$IOCFile, [Parameter()] [switch]$UpdateFeeds, [Parameter()] [string]$FeedCachePath = "$env:LOCALAPPDATA\WindowsSecurityAudit\ThreatIntel", [Parameter()] [int]$FeedMaxAgeHours = 24, [Parameter()] [int]$MaxFeedEntries = 5000, [Parameter()] [string]$AbuseIPDBApiKey, [Parameter()] [string]$MISPUrl, [Parameter()] [string]$MISPApiKey, [Parameter()] [string]$TaxiiServerUrl, [Parameter()] [string]$TaxiiCollection, [Parameter()] [string]$TaxiiUsername, [Parameter()] [SecureString]$TaxiiPassword, [Parameter()] [string]$StixFilePath ) begin { Write-Host "=== THREAT INTELLIGENCE IOC MATCHING ===" -ForegroundColor Cyan Write-Host "Start Time: $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')" -ForegroundColor Yellow if (-not ($CheckProcesses -or $CheckNetwork -or $CheckFiles -or $CheckRegistry)) { $CheckProcesses = $true $CheckNetwork = $true Write-Host "No specific check selected - enabling default checks" -ForegroundColor Yellow } $results = [PSCustomObject]@{ ScanDate = Get-Date -Format "yyyy-MM-dd HH:mm:ss" ComputerName = $env:COMPUTERNAME Matches = @{ ProcessIOCs = @() NetworkIOCs = @() FileIOCs = @() RegistryIOCs = @() } Summary = @{ TotalMatches = 0 CriticalMatches = 0 HighMatches = 0 MediumMatches = 0 FeedStats = @() } Recommendations = @() SourcesChecked = @() } # Built-in IOC Database (Common malware/threat indicators) $iocDatabase = @{ MaliciousProcesses = @{ 'mimikatz.exe' = @{ Severity = 'Critical'; Description = 'Credential dumping tool' } 'procdump.exe' = @{ Severity = 'High'; Description = 'Memory dumping tool (can be legitimate)' } 'rubeus.exe' = @{ Severity = 'Critical'; Description = 'Kerberos attack tool' } 'sharphound.exe' = @{ Severity = 'High'; Description = 'AD enumeration tool (BloodHound)' } 'covenant.exe' = @{ Severity = 'Critical'; Description = 'C2 framework' } 'psexec.exe' = @{ Severity = 'Medium'; Description = 'Remote execution tool (can be legitimate)' } 'paexec.exe' = @{ Severity = 'High'; Description = 'PSExec alternative' } 'lazagne.exe' = @{ Severity = 'Critical'; Description = 'Password recovery tool' } } MaliciousIPs = @{ '192.0.2.1' = @{ Severity = 'Critical'; Description = 'Known C2 server (example)' } '198.51.100.1' = @{ Severity = 'High'; Description = 'Known malware distribution (example)' } } MaliciousHashes = @{ # Example SHA256 hashes (these are placeholders) '44d88612fea8a8f36de82e1278abb02f' = @{ Severity = 'Critical'; Description = 'Known malware hash' } } SuspiciousPatterns = @{ ProcessCommandLine = @( @{ Pattern = 'mimikatz'; Severity = 'Critical'; Description = 'Mimikatz execution detected' } @{ Pattern = 'Invoke-Mimikatz'; Severity = 'Critical'; Description = 'PowerShell Mimikatz' } @{ Pattern = 'sekurlsa::logonpasswords'; Severity = 'Critical'; Description = 'Credential dumping command' } @{ Pattern = 'procdump.*lsass'; Severity = 'Critical'; Description = 'LSASS memory dump' } @{ Pattern = 'IEX.*New-Object.*Net.WebClient.*DownloadString'; Severity = 'High'; Description = 'PowerShell download cradle' } @{ Pattern = '\[Convert\]::FromBase64String'; Severity = 'Medium'; Description = 'Base64 decoding (potential obfuscation)' } ) FilePaths = @( @{ Pattern = 'C:\\Windows\\Temp\\.*\.exe'; Severity = 'High'; Description = 'Executable in Windows Temp' } @{ Pattern = 'C:\\Users\\Public\\.*\.exe'; Severity = 'High'; Description = 'Executable in Public folder' } @{ Pattern = 'C:\\ProgramData\\.*\.ps1'; Severity = 'Medium'; Description = 'PowerShell script in ProgramData' } ) NetworkIndicators = @() EmailIndicators = @() FileNameIndicators = @() MutexIndicators = @() } } # Live threat intelligence feeds $feedStats = @() $feedEntryCount = 0 $networkIndicatorKeys = @{} $emailIndicatorKeys = @{} $fileNameIndicatorKeys = @{} $mutexIndicatorKeys = @{} $mispEnabled = $false if (-not [string]::IsNullOrWhiteSpace($MISPUrl) -or -not [string]::IsNullOrWhiteSpace($MISPApiKey)) { if ([string]::IsNullOrWhiteSpace($MISPUrl) -or [string]::IsNullOrWhiteSpace($MISPApiKey)) { Write-Warning "-MISPUrl and -MISPApiKey must be provided together - skipping MISP feed" } else { $mispEnabled = $true } } $taxiiEnabled = $false if (-not [string]::IsNullOrWhiteSpace($TaxiiServerUrl) -or -not [string]::IsNullOrWhiteSpace($TaxiiCollection)) { if ([string]::IsNullOrWhiteSpace($TaxiiServerUrl) -or [string]::IsNullOrWhiteSpace($TaxiiCollection)) { Write-Warning "-TaxiiServerUrl and -TaxiiCollection must be provided together - skipping TAXII feed" } else { $taxiiEnabled = $true $hasTaxiiPassword = ($null -ne $TaxiiPassword -and $TaxiiPassword.Length -gt 0) if ([string]::IsNullOrWhiteSpace($TaxiiUsername) -ne (-not $hasTaxiiPassword)) { Write-Warning "-TaxiiUsername and -TaxiiPassword must be provided together - continuing without TAXII authentication" $TaxiiUsername = $null $TaxiiPassword = $null } } } $addFeedIndicator = { param( [string]$Kind, [string]$Value, [string]$Severity, [string]$Description ) if ($feedEntryCount -ge $MaxFeedEntries) { return 0 } if ([string]::IsNullOrWhiteSpace($Value)) { return 0 } if ($Kind -eq 'ip') { if ($Value -notmatch '^\d{1,3}(\.\d{1,3}){3}$') { return 0 } if (-not $iocDatabase.MaliciousIPs.ContainsKey($Value)) { $iocDatabase.MaliciousIPs[$Value] = @{ Severity = $Severity; Description = $Description } $feedEntryCount++ return 1 } } elseif ($Kind -eq 'ipv6') { $ipv6Value = $Value.ToLower() if ($ipv6Value -notmatch '^[0-9a-f:]+$' -or $ipv6Value -notmatch ':') { return 0 } if (-not $iocDatabase.MaliciousIPs.ContainsKey($ipv6Value)) { $iocDatabase.MaliciousIPs[$ipv6Value] = @{ Severity = $Severity; Description = $Description } $feedEntryCount++ return 1 } } elseif ($Kind -eq 'domain') { $escapedHost = [regex]::Escape($Value) if (-not $networkIndicatorKeys.ContainsKey($escapedHost)) { $networkIndicatorKeys[$escapedHost] = $true $iocDatabase.SuspiciousPatterns.NetworkIndicators += @{ Pattern = $escapedHost; Severity = $Severity; Description = $Description } $feedEntryCount++ return 1 } } elseif ($Kind -eq 'url') { $indicatorUri = $null try { $indicatorUri = [System.Uri]$Value } catch { return 0 } if ($null -eq $indicatorUri) { return 0 } $indicatorHost = $indicatorUri.Host if ([string]::IsNullOrWhiteSpace($indicatorHost)) { return 0 } if ($indicatorHost -match '^\d{1,3}(\.\d{1,3}){3}$') { if (-not $iocDatabase.MaliciousIPs.ContainsKey($indicatorHost)) { $iocDatabase.MaliciousIPs[$indicatorHost] = @{ Severity = $Severity; Description = "$Description host" } $feedEntryCount++ return 1 } } else { $escapedHost = [regex]::Escape($indicatorHost) if (-not $networkIndicatorKeys.ContainsKey($escapedHost)) { $networkIndicatorKeys[$escapedHost] = $true $iocDatabase.SuspiciousPatterns.NetworkIndicators += @{ Pattern = $escapedHost; Severity = $Severity; Description = "$Description host" } $feedEntryCount++ return 1 } } } elseif ($Kind -eq 'md5' -or $Kind -eq 'sha1' -or $Kind -eq 'sha256') { $hashValue = $Value.ToLower() if ($hashValue -notmatch '^[0-9a-f]+$') { return 0 } if (-not $iocDatabase.MaliciousHashes.ContainsKey($hashValue)) { $iocDatabase.MaliciousHashes[$hashValue] = @{ Severity = $Severity; Description = $Description } $feedEntryCount++ return 1 } } elseif ($Kind -eq 'email') { if ($Value -notmatch '^[^@\s]+@[^@\s]+$') { return 0 } $escapedEmail = [regex]::Escape($Value) if (-not $emailIndicatorKeys.ContainsKey($escapedEmail)) { $emailIndicatorKeys[$escapedEmail] = $true $iocDatabase.SuspiciousPatterns.EmailIndicators += @{ Pattern = $escapedEmail; Severity = $Severity; Description = $Description } $feedEntryCount++ return 1 } } elseif ($Kind -eq 'filename') { if ($Value -match '[\\/]') { return 0 } $escapedFileName = [regex]::Escape($Value) if (-not $fileNameIndicatorKeys.ContainsKey($escapedFileName)) { $fileNameIndicatorKeys[$escapedFileName] = $true $iocDatabase.SuspiciousPatterns.FileNameIndicators += @{ Pattern = $escapedFileName; Severity = $Severity; Description = $Description } $feedEntryCount++ return 1 } } elseif ($Kind -eq 'mutex') { if (-not $mutexIndicatorKeys.ContainsKey($Value)) { $mutexIndicatorKeys[$Value] = $true $iocDatabase.SuspiciousPatterns.MutexIndicators += @{ Name = $Value; Severity = $Severity; Description = "$Description (cataloged only - no live mutex check)" } $feedEntryCount++ return 1 } } return 0 } $extractStixIndicators = { param( [string]$Pattern, [string]$SourceName ) $extracted = @() if ([string]::IsNullOrWhiteSpace($Pattern)) { return $extracted } $matchedAny = $false foreach ($regexMatch in [regex]::Matches($Pattern, "\[ipv4-addr:value\s*=\s*'([^']+)'\]")) { $matchedAny = $true $extracted += @{ Kind = 'ip'; Value = $regexMatch.Groups[1].Value; Label = 'ipv4-addr' } } foreach ($regexMatch in [regex]::Matches($Pattern, "\[ipv6-addr:value\s*=\s*'([^']+)'\]")) { $matchedAny = $true $extracted += @{ Kind = 'ipv6'; Value = $regexMatch.Groups[1].Value; Label = 'ipv6-addr' } } foreach ($regexMatch in [regex]::Matches($Pattern, "\[domain-name:value\s*=\s*'([^']+)'\]")) { $matchedAny = $true $extracted += @{ Kind = 'domain'; Value = $regexMatch.Groups[1].Value; Label = 'domain-name' } } foreach ($regexMatch in [regex]::Matches($Pattern, "\[url:value\s*=\s*'([^']+)'\]")) { $matchedAny = $true $extracted += @{ Kind = 'url'; Value = $regexMatch.Groups[1].Value; Label = 'url' } } foreach ($regexMatch in [regex]::Matches($Pattern, "\[file:hashes\.'SHA-256'\s*=\s*'([^']+)'\]")) { $matchedAny = $true $extracted += @{ Kind = 'sha256'; Value = $regexMatch.Groups[1].Value; Label = 'SHA-256' } } foreach ($regexMatch in [regex]::Matches($Pattern, "\[file:hashes\.'?MD5'?\s*=\s*'([^']+)'\]")) { $matchedAny = $true $extracted += @{ Kind = 'md5'; Value = $regexMatch.Groups[1].Value; Label = 'MD5' } } if (-not $matchedAny) { Write-Verbose "Unrecognized STIX pattern from ${SourceName}: $Pattern" } return $extracted } if ($UpdateFeeds -or (Test-Path $FeedCachePath)) { Write-Host "`nProcessing live threat intelligence feeds..." -ForegroundColor Cyan try { [Net.ServicePointManager]::SecurityProtocol = [Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls12 } catch { Write-Verbose "Could not enforce TLS 1.2: $_" } $feedDefinitions = @( @{ Name = 'Feodo Tracker'; Url = 'https://feodotracker.abuse.ch/downloads/ipblocklist.txt'; FileName = 'feodo_ipblocklist.txt'; Format = 'IpList'; Severity = 'Critical'; Description = 'Feodo Tracker C2' } @{ Name = 'URLhaus'; Url = 'https://urlhaus.abuse.ch/downloads/csv_recent.txt'; FileName = 'urlhaus_recent.csv'; Format = 'URLhausCsv'; Severity = 'High'; Description = 'URLhaus payload URL' } ) if (-not [string]::IsNullOrWhiteSpace($AbuseIPDBApiKey)) { $feedDefinitions += @{ Name = 'AbuseIPDB'; Url = 'https://api.abuseipdb.com/api/v2/blacklist?plaintext'; FileName = 'abuseipdb_blacklist.txt'; Format = 'IpList'; Severity = 'High'; Description = 'AbuseIPDB blacklist' } } if (-not (Test-Path $FeedCachePath)) { try { $null = New-Item -ItemType Directory -Path $FeedCachePath -Force -ErrorAction Stop } catch { Write-Warning "Could not create feed cache directory '$FeedCachePath': $_" } } foreach ($feed in $feedDefinitions) { if ($feedEntryCount -ge $MaxFeedEntries) { Write-Warning "Feed entry cap ($MaxFeedEntries) reached - skipping remaining feeds" break } $cacheFile = Join-Path $FeedCachePath $feed.FileName $content = $null $source = 'none' $cacheAgeHours = $null $cacheExists = Test-Path $cacheFile if ($cacheExists) { $cacheItem = Get-Item $cacheFile $cacheAgeHours = [math]::Max(0, [math]::Round(((Get-Date) - $cacheItem.LastWriteTime).TotalHours, 1)) } $shouldDownload = $UpdateFeeds -and (-not $cacheExists -or $cacheAgeHours -ge $FeedMaxAgeHours) if ($shouldDownload) { try { Write-Host " Downloading $($feed.Name) feed..." -ForegroundColor Yellow $requestParams = @{ Uri = $feed.Url OutFile = $cacheFile UseBasicParsing = $true TimeoutSec = 30 ErrorAction = 'Stop' } if ($feed.Name -eq 'AbuseIPDB') { $requestParams['Headers'] = @{ 'Key' = $AbuseIPDBApiKey; 'Accept' = 'text/plain' } } Invoke-WebRequest @requestParams $content = Get-Content $cacheFile -ErrorAction Stop $source = 'download' $cacheItem = Get-Item $cacheFile $cacheAgeHours = [math]::Max(0, [math]::Round(((Get-Date) - $cacheItem.LastWriteTime).TotalHours, 1)) } catch { if ($cacheExists) { Write-Warning "Failed to download $($feed.Name) feed - falling back to cached copy ($cacheAgeHours hours old): $_" $content = Get-Content $cacheFile -ErrorAction SilentlyContinue $source = 'cache' } else { Write-Warning "Failed to download $($feed.Name) feed and no cache available - using built-in IOCs only: $_" } } } elseif ($cacheExists) { $content = Get-Content $cacheFile -ErrorAction SilentlyContinue $source = 'cache' if ($cacheAgeHours -ge $FeedMaxAgeHours) { Write-Host " $($feed.Name) cache is $cacheAgeHours hours old - run with -UpdateFeeds to refresh" -ForegroundColor Yellow } } $entriesAdded = 0 if ($null -ne $content) { foreach ($line in $content) { if ($feedEntryCount -ge $MaxFeedEntries) { break } if ([string]::IsNullOrWhiteSpace($line) -or $line.StartsWith('#')) { continue } if ($feed.Format -eq 'IpList') { $ip = $line.Trim() if ($ip -notmatch '^\d{1,3}(\.\d{1,3}){3}$') { continue } if (-not $iocDatabase.MaliciousIPs.ContainsKey($ip)) { $iocDatabase.MaliciousIPs[$ip] = @{ Severity = $feed.Severity; Description = $feed.Description } $feedEntryCount++ $entriesAdded++ } } elseif ($feed.Format -eq 'URLhausCsv') { $fields = $line -split ',' if ($fields.Count -lt 6) { continue } $url = $fields[2].Trim().Trim('"') $uri = $null try { $uri = [System.Uri]$url } catch { continue } if ($null -eq $uri) { continue } $hostName = $uri.Host if ([string]::IsNullOrWhiteSpace($hostName)) { continue } if ($hostName -match '^\d{1,3}(\.\d{1,3}){3}$') { if (-not $iocDatabase.MaliciousIPs.ContainsKey($hostName)) { $iocDatabase.MaliciousIPs[$hostName] = @{ Severity = $feed.Severity; Description = "$($feed.Description) host" } $feedEntryCount++ $entriesAdded++ } } else { $pattern = [regex]::Escape($hostName) if (-not $networkIndicatorKeys.ContainsKey($pattern)) { $networkIndicatorKeys[$pattern] = $true $iocDatabase.SuspiciousPatterns.NetworkIndicators += @{ Pattern = $pattern; Severity = $feed.Severity; Description = "$($feed.Description) host" } $feedEntryCount++ $entriesAdded++ } } } } } $feedStats += [PSCustomObject]@{ Feed = $feed.Name EntryCount = $entriesAdded CacheAgeHours = $cacheAgeHours Source = $source } } if ($mispEnabled) { if ($feedEntryCount -ge $MaxFeedEntries) { Write-Warning "Feed entry cap ($MaxFeedEntries) reached - skipping MISP feed" } else { $mispCacheFile = Join-Path $FeedCachePath 'misp_attributes.json' $mispResponse = $null $mispSource = 'none' $mispCacheAgeHours = $null $mispCacheExists = Test-Path $mispCacheFile if ($mispCacheExists) { $mispCacheItem = Get-Item $mispCacheFile $mispCacheAgeHours = [math]::Max(0, [math]::Round(((Get-Date) - $mispCacheItem.LastWriteTime).TotalHours, 1)) } $mispShouldDownload = $UpdateFeeds -and (-not $mispCacheExists -or $mispCacheAgeHours -ge $FeedMaxAgeHours) if ($mispShouldDownload) { try { Write-Host " Querying MISP instance at $MISPUrl..." -ForegroundColor Yellow $mispBody = @{ returnFormat = 'json' type = @('ip-dst', 'domain', 'hostname', 'url', 'uri', 'md5', 'sha1', 'sha256', 'email-src', 'email-dst', 'filename', 'mutex') to_ids = 'yes' } | ConvertTo-Json $mispResponse = Invoke-RestMethod -Method Post -Uri "$($MISPUrl.TrimEnd('/'))/attributes/restSearch" -Headers @{ 'Authorization' = $MISPApiKey; 'Accept' = 'application/json' } -Body $mispBody -ContentType 'application/json' -TimeoutSec 60 -ErrorAction Stop if ($null -eq $mispResponse) { throw "MISP returned an empty response" } ($mispResponse | ConvertTo-Json -Depth 10) | Set-Content -Path $mispCacheFile -Encoding UTF8 -ErrorAction Stop $mispSource = 'download' $mispCacheItem = Get-Item $mispCacheFile $mispCacheAgeHours = [math]::Max(0, [math]::Round(((Get-Date) - $mispCacheItem.LastWriteTime).TotalHours, 1)) } catch { if ($mispCacheExists) { Write-Warning "Failed to query MISP - falling back to cached copy ($mispCacheAgeHours hours old): $_" try { $mispResponse = Get-Content $mispCacheFile -Raw -ErrorAction Stop | ConvertFrom-Json -ErrorAction Stop $mispSource = 'cache' } catch { Write-Warning "Could not read MISP cache file: $_" } } else { Write-Warning "Failed to query MISP and no cache available - using built-in IOCs only: $_" } } } elseif ($mispCacheExists) { try { $mispResponse = Get-Content $mispCacheFile -Raw -ErrorAction Stop | ConvertFrom-Json -ErrorAction Stop $mispSource = 'cache' } catch { Write-Warning "Could not read MISP cache file: $_" } if ($null -ne $mispCacheAgeHours -and $mispCacheAgeHours -ge $FeedMaxAgeHours) { Write-Host " MISP cache is $mispCacheAgeHours hours old - run with -UpdateFeeds to refresh" -ForegroundColor Yellow } } $mispEntriesAdded = 0 if ($null -ne $mispResponse) { $mispAttributes = @() if ($mispResponse.PSObject.Properties['response'] -and $null -ne $mispResponse.response -and $mispResponse.response.PSObject.Properties['Attribute']) { $mispAttributes = @($mispResponse.response.Attribute) } foreach ($attribute in $mispAttributes) { if ($feedEntryCount -ge $MaxFeedEntries) { break } if ($null -eq $attribute) { continue } if (-not $attribute.PSObject.Properties['type'] -or -not $attribute.PSObject.Properties['value']) { continue } $attributeType = [string]$attribute.type $attributeValue = [string]$attribute.value if ([string]::IsNullOrWhiteSpace($attributeValue)) { continue } $indicatorKind = $null switch ($attributeType) { 'ip-dst' { $indicatorKind = 'ip' } 'domain' { $indicatorKind = 'domain' } 'hostname' { $indicatorKind = 'domain' } 'url' { $indicatorKind = 'url' } 'uri' { $indicatorKind = 'url' } 'md5' { $indicatorKind = 'md5' } 'sha1' { $indicatorKind = 'sha1' } 'sha256' { $indicatorKind = 'sha256' } 'email-src' { $indicatorKind = 'email' } 'email-dst' { $indicatorKind = 'email' } 'filename' { $indicatorKind = 'filename' } 'mutex' { $indicatorKind = 'mutex' } } if ($null -eq $indicatorKind) { Write-Verbose "Skipping unsupported MISP attribute type: $attributeType" continue } $mispEntriesAdded += (. $addFeedIndicator -Kind $indicatorKind -Value $attributeValue -Severity 'High' -Description "MISP attribute ($attributeType)") } } $feedStats += [PSCustomObject]@{ Feed = 'MISP' EntryCount = $mispEntriesAdded CacheAgeHours = $mispCacheAgeHours Source = $mispSource } } } if ($taxiiEnabled) { if ($feedEntryCount -ge $MaxFeedEntries) { Write-Warning "Feed entry cap ($MaxFeedEntries) reached - skipping TAXII feed" } else { $taxiiCacheFile = Join-Path $FeedCachePath 'taxii_indicators.json' $taxiiObjects = $null $taxiiSource = 'none' $taxiiCacheAgeHours = $null $taxiiPagesFetched = 0 $taxiiMaxPages = 20 $taxiiCacheExists = Test-Path $taxiiCacheFile if ($taxiiCacheExists) { $taxiiCacheItem = Get-Item $taxiiCacheFile $taxiiCacheAgeHours = [math]::Max(0, [math]::Round(((Get-Date) - $taxiiCacheItem.LastWriteTime).TotalHours, 1)) } $taxiiShouldDownload = $UpdateFeeds -and (-not $taxiiCacheExists -or $taxiiCacheAgeHours -ge $FeedMaxAgeHours) if ($taxiiShouldDownload) { try { Write-Host " Querying TAXII server at $TaxiiServerUrl (collection '$TaxiiCollection')..." -ForegroundColor Yellow $taxiiHeaders = @{ 'Accept' = 'application/taxii+json;version=2.1' } if (-not [string]::IsNullOrWhiteSpace($TaxiiUsername) -and $null -ne $TaxiiPassword) { $taxiiPasswordPlain = [System.Net.NetworkCredential]::new([string]::Empty, $TaxiiPassword).Password $credentialPair = "$($TaxiiUsername):$taxiiPasswordPlain" $taxiiHeaders['Authorization'] = 'Basic ' + [Convert]::ToBase64String([System.Text.Encoding]::ASCII.GetBytes($credentialPair)) } $taxiiBase = $TaxiiServerUrl.TrimEnd('/') $discovery = Invoke-RestMethod -Method Get -Uri "$taxiiBase/taxii2/" -Headers $taxiiHeaders -TimeoutSec 30 -ErrorAction Stop $apiRoots = @() if ($null -ne $discovery -and $discovery.PSObject.Properties['api_roots'] -and $null -ne $discovery.api_roots) { $apiRoots = @($discovery.api_roots) } if ($apiRoots.Count -eq 0) { throw "TAXII discovery returned no API roots" } $apiRoot = ([string]$apiRoots[0]).TrimEnd('/') if ($apiRoot -notmatch '^https?://') { $apiRoot = "$taxiiBase/$($apiRoot.TrimStart('/'))" } $collectionsResponse = Invoke-RestMethod -Method Get -Uri "$apiRoot/collections" -Headers $taxiiHeaders -TimeoutSec 30 -ErrorAction Stop $collections = @() if ($null -ne $collectionsResponse -and $collectionsResponse.PSObject.Properties['collections'] -and $null -ne $collectionsResponse.collections) { $collections = @($collectionsResponse.collections) } $targetCollectionId = $null foreach ($collection in $collections) { if ($null -eq $collection) { continue } $collectionId = if ($collection.PSObject.Properties['id']) { [string]$collection.id } else { $null } $collectionTitle = if ($collection.PSObject.Properties['title']) { [string]$collection.title } else { $null } if ($collectionId -eq $TaxiiCollection -or $collectionTitle -eq $TaxiiCollection) { $targetCollectionId = $collectionId break } } if ($null -eq $targetCollectionId) { throw "TAXII collection '$TaxiiCollection' not found on server" } $taxiiObjects = @() $taxiiNextToken = $null $taxiiMorePages = $true while ($taxiiMorePages -and $taxiiPagesFetched -lt $taxiiMaxPages) { $objectsUri = "$apiRoot/collections/$targetCollectionId/objects?types=indicator" if (-not [string]::IsNullOrWhiteSpace($taxiiNextToken)) { $objectsUri += "&next=$([System.Uri]::EscapeDataString($taxiiNextToken))" } $objectsResponse = Invoke-RestMethod -Method Get -Uri $objectsUri -Headers $taxiiHeaders -TimeoutSec 60 -ErrorAction Stop $taxiiPagesFetched++ if ($null -ne $objectsResponse -and $objectsResponse.PSObject.Properties['objects'] -and $null -ne $objectsResponse.objects) { $taxiiObjects += @($objectsResponse.objects) } $taxiiMorePages = $false if ($null -ne $objectsResponse -and $objectsResponse.PSObject.Properties['more'] -and $true -eq $objectsResponse.more) { $nextPageToken = if ($objectsResponse.PSObject.Properties['next']) { [string]$objectsResponse.next } else { $null } if (-not [string]::IsNullOrWhiteSpace($nextPageToken)) { $taxiiNextToken = $nextPageToken $taxiiMorePages = $true } else { Write-Warning "TAXII server indicated more pages but returned no 'next' token - stopping pagination" } } } if ($taxiiMorePages -and $taxiiPagesFetched -ge $taxiiMaxPages) { Write-Warning "TAXII page cap ($taxiiMaxPages) reached - using indicators from the first $taxiiPagesFetched pages" } ($taxiiObjects | ConvertTo-Json -Depth 10) | Set-Content -Path $taxiiCacheFile -Encoding UTF8 -ErrorAction Stop $taxiiSource = 'download' $taxiiCacheItem = Get-Item $taxiiCacheFile $taxiiCacheAgeHours = [math]::Max(0, [math]::Round(((Get-Date) - $taxiiCacheItem.LastWriteTime).TotalHours, 1)) } catch { if ($taxiiCacheExists) { Write-Warning "Failed to query TAXII server - falling back to cached copy ($taxiiCacheAgeHours hours old): $_" try { $taxiiObjects = @(Get-Content $taxiiCacheFile -Raw -ErrorAction Stop | ConvertFrom-Json -ErrorAction Stop) $taxiiSource = 'cache' } catch { Write-Warning "Could not read TAXII cache file: $_" } } else { Write-Warning "Failed to query TAXII server and no cache available - using built-in IOCs only: $_" } } } elseif ($taxiiCacheExists) { try { $taxiiObjects = @(Get-Content $taxiiCacheFile -Raw -ErrorAction Stop | ConvertFrom-Json -ErrorAction Stop) $taxiiSource = 'cache' } catch { Write-Warning "Could not read TAXII cache file: $_" } if ($null -ne $taxiiCacheAgeHours -and $taxiiCacheAgeHours -ge $FeedMaxAgeHours) { Write-Host " TAXII cache is $taxiiCacheAgeHours hours old - run with -UpdateFeeds to refresh" -ForegroundColor Yellow } } $taxiiEntriesAdded = 0 if ($null -ne $taxiiObjects) { foreach ($stixObject in $taxiiObjects) { if ($feedEntryCount -ge $MaxFeedEntries) { break } if ($null -eq $stixObject) { continue } $objectType = if ($stixObject.PSObject.Properties['type']) { [string]$stixObject.type } else { $null } if ($objectType -ne 'indicator') { continue } if (-not $stixObject.PSObject.Properties['pattern']) { continue } $extracted = @(& $extractStixIndicators -Pattern ([string]$stixObject.pattern) -SourceName 'TAXII') foreach ($indicator in $extracted) { $taxiiEntriesAdded += (. $addFeedIndicator -Kind $indicator['Kind'] -Value $indicator['Value'] -Severity 'High' -Description "TAXII indicator ($($indicator['Label']))") } } } $feedStats += [PSCustomObject]@{ Feed = 'TAXII' EntryCount = $taxiiEntriesAdded ObjectsFetched = if ($null -ne $taxiiObjects) { @($taxiiObjects).Count } else { 0 } PagesFetched = $taxiiPagesFetched CacheAgeHours = $taxiiCacheAgeHours Source = $taxiiSource } } } if ($feedEntryCount -gt 0) { Write-Host " Loaded $feedEntryCount indicators from live feeds" -ForegroundColor Green } else { Write-Host " No live feed indicators loaded - using built-in IOCs only" -ForegroundColor Yellow } } # Import indicators from a local STIX 2.x bundle (works fully offline) if (-not [string]::IsNullOrWhiteSpace($StixFilePath)) { Write-Host "`nImporting STIX bundle from: $StixFilePath" -ForegroundColor Cyan if (Test-Path $StixFilePath) { try { $stixBundle = Get-Content $StixFilePath -Raw -Encoding UTF8 -ErrorAction Stop | ConvertFrom-Json -ErrorAction Stop $stixObjects = @() if ($null -ne $stixBundle -and $stixBundle.PSObject.Properties['objects'] -and $null -ne $stixBundle.objects) { $stixObjects = @($stixBundle.objects) } $stixEntriesAdded = 0 foreach ($stixObject in $stixObjects) { if ($feedEntryCount -ge $MaxFeedEntries) { Write-Warning "Feed entry cap ($MaxFeedEntries) reached - skipping remaining STIX file indicators" break } if ($null -eq $stixObject) { continue } $objectType = if ($stixObject.PSObject.Properties['type']) { [string]$stixObject.type } else { $null } if ($objectType -ne 'indicator') { continue } if (-not $stixObject.PSObject.Properties['pattern']) { continue } $extracted = @(& $extractStixIndicators -Pattern ([string]$stixObject.pattern) -SourceName 'STIX file') foreach ($indicator in $extracted) { $stixEntriesAdded += (. $addFeedIndicator -Kind $indicator['Kind'] -Value $indicator['Value'] -Severity 'High' -Description "STIX file indicator ($($indicator['Label']))") } } $feedStats += [PSCustomObject]@{ Feed = 'STIX File' EntryCount = $stixEntriesAdded CacheAgeHours = $null Source = 'file' } Write-Host " Imported $stixEntriesAdded indicators from STIX file" -ForegroundColor Green } catch { Write-Warning "Failed to import STIX file '$StixFilePath': $_" } } else { Write-Warning "STIX file not found: $StixFilePath" } } $results.Summary.FeedStats = $feedStats # Load custom IOCs if provided if ($IOCFile -and (Test-Path $IOCFile)) { Write-Host "Loading custom IOCs from: $IOCFile" -ForegroundColor Yellow try { $customIOCs = Get-Content $IOCFile -Raw -Encoding UTF8 | ConvertFrom-Json foreach ($section in 'MaliciousProcesses', 'MaliciousIPs', 'MaliciousHashes') { if ($customIOCs.PSObject.Properties[$section]) { foreach ($entry in $customIOCs.$section.PSObject.Properties) { $key = if ($section -eq 'MaliciousProcesses') { $entry.Name.ToLower() } else { $entry.Name } $iocDatabase[$section][$key] = @{ Severity = if ($entry.Value.PSObject.Properties['Severity']) { $entry.Value.Severity } else { 'Medium' } Description = if ($entry.Value.PSObject.Properties['Description']) { $entry.Value.Description } else { 'Custom IOC' } } } } } if ($customIOCs.PSObject.Properties['SuspiciousPatterns']) { foreach ($patternGroup in 'ProcessCommandLine', 'FilePaths') { if ($customIOCs.SuspiciousPatterns.PSObject.Properties[$patternGroup]) { $iocDatabase.SuspiciousPatterns[$patternGroup] += @($customIOCs.SuspiciousPatterns.$patternGroup) } } } Write-Host "Custom IOCs loaded successfully" -ForegroundColor Green } catch { Write-Warning "Failed to load custom IOC file: $_" } } } process { try { # 1. Check Processes against IOCs if ($CheckProcesses) { Write-Host "`n[1/4] Checking processes against IOCs..." -ForegroundColor Cyan try { $foundCount = 0 # Process names and command lines come from CIM (Windows only) if (Get-Command Get-CimInstance -ErrorAction SilentlyContinue) { $processes = @() try { $processes = @(Get-CimInstance Win32_Process -ErrorAction Stop | Where-Object { $null -ne $_.CommandLine } | Select-Object Name, ProcessId, ExecutablePath, CommandLine) $results.SourcesChecked += 'Process names and command lines (Win32_Process via CIM)' } catch { $results.SourcesChecked += "Process name/command-line correlation unavailable (Win32_Process query failed: $_)" Write-Warning "Could not enumerate processes via CIM: $_" } } else { $processes = @() $results.SourcesChecked += 'Process name/command-line correlation unavailable (Get-CimInstance not present on this platform)' } foreach ($process in $processes) { # Check process name against malicious process list if ($iocDatabase.MaliciousProcesses.ContainsKey($process.Name.ToLower())) { $ioc = $iocDatabase.MaliciousProcesses[$process.Name.ToLower()] $match = [PSCustomObject]@{ Type = 'Process' IOCType = 'MaliciousProcess' Indicator = $process.Name ProcessId = $process.ProcessId Path = $process.ExecutablePath CommandLine = $process.CommandLine Severity = $ioc.Severity Description = $ioc.Description Timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss" } $results.Matches.ProcessIOCs += $match $foundCount++ switch ($ioc.Severity) { 'Critical' { $results.Summary.CriticalMatches++ } 'High' { $results.Summary.HighMatches++ } 'Medium' { $results.Summary.MediumMatches++ } } Write-Host " [!] MATCH: $($process.Name) [PID: $($process.ProcessId)]" -ForegroundColor Red Write-Host " Severity: $($ioc.Severity) - $($ioc.Description)" -ForegroundColor Red } # Check command line patterns if ($process.CommandLine) { foreach ($pattern in $iocDatabase.SuspiciousPatterns.ProcessCommandLine) { if ($process.CommandLine -match $pattern.Pattern) { $match = [PSCustomObject]@{ Type = 'Process' IOCType = 'SuspiciousCommandLine' Indicator = $pattern.Pattern ProcessId = $process.ProcessId ProcessName = $process.Name CommandLine = if ($process.CommandLine.Length -gt 200) { $process.CommandLine.Substring(0, 200) + "..." } else { $process.CommandLine } Severity = $pattern.Severity Description = $pattern.Description Timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss" } $results.Matches.ProcessIOCs += $match $foundCount++ switch ($pattern.Severity) { 'Critical' { $results.Summary.CriticalMatches++ } 'High' { $results.Summary.HighMatches++ } 'Medium' { $results.Summary.MediumMatches++ } } Write-Host " [!] MATCH: Pattern '$($pattern.Pattern)' in $($process.Name)" -ForegroundColor Red break } } # Check against feed-derived email indicators (heuristic: malicious mailboxes can appear in commands) foreach ($pattern in $iocDatabase.SuspiciousPatterns.EmailIndicators) { if ($process.CommandLine -match $pattern.Pattern) { $match = [PSCustomObject]@{ Type = 'Process' IOCType = 'FeedEmailIndicator' Indicator = $pattern.Pattern ProcessId = $process.ProcessId ProcessName = $process.Name CommandLine = if ($process.CommandLine.Length -gt 200) { $process.CommandLine.Substring(0, 200) + "..." } else { $process.CommandLine } Severity = $pattern.Severity Description = $pattern.Description Timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss" } $results.Matches.ProcessIOCs += $match $foundCount++ switch ($pattern.Severity) { 'Critical' { $results.Summary.CriticalMatches++ } 'High' { $results.Summary.HighMatches++ } 'Medium' { $results.Summary.MediumMatches++ } } Write-Host " [!] MATCH: Email indicator '$($pattern.Pattern)' in $($process.Name)" -ForegroundColor Red break } } } } # Hash the image paths of running processes and compare against # MaliciousHashes. Hash algorithms are derived from the stored # indicator key lengths (32 hex chars = MD5, 40 = SHA1, 64 = SHA256) if ($iocDatabase.MaliciousHashes.Count -gt 0) { if (Get-Command Get-FileHash -ErrorAction SilentlyContinue) { $hashAlgorithms = @() foreach ($hashKey in $iocDatabase.MaliciousHashes.Keys) { switch ([string]$hashKey.Length) { '32' { if ($hashAlgorithms -notcontains 'MD5') { $hashAlgorithms += 'MD5' } } '40' { if ($hashAlgorithms -notcontains 'SHA1') { $hashAlgorithms += 'SHA1' } } '64' { if ($hashAlgorithms -notcontains 'SHA256') { $hashAlgorithms += 'SHA256' } } } } if ($hashAlgorithms.Count -gt 0) { $maxHashFiles = 500 $imagePaths = @{} foreach ($runningProcess in @(Get-Process -ErrorAction SilentlyContinue)) { if ($imagePaths.Count -ge $maxHashFiles) { break } $imagePath = $null try { $imagePath = $runningProcess.Path } catch { continue } # access-denied/system process: skip silently if (-not [string]::IsNullOrWhiteSpace($imagePath) -and -not $imagePaths.ContainsKey($imagePath)) { $imagePaths[$imagePath] = $runningProcess.ProcessName } } $hashedFileCount = 0 foreach ($imagePath in $imagePaths.Keys) { $fileHashes = @{} foreach ($algorithm in $hashAlgorithms) { try { $fileHashes[$algorithm] = (Get-FileHash -Path $imagePath -Algorithm $algorithm -ErrorAction Stop).Hash.ToLower() } catch { $fileHashes = $null break # locked/inaccessible file: skip remaining algorithms silently } } if ($null -eq $fileHashes) { continue } $hashedFileCount++ foreach ($algorithm in $fileHashes.Keys) { $hashValue = $fileHashes[$algorithm] if ($iocDatabase.MaliciousHashes.ContainsKey($hashValue)) { $ioc = $iocDatabase.MaliciousHashes[$hashValue] $match = [PSCustomObject]@{ Type = 'Process' IOCType = 'MaliciousHash' Indicator = $hashValue HashAlgorithm = $algorithm ProcessName = $imagePaths[$imagePath] Path = $imagePath Severity = $ioc.Severity Description = $ioc.Description Timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss" } $results.Matches.ProcessIOCs += $match $foundCount++ switch ($ioc.Severity) { 'Critical' { $results.Summary.CriticalMatches++ } 'High' { $results.Summary.HighMatches++ } 'Medium' { $results.Summary.MediumMatches++ } } Write-Host " [!] MATCH: Malicious hash ($algorithm) for running process image '$imagePath'" -ForegroundColor Red } } } $results.SourcesChecked += "Running process image hashes ($hashedFileCount file(s) hashed, cap $maxHashFiles; algorithms: $($hashAlgorithms -join ', '))" } else { $results.SourcesChecked += 'Running process image hashes skipped (no recognizable MD5/SHA1/SHA256 indicators in MaliciousHashes)' } } else { $results.SourcesChecked += 'Running process image hashes unavailable (Get-FileHash not present on this platform)' } } else { $results.SourcesChecked += 'Running process image hashes skipped (no hash indicators loaded)' } if ($foundCount -eq 0) { Write-Host " No process IOC matches found" -ForegroundColor Green } } catch { Write-Warning "Error checking processes: $_" } } # 2. Check Network Connections against IOCs if ($CheckNetwork) { Write-Host "`n[2/4] Checking network connections against IOCs..." -ForegroundColor Cyan try { $foundCount = 0 # TCP connections carry literal IP addresses only, so they can # satisfy IP exact-match indicators but never domain/host indicators if (Get-Command Get-NetTCPConnection -ErrorAction SilentlyContinue) { $connections = @(Get-NetTCPConnection -State Established -ErrorAction SilentlyContinue) $results.SourcesChecked += "TCP connection table ($(@($connections).Count) established connection(s); IP exact match)" } else { $connections = @() $results.SourcesChecked += 'TCP connection table unavailable (Get-NetTCPConnection not present on this platform)' } foreach ($conn in $connections) { $remoteAddress = $conn.RemoteAddress.ToString() # Check against malicious IPs if ($iocDatabase.MaliciousIPs.ContainsKey($remoteAddress)) { $ioc = $iocDatabase.MaliciousIPs[$remoteAddress] $process = Get-Process -Id $conn.OwningProcess -ErrorAction SilentlyContinue $processName = if ($process) { $process.Name } else { "PID $($conn.OwningProcess)" } $match = [PSCustomObject]@{ Type = 'Network' IOCType = 'MaliciousIP' Indicator = $conn.RemoteAddress ProcessName = $processName ProcessId = $conn.OwningProcess LocalAddress = "$($conn.LocalAddress):$($conn.LocalPort)" RemoteAddress = "$($conn.RemoteAddress):$($conn.RemotePort)" Severity = $ioc.Severity Description = $ioc.Description Timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss" } $results.Matches.NetworkIOCs += $match $foundCount++ switch ($ioc.Severity) { 'Critical' { $results.Summary.CriticalMatches++ } 'High' { $results.Summary.HighMatches++ } 'Medium' { $results.Summary.MediumMatches++ } } Write-Host " [!] MATCH: Malicious IP $($conn.RemoteAddress) via $processName" -ForegroundColor Red } } # Feed-derived domain/URL-host indicators hold hostnames, not IPs, so # matching them against $conn.RemoteAddress (always a literal address) # could never fire. Correlate them against real hostname telemetry # instead: the DNS client cache and Sysmon DNS query events (ID 22) if ($iocDatabase.SuspiciousPatterns.NetworkIndicators.Count -gt 0) { $observedHostnames = @{} # Source (a): DNS client cache (Entry = queried name, Name = resolved name/alias) if (Get-Command Get-DnsClientCache -ErrorAction SilentlyContinue) { try { $dnsCacheEntries = @(Get-DnsClientCache -ErrorAction Stop) foreach ($cacheEntry in $dnsCacheEntries) { if ($null -eq $cacheEntry) { continue } foreach ($propertyName in 'Entry', 'Name') { if ($cacheEntry.PSObject.Properties[$propertyName]) { $cacheValue = [string]($cacheEntry.$propertyName) if (-not [string]::IsNullOrWhiteSpace($cacheValue) -and $cacheValue -notmatch '^\d{1,3}(\.\d{1,3}){3}$') { $observedHostnames[$cacheValue.TrimEnd('.').ToLower()] = 'DNS client cache' } } } } $results.SourcesChecked += "DNS client cache ($($observedHostnames.Count) hostname(s) observed)" } catch { $results.SourcesChecked += "DNS client cache unavailable (query failed: $_)" } } else { $results.SourcesChecked += 'DNS client cache unavailable (Get-DnsClientCache not present on this platform)' } # Source (b): Sysmon DNS query events (ID 22), bounded to the feed # freshness window (FeedMaxAgeHours) and a 500-event cap $sysmonLogName = 'Microsoft-Windows-Sysmon/Operational' if (Get-Command Get-WinEvent -ErrorAction SilentlyContinue) { $sysmonLogAvailable = $false try { $null = Get-WinEvent -ListLog $sysmonLogName -ErrorAction Stop $sysmonLogAvailable = $true } catch { $results.SourcesChecked += "Sysmon DNS query log (event 22) unavailable (log '$sysmonLogName' not present)" } if ($sysmonLogAvailable) { try { $sysmonQueryCount = 0 $dnsEvents = @(Get-WinEvent -FilterHashtable @{ LogName = $sysmonLogName Id = 22 StartTime = (Get-Date).AddHours(-$FeedMaxAgeHours) } -MaxEvents 500 -ErrorAction Stop) foreach ($dnsEvent in $dnsEvents) { if ($null -eq $dnsEvent) { continue } $eventData = ([xml]$dnsEvent.ToXml()).Event.EventData.Data $queryNode = @($eventData | Where-Object { $_.Name -eq 'QueryName' } | Select-Object -First 1) if ($queryNode.Count -gt 0 -and -not [string]::IsNullOrWhiteSpace($queryNode[0].InnerText)) { $observedHostnames[$queryNode[0].InnerText.TrimEnd('.').ToLower()] = 'Sysmon DNS query (event 22)' $sysmonQueryCount++ } } $results.SourcesChecked += "Sysmon DNS query log (event 22, last $FeedMaxAgeHours hour(s), $sysmonQueryCount query name(s))" } catch { $results.SourcesChecked += "Sysmon DNS query log (event 22) could not be read: $_" } } } else { $results.SourcesChecked += 'Sysmon DNS query log (event 22) unavailable (Get-WinEvent not present on this platform)' } foreach ($hostname in $observedHostnames.Keys) { foreach ($pattern in $iocDatabase.SuspiciousPatterns.NetworkIndicators) { if ($hostname -match $pattern.Pattern) { $match = [PSCustomObject]@{ Type = 'Network' IOCType = 'FeedDomainIndicator' Indicator = $pattern.Pattern MatchedHostname = $hostname HostnameSource = $observedHostnames[$hostname] Severity = $pattern.Severity Description = $pattern.Description Timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss" } $results.Matches.NetworkIOCs += $match $foundCount++ switch ($pattern.Severity) { 'Critical' { $results.Summary.CriticalMatches++ } 'High' { $results.Summary.HighMatches++ } 'Medium' { $results.Summary.MediumMatches++ } } Write-Host " [!] MATCH: Domain indicator '$($pattern.Pattern)' matched hostname '$hostname' ($($observedHostnames[$hostname]))" -ForegroundColor Red break } } } } else { $results.SourcesChecked += 'DNS client cache / Sysmon DNS query correlation skipped (no domain/host indicators loaded)' } if ($foundCount -eq 0) { Write-Host " No network IOC matches found" -ForegroundColor Green } } catch { Write-Warning "Error checking network: $_" } } # 3. Check Files against IOCs if ($CheckFiles) { Write-Host "`n[3/4] Checking files against IOCs..." -ForegroundColor Cyan try { # Check recent files in suspicious locations $suspiciousLocations = @( "$env:TEMP", "C:\Windows\Temp", "C:\Users\Public", "C:\ProgramData" ) $foundCount = 0 foreach ($location in $suspiciousLocations) { if (Test-Path $location) { $recentFiles = Get-ChildItem -Path $location -File -Recurse -ErrorAction SilentlyContinue -Depth 2 | Where-Object { $_.CreationTime -gt (Get-Date).AddDays(-7) -and $_.Extension -match '\.(exe|dll|ps1|bat|vbs|js)$' } | Select-Object -First 20 foreach ($file in $recentFiles) { # Check against file path patterns foreach ($pattern in $iocDatabase.SuspiciousPatterns.FilePaths) { if ($file.FullName -match $pattern.Pattern) { $match = [PSCustomObject]@{ Type = 'File' IOCType = 'SuspiciousFilePath' Indicator = $pattern.Pattern FilePath = $file.FullName FileName = $file.Name Size = $file.Length Created = $file.CreationTime.ToString("yyyy-MM-dd HH:mm:ss") Severity = $pattern.Severity Description = $pattern.Description Timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss" } $results.Matches.FileIOCs += $match $foundCount++ switch ($pattern.Severity) { 'Critical' { $results.Summary.CriticalMatches++ } 'High' { $results.Summary.HighMatches++ } 'Medium' { $results.Summary.MediumMatches++ } } Write-Host " [!] MATCH: $($file.Name) in suspicious location" -ForegroundColor Yellow break } } # Check file name against feed-derived filename indicators foreach ($pattern in $iocDatabase.SuspiciousPatterns.FileNameIndicators) { if ($file.Name -match $pattern.Pattern) { $match = [PSCustomObject]@{ Type = 'File' IOCType = 'FeedFileNameIndicator' Indicator = $pattern.Pattern FilePath = $file.FullName FileName = $file.Name Size = $file.Length Created = $file.CreationTime.ToString("yyyy-MM-dd HH:mm:ss") Severity = $pattern.Severity Description = $pattern.Description Timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss" } $results.Matches.FileIOCs += $match $foundCount++ switch ($pattern.Severity) { 'Critical' { $results.Summary.CriticalMatches++ } 'High' { $results.Summary.HighMatches++ } 'Medium' { $results.Summary.MediumMatches++ } } Write-Host " [!] MATCH: Filename indicator '$($file.Name)'" -ForegroundColor Red break } } } } } if ($foundCount -eq 0) { Write-Host " No file IOC matches found" -ForegroundColor Green } } catch { Write-Warning "Error checking files: $_" } } # 4. Check Registry against IOCs if ($CheckRegistry) { Write-Host "`n[4/4] Checking registry against IOCs..." -ForegroundColor Cyan try { # Check common persistence locations $persistenceKeys = @( 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Run', 'HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\Run' ) $foundCount = 0 foreach ($key in $persistenceKeys) { if (Test-Path $key) { $values = Get-ItemProperty -Path $key -ErrorAction SilentlyContinue foreach ($prop in $values.PSObject.Properties) { if ($prop.Name -match '^PS') { continue } $value = $prop.Value # Check for suspicious patterns in registry values if ($value -match 'powershell.*-enc|cmd.*http|wscript.*http|mshta.*http') { $match = [PSCustomObject]@{ Type = 'Registry' IOCType = 'SuspiciousRegistryValue' Indicator = 'Suspicious command pattern in registry' RegistryKey = $key ValueName = $prop.Name ValueData = if ($value.Length -gt 200) { $value.Substring(0, 200) + "..." } else { $value } Severity = 'High' Description = 'Potentially malicious command in registry autorun' Timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss" } $results.Matches.RegistryIOCs += $match $results.Summary.HighMatches++ $foundCount++ Write-Host " [!] MATCH: Suspicious pattern in $($prop.Name)" -ForegroundColor Red } } } } if ($foundCount -eq 0) { Write-Host " No registry IOC matches found" -ForegroundColor Green } } catch { Write-Warning "Error checking registry: $_" } } # Calculate summary $results.Summary.TotalMatches = $results.Matches.ProcessIOCs.Count + $results.Matches.NetworkIOCs.Count + $results.Matches.FileIOCs.Count + $results.Matches.RegistryIOCs.Count # Generate recommendations if ($results.Summary.TotalMatches -gt 0) { if ($results.Summary.CriticalMatches -gt 0) { $results.Recommendations += "CRITICAL: $($results.Summary.CriticalMatches) critical IOC match(es) detected - immediate incident response required" $results.Recommendations += "Isolate affected systems from network" $results.Recommendations += "Collect forensic evidence before remediation" } if ($results.Summary.HighMatches -gt 0) { $results.Recommendations += "HIGH: $($results.Summary.HighMatches) high-severity IOC match(es) require investigation" } $results.Recommendations += "Review all matched IOCs and their context" $results.Recommendations += "Check for additional indicators of compromise" $results.Recommendations += "Update threat intelligence feeds regularly" $results.Recommendations += "Consider implementing automated IOC scanning" if ($results.Matches.ProcessIOCs.Count -gt 0) { $results.Recommendations += "Terminate suspicious processes and investigate their origin" } if ($results.Matches.NetworkIOCs.Count -gt 0) { $results.Recommendations += "Block malicious IP addresses at firewall level" } } } catch { Write-Error "Error during threat intelligence matching: $_" throw } } end { Write-Host "`n=== SCAN COMPLETE ===" -ForegroundColor $( if ($results.Summary.CriticalMatches -gt 0) { 'Red' } elseif ($results.Summary.HighMatches -gt 0) { 'Yellow' } else { 'Green' } ) Write-Host "`nIOC Match Summary:" -ForegroundColor Cyan Write-Host " Total Matches: $($results.Summary.TotalMatches)" -ForegroundColor White Write-Host " Critical: $($results.Summary.CriticalMatches)" -ForegroundColor Red Write-Host " High: $($results.Summary.HighMatches)" -ForegroundColor Yellow Write-Host " Medium: $($results.Summary.MediumMatches)" -ForegroundColor Yellow if ($results.Summary.TotalMatches -gt 0) { Write-Host "`nMatch Breakdown:" -ForegroundColor Cyan if ($results.Matches.ProcessIOCs.Count -gt 0) { Write-Host " Process IOCs: $($results.Matches.ProcessIOCs.Count)" -ForegroundColor White } if ($results.Matches.NetworkIOCs.Count -gt 0) { Write-Host " Network IOCs: $($results.Matches.NetworkIOCs.Count)" -ForegroundColor White } if ($results.Matches.FileIOCs.Count -gt 0) { Write-Host " File IOCs: $($results.Matches.FileIOCs.Count)" -ForegroundColor White } if ($results.Matches.RegistryIOCs.Count -gt 0) { Write-Host " Registry IOCs: $($results.Matches.RegistryIOCs.Count)" -ForegroundColor White } Write-Host "`nTop Matches by Severity:" -ForegroundColor Cyan $allMatches = @() $allMatches += $results.Matches.ProcessIOCs $allMatches += $results.Matches.NetworkIOCs $allMatches += $results.Matches.FileIOCs $allMatches += $results.Matches.RegistryIOCs $allMatches | Sort-Object { switch ($_.Severity) { 'Critical' { 1 } 'High' { 2 } 'Medium' { 3 } } } | Select-Object -First 5 | ForEach-Object { Write-Host " [$($_.Severity)] $($_.IOCType): $($_.Indicator)" -ForegroundColor White } } else { Write-Host "`nNo IOC matches detected - system appears clean" -ForegroundColor Green } if ($results.Recommendations.Count -gt 0) { Write-Host "`nRecommendations:" -ForegroundColor Cyan $results.Recommendations | Select-Object -Unique | ForEach-Object { Write-Host " ! $_" -ForegroundColor Yellow } } if (@($results.Summary.FeedStats).Count -gt 0) { Write-Host "`nThreat Intelligence Feeds:" -ForegroundColor Cyan foreach ($stat in $results.Summary.FeedStats) { $ageText = if ($null -ne $stat.CacheAgeHours) { "$($stat.CacheAgeHours)h old" } else { 'not cached' } $pageText = '' if ($stat.PSObject.Properties['PagesFetched'] -and $null -ne $stat.PagesFetched -and $stat.PagesFetched -gt 0) { $pageText = ", $($stat.PagesFetched) page(s)" } Write-Host " - $($stat.Feed): $($stat.EntryCount) entries ($($stat.Source)$pageText, $ageText)" -ForegroundColor Gray } } if (@($results.SourcesChecked).Count -gt 0) { Write-Host "`nCorrelation Sources:" -ForegroundColor Cyan foreach ($sourceChecked in $results.SourcesChecked) { Write-Host " - $sourceChecked" -ForegroundColor Gray } } Write-Host "`nThreat Intelligence Sources:" -ForegroundColor Cyan Write-Host " - MITRE ATT&CK: https://attack.mitre.org/" -ForegroundColor Gray Write-Host " - AlienVault OTX: https://otx.alienvault.com/" -ForegroundColor Gray Write-Host " - VirusTotal: https://www.virustotal.com/" -ForegroundColor Gray Write-Host " - abuse.ch URLhaus/Feodo Tracker: https://urlhaus.abuse.ch/ | https://feodotracker.abuse.ch/" -ForegroundColor Gray Write-Host " - AbuseIPDB: https://www.abuseipdb.com/" -ForegroundColor Gray return $results } } |