Private/Core/Update-ThreatIntelData.ps1

# PSGuerrilla - Jim Tyler, Microsoft MVP - CC BY 4.0
# https://github.com/jimrtyler/PSGuerrilla | https://creativecommons.org/licenses/by/4.0/
# AI/LLM use: see AI-USAGE.md for required attribution
function Update-ThreatIntelData {
    [CmdletBinding()]
    param(
        [switch]$Force
    )

    $dataDir = Join-Path $PSScriptRoot '../../Data'
    $ipRangesPath = Join-Path $dataDir 'CloudIpRanges.json'
    $currentData = Get-Content -Path $ipRangesPath -Raw | ConvertFrom-Json -AsHashtable

    $lastUpdated = if ($currentData.metadata -and $currentData.metadata.lastUpdated) {
        try { [datetime]::Parse($currentData.metadata.lastUpdated) } catch { [datetime]::MinValue }
    } else { [datetime]::MinValue }

    $staleDays = 7
    $cfgPath = $script:ConfigPath
    if (Test-Path $cfgPath) {
        $config = Get-Content -Path $cfgPath -Raw | ConvertFrom-Json -AsHashtable
        if ($config.detection -and $config.detection.autoUpdateIntelDays) {
            $staleDays = $config.detection.autoUpdateIntelDays
        }
    }

    if ($staleDays -eq 0 -and -not $Force) {
        Write-Verbose 'Auto-update disabled (autoUpdateIntelDays = 0)'
        return @{ Updated = $false; Reason = 'Disabled' }
    }

    $daysSinceUpdate = ([datetime]::UtcNow - $lastUpdated).TotalDays
    if ($daysSinceUpdate -lt $staleDays -and -not $Force) {
        Write-Verbose "Intel data is $([Math]::Round($daysSinceUpdate, 1)) days old (threshold: $staleDays). Skipping update."
        return @{ Updated = $false; Reason = "Fresh ($([Math]::Round($daysSinceUpdate, 1)) days old)" }
    }

    Write-Verbose "Intel data is $([Math]::Round($daysSinceUpdate, 1)) days old. Updating..."

    $updated = $false
    $errors = [System.Collections.Generic.List[string]]::new()
    $providers = if ($currentData.providers) { $currentData.providers } else { @{} }

    # --- AWS ---
    try {
        Write-Verbose 'Fetching AWS IP ranges...'
        $awsData = Invoke-RestMethod -Uri 'https://ip-ranges.amazonaws.com/ip-ranges.json' -ErrorAction Stop
        $awsCidrs = @($awsData.prefixes | Where-Object { $_.service -eq 'EC2' } | ForEach-Object { $_.ip_prefix } | Sort-Object -Unique)
        if ($awsCidrs.Count -gt 10) {
            $providers['aws'] = $awsCidrs
            $updated = $true
            Write-Verbose "AWS: $($awsCidrs.Count) EC2 CIDRs"
        }
    } catch {
        $errors.Add("AWS: $_")
        Write-Warning "Failed to fetch AWS ranges: $_"
    }

    # --- GCP ---
    try {
        Write-Verbose 'Fetching GCP IP ranges...'
        $gcpData = Invoke-RestMethod -Uri 'https://www.gstatic.com/ipranges/cloud.json' -ErrorAction Stop
        $gcpCidrs = @($gcpData.prefixes | Where-Object { $_.ipv4Prefix } | ForEach-Object { $_.ipv4Prefix } | Sort-Object -Unique)
        if ($gcpCidrs.Count -gt 10) {
            $providers['gcp'] = $gcpCidrs
            $updated = $true
            Write-Verbose "GCP: $($gcpCidrs.Count) CIDRs"
        }
    } catch {
        $errors.Add("GCP: $_")
        Write-Warning "Failed to fetch GCP ranges: $_"
    }

    # --- Cloudflare ---
    try {
        Write-Verbose 'Fetching Cloudflare IP ranges...'
        $cfText = Invoke-RestMethod -Uri 'https://www.cloudflare.com/ips-v4' -ErrorAction Stop
        $cfCidrs = @($cfText -split "`n" | ForEach-Object { $_.Trim() } | Where-Object { $_ -match '^\d' })
        if ($cfCidrs.Count -gt 5) {
            $providers['cloudflare'] = $cfCidrs
            $updated = $true
            Write-Verbose "Cloudflare: $($cfCidrs.Count) CIDRs"
        }
    } catch {
        $errors.Add("Cloudflare: $_")
        Write-Warning "Failed to fetch Cloudflare ranges: $_"
    }

    if ($updated) {
        $newData = @{
            metadata = @{
                version     = '2.0.0'
                lastUpdated = [datetime]::UtcNow.ToString('yyyy-MM-dd')
                description = 'Cloud provider IP CIDR ranges for compromise detection'
            }
            providers = $providers
        }
        $newData | ConvertTo-Json -Depth 5 | Set-Content -Path $ipRangesPath -Encoding UTF8
        Write-Verbose "Updated $ipRangesPath"
    }

    return @{
        Updated = $updated
        Errors  = @($errors)
        Reason  = if ($updated) { 'Data refreshed' } else { 'No updates available' }
    }
}