Response/Export-SecurityReport.ps1
|
function Export-SecurityReport { <# .SYNOPSIS Generates comprehensive security assessment reports .DESCRIPTION Creates detailed security reports in multiple formats (HTML, JSON, CSV, NDJSON, CEF) with findings, recommendations, and executive summaries. NDJSON and CEF formats produce SIEM-ready finding exports via ConvertTo-SiemEvent (.ndjson / .cef files). When -StreamTo is specified, findings are additionally streamed live to a SIEM HTTP endpoint (Splunk HEC, Microsoft Sentinel, or a generic webhook) via Send-SiemEvent after the report files are written. A streaming failure never fails report generation; the outcome is noted on the console and in the StreamResult property of the output. .PARAMETER ReportType Type of report: Executive, Technical, or Compliance .PARAMETER Format Output format: HTML, JSON, CSV, NDJSON, CEF, or All .PARAMETER OutputPath Path to save the report .PARAMETER IncludeCharts Include visual charts and graphs in HTML reports .PARAMETER NoAutoOpen Do not automatically open the HTML report after generation. Use for headless or SYSTEM-context runs where no interactive session exists. .PARAMETER StreamTo Optional SIEM destination type to stream findings to after the report files are written: SplunkHEC, Sentinel, or GenericWebhook. Requires -StreamUri. .PARAMETER StreamUri SIEM endpoint URL used with -StreamTo. .PARAMETER StreamToken Credential for the streaming endpoint (Splunk HEC token or Sentinel shared key). Sent only in request headers, never logged in full. .PARAMETER StreamFormat Event format for streaming: NDJSON (default) or CEF. .EXAMPLE Export-SecurityReport -ReportType Technical -Format HTML -OutputPath "C:\Reports" Export-SecurityReport -ReportType Executive -Format All -IncludeCharts Export-SecurityReport -ReportType Technical -Format NDJSON -NoAutoOpen Export-SecurityReport -ReportType Technical -Format JSON -NoAutoOpen -StreamTo SplunkHEC -StreamUri "https://splunk.contoso.com:8088/services/collector" -StreamToken $hecToken .OUTPUTS String path(s) to generated report files. When -StreamTo is specified, returns a PSCustomObject with Files (string array) and StreamResult (PSCustomObject with BatchesSent, EventsSent, Failed, DurationSec; $null if streaming did not run) properties. #> [CmdletBinding()] param( [Parameter(Mandatory = $true)] [ValidateSet('Executive', 'Technical', 'Compliance')] [string]$ReportType, [Parameter()] [ValidateSet('HTML', 'JSON', 'CSV', 'NDJSON', 'CEF', 'All')] [string]$Format = 'HTML', [Parameter()] [string]$OutputPath = "C:\SecurityReports", [Parameter()] [switch]$IncludeCharts, [Parameter()] [switch]$NoAutoOpen, [Parameter()] [ValidateSet('SplunkHEC', 'Sentinel', 'GenericWebhook')] [string]$StreamTo, [Parameter()] [string]$StreamUri, [Parameter()] [string]$StreamToken, [Parameter()] [ValidateSet('NDJSON', 'CEF')] [string]$StreamFormat = 'NDJSON' ) begin { Write-Host "Generating $ReportType Security Report..." -ForegroundColor Cyan if (-not (Test-Path $OutputPath)) { New-Item -Path $OutputPath -ItemType Directory -Force | Out-Null } $timestamp = Get-Date -Format 'yyyyMMdd_HHmmss' $reportBaseName = "SecurityReport_${ReportType}_$timestamp" $streamResult = $null } process { try { Write-Host "Collecting security data..." -ForegroundColor Yellow # Null-safe property reader (same shape as New-ConsolidatedReport/Get-SafeProperty) function Get-SafeProperty { param($Object, $PropertyName, $Default = $null) if ($null -eq $Object) { return $Default } if ($Object -is [System.Collections.IDictionary]) { if ($Object.Contains($PropertyName)) { $value = $Object[$PropertyName] if ($null -eq $value) { return $Default } return $value } return $Default } if ($Object.PSObject.Properties[$PropertyName]) { $value = $Object.$PropertyName if ($null -eq $value) { return $Default } return $value } return $Default } # Collect all security data - each collector is isolated so a single # failing collector degrades its section instead of aborting the report $baseline = $null try { $baseline = Get-SecurityBaseline -ErrorAction Stop } catch { Write-Warning "Get-SecurityBaseline failed: $_" } $systemInfo = $null try { $systemInfo = Get-SystemInfo -ErrorAction Stop } catch { Write-Warning "Get-SystemInfo failed: $_" } $persistence = $null try { $persistence = Find-PersistenceMechanisms -ErrorAction Stop } catch { Write-Warning "Find-PersistenceMechanisms failed: $_" } $processes = $null try { $processes = Find-SuspiciousProcesses -ErrorAction Stop } catch { Write-Warning "Find-SuspiciousProcesses failed: $_" } $network = $null try { $network = Find-NetworkAnomalies -IncludeEstablished -ErrorAction Stop } catch { Write-Warning "Find-NetworkAnomalies failed: $_" } $auth = $null try { $auth = Find-SuspiciousAuthentication -Hours 168 -ErrorAction Stop } catch { Write-Warning "Find-SuspiciousAuthentication failed: $_" } $eventLog = $null try { $eventLog = Get-EventLogAnalysis -Hours 168 -ErrorAction Stop } catch { Write-Warning "Get-EventLogAnalysis failed: $_" } $registry = $null try { $registry = Get-RegistryAnalysis -IncludeAutoRun -ErrorAction Stop } catch { Write-Warning "Get-RegistryAnalysis failed: $_" } # Safe scalar views of collector output (strict-mode safe for null/malformed results) $baselineScore = Get-SafeProperty $baseline 'SecurityScore' $null $baselineDefender = Get-SafeProperty $baseline 'WindowsDefender' $null $baselineFirewall = Get-SafeProperty $baseline 'Firewall' $null $baselineUac = Get-SafeProperty $baseline 'UAC' $null $persistenceSuspicious = Get-SafeProperty $persistence 'SuspiciousCount' 0 $persistenceTotal = Get-SafeProperty $persistence 'TotalFindings' 0 $authStats = Get-SafeProperty $auth 'Statistics' $null $authLockouts = Get-SafeProperty $authStats 'TotalLockouts' 0 $authFailedLogons = Get-SafeProperty $authStats 'TotalFailedLogons' 0 $networkAnomalies = Get-SafeProperty $network 'TotalAnomalies' 0 # Build report data structure $reportData = [PSCustomObject]@{ ReportType = $ReportType GeneratedDate = Get-Date -Format "yyyy-MM-dd HH:mm:ss" ComputerName = $env:COMPUTERNAME GeneratedBy = $env:USERNAME SecurityScore = $baselineScore Summary = @{ CriticalFindings = 0 HighFindings = 0 MediumFindings = 0 LowFindings = 0 } Findings = @() Recommendations = @() Details = @{ Baseline = $baseline SystemInfo = $systemInfo Persistence = $persistence Processes = $processes Network = $network Authentication = $auth EventLog = $eventLog Registry = $registry } } # Analyze findings and create recommendations Write-Host "Analyzing findings..." -ForegroundColor Yellow # Critical findings (only when a baseline score was actually collected - # a failed collector must not fabricate a "critically low" finding) if ($null -ne $baselineScore -and $baselineScore -lt 50) { $reportData.Summary.CriticalFindings++ $reportData.Findings += [PSCustomObject]@{ Severity = 'Critical' Category = 'Security Baseline' Finding = "Security score is critically low: $baselineScore/100" Impact = 'System is highly vulnerable to attacks' Recommendation = 'Immediate remediation required' } } if ($persistenceSuspicious -gt 5) { $reportData.Summary.HighFindings++ $reportData.Findings += [PSCustomObject]@{ Severity = 'High' Category = 'Persistence' Finding = "$persistenceSuspicious suspicious persistence mechanisms detected" Impact = 'Potential malware or unauthorized software present' Recommendation = 'Review and remove unauthorized persistence mechanisms' } } if (@($processes).Count -gt 0) { $processArray = @($processes) $highSeverityProcs = @($processArray | Where-Object { $null -ne $_ -and $_.PSObject.Properties['SuspicionScore'] -and $_.SuspicionScore -ge 70 }) if ($highSeverityProcs.Count -gt 0) { $reportData.Summary.HighFindings++ $reportData.Findings += [PSCustomObject]@{ Severity = 'High' Category = 'Processes' Finding = "$($highSeverityProcs.Count) highly suspicious processes running" Impact = 'Potential active threat on system' Recommendation = 'Investigate and terminate malicious processes' } } } if ($authLockouts -gt 0) { $reportData.Summary.MediumFindings++ $reportData.Findings += [PSCustomObject]@{ Severity = 'Medium' Category = 'Authentication' Finding = "$authLockouts account lockouts detected" Impact = 'Possible brute force attempts' Recommendation = 'Review failed authentication events and implement MFA' } } if ($networkAnomalies -gt 10) { $reportData.Summary.MediumFindings++ $reportData.Findings += [PSCustomObject]@{ Severity = 'Medium' Category = 'Network' Finding = "$networkAnomalies network anomalies detected" Impact = 'Unusual network activity may indicate compromise' Recommendation = 'Review network connections and implement network monitoring' } } # Generate recommendations based on report type if ($ReportType -eq 'Executive') { $reportData.Recommendations = @( "$(if ($null -ne $baselineScore) { "Overall Security Score: $baselineScore/100 - $(if ($baselineScore -ge 80) { 'Good' } elseif ($baselineScore -ge 60) { 'Fair' } else { 'Poor' })" } else { 'Overall Security Score: N/A - baseline collection failed' })", "Critical Issues: $($reportData.Summary.CriticalFindings)", "High Priority Issues: $($reportData.Summary.HighFindings)", "Immediate action required on critical and high priority items", "Regular security assessments recommended" ) } elseif ($ReportType -eq 'Technical') { $reportData.Recommendations = @( "Enable PowerShell logging and monitoring", "Implement application whitelisting", "Review and harden security baselines", "Enable Windows Defender real-time protection", "Implement network segmentation", "Deploy EDR solution for advanced threat detection", "Regular vulnerability assessments", "Incident response plan testing" ) } else { # Compliance $reportData.Recommendations = @( "Document all security controls", "Implement continuous compliance monitoring", "Regular audit log reviews", "Access control reviews", "Patch management process", "Security awareness training", "Incident response procedures", "Business continuity planning" ) } # Generate reports in requested formats $generatedFiles = @() if ($Format -in @('JSON', 'All')) { Write-Host "Generating JSON report..." -ForegroundColor Yellow $jsonPath = Join-Path $OutputPath "$reportBaseName.json" $reportData | ConvertTo-Json -Depth 10 | Out-File $jsonPath -Encoding UTF8 $generatedFiles += $jsonPath } if ($Format -in @('CSV', 'All')) { Write-Host "Generating CSV reports..." -ForegroundColor Yellow $csvPath = Join-Path $OutputPath "${reportBaseName}_Findings.csv" $reportData.Findings | Export-Csv $csvPath -NoTypeInformation $generatedFiles += $csvPath } if ($Format -in @('NDJSON', 'All')) { Write-Host "Generating NDJSON SIEM export..." -ForegroundColor Yellow try { $ndjsonPath = Join-Path $OutputPath "${reportBaseName}_findings.ndjson" $ndjsonLines = @($reportData.Findings | ConvertTo-SiemEvent -Format NDJSON) $ndjsonLines | Out-File $ndjsonPath -Encoding UTF8 $generatedFiles += $ndjsonPath } catch { Write-Warning "Could not generate NDJSON export: $_" } } if ($Format -in @('CEF', 'All')) { Write-Host "Generating CEF SIEM export..." -ForegroundColor Yellow try { $cefPath = Join-Path $OutputPath "${reportBaseName}_findings.cef" $cefLines = @($reportData.Findings | ConvertTo-SiemEvent -Format CEF) $cefLines | Out-File $cefPath -Encoding UTF8 $generatedFiles += $cefPath } catch { Write-Warning "Could not generate CEF export: $_" } } if ($Format -in @('HTML', 'All')) { Write-Host "Generating HTML report..." -ForegroundColor Yellow # Build findings-by-severity bar chart (inline SVG) if requested $chartHtml = '' if ($IncludeCharts) { $severityData = @( @{ Label = 'Critical'; Count = $reportData.Summary.CriticalFindings; Color = '#e74c3c' } @{ Label = 'High'; Count = $reportData.Summary.HighFindings; Color = '#e67e22' } @{ Label = 'Medium'; Count = $reportData.Summary.MediumFindings; Color = '#f39c12' } @{ Label = 'Low'; Count = $reportData.Summary.LowFindings; Color = '#95a5a6' } ) $maxCount = ($severityData | ForEach-Object { $_.Count } | Measure-Object -Maximum).Maximum if ($null -eq $maxCount -or $maxCount -lt 1) { $maxCount = 1 } $chartWidth = 500 $barHeight = 30 $barSpacing = 15 $labelWidth = 80 $maxBarWidth = $chartWidth - $labelWidth - 50 $chartHeight = ($barHeight + $barSpacing) * $severityData.Count + 10 $chartHtml = "<div class='summary'><h2>Findings by Severity</h2>`n" $chartHtml += "<svg width='$chartWidth' height='$chartHeight' xmlns='http://www.w3.org/2000/svg' role='img' aria-label='Findings by severity bar chart'>`n" $barY = 5 foreach ($item in $severityData) { $barWidth = [math]::Round(($item.Count / $maxCount) * $maxBarWidth) if ($item.Count -gt 0 -and $barWidth -lt 2) { $barWidth = 2 } if ($barWidth -lt 0) { $barWidth = 0 } $chartHtml += "<text x='0' y='$($barY + 20)' font-size='13' fill='#333'>$($item.Label)</text>`n" $chartHtml += "<rect x='$labelWidth' y='$barY' width='$barWidth' height='$barHeight' fill='$($item.Color)' rx='3' />`n" $chartHtml += "<text x='$($labelWidth + $barWidth + 8)' y='$($barY + 20)' font-size='13' font-weight='bold' fill='#333'>$($item.Count)</text>`n" $barY += $barHeight + $barSpacing } $chartHtml += "</svg></div>" } # Create HTML report $htmlContent = @" <!DOCTYPE html> <html> <head> <title>$ReportType Security Report - $($reportData.ComputerName)</title> <style> body { font-family: Arial, sans-serif; margin: 20px; background-color: #f5f5f5; } .header { background-color: #2c3e50; color: white; padding: 20px; border-radius: 5px; } .summary { background-color: white; padding: 20px; margin: 20px 0; border-radius: 5px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); } .score { font-size: 48px; font-weight: bold; color: #3498db; } .finding { background-color: white; padding: 15px; margin: 10px 0; border-radius: 5px; border-left: 4px solid; } .critical { border-left-color: #e74c3c; } .high { border-left-color: #e67e22; } .medium { border-left-color: #f39c12; } .low { border-left-color: #95a5a6; } table { width: 100%; border-collapse: collapse; background-color: white; } th, td { padding: 12px; text-align: left; border-bottom: 1px solid #ddd; } th { background-color: #34495e; color: white; } .recommendation { background-color: #ecf0f1; padding: 10px; margin: 5px 0; border-radius: 3px; } </style> </head> <body> <div class="header"> <h1>$ReportType Security Assessment Report</h1> <p>Computer: $($reportData.ComputerName)</p> <p>Generated: $($reportData.GeneratedDate)</p> <p>Generated By: $($reportData.GeneratedBy)</p> </div> <div class="summary"> <h2>Executive Summary</h2> <div class="score">Security Score: $($reportData.SecurityScore)/100</div> <table> <tr> <th>Severity</th> <th>Count</th> </tr> <tr> <td>Critical</td> <td style="color: #e74c3c; font-weight: bold;">$($reportData.Summary.CriticalFindings)</td> </tr> <tr> <td>High</td> <td style="color: #e67e22; font-weight: bold;">$($reportData.Summary.HighFindings)</td> </tr> <tr> <td>Medium</td> <td style="color: #f39c12; font-weight: bold;">$($reportData.Summary.MediumFindings)</td> </tr> <tr> <td>Low</td> <td style="color: #95a5a6;">$($reportData.Summary.LowFindings)</td> </tr> </table> </div> $chartHtml <div class="summary"> <h2>Key Findings</h2> $($reportData.Findings | ForEach-Object { $class = $_.Severity.ToLower() "<div class='finding $class'> <h3>[$($_.Severity)] $($_.Category)</h3> <p><strong>Finding:</strong> $($_.Finding)</p> <p><strong>Impact:</strong> $($_.Impact)</p> <p><strong>Recommendation:</strong> $($_.Recommendation)</p> </div>" } | Out-String) </div> <div class="summary"> <h2>Recommendations</h2> $($reportData.Recommendations | ForEach-Object { "<div class='recommendation'>$_</div>" } | Out-String) </div> <div class="summary"> <h2>System Details</h2> <table> <tr><th>Component</th><th>Status</th></tr> <tr><td>Windows Defender</td><td>$(if ($null -ne $baselineDefender -and $baselineDefender.PSObject.Properties['RealTimeProtectionEnabled']) { $baselineDefender.RealTimeProtectionEnabled } else { $baselineDefender })</td></tr> <tr><td>Firewall</td><td>$(if ($baselineFirewall -is [array] -and $baselineFirewall.Count -gt 0) { $baselineFirewall[0].Enabled } else { $baselineFirewall })</td></tr> <tr><td>UAC</td><td>$(if ($null -ne $baselineUac -and $baselineUac.PSObject.Properties['EnableLUA']) { $baselineUac.EnableLUA -eq 1 } else { $baselineUac })</td></tr> <tr><td>Persistence Mechanisms</td><td>$persistenceTotal total ($persistenceSuspicious suspicious)</td></tr> <tr><td>Network Anomalies</td><td>$networkAnomalies</td></tr> <tr><td>Failed Logons</td><td>$authFailedLogons</td></tr> </table> </div> <div class="summary"> <p><em>This report was generated by WindowsSecurityAudit PowerShell Module</em></p> </div> </body> </html> "@ $htmlPath = Join-Path $OutputPath "$reportBaseName.html" $htmlContent | Out-File $htmlPath -Encoding UTF8 $generatedFiles += $htmlPath } if (-not [string]::IsNullOrWhiteSpace($StreamTo)) { Write-Host "Streaming findings to $StreamTo endpoint..." -ForegroundColor Yellow try { if ([string]::IsNullOrWhiteSpace($StreamUri)) { Write-Warning "StreamTo was specified without StreamUri; skipping SIEM streaming." } else { $streamParams = @{ Destination = $StreamTo Uri = $StreamUri Format = $StreamFormat } if (-not [string]::IsNullOrWhiteSpace($StreamToken)) { $streamParams['Token'] = $StreamToken } $streamResult = @($reportData.Findings) | Send-SiemEvent @streamParams Write-Host "Streamed $($streamResult.EventsSent) of $(@($reportData.Findings).Count) finding event(s) to $StreamTo ($($streamResult.Failed) failed)." -ForegroundColor Green } } catch { Write-Warning "SIEM streaming failed; report files were still generated: $_" } } } catch { Write-Error "Error generating report: $_" throw } } end { Write-Host "`n=== REPORT GENERATION COMPLETE ===" -ForegroundColor Green Write-Host "Generated Files:" -ForegroundColor Cyan foreach ($file in $generatedFiles) { Write-Host " - $file" -ForegroundColor White } if (-not [string]::IsNullOrWhiteSpace($StreamTo)) { if ($null -ne $streamResult) { Write-Host "SIEM Stream ($StreamTo): $($streamResult.EventsSent) event(s) sent in $($streamResult.BatchesSent) batch(es), $($streamResult.Failed) failed, $($streamResult.DurationSec)s" -ForegroundColor Cyan } else { Write-Host "SIEM Stream ($StreamTo): not delivered (see warnings above)" -ForegroundColor Yellow } } # Open HTML report if generated if (-not $NoAutoOpen -and @($generatedFiles | Where-Object { $_ -match '\.html$' }).Count -gt 0) { $htmlFile = @($generatedFiles | Where-Object { $_ -match '\.html$' }) | Select-Object -First 1 try { Start-Process $htmlFile } catch { Write-Warning "Could not auto-open HTML report: $_" } } if (-not [string]::IsNullOrWhiteSpace($StreamTo)) { return [PSCustomObject]@{ Files = $generatedFiles StreamResult = $streamResult } } return $generatedFiles } } |