Skip to content

Instantly share code, notes, and snippets.

@TechByTom
Last active February 27, 2025 19:57
Show Gist options
  • Save TechByTom/04d3ac248b0a197048e27f44700c94f7 to your computer and use it in GitHub Desktop.
Save TechByTom/04d3ac248b0a197048e27f44700c94f7 to your computer and use it in GitHub Desktop.
Internal Admin Interface Discovery + Categorization + Reporting.
param(
[Parameter(Mandatory=$true)]
[string]$ScanDirectory,
[Parameter(Mandatory=$true)]
[string]$FingerprintsFile,
[Parameter(Mandatory=$false)]
[string]$OutputDirectory = ".\categorized_interfaces"
)
# Create Windows shortcut (.lnk) file
function New-Shortcut {
param(
[Parameter(Mandatory=$true)]
[string]$SourcePath,
[Parameter(Mandatory=$true)]
[string]$ShortcutPath
)
# Convert paths to absolute paths
$SourcePath = [System.IO.Path]::GetFullPath($SourcePath)
$ShortcutPath = [System.IO.Path]::GetFullPath($ShortcutPath)
$WScriptShell = New-Object -ComObject WScript.Shell
$Shortcut = $WScriptShell.CreateShortcut($ShortcutPath)
$Shortcut.TargetPath = $SourcePath
$Shortcut.WorkingDirectory = [System.IO.Path]::GetDirectoryName($SourcePath)
$Shortcut.Save()
}
# Get original file path from .lnk file
function Get-ShortcutTarget {
param(
[string]$ShortcutPath
)
$WScriptShell = New-Object -ComObject WScript.Shell
$Shortcut = $WScriptShell.CreateShortcut($ShortcutPath)
return $Shortcut.TargetPath
}
# Function to test if a string matches any pattern in an array
function Test-Patterns {
param(
[string]$Text,
[array]$Patterns
)
foreach ($pattern in $Patterns) {
if ($Text -match [regex]::Escape($pattern)) {
return $true
}
}
return $false
}
# Function to check if headers match the specified patterns
function Test-Headers {
param(
[array]$Headers,
$HeaderPatterns
)
# Convert PSObject to Hashtable if needed
$headerPatternsHash = $HeaderPatterns | ConvertTo-Hashtable
foreach ($headerPattern in $headerPatternsHash.GetEnumerator()) {
$headerFound = $false
foreach ($header in $Headers) {
if ($header.Key -eq $headerPattern.Key) {
foreach ($value in $header.Value) {
if (Test-Patterns -Text $value -Patterns $headerPattern.Value) {
$headerFound = $true
break
}
}
}
}
if (-not $headerFound) {
return $false
}
}
return $true
}
# Function to convert PSObject to Hashtable
function ConvertTo-Hashtable {
param (
[Parameter(ValueFromPipeline)]
$InputObject
)
process {
if ($null -eq $InputObject) { return $null }
if ($InputObject -is [System.Collections.Hashtable]) { return $InputObject }
$hash = @{}
$InputObject.PSObject.Properties | ForEach-Object {
$hash[$_.Name] = $_.Value
}
return $hash
}
}
# Function to categorize a single interface based on fingerprints
function Get-InterfaceCategory {
param(
[PSCustomObject]$InterfaceData,
[PSCustomObject]$Fingerprints
)
foreach ($category in $Fingerprints.PSObject.Properties) {
$fingerprint = $category.Value
$allPropertiesMatched = $true
# Check URL patterns if specified
if ($fingerprint.urlPatterns) {
$urlFound = $false
foreach ($port in $InterfaceData.OpenPorts) {
if ($port.WebRequest) {
$requests = @($port.WebRequest)
if ($requests[0] -isnot [array]) { $requests = @($requests) }
foreach ($request in $requests) {
if (Test-Patterns -Text $request.Url -Patterns $fingerprint.urlPatterns) {
$urlFound = $true
break
}
}
}
}
if (-not $urlFound) {
$allPropertiesMatched = $false
continue
}
}
# Check content patterns if specified
if ($allPropertiesMatched -and $fingerprint.contentPatterns) {
$contentFound = $false
foreach ($port in $InterfaceData.OpenPorts) {
if ($port.WebRequest) {
$requests = @($port.WebRequest)
if ($requests[0] -isnot [array]) { $requests = @($requests) }
foreach ($request in $requests) {
if ($request.Content -and (Test-Patterns -Text $request.Content -Patterns $fingerprint.contentPatterns)) {
$contentFound = $true
break
}
}
}
}
if (-not $contentFound) {
$allPropertiesMatched = $false
continue
}
}
# Check header patterns if specified
if ($allPropertiesMatched -and $fingerprint.headerPatterns) {
$headersFound = $false
foreach ($port in $InterfaceData.OpenPorts) {
if ($port.WebRequest) {
$requests = @($port.WebRequest)
if ($requests[0] -isnot [array]) { $requests = @($requests) }
foreach ($request in $requests) {
if ($request.Headers -and (Test-Headers -Headers $request.Headers -HeaderPatterns $fingerprint.headerPatterns)) {
$headersFound = $true
break
}
}
}
}
if (-not $headersFound) {
$allPropertiesMatched = $false
continue
}
}
# Only return the category if ALL specified properties match
if ($allPropertiesMatched) {
return $category.Name
}
}
return "Unknown"
}
# Function to extract title from HTML content
function Get-HtmlTitle {
param(
[string]$Content
)
if (-not $Content) { return $null }
if ($Content -match '<title[^>]*>(.*?)</title>') {
return $matches[1].Trim()
}
return $null
}
# Updated report generation function
function New-CategoryReport {
param(
[string]$OutputDirectory,
[string]$ScanDirectory
)
$timestamp = Get-Date -Format "yyyyMMdd_HHmmss"
$reportPath = Join-Path $OutputDirectory "categorization_report_${timestamp}.html"
$categories = Get-ChildItem -Path $OutputDirectory -Directory
$html = @"
<!DOCTYPE html>
<html>
<head>
<title>Interface Categorization Report</title>
<style>
body { font-family: Arial, sans-serif; margin: 20px; }
h1 { color: #333; }
h2 { color: #666; margin-top: 30px; }
h3 { color: #666; margin-top: 20px; margin-left: 20px; }
.category { margin-bottom: 30px; }
table { border-collapse: collapse; width: 100%; margin-top: 10px; }
th, td { border: 1px solid #ddd; padding: 8px; text-align: left; }
th { background-color: #f5f5f5; }
tr:nth-child(even) { background-color: #f9f9f9; }
tr:hover { background-color: #f5f5f5; }
.stats { margin: 20px 0; padding: 10px; background-color: #f8f9fa; border-radius: 4px; }
.category-count { color: #666; font-size: 0.9em; margin-left: 10px; }
.collapse-btn {
background: none;
border: none;
color: #666;
cursor: pointer;
font-size: 1.2em;
padding: 0 10px;
vertical-align: middle;
}
.collapse-btn:hover { color: #333; }
.section-header {
display: flex;
align-items: center;
}
.collapsed { display: none; }
</style>
<script>
function toggleSection(btn, targetId) {
const section = document.getElementById(targetId);
const isCollapsed = section.classList.contains('collapsed');
section.classList.toggle('collapsed');
btn.textContent = isCollapsed ? '▼' : '▶';
}
</script>
</head>
<body>
<h1>Interface Categorization Report</h1>
<div class="stats">
<p>Total Categories: $($categories.Count)</p>
<p>Generated: $(Get-Date)</p>
</div>
"@
# First process all non-Unknown categories
foreach ($category in ($categories | Where-Object { $_.Name -ne "Unknown" })) {
$files = Get-ChildItem -Path $category.FullName -Filter "*.lnk"
$sectionId = "category-$($category.Name.ToLower())"
$html += @"
<div class="category">
<div class="section-header">
<button class="collapse-btn" onclick="toggleSection(this, '$sectionId')">▼</button>
<h2>$($category.Name) <span class="category-count">($($files.Count) interfaces)</span></h2>
</div>
<div id="$sectionId">
<table>
<tr>
<th>IP Address</th>
<th>Hostname</th>
<th>Ports</th>
<th>Web Links</th>
<th>JSON File</th>
</tr>
"@
foreach ($file in $files) {
$originalPath = Get-ShortcutTarget -ShortcutPath $file.FullName
$interfaceData = Get-Content $originalPath | ConvertFrom-Json
$ports = ($interfaceData.OpenPorts | ForEach-Object { $_.PortNumber }) -join ", "
$links = @()
foreach ($port in $interfaceData.OpenPorts) {
if ($port.WebRequest) {
$requests = @($port.WebRequest)
if ($requests[0] -isnot [array]) { $requests = @($requests) }
foreach ($request in $requests) {
if ($request.Url) {
$links += $request.Url
}
}
}
}
$linksHtml = ($links | Select-Object -Unique | ForEach-Object {
"<a href='$_' target='_blank'>$_</a>"
}) -join "<br>"
$html += @"
<tr>
<td>$($interfaceData.IP)</td>
<td>$($interfaceData.Hostname)</td>
<td>$ports</td>
<td>$linksHtml</td>
<td><a href="file://$originalPath">$([System.IO.Path]::GetFileName($originalPath))</a></td>
</tr>
"@
}
$html += @"
</table>
</div>
</div>
"@
}
# Process Unknown category with title grouping
$unknownCategory = $categories | Where-Object { $_.Name -eq "Unknown" }
if ($unknownCategory) {
$files = Get-ChildItem -Path $unknownCategory.FullName -Filter "*.lnk"
if ($files) {
$html += @"
<div class="category">
<div class="section-header">
<button class="collapse-btn" onclick="toggleSection(this, 'category-unknown')">▼</button>
<h2>Unknown <span class="category-count">($($files.Count) interfaces)</span></h2>
</div>
<div id="category-unknown">
"@
# Group unknown files by title
$titleGroups = @{}
foreach ($file in $files) {
$originalPath = Get-ShortcutTarget -ShortcutPath $file.FullName
$interfaceData = Get-Content $originalPath | ConvertFrom-Json
$title = "No Title"
# Try to find a title in any of the web responses
foreach ($port in $interfaceData.OpenPorts) {
if ($port.WebRequest) {
$requests = @($port.WebRequest)
if ($requests[0] -isnot [array]) { $requests = @($requests) }
foreach ($request in $requests) {
if ($request.Content) {
$extractedTitle = Get-HtmlTitle -Content $request.Content
if ($extractedTitle) {
$title = $extractedTitle
break
}
}
}
}
if ($title -ne "No Title") { break }
}
if (-not $titleGroups.ContainsKey($title)) {
$titleGroups[$title] = @()
}
$titleGroups[$title] += $file
}
# Sort title groups by count (most to least)
foreach ($titleGroup in $titleGroups.GetEnumerator() | Sort-Object { $_.Value.Count } -Descending) {
$titleId = "unknown-title-$([System.Web.HttpUtility]::UrlEncode($titleGroup.Key))"
$html += @"
<div class="section-header">
<button class="collapse-btn" onclick="toggleSection(this, '$titleId')">▼</button>
<h3>Title: $($titleGroup.Key) <span class="category-count">($($titleGroup.Value.Count) interfaces)</span></h3>
</div>
<div id="$titleId">
<table>
<tr>
<th>IP Address</th>
<th>Hostname</th>
<th>Ports</th>
<th>Web Links</th>
<th>JSON File</th>
</tr>
"@
foreach ($file in $titleGroup.Value) {
$originalPath = Get-ShortcutTarget -ShortcutPath $file.FullName
$interfaceData = Get-Content $originalPath | ConvertFrom-Json
$ports = ($interfaceData.OpenPorts | ForEach-Object { $_.PortNumber }) -join ", "
$links = @()
foreach ($port in $interfaceData.OpenPorts) {
if ($port.WebRequest) {
$requests = @($port.WebRequest)
if ($requests[0] -isnot [array]) { $requests = @($requests) }
foreach ($request in $requests) {
if ($request.Url) {
$links += $request.Url
}
}
}
}
$linksHtml = ($links | Select-Object -Unique | ForEach-Object {
"<a href='$_' target='_blank'>$_</a>"
}) -join "<br>"
$html += @"
<tr>
<td>$($interfaceData.IP)</td>
<td>$($interfaceData.Hostname)</td>
<td>$ports</td>
<td>$linksHtml</td>
<td><a href="file://$originalPath">$([System.IO.Path]::GetFileName($originalPath))</a></td>
</tr>
"@
}
$html += @"
</table>
</div>
"@
}
$html += @"
</div>
</div>
"@
}
}
$html += @"
</body>
</html>
"@
$html | Out-File -FilePath $reportPath -Encoding UTF8
return $reportPath
}
# Main script execution
try {
# Convert relative paths to absolute paths
$ScanDirectory = Convert-Path $ScanDirectory
$FingerprintsFile = Convert-Path $FingerprintsFile
# Create timestamped output directory using absolute path
$timestamp = Get-Date -Format "yyyyMMdd_HHmmss"
$OutputDirectory = Join-Path (Convert-Path .) $OutputDirectory
$OutputDirectory = Join-Path $OutputDirectory $timestamp
if (-not (Test-Path $OutputDirectory)) {
New-Item -ItemType Directory -Path $OutputDirectory | Out-Null
}
# Load fingerprints file
$fingerprints = Get-Content $FingerprintsFile | ConvertFrom-Json
# Track categorization results
$results = @{}
# Process each JSON file in the scan directory
Get-ChildItem -Path $ScanDirectory -Filter "*.json" | ForEach-Object {
Write-Host "Processing $($_.Name)..."
try {
# Read and parse JSON file
$interfaceData = Get-Content $_.FullName | ConvertFrom-Json
# Get category
$category = Get-InterfaceCategory -InterfaceData $interfaceData -Fingerprints $fingerprints
# Create category directory if it doesn't exist
$categoryDir = Join-Path $OutputDirectory $category
if (-not (Test-Path $categoryDir)) {
New-Item -ItemType Directory -Path $categoryDir | Out-Null
}
# Create .lnk file instead of symbolic link
$linkPath = Join-Path $categoryDir "$($_.BaseName).lnk"
if (-not (Test-Path $linkPath)) {
New-Shortcut -SourcePath $_.FullName -ShortcutPath $linkPath
}
# Track result
if (-not $results.ContainsKey($category)) {
$results[$category] = 0
}
$results[$category]++
Write-Host "Categorized as: $category"
}
catch {
Write-Warning "Error processing $($_.Name): $_"
}
}
# Generate HTML report
$reportPath = New-CategoryReport -OutputDirectory $OutputDirectory -ScanDirectory $ScanDirectory
# Display summary
Write-Host "`nCategorization Summary:"
$results.GetEnumerator() | Sort-Object Name | ForEach-Object {
Write-Host "$($_.Key): $($_.Value) interfaces"
}
Write-Host "`nReport generated at: $reportPath"
}
catch {
Write-Error "Script execution failed: $_"
}
{
"iDRAC": {
"urlPatterns": [
"/restgui/start.html",
"/login.html",
"/data/login"
],
"contentPatterns": [
"ng-app='loginapp'",
"idrac-start-screen",
"images/clarityicons"
],
"headerPatterns": {
"Server": ["Apache"],
"Content-Security-Policy": ["default-src 'self'; connect-src *"]
}
},
"CiscoIOS": {
"urlPatterns": [
"/level/15/exec/-/",
"/login.html",
"/cgi-bin/login"
],
"contentPatterns": [
"Cisco Systems",
"routername",
"class=\"cisco\""
],
"headerPatterns": {
"Server": ["cisco-IOS"]
}
},
"HPiLO": {
"urlPatterns": [
"/json/login_session",
"/html/login.html",
"/redfish/v1"
],
"contentPatterns": [
"iLO",
"hp-navbar",
"integrity_footer"
],
"headerPatterns": {
"Server": ["HP-iLO-Server", "HPE-iLO-Server"],
"X-Frame-Options": ["SAMEORIGIN"]
}
},
"Synology": {
"urlPatterns": [
"/webman/",
"/webapi/",
"/DSM.php"
],
"contentPatterns": [
"SYNO.SDS",
"SynologyDiskStation",
"webman"
],
"headerPatterns": {
"Server": ["Synology"],
"X-Powered-By": ["PHP"]
}
},
"QNAP": {
"urlPatterns": [
"/cgi-bin/authLogin.cgi",
"/cgi-bin/login.html"
],
"contentPatterns": [
"QTS Gateway",
"QNAP",
"qnap-loading-page"
],
"headerPatterns": {
"Server": ["http server 1.0"]
}
},
"UniFiController": {
"urlPatterns": [
"/manage",
"/login",
"/api/login"
],
"contentPatterns": [
"UniFi Network",
"ubnt-UniFi",
"ng-controller=\"LoginController\""
],
"headerPatterns": {
"Server": ["UniFi"],
"X-Frame-Options": ["SAMEORIGIN"]
}
},
"ESXi": {
"urlPatterns": [
"/ui/",
"/folder",
"/sdk"
],
"contentPatterns": [
"VMware ESXi",
"vsphere-client",
"loginButton"
],
"headerPatterns": {
"Server": ["VMware"],
"X-Frame-Options": ["DENY"]
}
},
"Proxmox": {
"urlPatterns": [
"/pve-docs",
"/api2/json/access/ticket",
"/pve2/mobile.html#v"
],
"contentPatterns": [
"Proxmox",
"pve-lang-name",
"proxmoxlib.js"
],
"headerPatterns": {
"Server": ["pve-api-daemon", "proxmox"]
}
},
"PfSense": {
"urlPatterns": [
"/index.php",
"/css/pfSense.css"
],
"contentPatterns": [
"pfSense",
"login-page",
"fadeOut"
],
"headerPatterns": {
"X-Powered-By": ["PHP"],
"X-Frame-Options": ["DENY"]
}
},
"TrueNAS": {
"urlPatterns": [
"/ui/sessions/signin",
"/api/v2.0"
],
"contentPatterns": [
"TrueNAS",
"ix-auto",
"freenas-login"
],
"headerPatterns": {
"Server": ["nginx"],
"X-Frame-Options": ["SAMEORIGIN"]
}
}
}
param(
[Parameter(Mandatory=$true)]
[string]$XmlPath,
[Parameter(Mandatory=$false)]
[int]$MaxHosts = 15,
[Parameter(Mandatory=$false)]
[int]$RequestTimeout = 10,
[Parameter(Mandatory=$false)]
[int]$MaxResponseSize = 20MB
)
# Function to enable all locally supported TLS/SSL versions
function Set-MaximumTlsSupport {
$protocols = [enum]::GetValues([System.Net.SecurityProtocolType]) |
Where-Object { $_ -ne 'SystemDefault' }
$supportedProtocols = $protocols | ForEach-Object {
try {
[System.Net.ServicePointManager]::SecurityProtocol = $_
$_
} catch {
Write-Host "Protocol $_ not supported"
$null
}
} | Where-Object { $_ -ne $null }
$finalProtocol = [System.Net.SecurityProtocolType]($supportedProtocols -join ',')
[System.Net.ServicePointManager]::SecurityProtocol = $finalProtocol
Write-Host "Enabled protocols: $finalProtocol"
}
# Function definitions must come before usage
function Test-DNSResolution {
param(
[string]$Hostname,
[string]$IP
)
try {
$dnsResults = [System.Net.Dns]::GetHostEntry($Hostname)
$ipAddresses = $dnsResults.AddressList | ForEach-Object { $_.IPAddressToString }
# Check if the IP we have matches any of the DNS results
$ipMatch = $ipAddresses -contains $IP
return @{
Resolves = $true
IPAddresses = $ipAddresses
IPMatch = $ipMatch
Error = $null
}
}
catch {
return @{
Resolves = $false
IPAddresses = @()
IPMatch = $false
Error = $_.Exception.Message
}
}
}
function Get-TargetHostname {
param(
[string]$Hostname,
[string]$IP
)
if (-not $Hostname) {
Write-Host " No hostname provided, using IP: $IP"
return $IP
}
$dnsCheck = Test-DNSResolution -Hostname $Hostname -IP $IP
if (-not $dnsCheck.Resolves) {
Write-Host " Hostname '$Hostname' does not resolve in DNS, using IP: $IP"
return $IP
}
if (-not $dnsCheck.IPMatch) {
Write-Host " Warning: Hostname '$Hostname' resolves to different IPs: $($dnsCheck.IPAddresses -join ', ')"
Write-Host " Target IP '$IP' not in DNS results, using IP instead of hostname"
return $IP
}
Write-Host " Hostname '$Hostname' resolves correctly to IP: $IP"
return $Hostname
}
function Get-Favicon {
param(
[string]$BaseUrl,
[bool]$IsSSL,
[int]$Timeout = 10
)
try {
# First try standard /favicon.ico
$faviconUrl = "{0}/favicon.ico" -f $BaseUrl
Write-Host " → Attempting: $faviconUrl"
if ($IsSSL) {
$response = Invoke-WebRequest -Uri $faviconUrl -MaximumRedirection 0 -SkipCertificateCheck -TimeoutSec $Timeout -ErrorAction SilentlyContinue
} else {
$response = Invoke-WebRequest -Uri $faviconUrl -MaximumRedirection 0 -TimeoutSec $Timeout -ErrorAction SilentlyContinue
}
if ($response.StatusCode -eq 200 -and $response.RawContentLength -gt 0) {
return [Convert]::ToBase64String($response.Content)
}
} catch {
# Don't output anything - the error is expected for sites without favicon.ico
}
return $null
}
function Fetch-Url {
param(
[string]$TargetHost,
[int]$Port,
[bool]$IsSSL,
[int]$Timeout = 10
)
$MaxRedirects = 10
$RedirectCount = 0
$results = @()
$protocol = if ($IsSSL) { "https" } else { "http" }
$currentUrl = "{0}://{1}:{2}" -f $protocol, $TargetHost, $Port
while ($true) {
try {
$requestResult = [ordered]@{
Url = $currentUrl
RedirectDepth = $RedirectCount
StatusCode = $null
Error = $null
Headers = @{}
Content = $null
Favicon = $null
RedirectedRequest = $null
}
Write-Host " → Attempting: $currentUrl"
# Create WebRequest to check content length first
$webRequest = [System.Net.WebRequest]::Create($currentUrl)
$webRequest.Method = "HEAD"
$webRequest.Timeout = $Timeout * 1000
if ($IsSSL) {
# For HTTPS requests
[System.Net.ServicePointManager]::ServerCertificateValidationCallback = { $true }
}
try {
$headResponse = $webRequest.GetResponse()
$contentLength = $headResponse.ContentLength
if ($contentLength -gt $MaxResponseSize) {
Write-Host " → Warning: Response too large ($([math]::Round($contentLength/1MB, 2)) MB). Skipping." -ForegroundColor Yellow
$requestResult.Error = "Response size ($([math]::Round($contentLength/1MB, 2)) MB) exceeds maximum allowed size ($([math]::Round($MaxResponseSize/1MB)) MB)"
$results += $requestResult
break
}
} catch {
# If HEAD request fails, proceed with normal GET but limit the read size
}
if ($IsSSL) {
$response = Invoke-WebRequest -Uri $currentUrl -MaximumRedirection 0 -SkipCertificateCheck -TimeoutSec $Timeout -MaximumRetryCount 0 -ErrorAction SilentlyContinue
} else {
$response = Invoke-WebRequest -Uri $currentUrl -MaximumRedirection 0 -TimeoutSec $Timeout -MaximumRetryCount 0 -ErrorAction SilentlyContinue
}
# Check actual content length after download
$actualLength = if ($response.RawContentLength -gt 0) {
$response.RawContentLength
} else {
[System.Text.Encoding]::UTF8.GetByteCount($response.Content)
}
if ($actualLength -gt $MaxResponseSize) {
Write-Host " → Warning: Response too large ($([math]::Round($actualLength/1MB, 2)) MB). Truncating." -ForegroundColor Yellow
$requestResult.Content = $response.Content.Substring(0, $MaxResponseSize)
$requestResult.Error = "Response truncated: exceeded maximum size of $([math]::Round($MaxResponseSize/1MB)) MB"
} else {
$requestResult.Content = $response.Content
Write-Host " → Received $([math]::Round($actualLength/1KB, 2)) KB"
}
# Try to get favicon
$favicon = Get-Favicon -BaseUrl $currentUrl -IsSSL $IsSSL -Timeout $Timeout
if ($favicon) {
$requestResult.Favicon = $favicon
}
$results += $requestResult
break
} catch {
if ($_.Exception.Response) {
$statusCode = [int]$_.Exception.Response.StatusCode
$requestResult.StatusCode = $statusCode
$requestResult.Headers = $_.Exception.Response.Headers
$requestResult.Error = $_.Exception.Message
# Try to get response content even for error status codes
try {
$stream = $_.Exception.Response.GetResponseStream()
$reader = New-Object System.IO.StreamReader($stream)
$responseContent = $reader.ReadToEnd()
$contentLength = [System.Text.Encoding]::UTF8.GetByteCount($responseContent)
$requestResult.Content = $responseContent
Write-Host " → Received $contentLength bytes"
} catch {
# If we can't read the response content, continue with error handling
}
$results += $requestResult
if ($statusCode -ge 300 -and $statusCode -lt 400) {
if ($RedirectCount -ge $MaxRedirects) {
Write-Host " → Warning: Maximum redirects reached" -ForegroundColor Yellow
break
}
$location = $_.Exception.Response.Headers.GetValues("Location")[0]
# Check if redirect goes from HTTP to HTTPS
$isHttpsRedirect = -not $IsSSL -and ($location.StartsWith("https://") -or $location.StartsWith("//"))
if ($isHttpsRedirect) {
Write-Host " → Not following redirect to HTTPS: $location" -ForegroundColor Yellow
break
}
if (-not $location.StartsWith("http")) {
$baseUri = [System.Uri]::new($currentUrl)
$location = "{0}://{1}:{2}{3}" -f $baseUri.Scheme, $baseUri.Host, $baseUri.Port, $location
}
$RedirectCount++
Write-Host " → Following redirect to: $location"
$currentUrl = $location
continue
}
Write-Host " → Warning: HTTP $statusCode - $($_.Exception.Message)" -ForegroundColor Yellow
} else {
$requestResult.Error = $_.Exception.Message
$results += $requestResult
Write-Host " → Warning: $($_.Exception.Message)" -ForegroundColor Yellow
}
break
}
}
return $results
}
# Enable all supported TLS/SSL versions
Set-MaximumTlsSupport
# Convert relative path to absolute
$XmlPath = if ([System.IO.Path]::IsPathRooted($XmlPath)) {
$XmlPath
} else {
Join-Path -Path (Get-Location) -ChildPath $XmlPath
}
if (-not (Test-Path $XmlPath)) {
Write-Error "XML file not found at path: $XmlPath"
exit 1
}
$timestamp = Get-Date -Format "yyyy-MM-dd_HH-mm-ss"
$outputDir = Join-Path -Path (Get-Location) -ChildPath "nmap_results_$timestamp"
New-Item -ItemType Directory -Path $outputDir -Force | Out-Null
try {
$settings = New-Object System.Xml.XmlReaderSettings
$settings.DtdProcessing = [System.Xml.DtdProcessing]::Parse
$reader = [System.Xml.XmlReader]::Create($XmlPath, $settings)
$processedHosts = 0
while ($reader.Read() -and ($processedHosts -lt $MaxHosts)) {
if ($reader.NodeType -eq [System.Xml.XmlNodeType]::Element -and $reader.Name -eq "host") {
$hostXml = [xml]$reader.ReadOuterXml()
$targetHost = $hostXml.host
$hostIP = $targetHost.address | Where-Object { $_.addrtype -eq "ipv4" } | Select-Object -ExpandProperty addr
$hostname = $targetHost.hostnames.hostname.name
Write-Host "`nScanning host: $hostIP $(if ($hostname) { "($hostname)" })"
# Determine the appropriate hostname/IP to use based on DNS resolution
$targetHostname = Get-TargetHostname -Hostname $hostname -IP $hostIP
# Get all open ports from the Nmap XML
$openPorts = @($targetHost.ports.port | Where-Object { $_.state.state -eq "open" })
if ($openPorts.Count -eq 0) {
Write-Host " No open ports found in Nmap results"
$ports = @()
} else {
$ports = $openPorts | ForEach-Object {
$isSSL = $false
if ($_.service.tunnel -eq "ssl" -or $_.service.name -match "ssl|tls|https" -or $_.service.script.output -match "ssl|tls") {
$isSSL = $true
}
Write-Host "Port $($_.portid) ($($_.service.name)$(if ($isSSL) { "/ssl" })):"
$webRequests = Fetch-Url -TargetHost $targetHostname -Port $_.portid -IsSSL $isSSL -Timeout $RequestTimeout
[PSCustomObject]@{
PortNumber = $_.portid
Protocol = $_.protocol
Service = $_.service.name
IsSSL = $isSSL
WebRequest = $webRequests
}
}
}
$dnsCheck = Test-DNSResolution -Hostname $hostname -IP $hostIP
$hostResult = [PSCustomObject]@{
IP = $hostIP
Hostname = $hostname
DNSResolution = $dnsCheck
OpenPorts = $ports
ScanTime = $targetHost.starttime
}
$outputFile = Join-Path -Path $outputDir -ChildPath "$hostIP.json"
$hostResult | ConvertTo-Json -Depth 10 | Out-File $outputFile
$processedHosts++
Write-Host "Completed host: $hostIP ($processedHosts of $MaxHosts)`n"
}
}
} catch {
Write-Error "Error processing NMAP XML: $_"
exit 1
} finally {
if ($reader) {
$reader.Close()
}
}
Write-Host "Processing complete. Results saved in: $outputDir"
param(
[Parameter(Mandatory=$true)]
[string]$XmlPath,
[Parameter(Mandatory=$false)]
[int]$MaxHosts = 0, # Will be set to total hosts if 0
[Parameter(Mandatory=$false)]
[int]$RequestTimeout = 10,
[Parameter(Mandatory=$false)]
[int]$MaxResponseSize = 20MB
)
# Function to count total hosts in the XML file
function Get-TotalHosts {
param(
[string]$XmlPath
)
try {
$settings = New-Object System.Xml.XmlReaderSettings
$settings.DtdProcessing = [System.Xml.DtdProcessing]::Parse
$reader = [System.Xml.XmlReader]::Create($XmlPath, $settings)
$count = 0
while ($reader.Read()) {
if ($reader.NodeType -eq [System.Xml.XmlNodeType]::Element -and $reader.Name -eq "host") {
$count++
}
}
return $count
}
finally {
if ($reader) {
$reader.Close()
}
}
}
# Function to format time span
function Format-TimeSpan {
param (
[TimeSpan]$TimeSpan
)
if ($TimeSpan.TotalHours -ge 1) {
return "{0:0}h {1:0}m {2:0}s" -f $TimeSpan.Hours, $TimeSpan.Minutes, $TimeSpan.Seconds
}
elseif ($TimeSpan.TotalMinutes -ge 1) {
return "{0:0}m {1:0}s" -f $TimeSpan.Minutes, $TimeSpan.Seconds
}
else {
return "{0:0}s" -f $TimeSpan.TotalSeconds
}
}
function Get-AvailableTlsProtocols {
# Define possible protocols in order from newest to oldest
$possibleProtocols = @(
@{ Name = "Tls13"; Enum = "Tls13" },
@{ Name = "Tls12"; Enum = "Tls12" },
@{ Name = "Tls11"; Enum = "Tls11" },
@{ Name = "Tls"; Enum = "Tls" },
@{ Name = "Ssl3"; Enum = "Ssl3" },
@{ Name = "Ssl2"; Enum = "Ssl2" }
)
$availableProtocols = @()
# Dynamically check which protocols are defined in the current .NET version
foreach ($protocol in $possibleProtocols) {
try {
# Try to access the enum value
$enumValue = [System.Security.Authentication.SslProtocols]::($protocol.Enum)
$availableProtocols += @{ Name = $protocol.Name; Value = $enumValue }
Write-Host " → Protocol $($protocol.Name) is available"
}
catch {
Write-Host " → Protocol $($protocol.Name) is not supported on this system" -ForegroundColor Yellow
}
}
if ($availableProtocols.Count -eq 0) {
Write-Host " → Warning: No SSL/TLS protocols are available on this system" -ForegroundColor Red
throw "No SSL/TLS protocols available"
}
Write-Host " → Available protocols (from newest to oldest): $($availableProtocols.Name -join ', ')"
return $availableProtocols
}
# Detect available TLS/SSL protocols once at script startup
Write-Host "Detecting available TLS/SSL protocols..."
$GlobalAvailableTlsProtocols = Get-AvailableTlsProtocols
Write-Host "TLS/SSL protocol detection complete. Ready to scan hosts."
# Function to enable all locally supported TLS/SSL versions
function Set-MaximumTlsSupport {
$protocols = [enum]::GetValues([System.Net.SecurityProtocolType]) |
Where-Object { $_ -ne 'SystemDefault' }
$supportedProtocols = $protocols | ForEach-Object {
try {
[System.Net.ServicePointManager]::SecurityProtocol = $_
$_
} catch {
Write-Host "Protocol $_ not supported"
$null
}
} | Where-Object { $_ -ne $null }
$finalProtocol = [System.Net.SecurityProtocolType]($supportedProtocols -join ',')
[System.Net.ServicePointManager]::SecurityProtocol = $finalProtocol
Write-Host "Enabled protocols: $finalProtocol"
}
function Get-CertificateDetails {
param(
[string]$Hostname,
[int]$Port,
[int]$Timeout = 10,
[array]$AvailableProtocols = $GlobalAvailableTlsProtocols # Use the global variable by default
)
Write-Host (" → Retrieving SSL certificate from ${Hostname}:${Port}")
# Helper function to display inner exceptions
function Get-FullExceptionDetails {
param([Exception]$Exception)
$details = $Exception.Message
$currentEx = $Exception
# Traverse inner exceptions
while ($currentEx.InnerException) {
$currentEx = $currentEx.InnerException
$details += "`n → Inner Exception: $($currentEx.Message)"
# Check for specific exception types
if ($currentEx -is [System.Security.Authentication.AuthenticationException]) {
$details += " (Authentication error - likely certificate or protocol mismatch)"
}
elseif ($currentEx -is [System.IO.IOException]) {
$details += " (IO error - likely connection reset or timeout)"
}
}
return $details
}
# Helper function to check if an exception is timeout-related
function Test-IsTimeoutException {
param([string]$ExceptionMessage)
return ($ExceptionMessage -match "timed? out|timeout|aborted|could not establish trust|operation has timed out|connection failure|connection attempt failed")
}
# Method 1: Try all SSL/TLS protocols from newest to oldest
try {
Write-Host " → Method 1: Using TLS/SSL protocols from newest to oldest"
# Skip protocol detection - use the provided list
if ($AvailableProtocols.Count -eq 0) {
Write-Host " → Warning: No SSL/TLS protocols are available" -ForegroundColor Red
throw "No SSL/TLS protocols available"
}
# Try each protocol with different SNI options (empty or hostname)
$sniOptions = @("", $Hostname)
$timeoutEncountered = $false
foreach ($protocol in $AvailableProtocols) {
# If we already encountered a timeout, skip remaining protocols
if ($timeoutEncountered) {
Write-Host " → Skipping protocol $($protocol.Name) due to previous timeout" -ForegroundColor Yellow
continue
}
foreach ($sniValue in $sniOptions) {
$tcpClient = $null
$sslStream = $null
try {
Write-Host " → Trying handshake with $($protocol.Name), SNI='$sniValue'"
# Create a completely new TCP client for each attempt
$tcpClient = New-Object System.Net.Sockets.TcpClient
$tcpClient.ReceiveTimeout = $Timeout * 1000
$tcpClient.SendTimeout = $Timeout * 1000
# Set a connection timeout
$connectResult = $tcpClient.BeginConnect($Hostname, $Port, $null, $null)
$connectSuccess = $connectResult.AsyncWaitHandle.WaitOne($Timeout * 1000)
if (-not $connectSuccess) {
$timeoutEncountered = $true
throw "TCP connection timed out after $Timeout seconds"
}
# Complete the connection
$tcpClient.EndConnect($connectResult)
# Create SSL stream with callback that ignores ALL certificate errors
$sslStream = New-Object System.Net.Security.SslStream(
$tcpClient.GetStream(),
$false, # don't leave inner stream open
{ param($sender, $cert, $chain, $errors)
# Log the specific errors for troubleshooting
if ($errors -ne [System.Net.Security.SslPolicyErrors]::None) {
# This is just for debugging, we'll still return true
Write-Host " → Certificate errors: $errors" -ForegroundColor Yellow
}
return $true # Always accept the certificate regardless of errors
}
)
# Try SSL/TLS handshake
try {
# Use a reasonable timeout for the handshake (2x the regular timeout)
# We'll set read/write timeouts first
$sslStream.ReadTimeout = $Timeout * 2000
$sslStream.WriteTimeout = $Timeout * 2000
# Then try the handshake
$sslStream.AuthenticateAsClient($sniValue, $null, $protocol.Value, $false)
}
catch [Exception] {
# Get detailed exception info
$exDetails = Get-FullExceptionDetails -Exception $_.Exception
# Check if this is a timeout-related exception
if (Test-IsTimeoutException -ExceptionMessage $exDetails) {
$timeoutEncountered = $true
Write-Host " → Timeout encountered during handshake - will skip remaining protocol attempts" -ForegroundColor Yellow
}
throw $exDetails
}
Write-Host " → SSL/TLS handshake successful with $($protocol.Name)!" -ForegroundColor Green
# Get the certificate
$remoteCertificate = $sslStream.RemoteCertificate
if ($remoteCertificate -eq $null) {
Write-Host " → No certificate found despite successful handshake" -ForegroundColor Yellow
continue
}
# Convert to X509Certificate2 for more details
$x509cert = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2($remoteCertificate)
# Display basic certificate info
Write-Host " → Certificate details retrieved:" -ForegroundColor Green
Write-Host " Subject: $($x509cert.Subject)"
Write-Host " Issuer: $($x509cert.Issuer)"
Write-Host " Valid from: $($x509cert.NotBefore) to $($x509cert.NotAfter)"
# Extract Subject and Issuer components
$subjectParts = $x509cert.Subject -split ', '
$issuerParts = $x509cert.Issuer -split ', '
$subjectCN = ($subjectParts | Where-Object { $_ -match '^CN=' } | ForEach-Object { $_ -replace '^CN=' })[0]
$issuerCN = ($issuerParts | Where-Object { $_ -match '^CN=' } | ForEach-Object { $_ -replace '^CN=' })[0]
# Check if self-signed
$isSelfSigned = ($x509cert.Subject -eq $x509cert.Issuer)
if ($isSelfSigned) {
Write-Host " Self-signed: Yes" -ForegroundColor Yellow
} else {
Write-Host " Self-signed: No"
}
# Calculate days until expiration
$daysUntilExpiration = [math]::Round(($x509cert.NotAfter - (Get-Date)).TotalDays, 1)
# Return certificate details
return @{
Status = "Success"
Method = "DirectTLS"
Protocol = $protocol.Name
SNI = $sniValue
Subject = @{
CN = $subjectCN
O = ($subjectParts | Where-Object { $_ -match '^O=' } | ForEach-Object { $_ -replace '^O=' })
OU = ($subjectParts | Where-Object { $_ -match '^OU=' } | ForEach-Object { $_ -replace '^OU=' })
C = ($subjectParts | Where-Object { $_ -match '^C=' } | ForEach-Object { $_ -replace '^C=' })
S = ($subjectParts | Where-Object { $_ -match '^S=' -or $_ -match '^ST=' } | ForEach-Object { $_ -replace '^S=|^ST=' })
}
Issuer = @{
CN = $issuerCN
O = ($issuerParts | Where-Object { $_ -match '^O=' } | ForEach-Object { $_ -replace '^O=' })
OU = ($issuerParts | Where-Object { $_ -match '^OU=' } | ForEach-Object { $_ -replace '^OU=' })
C = ($issuerParts | Where-Object { $_ -match '^C=' } | ForEach-Object { $_ -replace '^C=' })
S = ($issuerParts | Where-Object { $_ -match '^S=' -or $_ -match '^ST=' } | ForEach-Object { $_ -replace '^S=|^ST=' })
}
Validity = @{
NotBefore = $x509cert.NotBefore.ToString('yyyy-MM-dd HH:mm:ss')
NotAfter = $x509cert.NotAfter.ToString('yyyy-MM-dd HH:mm:ss')
DaysUntilExpiration = $daysUntilExpiration
}
SerialNumber = $x509cert.SerialNumber
Thumbprint = @{
SHA256 = $x509cert.GetCertHashString('SHA256')
SHA1 = $x509cert.GetCertHashString('SHA1')
}
SignatureAlgorithm = $x509cert.SignatureAlgorithm.FriendlyName
Version = $x509cert.Version
SelfSigned = $isSelfSigned
TLSSupport = @{
SupportedProtocols = @($protocol.Name)
}
}
}
catch {
$exMessage = $_
Write-Host " → Attempt failed: $exMessage" -ForegroundColor Yellow
# Check if this is a timeout-related exception
if (Test-IsTimeoutException -ExceptionMessage $exMessage) {
$timeoutEncountered = $true
Write-Host " → Timeout detected - will skip remaining protocol attempts" -ForegroundColor Yellow
break # Exit this SNI loop
}
# Clean up resources properly
if ($sslStream) {
try { $sslStream.Dispose() } catch {}
}
if ($tcpClient) {
try { $tcpClient.Dispose() } catch {}
}
}
}
# If we encountered a timeout in the inner loop, break out of the outer loop too
if ($timeoutEncountered) {
break
}
}
if ($timeoutEncountered) {
Write-Host " → Skipping remaining protocol attempts due to timeout" -ForegroundColor Yellow
} else {
Write-Host " → All SSL/TLS protocol and SNI combinations failed" -ForegroundColor Yellow
}
}
catch {
$exDetails = Get-FullExceptionDetails -Exception $_.Exception
Write-Host " → Method 1 failed: $exDetails" -ForegroundColor Yellow
}
# Method 2: WebRequest with custom TLS setting
# Only try Method 2 if we didn't encounter a timeout in Method 1
$skipMethod2 = $false
if (Test-Path variable:timeoutEncountered) {
$skipMethod2 = $timeoutEncountered
}
if ($skipMethod2) {
Write-Host " → Skipping Method 2 due to timeout in Method 1" -ForegroundColor Yellow
} else {
try {
Write-Host " → Method 2: Using WebRequest with custom TLS settings"
# Save original settings
$originalProtocol = [System.Net.ServicePointManager]::SecurityProtocol
$originalCallback = [System.Net.ServicePointManager]::ServerCertificateValidationCallback
try {
# Try to use all possible TLS/SSL protocols
# We'll use reflection to get all possible values for SecurityProtocolType
$allValues = 0
# Get all protocols via reflection from SecurityProtocolType enum
$protocolType = [System.Net.SecurityProtocolType]
$protocolFields = $protocolType.GetFields() | Where-Object { $_.IsStatic -and $_.IsLiteral }
foreach ($field in $protocolFields) {
try {
$value = $field.GetValue($null)
Write-Host " → Adding protocol $($field.Name) ($value)"
$allValues = $allValues -bor $value
} catch {
Write-Host " → Couldn't add protocol $($field.Name): $($_.Exception.Message)" -ForegroundColor Yellow
}
}
# If we couldn't get any protocols, fall back to hardcoded combination
if ($allValues -eq 0) {
Write-Host " → Using fallback protocol combination" -ForegroundColor Yellow
try {
$allValues = [System.Net.SecurityProtocolType]::Tls12 -bor
[System.Net.SecurityProtocolType]::Tls11 -bor
[System.Net.SecurityProtocolType]::Tls -bor
[System.Net.SecurityProtocolType]::Ssl3
}
catch {
# If even that fails, try just TLS 1.2
$allValues = [System.Net.SecurityProtocolType]::Tls12
}
}
# Apply our protocol selection
[System.Net.ServicePointManager]::SecurityProtocol = $allValues
# Accept all certificates regardless of errors
[System.Net.ServicePointManager]::ServerCertificateValidationCallback = {
param($sender, $cert, $chain, $errors)
# Log the specific certificate errors for diagnostics
if ($errors -ne [System.Net.Security.SslPolicyErrors]::None) {
Write-Host " → Certificate validation errors: $errors" -ForegroundColor Yellow
}
return $true
}
# Create HTTP request
$url = "https://${Hostname}:${Port}"
$webRequest = [System.Net.WebRequest]::Create($url)
$webRequest.Method = "HEAD"
$webRequest.Timeout = $Timeout * 1000
$webRequest.AllowAutoRedirect = $false
# Attempt to get response (may fail but we might still get the certificate)
try {
$response = $webRequest.GetResponse()
$response.Close()
}
catch [System.Net.WebException] {
# Extract detailed information from the WebException
$ex = $_.Exception
$statusCode = -1
$statusDesc = "Unknown"
if ($ex.Response -ne $null) {
$statusCode = [int]$ex.Response.StatusCode
$statusDesc = $ex.Response.StatusDescription
}
Write-Host " → WebException: Status Code=$statusCode, Description=$statusDesc" -ForegroundColor Yellow
# Check if we have inner exception details
if ($ex.InnerException) {
$innerExMsg = Get-FullExceptionDetails -Exception $ex.InnerException
Write-Host " → Inner exception details: $innerExMsg" -ForegroundColor Yellow
# Check if this is a timeout related exception
if (Test-IsTimeoutException -ExceptionMessage $innerExMsg) {
throw "Timeout encountered in WebRequest method"
}
}
# Check if this is a timeout-related exception directly
if (Test-IsTimeoutException -ExceptionMessage $ex.Message) {
throw "Timeout encountered in WebRequest method"
}
Write-Host " → Still checking for certificate..." -ForegroundColor Yellow
}
# Try to get certificate from the ServicePoint
$cert = $webRequest.ServicePoint.Certificate
if ($cert -eq $null) {
throw "No certificate found in ServicePoint"
}
# Convert to X509Certificate2
$x509cert = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2($cert)
# Display basic certificate info
Write-Host " → Certificate details retrieved via WebRequest:" -ForegroundColor Green
Write-Host " Subject: $($x509cert.Subject)"
Write-Host " Issuer: $($x509cert.Issuer)"
Write-Host " Valid from: $($x509cert.NotBefore) to $($x509cert.NotAfter)"
# Extract Subject and Issuer components
$subjectParts = $x509cert.Subject -split ', '
$issuerParts = $x509cert.Issuer -split ', '
$subjectCN = ($subjectParts | Where-Object { $_ -match '^CN=' } | ForEach-Object { $_ -replace '^CN=' })[0]
$issuerCN = ($issuerParts | Where-Object { $_ -match '^CN=' } | ForEach-Object { $_ -replace '^CN=' })[0]
# Calculate days until expiration
$daysUntilExpiration = [math]::Round(($x509cert.NotAfter - (Get-Date)).TotalDays, 1)
# Return certificate details
return @{
Status = "Success"
Method = "WebRequest"
Protocol = "Unknown (WebRequest)"
Subject = @{
CN = $subjectCN
O = ($subjectParts | Where-Object { $_ -match '^O=' } | ForEach-Object { $_ -replace '^O=' })
OU = ($subjectParts | Where-Object { $_ -match '^OU=' } | ForEach-Object { $_ -replace '^OU=' })
C = ($subjectParts | Where-Object { $_ -match '^C=' } | ForEach-Object { $_ -replace '^C=' })
S = ($subjectParts | Where-Object { $_ -match '^S=' -or $_ -match '^ST=' } | ForEach-Object { $_ -replace '^S=|^ST=' })
}
Issuer = @{
CN = $issuerCN
O = ($issuerParts | Where-Object { $_ -match '^O=' } | ForEach-Object { $_ -replace '^O=' })
OU = ($issuerParts | Where-Object { $_ -match '^OU=' } | ForEach-Object { $_ -replace '^OU=' })
C = ($issuerParts | Where-Object { $_ -match '^C=' } | ForEach-Object { $_ -replace '^C=' })
S = ($issuerParts | Where-Object { $_ -match '^S=' -or $_ -match '^ST=' } | ForEach-Object { $_ -replace '^S=|^ST=' })
}
Validity = @{
NotBefore = $x509cert.NotBefore.ToString('yyyy-MM-dd HH:mm:ss')
NotAfter = $x509cert.NotAfter.ToString('yyyy-MM-dd HH:mm:ss')
DaysUntilExpiration = $daysUntilExpiration
}
SerialNumber = $x509cert.SerialNumber
Thumbprint = @{
SHA256 = $x509cert.GetCertHashString('SHA256')
SHA1 = $x509cert.GetCertHashString('SHA1')
}
SignatureAlgorithm = $x509cert.SignatureAlgorithm.FriendlyName
Version = $x509cert.Version
SelfSigned = ($x509cert.Subject -eq $x509cert.Issuer)
TLSSupport = @{
SupportedProtocols = @("Unknown - WebRequest method")
}
}
}
finally {
# Restore original settings
[System.Net.ServicePointManager]::SecurityProtocol = $originalProtocol
[System.Net.ServicePointManager]::ServerCertificateValidationCallback = $originalCallback
}
}
catch {
$exDetails = Get-FullExceptionDetails -Exception $_.Exception
# Check if this was a timeout
if (Test-IsTimeoutException -ExceptionMessage $exDetails) {
Write-Host " → Method 2 timed out: $exDetails" -ForegroundColor Yellow
} else {
Write-Host " → Method 2 failed: $exDetails" -ForegroundColor Yellow
}
}
}
# If all methods fail, return a consistent failure object
Write-Host " → Certificate retrieval failed - all methods exhausted" -ForegroundColor Yellow
# Return a failure object
return @{
Status = "Failed"
Method = "AllMethodsFailed"
Error = "Could not retrieve certificate from ${Hostname}:${Port} after trying all methods"
Subject = @{ CN = "Unknown"; O = @(); OU = @() }
Issuer = @{ CN = "Unknown"; O = @(); OU = @() }
Validity = @{
NotBefore = "Unknown"
NotAfter = "Unknown"
DaysUntilExpiration = $null
}
SerialNumber = "Unknown"
SelfSigned = $null
TLSSupport = @{
SupportedProtocols = @()
}
}
}
function Test-DNSResolution {
param(
[string]$Hostname,
[string]$IP
)
try {
$dnsResults = [System.Net.Dns]::GetHostEntry($Hostname)
$ipAddresses = $dnsResults.AddressList | ForEach-Object { $_.IPAddressToString }
# Check if the IP we have matches any of the DNS results
$ipMatch = $ipAddresses -contains $IP
return @{
Resolves = $true
IPAddresses = $ipAddresses
IPMatch = $ipMatch
Error = $null
}
}
catch {
return @{
Resolves = $false
IPAddresses = @()
IPMatch = $false
Error = $_.Exception.Message
}
}
}
# Enhanced URL sanitization function
function Sanitize-Url {
param(
[string]$Url
)
if ([string]::IsNullOrWhiteSpace($Url)) {
return $Url
}
# Remove any backslashes from the URL, but preserve double forward slashes
$Url = $Url.Replace('\', '/')
# Fix any double forward slashes that aren't part of the protocol
if ($Url -match '^(?:https?:\/\/)') {
$protocol = $matches[0]
$rest = $Url.Substring($protocol.Length)
$rest = $rest -replace '//+', '/'
$Url = $protocol + $rest
}
# Ensure no trailing slashes in hostname portion
if ($Url -match '^(https?:\/\/[^\/]+)(.*)$') {
$hostname = $matches[1].TrimEnd('/')
$path = $matches[2]
$Url = $hostname + $path
}
return $Url.Trim()
}
function Get-TargetHostname {
param(
[string]$Hostname,
[string]$IP
)
if (-not $Hostname) {
Write-Host " No hostname provided, using IP: $IP"
return $IP
}
$dnsCheck = Test-DNSResolution -Hostname $Hostname -IP $IP
if (-not $dnsCheck.Resolves) {
Write-Host " Hostname '$Hostname' does not resolve in DNS, using IP: $IP"
return $IP
}
if (-not $dnsCheck.IPMatch) {
Write-Host " Warning: Hostname '$Hostname' resolves to different IPs: $($dnsCheck.IPAddresses -join ', ')"
Write-Host " Target IP '$IP' not in DNS results, using IP instead of hostname"
return $IP
}
Write-Host " Hostname '$Hostname' resolves correctly to IP: $IP"
return $Hostname
}
function Get-Favicon {
param(
[string]$BaseUrl,
[bool]$IsSSL,
[int]$Timeout = 10
)
try {
# First try standard /favicon.ico
$faviconUrl = "{0}/favicon.ico" -f $BaseUrl
Write-Host " → Attempting: $faviconUrl"
if ($IsSSL) {
$response = Invoke-WebRequest -Uri $faviconUrl -MaximumRedirection 0 -SkipCertificateCheck -TimeoutSec $Timeout -ErrorAction SilentlyContinue
} else {
$response = Invoke-WebRequest -Uri $faviconUrl -MaximumRedirection 0 -TimeoutSec $Timeout -ErrorAction SilentlyContinue
}
if ($response.StatusCode -eq 200 -and $response.RawContentLength -gt 0) {
return [Convert]::ToBase64String($response.Content)
}
} catch {
# Don't output anything - the error is expected for sites without favicon.ico
}
return $null
}
# Enable all supported TLS/SSL versions
Set-MaximumTlsSupport
function Sanitize-Hostname {
param(
[string]$Hostname
)
return $Hostname.Replace('\', '').Trim()
}
function Fetch-Url {
param(
[string]$TargetHost,
[int]$Port,
[bool]$IsSSL,
[int]$Timeout = 10
)
$MaxRedirects = 10
$RedirectCount = 0
$results = @()
$protocol = if ($IsSSL) { "https" } else { "http" }
$currentUrl = "${protocol}://${TargetHost}:${Port}"
$originalHost = $TargetHost
$originalPort = $Port
# Helper function to check if an exception is timeout-related
function Test-IsTimeoutException {
param([string]$ExceptionMessage)
return ($ExceptionMessage -match "timed? out|timeout|aborted|could not establish trust|operation has timed out|connection failure|connection attempt failed")
}
# Get certificate details early for SSL connections
$certDetails = $null
if ($IsSSL) {
Write-Host " → Getting certificate details for ${TargetHost}:${Port}"
$certDetails = Get-CertificateDetails -Hostname $TargetHost -Port $Port -Timeout $Timeout -AvailableProtocols $GlobalAvailableTlsProtocols
}
# Create the initial result object with certificate info
$requestResult = [ordered]@{
Url = $currentUrl
RedirectDepth = $RedirectCount
StatusCode = $null
Message = $null # Changed from "Error" to "Message"
Headers = @{}
Content = $null
Favicon = $null
Title = $null
RedirectedRequest = $null
Certificate = $certDetails
}
# Check if we should skip HTTPS connection due to certificate failure or timeout
$skipHttpsRequest = $false
if ($IsSSL) {
# Check if certificate retrieval failed or timed out
if (-not $certDetails -or $certDetails.Status -eq "Failed") {
# Check if the error indicates a timeout
$timeoutError = $false
if ($certDetails -and $certDetails.Error -and (Test-IsTimeoutException -ExceptionMessage $certDetails.Error)) {
$timeoutError = $true
Write-Host " → Certificate retrieval timed out - skipping HTTPS connection attempt" -ForegroundColor Yellow
$requestResult.Message = "HTTPS connection skipped due to certificate retrieval timeout"
} else {
Write-Host " → Certificate retrieval failed - skipping HTTPS connection attempt" -ForegroundColor Yellow
$requestResult.Message = "HTTPS connection skipped due to certificate retrieval failure"
}
$results += $requestResult
$skipHttpsRequest = $true
}
}
# Only attempt HTTP/HTTPS connections if not skipped
if (-not $skipHttpsRequest) {
while ($true) {
try {
# Sanitize the current URL before making the request
$currentUrl = Sanitize-Url -Url $currentUrl
# Update URL in the result object
$requestResult.Url = $currentUrl
Write-Host " → Attempting: $currentUrl"
# Create WebRequest to check content length first
$webRequest = [System.Net.WebRequest]::Create($currentUrl)
$webRequest.Method = "HEAD"
$webRequest.Timeout = $Timeout * 1000
# Only set certificate validation callback for HTTPS requests
if ($IsSSL) {
[System.Net.ServicePointManager]::ServerCertificateValidationCallback = { $true }
}
try {
$headResponse = $webRequest.GetResponse()
$contentLength = $headResponse.ContentLength
if ($contentLength -gt $MaxResponseSize) {
Write-Host " → Warning: Response too large ($([math]::Round($contentLength/1MB, 2)) MB). Skipping." -ForegroundColor Yellow
$requestResult.Message = "Response size ($([math]::Round($contentLength/1MB, 2)) MB) exceeds maximum allowed size ($([math]::Round($MaxResponseSize/1MB)) MB)"
$results += $requestResult
break
}
} catch {
# Check if this is a timeout exception
if (Test-IsTimeoutException -ExceptionMessage $_.Exception.Message) {
Write-Host " → HEAD request timed out" -ForegroundColor Yellow
$requestResult.Message = "HEAD request timed out: $($_.Exception.Message)"
$results += $requestResult
break
}
# If HEAD request fails with non-timeout error, proceed with normal GET but limit the read size
Write-Host " → HEAD request failed: $($_.Exception.Message). Trying GET instead." -ForegroundColor Yellow
}
# Use different Invoke-WebRequest parameters based on PowerShell version
$psVersion = $PSVersionTable.PSVersion.Major
$webRequestParams = @{
Uri = $currentUrl
MaximumRedirection = 0
TimeoutSec = $Timeout
MaximumRetryCount = 0
ErrorAction = 'SilentlyContinue'
}
# Different handling for HTTP and HTTPS
if ($IsSSL) {
# HTTPS handling
if ($psVersion -ge 6) {
# For PowerShell 6+ we can use SkipCertificateCheck
$webRequestParams.Add('SkipCertificateCheck', $true)
$response = Invoke-WebRequest @webRequestParams
} else {
# For older PowerShell versions
try {
# Temporarily set certificate callback
$originalCallback = [System.Net.ServicePointManager]::ServerCertificateValidationCallback
[System.Net.ServicePointManager]::ServerCertificateValidationCallback = { $true }
$response = Invoke-WebRequest @webRequestParams
} finally {
# Restore original callback
[System.Net.ServicePointManager]::ServerCertificateValidationCallback = $originalCallback
}
}
} else {
# HTTP handling - no SSL/TLS specific code
$response = Invoke-WebRequest @webRequestParams
}
# Check actual content length after download
$actualLength = if ($response.RawContentLength -gt 0) {
$response.RawContentLength
} else {
[System.Text.Encoding]::UTF8.GetByteCount($response.Content)
}
if ($actualLength -gt $MaxResponseSize) {
Write-Host " → Warning: Response too large ($([math]::Round($actualLength/1MB, 2)) MB). Truncating." -ForegroundColor Yellow
$requestResult.Content = $response.Content.Substring(0, $MaxResponseSize)
$requestResult.Message = "Response truncated: exceeded maximum size of $([math]::Round($MaxResponseSize/1MB)) MB"
} else {
$requestResult.Content = $response.Content
Write-Host " → Received $([math]::Round($actualLength/1KB, 2)) KB"
}
# Extract title from HTML content if available
if ($requestResult.Content) {
if ($requestResult.Content -match '<title[^>]*>(.*?)</title>') {
$requestResult.Title = $matches[1].Trim()
Write-Host " → Page title: $($requestResult.Title)"
} else {
$requestResult.Title = $null
Write-Host " → No title found in page"
}
}
# Try to get favicon
$favicon = Get-Favicon -BaseUrl $currentUrl -IsSSL $IsSSL -Timeout $Timeout
if ($favicon) {
$requestResult.Favicon = $favicon
}
$results += $requestResult
break
} catch {
if ($_.Exception.Response) {
$statusCode = [int]$_.Exception.Response.StatusCode
$requestResult.StatusCode = $statusCode
$requestResult.Headers = $_.Exception.Response.Headers
# Instead of treating redirects as errors, treat them as normal status codes
if ($statusCode -ge 300 -and $statusCode -lt 400) {
$requestResult.Message = "Redirect: $statusCode $($_.Exception.Response.StatusDescription)"
} else {
$requestResult.Message = "$statusCode $($_.Exception.Response.StatusDescription)"
}
# Try to get response content even for error status codes
try {
$stream = $_.Exception.Response.GetResponseStream()
$reader = New-Object System.IO.StreamReader($stream)
$responseContent = $reader.ReadToEnd()
$contentLength = [System.Text.Encoding]::UTF8.GetByteCount($responseContent)
$requestResult.Content = $responseContent
Write-Host " → Received $contentLength bytes"
# Extract title from HTML content if available
if ($responseContent -match '<title[^>]*>(.*?)</title>') {
$requestResult.Title = $matches[1].Trim()
Write-Host " → Page title: $($requestResult.Title)"
}
} catch {
# If we can't read the response content, continue with error handling
}
$results += $requestResult
if ($statusCode -ge 300 -and $statusCode -lt 400) {
if ($RedirectCount -ge $MaxRedirects) {
Write-Host " → Warning: Maximum redirects reached" -ForegroundColor Yellow
break
}
$location = $_.Exception.Response.Headers.GetValues("Location")[0]
$location = Sanitize-Url -Url $location
# Check if redirect goes from HTTP to HTTPS
$isHttpsRedirect = -not $IsSSL -and ($location.StartsWith("https://") -or $location.StartsWith("//"))
if ($isHttpsRedirect) {
Write-Host " → Not following redirect to HTTPS: $location" -ForegroundColor Yellow
break
}
try {
if ($location.StartsWith("http://") -or $location.StartsWith("https://")) {
$uri = [System.Uri]::new($location)
if ($uri.Host -ne $originalHost -or $uri.Port -ne $originalPort) {
Write-Host " → Not following redirect to different host/port: $location" -ForegroundColor Yellow
break
}
$currentUrl = $location
}
elseif ($location.StartsWith("//")) {
$currentUrl = "${protocol}:${location}"
}
elseif ($location.StartsWith("/")) {
$currentUrl = "${protocol}://${originalHost}:${originalPort}${location}"
}
else {
$baseUri = [System.Uri]$currentUrl
$currentUrl = [System.Uri]::new($baseUri, $location).ToString()
}
$RedirectCount++
Write-Host " → Following redirect to: $currentUrl"
# Create a new request result for this redirect
$requestResult = [ordered]@{
Url = $currentUrl
RedirectDepth = $RedirectCount
StatusCode = $null
Message = $null # Changed from "Error" to "Message"
Headers = @{}
Content = $null
Favicon = $null
Title = $null
RedirectedRequest = $null
Certificate = $certDetails
}
continue
}
catch {
Write-Host " → Warning: Invalid redirect URL: $location" -ForegroundColor Yellow
break
}
}
Write-Host " → Status: HTTP $statusCode - $($_.Exception.Response.StatusDescription)" -ForegroundColor Yellow
} else {
# Check for timeout exception
if (Test-IsTimeoutException -ExceptionMessage $_.Exception.Message) {
Write-Host " → Request timed out: $($_.Exception.Message)" -ForegroundColor Yellow
} else {
Write-Host " → Warning: $($_.Exception.Message)" -ForegroundColor Yellow
}
$requestResult.Message = $_.Exception.Message
$results += $requestResult
}
break
}
}
}
return $results
}
# Function to export JSON data to CSV in both wide and long formats
function Append-HostToCSV {
param(
[string]$OutputDir,
[string]$WideCSVPath,
[string]$LongCSVPath,
[PSCustomObject]$HostData
)
# Helper function to extract title from HTML content
function Get-HtmlTitle {
param([string]$HtmlContent)
if ([string]::IsNullOrWhiteSpace($HtmlContent)) {
return $null
}
try {
# Try to match the title tag
if ($HtmlContent -match '<title[^>]*>(.*?)</title>') {
return $matches[1].Trim()
}
} catch {
# If there's any error in regex processing, return null
return $null
}
return $null
}
# Create wide format row(s)
$wideRows = @()
foreach ($port in $HostData.OpenPorts) {
$rowData = [ordered]@{
IP = $HostData.IP
Hostname = $HostData.Hostname
DNSResolution = $HostData.DNSResolution.Resolves
DNSMatchesIP = $HostData.DNSResolution.IPMatch
Port = $port.PortNumber
Protocol = $port.Protocol
Service = $port.Service
IsSSL = $port.IsSSL
TotalRedirects = ($port.WebRequest | Measure-Object).Count
}
# Add redirect information
for ($i = 0; $i -lt $port.WebRequest.Count; $i++) {
$request = $port.WebRequest[$i]
$prefix = "Redirect$($i+1)_"
# Extract title from HTML content
$title = Get-HtmlTitle -HtmlContent $request.Content
# Add request details
$rowData["${prefix}URL"] = $request.Url
$rowData["${prefix}Message"] = $request.Message
$rowData["${prefix}Error"] = $request.Error
$rowData["${prefix}HasContent"] = if ($request.Content) { $true } else { $false }
$rowData["${prefix}ContentLength"] = if ($request.Content) { $request.Content.Length } else { 0 }
$rowData["${prefix}HasFavicon"] = if ($request.Favicon) { $true } else { $false }
$rowData["${prefix}Content"] = $request.Content
$rowData["${prefix}Favicon"] = $request.Favicon
$rowData["${prefix}Title"] = $title
# Add headers if available
if ($request.Headers -and $request.Headers.Count -gt 0) {
$headerString = ($request.Headers.GetEnumerator() | ForEach-Object { "$($_.Key): $($_.Value)" }) -join "; "
$rowData["${prefix}Headers"] = $headerString
} else {
$rowData["${prefix}Headers"] = ""
}
# Certificate information (if SSL)
if ($port.IsSSL -and $request.Certificate) {
$cert = $request.Certificate
# Add certificate status and method
$rowData["${prefix}CertStatus"] = if ($cert.PSObject.Properties.Name -contains "Status") { $cert.Status } else { "Unknown" }
$rowData["${prefix}CertMethod"] = if ($cert.PSObject.Properties.Name -contains "Method") { $cert.Method } else { "Unknown" }
$rowData["${prefix}CertError"] = if ($cert.PSObject.Properties.Name -contains "Error") { $cert.Error } else { $null }
# Add certificate details if available
if ($cert.PSObject.Properties.Name -contains "Subject" -and $cert.Subject -ne $null) {
$rowData["${prefix}CertSubjectCN"] = $cert.Subject.CN
$rowData["${prefix}CertIssuerCN"] = $cert.Issuer.CN
$rowData["${prefix}CertValidFrom"] = $cert.Validity.NotBefore
$rowData["${prefix}CertValidTo"] = $cert.Validity.NotAfter
$rowData["${prefix}CertDaysUntilExpiration"] = $cert.Validity.DaysUntilExpiration
$rowData["${prefix}CertSignatureAlgorithm"] = $cert.SignatureAlgorithm
$rowData["${prefix}CertSelfSigned"] = $cert.SelfSigned
$rowData["${prefix}CertSupportedTLSVersions"] = if ($cert.TLSSupport -and $cert.TLSSupport.SupportedProtocols) {
($cert.TLSSupport.SupportedProtocols -join ", ")
} else {
"Unknown"
}
# Add thumbprints
if ($cert.Thumbprint) {
$rowData["${prefix}CertThumbprintSHA1"] = $cert.Thumbprint.SHA1
$rowData["${prefix}CertThumbprintSHA256"] = $cert.Thumbprint.SHA256
}
# Include the serial number
$rowData["${prefix}CertSerialNumber"] = $cert.SerialNumber
}
}
}
$wideRows += [PSCustomObject]$rowData
}
# Create long format row(s)
$longRows = @()
foreach ($port in $HostData.OpenPorts) {
# For each request in the redirect chain
for ($i = 0; $i -lt $port.WebRequest.Count; $i++) {
$request = $port.WebRequest[$i]
# Extract title from HTML content
$title = Get-HtmlTitle -HtmlContent $request.Content
# Create a new ordered dictionary for each row
$rowData = [ordered]@{
IP = $HostData.IP
Hostname = $HostData.Hostname
DNSResolution = $HostData.DNSResolution.Resolves
DNSMatchesIP = $HostData.DNSResolution.IPMatch
Port = $port.PortNumber
Protocol = $port.Protocol
Service = $port.Service
IsSSL = $port.IsSSL
RedirectIndex = $i
URL = $request.Url
StatusCode = $request.StatusCode
Message = $request.Message
HasContent = if ($request.Content) { $true } else { $false }
ContentLength = if ($request.Content) { $request.Content.Length } else { 0 }
HasFavicon = if ($request.Favicon) { $true } else { $false }
Content = $request.Content
Favicon = $request.Favicon
Title = $title
}
# Add headers if available
if ($request.Headers -and $request.Headers.Count -gt 0) {
$headerString = ($request.Headers.GetEnumerator() | ForEach-Object { "$($_.Key): $($_.Value)" }) -join "; "
$rowData["Headers"] = $headerString
} else {
$rowData["Headers"] = ""
}
# Certificate information (if SSL)
if ($port.IsSSL -and $request.Certificate) {
$cert = $request.Certificate
# Add certificate status and method
$rowData["CertStatus"] = if ($cert.PSObject.Properties.Name -contains "Status") { $cert.Status } else { "Unknown" }
$rowData["CertMethod"] = if ($cert.PSObject.Properties.Name -contains "Method") { $cert.Method } else { "Unknown" }
$rowData["CertError"] = if ($cert.PSObject.Properties.Name -contains "Error") { $cert.Error } else { $null }
# Add certificate details if available
if ($cert.PSObject.Properties.Name -contains "Subject" -and $cert.Subject -ne $null) {
$rowData["CertSubjectCN"] = $cert.Subject.CN
$rowData["CertIssuerCN"] = $cert.Issuer.CN
$rowData["CertValidFrom"] = $cert.Validity.NotBefore
$rowData["CertValidTo"] = $cert.Validity.NotAfter
$rowData["CertDaysUntilExpiration"] = $cert.Validity.DaysUntilExpiration
$rowData["CertSignatureAlgorithm"] = $cert.SignatureAlgorithm
$rowData["CertSelfSigned"] = $cert.SelfSigned
$rowData["CertSupportedTLSVersions"] = if ($cert.TLSSupport -and $cert.TLSSupport.SupportedProtocols) {
($cert.TLSSupport.SupportedProtocols -join ", ")
} else {
"Unknown"
}
# Add thumbprints
if ($cert.Thumbprint) {
$rowData["CertThumbprintSHA1"] = $cert.Thumbprint.SHA1
$rowData["CertThumbprintSHA256"] = $cert.Thumbprint.SHA256
}
# Include the serial number
$rowData["CertSerialNumber"] = $cert.SerialNumber
}
}
$longRows += [PSCustomObject]$rowData
}
}
# Append to CSV files
try {
# If file doesn't exist, create it with headers
if (-not (Test-Path $WideCSVPath)) {
$wideRows | Export-Csv -Path $WideCSVPath -NoTypeInformation -Encoding UTF8
} else {
$wideRows | Export-Csv -Path $WideCSVPath -NoTypeInformation -Encoding UTF8 -Append -Force
}
if (-not (Test-Path $LongCSVPath)) {
$longRows | Export-Csv -Path $LongCSVPath -NoTypeInformation -Encoding UTF8
} else {
$longRows | Export-Csv -Path $LongCSVPath -NoTypeInformation -Encoding UTF8 -Append -Force
}
Write-Host "CSV data appended for host: $($HostData.IP)"
}
catch {
Write-Host "Error appending CSV data for host $($HostData.IP): $_" -ForegroundColor Red
}
}
# Add this function to the script
function Append-HostToCSV {
param(
[string]$OutputDir,
[string]$WideCSVPath,
[string]$LongCSVPath,
[PSCustomObject]$HostData
)
# Helper function to extract title from HTML content
function Get-HtmlTitle {
param([string]$HtmlContent)
if ([string]::IsNullOrWhiteSpace($HtmlContent)) {
return $null
}
try {
# Try to match the title tag
if ($HtmlContent -match '<title[^>]*>(.*?)</title>') {
return $matches[1].Trim()
}
} catch {
# If there's any error in regex processing, return null
return $null
}
return $null
}
# Create wide format row(s)
$wideRows = @()
foreach ($port in $HostData.OpenPorts) {
$rowData = [ordered]@{
IP = $HostData.IP
Hostname = $HostData.Hostname
DNSResolution = $HostData.DNSResolution.Resolves
DNSMatchesIP = $HostData.DNSResolution.IPMatch
Port = $port.PortNumber
Protocol = $port.Protocol
Service = $port.Service
IsSSL = $port.IsSSL
TotalRedirects = ($port.WebRequest | Measure-Object).Count
}
# Add redirect information
for ($i = 0; $i -lt $port.WebRequest.Count; $i++) {
$request = $port.WebRequest[$i]
$prefix = "Redirect$($i+1)_"
# Extract title from HTML content
$title = Get-HtmlTitle -HtmlContent $request.Content
# Add request details
$rowData["${prefix}URL"] = $request.Url
$rowData["${prefix}StatusCode"] = $request.StatusCode
$rowData["${prefix}Error"] = $request.Error
$rowData["${prefix}HasContent"] = if ($request.Content) { $true } else { $false }
$rowData["${prefix}ContentLength"] = if ($request.Content) { $request.Content.Length } else { 0 }
$rowData["${prefix}HasFavicon"] = if ($request.Favicon) { $true } else { $false }
$rowData["${prefix}Content"] = $request.Content
$rowData["${prefix}Favicon"] = $request.Favicon
$rowData["${prefix}Title"] = $title
# Add headers if available
if ($request.Headers -and $request.Headers.Count -gt 0) {
$headerString = ($request.Headers.GetEnumerator() | ForEach-Object { "$($_.Key): $($_.Value)" }) -join "; "
$rowData["${prefix}Headers"] = $headerString
} else {
$rowData["${prefix}Headers"] = ""
}
# Certificate information (if SSL)
if ($port.IsSSL -and $request.Certificate) {
$cert = $request.Certificate
# Add certificate status and method
$rowData["${prefix}CertStatus"] = if ($cert.PSObject.Properties.Name -contains "Status") { $cert.Status } else { "Unknown" }
$rowData["${prefix}CertMethod"] = if ($cert.PSObject.Properties.Name -contains "Method") { $cert.Method } else { "Unknown" }
$rowData["${prefix}CertError"] = if ($cert.PSObject.Properties.Name -contains "Error") { $cert.Error } else { $null }
# Add certificate details if available
if ($cert.PSObject.Properties.Name -contains "Subject" -and $cert.Subject -ne $null) {
$rowData["${prefix}CertSubjectCN"] = $cert.Subject.CN
$rowData["${prefix}CertIssuerCN"] = $cert.Issuer.CN
$rowData["${prefix}CertValidFrom"] = $cert.Validity.NotBefore
$rowData["${prefix}CertValidTo"] = $cert.Validity.NotAfter
$rowData["${prefix}CertDaysUntilExpiration"] = $cert.Validity.DaysUntilExpiration
$rowData["${prefix}CertSignatureAlgorithm"] = $cert.SignatureAlgorithm
$rowData["${prefix}CertSelfSigned"] = $cert.SelfSigned
$rowData["${prefix}CertSupportedTLSVersions"] = if ($cert.TLSSupport -and $cert.TLSSupport.SupportedProtocols) {
($cert.TLSSupport.SupportedProtocols -join ", ")
} else {
"Unknown"
}
# Add thumbprints
if ($cert.Thumbprint) {
$rowData["${prefix}CertThumbprintSHA1"] = $cert.Thumbprint.SHA1
$rowData["${prefix}CertThumbprintSHA256"] = $cert.Thumbprint.SHA256
}
# Include the serial number
$rowData["${prefix}CertSerialNumber"] = $cert.SerialNumber
}
}
}
$wideRows += [PSCustomObject]$rowData
}
# Create long format row(s)
$longRows = @()
foreach ($port in $HostData.OpenPorts) {
# For each request in the redirect chain
for ($i = 0; $i -lt $port.WebRequest.Count; $i++) {
$request = $port.WebRequest[$i]
# Extract title from HTML content
$title = Get-HtmlTitle -HtmlContent $request.Content
# Create a new ordered dictionary for each row
$rowData = [ordered]@{
IP = $HostData.IP
Hostname = $HostData.Hostname
DNSResolution = $HostData.DNSResolution.Resolves
DNSMatchesIP = $HostData.DNSResolution.IPMatch
Port = $port.PortNumber
Protocol = $port.Protocol
Service = $port.Service
IsSSL = $port.IsSSL
RedirectIndex = $i
URL = $request.Url
StatusCode = $request.StatusCode
Error = $request.Error
HasContent = if ($request.Content) { $true } else { $false }
ContentLength = if ($request.Content) { $request.Content.Length } else { 0 }
HasFavicon = if ($request.Favicon) { $true } else { $false }
Content = $request.Content
Favicon = $request.Favicon
Title = $title
}
# Add headers if available
if ($request.Headers -and $request.Headers.Count -gt 0) {
$headerString = ($request.Headers.GetEnumerator() | ForEach-Object { "$($_.Key): $($_.Value)" }) -join "; "
$rowData["Headers"] = $headerString
} else {
$rowData["Headers"] = ""
}
# Certificate information (if SSL)
if ($port.IsSSL -and $request.Certificate) {
$cert = $request.Certificate
# Add certificate status and method
$rowData["CertStatus"] = if ($cert.PSObject.Properties.Name -contains "Status") { $cert.Status } else { "Unknown" }
$rowData["CertMethod"] = if ($cert.PSObject.Properties.Name -contains "Method") { $cert.Method } else { "Unknown" }
$rowData["CertError"] = if ($cert.PSObject.Properties.Name -contains "Error") { $cert.Error } else { $null }
# Add certificate details if available
if ($cert.PSObject.Properties.Name -contains "Subject" -and $cert.Subject -ne $null) {
$rowData["CertSubjectCN"] = $cert.Subject.CN
$rowData["CertIssuerCN"] = $cert.Issuer.CN
$rowData["CertValidFrom"] = $cert.Validity.NotBefore
$rowData["CertValidTo"] = $cert.Validity.NotAfter
$rowData["CertDaysUntilExpiration"] = $cert.Validity.DaysUntilExpiration
$rowData["CertSignatureAlgorithm"] = $cert.SignatureAlgorithm
$rowData["CertSelfSigned"] = $cert.SelfSigned
$rowData["CertSupportedTLSVersions"] = if ($cert.TLSSupport -and $cert.TLSSupport.SupportedProtocols) {
($cert.TLSSupport.SupportedProtocols -join ", ")
} else {
"Unknown"
}
# Add thumbprints
if ($cert.Thumbprint) {
$rowData["CertThumbprintSHA1"] = $cert.Thumbprint.SHA1
$rowData["CertThumbprintSHA256"] = $cert.Thumbprint.SHA256
}
# Include the serial number
$rowData["CertSerialNumber"] = $cert.SerialNumber
}
}
$longRows += [PSCustomObject]$rowData
}
}
# Append to CSV files
try {
# If file doesn't exist, create it with headers
if (-not (Test-Path $WideCSVPath)) {
$wideRows | Export-Csv -Path $WideCSVPath -NoTypeInformation -Encoding UTF8
} else {
$wideRows | Export-Csv -Path $WideCSVPath -NoTypeInformation -Encoding UTF8 -Append -Force
}
if (-not (Test-Path $LongCSVPath)) {
$longRows | Export-Csv -Path $LongCSVPath -NoTypeInformation -Encoding UTF8
} else {
$longRows | Export-Csv -Path $LongCSVPath -NoTypeInformation -Encoding UTF8 -Append -Force
}
Write-Host "CSV data appended for host: $($HostData.IP)"
}
catch {
Write-Host "Error appending CSV data for host $($HostData.IP): $_" -ForegroundColor Red
}
}
# Convert relative path to absolute
$XmlPath = if ([System.IO.Path]::IsPathRooted($XmlPath)) {
$XmlPath
} else {
Join-Path -Path (Get-Location) -ChildPath $XmlPath
}
if (-not (Test-Path $XmlPath)) {
Write-Error "XML file not found at path: $XmlPath"
exit 1
}
# Count total hosts and set MaxHosts if not specified
$totalHosts = Get-TotalHosts -XmlPath $XmlPath
if ($MaxHosts -eq 0) {
$MaxHosts = $totalHosts
Write-Host "Total hosts in file: $totalHosts"
} else {
Write-Host "Processing $MaxHosts of $totalHosts hosts"
}
$timestamp = Get-Date -Format "yyyy-MM-dd_HH-mm-ss"
$outputDir = Join-Path -Path (Get-Location) -ChildPath "webinterface_capture_$timestamp"
New-Item -ItemType Directory -Path $outputDir -Force | Out-Null
$startTime = Get-Date
$processedHosts = 0
try {
$settings = New-Object System.Xml.XmlReaderSettings
$settings.DtdProcessing = [System.Xml.DtdProcessing]::Parse
$reader = [System.Xml.XmlReader]::Create($XmlPath, $settings)
while ($reader.Read() -and ($processedHosts -lt $MaxHosts)) {
if ($reader.NodeType -eq [System.Xml.XmlNodeType]::Element -and $reader.Name -eq "host") {
$hostXml = [xml]$reader.ReadOuterXml()
$targetHost = $hostXml.host
$hostIP = $targetHost.address | Where-Object { $_.addrtype -eq "ipv4" } | Select-Object -ExpandProperty addr
$hostname = $targetHost.hostnames.hostname.name
Write-Host "`nScanning host: $hostIP $(if ($hostname) { "($hostname)" })"
$hostStartTime = Get-Date
# Determine the appropriate hostname/IP to use based on DNS resolution
$targetHostname = Get-TargetHostname -Hostname $hostname -IP $hostIP
# Get all open ports from the Nmap XML
$openPorts = @($targetHost.ports.port | Where-Object { $_.state.state -eq "open" })
if ($openPorts.Count -eq 0) {
Write-Host " No open ports found in Nmap results"
$ports = @()
} else {
$ports = $openPorts | ForEach-Object {
$isSSL = $false
if ($_.service.tunnel -eq "ssl" -or $_.service.name -match "ssl|tls|https" -or $_.service.script.output -match "ssl|tls") {
$isSSL = $true
}
Write-Host "Port $($_.portid) ($($_.service.name)$(if ($isSSL) { "/ssl" })):"
$webRequests = Fetch-Url -TargetHost $targetHostname -Port $_.portid -IsSSL $isSSL -Timeout $RequestTimeout
[PSCustomObject]@{
PortNumber = $_.portid
Protocol = $_.protocol
Service = $_.service.name
IsSSL = $isSSL
WebRequest = $webRequests
}
}
}
$dnsCheck = Test-DNSResolution -Hostname $hostname -IP $hostIP
$hostResult = [PSCustomObject]@{
IP = $hostIP
Hostname = $hostname
DNSResolution = $dnsCheck
OpenPorts = $ports
ScanTime = $targetHost.starttime
}
$outputFile = Join-Path -Path $outputDir -ChildPath "$hostIP.json"
$hostResult | ConvertTo-Json -Depth 10 | Out-File $outputFile
# Generate the CSV File content for this host
$csvWideFile = Join-Path -Path $outputDir -ChildPath "web_interfaces_wide.csv"
$csvLongFile = Join-Path -Path $outputDir -ChildPath "web_interfaces_long.csv"
Append-HostToCSV -OutputDir $outputDir -WideCSVPath $csvWideFile -LongCSVPath $csvLongFile -HostData $hostResult
$processedHosts++
$percentComplete = [math]::Round(($processedHosts / $MaxHosts) * 100, 1)
# Calculate time estimates
$elapsedTime = (Get-Date) - $startTime
$averageTimePerHost = $elapsedTime.TotalSeconds / $processedHosts
$remainingHosts = $MaxHosts - $processedHosts
$estimatedRemainingSeconds = $averageTimePerHost * $remainingHosts
$estimatedRemaining = [TimeSpan]::FromSeconds($estimatedRemainingSeconds)
# Format the remaining time
$remainingStr = Format-TimeSpan -TimeSpan $estimatedRemaining
Write-Host ("Completed host: {0} ({1} of {2} - {3}% complete, est. {4} remaining)`n" -f
$hostIP, $processedHosts, $MaxHosts, $percentComplete, $remainingStr)
}
}
} catch {
Write-Error "Error processing NMAP XML: $_"
exit 1
} finally {
if ($reader) {
$reader.Close()
}
}
$totalTime = Format-TimeSpan -TimeSpan ((Get-Date) - $startTime)
Write-Host "Processing complete in $totalTime. Results saved in: $outputDir"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment