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 .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 = @() } # 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 { $processes = Get-CimInstance Win32_Process | Where-Object { $null -ne $_.CommandLine } | Select-Object Name, ProcessId, ExecutablePath, CommandLine $foundCount = 0 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 } } } } 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 { $connections = Get-NetTCPConnection -State Established -ErrorAction SilentlyContinue $foundCount = 0 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 } # Check against feed-derived network indicators foreach ($pattern in $iocDatabase.SuspiciousPatterns.NetworkIndicators) { if ($remoteAddress -match $pattern.Pattern) { $process = Get-Process -Id $conn.OwningProcess -ErrorAction SilentlyContinue $processName = if ($process) { $process.Name } else { "PID $($conn.OwningProcess)" } $match = [PSCustomObject]@{ Type = 'Network' IOCType = 'FeedNetworkIndicator' Indicator = $pattern.Pattern ProcessName = $processName ProcessId = $conn.OwningProcess LocalAddress = "$($conn.LocalAddress):$($conn.LocalPort)" RemoteAddress = "$($conn.RemoteAddress):$($conn.RemotePort)" 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: Feed indicator '$($pattern.Pattern)' via $processName" -ForegroundColor Red break } } } 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 } } 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 } } |