SnakeMove.psm1

#Requires -Version 5.1

# =============================================================================
# PRIVATE FUNCTIONS - internal workers, not exported
# =============================================================================

function Test-RdpStatus {

    $results = [PSCustomObject]@{

        Technique = "Remote Desktop Protocol (RDP)"
        MITRE_ID = "T1021.001"
        Status = "UNKNOWN"
        Risk = "High"
        Detail = ""

    }

    try {

        $regkey = Get-ItemProperty `
        -path "HKLM:\System\CurrentControlSet\Control\Terminal Server" `
        -Name "fDenyTSConnections" `
        -ErrorAction Stop

        $rdpEnabled = ($regkey.fDenyTSConnections -eq 0)
        $portListening = Get-NetTCPConnection -LocalPort 3389 -State listen `
        -ErrorAction SilentlyContinue

        if ($rdpEnabled -and $portListening) {

            $results.Status = "OPEN"
            $results.Detail = "RDP is enabled in registry (fDenyTSConnections=0) and port 3389 is listening. " +
                             "Remote login is possible with valid credentials."


        }

        elseif ($rdpEnabled -and -not $portListening) {

            $results.Status = "PARTIAL"
            $results.Risk = "Medium"
            $results.Detail = "RDP is enabled in registry but port 3389 is not listening. " +
                             "A firewall rule is likely blocking it."

        }

        else {

            $results.Status = "CLOSED"
            $results.Risk = "Low"
            $results.Detail = "RDP is disabled in Registry (fDenyTSConnections=1)"

        }

    }

    catch {

        $results.Status = "ERROR"
        $results.Detail = "Failed to read registry: $($_.Exception.Message)"

    }

    return $results

}

function Test-SMBSigningStatus {
    # Checks RequireSecuritySignature via Get-SmbServerConfiguration
    # If not required, NTLM relay attacks are feasible
    # MITRE T1021.002

    $results = [PSCustomObject]@{

    Technique = "SMB Signing / NTLM Relay Risk"
    MITRE_ID = "T1021.002"
    Status = "UNKNOWN"
    Risk = "High"
    Detail = ""

    }

    try {
        $smbConfig = Get-SMBServerConfiguration `
        -ErrorAction Stop

        if ($smbConfig.RequireSecuritySignature) {

            $results.Status = "CLOSED"
            $results.Risk = "Low"
            $results.Detail = "SMB signing is enabled and required. NTLM relay is mitigated"

        }

        elseif ($smbConfig.EnableSecuritySignature) {

            $results.Status = "PARTIAL"
            $results.Risk = "Medium"
            $results.Detail = "SMB signing is enabled, but not required. " `
                            + "Relay may succeed if the client does not negotiate signing"
        }

        else {

            $results.Status = "OPEN"
            $results.Detail = "SMB signing is not enabled and not required. " `
                            + "NTLM relay attack is feasible via Responder + ntlmrelayx"

        }

    }

    catch {

        $results.Status = "ERROR"
        $results.Detail = "Failed to retrieve SMB configuration: $($_.Exception.Message)"

    }

    return $results
}

function Test-WinRMStatus {
    # Checks WinRM service state and whether ports 5985 / 5986 are listening
    # MITRE T1021.006

    $results = [PSCustomObject]@{

        Technique = "WinRM / PowerShell Remoting"
        MITRE_ID = "T1021.006"
        Status = "UNKNOWN"
        Risk = "High"
        Detail = ""

    }

    try {

        $service = Get-Service -Name "WinRM" `
        -ErrorAction Stop
        $port5985 = Get-NetTCPConnection -LocalPort 5985 -State Listen `
        -ErrorAction SilentlyContinue
        $port5986 = Get-NetTCPConnection -LocalPort 5986 -State Listen `
        -ErrorAction SilentlyContinue

        if ($service.Status -eq "Running" -and ($port5985 -or $port5986)) {

            $openPorts = @()
            if ($port5985) {$openPorts += "5985 (HTTP)"}
            if ($port5986) {$openPorts += "5986 (HTTPS)"}

            $results.Status = "OPEN"
            $results.Detail = "WinRM is running. Listening on: $($openPorts -join ', ' ). " `
                            + "Remote PowerShell sessions are possible with valid credentials."

        }

        elseif ($service.Status -eq "Running") {

            $results.Status = "PARTIAL"
            $results.Risk = "Medium"
            $results.Detail = "WinRM is running, but ports 5985/5986 are not listening. " `
                            + "Firewall is likely blocking remote access."
        }
        else {

            $results.Status = "CLOSED"
            $results.Risk = "Low"
            $results.Detail = "WinRM service is Stopped. PowerShell remoting is unavailable."

        }

    }

    catch {

        $results.Status = "ERROR"
        $results.Detail = "Failed to check WinRM: $($_.Exception.Message)"

    }

    return $results

}

function Test-WMIStatus {
    # Checks Winmgmt service state and whether DCOM port 135 is listening
    # MITRE T1021.003

    $results = [PSCustomObject]@{

        Technique = "WMI / DCOM Remote Execution"
        MITRE_ID = "T1021.003"
        Status = "UNKNOWN"
        Risk = "High"
        Detail = ""

    }

    try {

        $service = Get-Service -Name "Winmgmt" `
        -ErrorAction Stop
        $dcomPort = Get-NetTCPConnection -LocalPort 135 -State Listen `
        -ErrorAction SilentlyContinue

        if ($service.Status -eq "Running" -and $dcomPort) {

            $results.Status = "OPEN"
            $results.Detail = "WMI service is running and DCOM port 135 is listening. " `
                            + "Remote execution via Invoke-WmiMethod or wmiexec is possible with valid credentials."

        }

        elseif ($service.Status -eq "Running") {

            $results.Status = "PARTIAL"
            $results.Risk = "Medium"
            $results.Detail = "WMI service is running but DCOM port 135 is not reachable. " `
                            + "Firewall may be blocking remote WMI."
                    
        }

        else {

            $results.Status = "CLOSED"
            $results.Risk = "Low"
            $results.Detail = "WMI service (Winmgmt) is stopped. Remote WMI execution unavailable."

        }

    }        
    catch {

            $results.Status = "ERROR"
            $results.Detail = "Failed to check WMI: $($_.Exception.Message)"
        }

    return $results

}

function Test-AdminShareStatus {
    # Enumerates whether default admin shares C$, ADMIN$, IPC$ are present
    # MITRE T1021.002

    $results = [PSCustomObject]@{

        Technique = "Default Admin Shares (C$, ADMIN$, IPC$)"
        MITRE_ID = "T1021.002"
        Status = "UNKNOWN"
        Risk = "High"
        Detail = ""

    }

    try {
        $adminShares = Get-SMBShare `
        -ErrorAction Stop | 
                        Where-Object {$_.Name -match "^(C\$|ADMIN\$|IPC\$)$"}

        if ($adminShares) {

            $shareList = ($adminShares.Name) -join ', '
            $results.Status = "OPEN"
            $results.Detail = "Admin shares present: $shareList. " `
                        + "With administrator credentials, full remote filesystem access is possible via net use or PsExec."

        }

        else {

            $results.Status = "CLOSED"
            $results.Risk = "Low"
            $results.Detail = "No default admin shares found. " `
                            + "Likely disabled via registry (AutoShareServer=0)."

        }
    }
    catch {

        $results.Status = "ERROR"
        $results.Detail = "Failed to enumerate SMB shares: $($_.Exception.Message)"

    }

    return $results

}

# =============================================================================
# PUBLIC FUNCTION
# =============================================================================

function Get-SnakeMove {
<#
.SYNOPSIS
    Enumerates lateral movement opportunities on the current Windows host.

.DESCRIPTION
    Get-SnakeMove runs the following checks against the target host to identify
    which lateral movement techniques are currently viable. Each check reads
    actual registry keys, service states, and port bindings - no hardcoded
    values or simulations.
 
    Checks performed:
        [1] RDP availability (T1021.001)
        [2] SMB signing / NTLM relay risk (T1021.002)
        [3] WinRM / PowerShell Remoting (T1021.006)
        [4] WMI / DCOM remote execution (T1021.003)
        [5] Default admin shares (T1021.002)

.PARAMETER ExportCSV
    Optional. Specifies the full target filesystem path where the raw audit results
    will be saved as a flat CSV file. This format is optimized for SIEM ingestion or automated parsing scripts.

.PARAMETER ExportHTML
    Optional. Specifies the full target filesystem path where a standalone, styled HTML report
    will be generated. This file includes an embedded CSS layout, an Executive Summary block,
    and a detailed findings table.

.PARAMETER Quiet
    Suppresses all console output including the banner, live check results,
    and summary. Recommended for scripted or OpSec-sensitive use cases.

.EXAMPLE
    Get-SnakeMove
 
    Runs all checks and displays results in the console.
 
.EXAMPLE
    Get-SnakeMove -Quiet -ExportCSV "C:\Windows\Diagnostics\system_audit.csv"
 
    Runs silently and saves results without producing any console output.
 
.EXAMPLE
    $Data = Get-SnakeMove -Quiet | Where-Object { $_.Status -eq "OPEN" -or $_.Status -eq "PARTIAL" }
 
    Isolates both immediate vulnerabilities and conditionally viable attack paths in memory,
    streamlining data collection for tactical pivot planning
    while ignoring fully mitigated systems.
 
.EXAMPLE
    Get-SnakeMove -ExportHTML "C:\Reports\snakemove.html"
 
    Runs all checks and saves a styled, dark-themed HTML report with an
    executive summary and color-coded findings table.
 
.EXAMPLE
    Get-SnakeMove -ExportCSV "C:\Reports\audit.csv" -ExportHTML "C:\Reports\audit.html"
 
    Runs all checks once and produces both a CSV and an HTML report in a
    single pass, useful for feeding a SIEM while also keeping a human-readable
    copy for a report appendix.
 
.EXAMPLE
    Get-SnakeMove -Quiet | ConvertTo-Json | Out-File "findings.json"
 
    Runs silently and serializes the results to JSON, suitable for ingestion
    by external tooling or log forwarding pipelines.

.OUTPUTS
    [PSCustomObject[]]
    Each object contains: Technique, MITRE_ID, Status, Risk, Detail,
    ComputerName, ScanUser, ScanTime.

.NOTES
    Author : Ihar Shcharbitski
    GitHub : https://github.com/Shcherbaa/SnakeMove
    Version : 1.0.0
    Requires : PowerShell 5.1+, Administrator privileges recommended
 
    For authorized penetration testing and educational use only.

.LINK
    https://attack.mitre.org/techniques/T1021/

.LINK
    https://github.com/Shcherbaa/SnakeMove
#>


    [CmdletBinding()]
    param (

        [Parameter (Mandatory = $false)]
        [ValidateScript({

            $parent = Split-Path $_ -Parent
            $parentExist = Test-Path $parent -PathType Container

            # Checking whether a folder exists
            if (-not $parentExist) {

                throw "Directory does not exist: $parent"

            }

            # Checking write permissions individually
            try {

                $testFile = Join-Path $parent ([System.IO.Path]::GetRandomFileName())
                [System.IO.File]::Create($testFile).Close()
                Remove-Item $testFile -ErrorAction Stop

            }

            catch {

                throw "Access denied or cannot write to directory: $parent"

            }

            $true

        })]
        [string]$ExportCSV,

        [Parameter (Mandatory = $false)]
        [ValidateScript ({

            
            $parent = Split-Path $_ -Parent
            $parentExist = Test-Path $parent -PathType Container

            if (-not $parentExist) {

                throw "Directory does not exist: $parent"

            }

            try {

                $testFile = Join-Path $parent ([System.IO.Path]::GetRandomFileName())
                [System.IO.File]::Create($testFile).Close()
                Remove-Item $testFile -ErrorAction Stop

            }

            catch {

                throw "Access denied or cannot write to directory: $parent"

            }

            $true

        })]
        [string]$ExportHTML,
        

        [Parameter (Mandatory = $false)]
        [switch]$Quiet

    )

    $moduleVersion = (Get-Module SnakeMove).Version.ToString()

    if (-not $Quiet) {

        # Banner
        Write-Host ""
        Write-Host " ============================================================" -ForegroundColor DarkCyan
        Write-Host " SNAKEMOVE | v$moduleVersion | MITRE ATT&CK T1021" -ForegroundColor Cyan
        Write-Host " https://github.com/Shcherbaa/SnakeMove" -ForegroundColor DarkGray
        Write-Host " For authorized penetration testing use only." -ForegroundColor DarkGray
        Write-Host " ============================================================" -ForegroundColor DarkCyan
        Write-Host ""
        Write-Host " Target : " -NoNewline -ForegroundColor DarkGray
        Write-Host $env:COMPUTERNAME -ForegroundColor Yellow
        Write-Host " User : " -NoNewline -ForegroundColor DarkGray
        Write-Host "$env:USERDOMAIN\$env:USERNAME" -ForegroundColor Yellow
        Write-Host " DateTime : " -NoNewline -ForegroundColor DarkGray
        Write-Host (Get-Date -Format "yyyy-MM-dd HH:mm:ss") -ForegroundColor Yellow
        Write-Host ""
        Write-Host " Running checks..." -ForegroundColor DarkGray
        Write-Host (" " + ("-" * 62)) -ForegroundColor DarkGray
        Write-Host ""

    }

    # Elevation check
    $isAdmin = ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)

               if (-not $isAdmin -and -not $Quiet) {

                Write-Host " [!] Not running as Administrator. Some checks may return incomplete results." -ForegroundColor Yellow
                Write-Host ""

               }

    $results = @(

        Test-RdpStatus
        Test-SMBSigningStatus
        Test-WinRMStatus
        Test-WMIStatus
        Test-AdminShareStatus

    )

    # Enrich collected objects with environmental metadata for export context
    foreach ($report in $results) {

        $report | Add-Member -NotePropertyMembers @{

            ComputerName = $env:ComputerName
            ScanUser = "$env:USERDOMAIN\$env:USERNAME"
            ScanTime = (Get-Date -Format "yyyy-MM-dd HH:mm:ss")

        }
    }

    if (-not $Quiet) {
        # Display results
        foreach ($item in $results) {

            switch ($item.Status) {

                "OPEN"    { $statusColor = "Red";     $statusLabel = "[ OPEN ]" }
                "PARTIAL" { $statusColor = "Yellow";  $statusLabel = "[ PARTIAL ]" }
                "CLOSED"  { $statusColor = "Green";   $statusLabel = "[ CLOSED ]" }
                "ERROR"   { $statusColor = "Magenta"; $statusLabel = "[ ERROR ]" }
                default   { $statusColor = "White";   $statusLabel = "[ UNKNOWN ]" }

            }

            $riskColor = switch ($item.Risk) {

                "High" { "Red" }
                "Medium" { "Yellow" }
                "Low" { "Green" }
                default { "White" }

            }

                Write-Host " Technique : " -NoNewline -ForegroundColor DarkGray
                Write-Host $item.Technique -ForegroundColor White
                Write-Host " MITRE ID : " -NoNewline -ForegroundColor DarkGray
                Write-Host $item.MITRE_ID -ForegroundColor DarkCyan
                Write-Host " Status : " -NoNewline -ForegroundColor DarkGray
                Write-Host $statusLabel -ForegroundColor $statusColor
                Write-Host " Risk : " -NoNewline -ForegroundColor DarkGray
                Write-Host $item.Risk -ForegroundColor $riskColor
                Write-Host " Detail : " -NoNewline -ForegroundColor DarkGray
                Write-Host $item.Detail -ForegroundColor Gray
                Write-Host (" " + ("-" * 62)) -ForegroundColor DarkGray
                Write-Host ""
        
        }

    }
    
    # Summary
    $openCount = @($results | Where-Object {$_.Status -eq "OPEN"} ).Count
    $partialCount = @($results | Where-Object {$_.Status -eq "PARTIAL"} ).Count
    $closedCount = @($results | Where-Object {$_.Status -eq "CLOSED"} ).Count
    $errorCount = @($results | Where-Object {$_.Status -eq "ERROR"} ).Count

    if (-not $Quiet) {

    Write-Host " SUMMARY" -ForegroundColor White
    Write-Host (" " + ("-" * 30)) -ForegroundColor DarkGray
    Write-Host " OPEN : " -NoNewline -ForegroundColor DarkGray
    Write-Host "$openCount" -NoNewline -ForegroundColor Red
    Write-Host " (active attack paths)" -ForegroundColor DarkGray
    Write-Host " PARTIAL : " -NoNewline -ForegroundColor DarkGray
    Write-Host "$partialCount" -NoNewline -ForegroundColor Yellow
    Write-Host " (conditionally accessible)" -ForegroundColor DarkGray
    Write-Host " CLOSED : " -NoNewline -ForegroundColor DarkGray
    Write-Host "$closedCount" -NoNewline -ForegroundColor Green
    Write-Host " (mitigated)" -ForegroundColor DarkGray

    if ($errorCount -gt 0) {

        Write-Host " ERROR : " -NoNewline -ForegroundColor DarkGray
        Write-Host "$errorCount" -NoNewline -ForegroundColor Magenta
        Write-Host " (try running as Administrator)" -ForegroundColor DarkGray

    }
 
    Write-Host ""

    }

    # Optional CSV Export
    if ($ExportCSV) {

        try {

            $results | Export-Csv -Path $ExportCSV -NoTypeInformation -Encoding UTF8 `
            -ErrorAction Stop

            if (-not $Quiet) {

                Write-Host " [+] CSV results exported to: $ExportCSV" -ForegroundColor Green

            }

        }

        catch {

            Write-Host " [!] CSV Export failed: $($_.Exception.Message)" -ForegroundColor Red

        }

        Write-Host ""

    }

    # Optional HTML Export
    if ($ExportHTML) {

        $reportHeader = "SnakeMove - Lateral Movement Audit | $($env:COMPUTERNAME) | $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')"

        $cssStyle = @"
<style>
    * { box-sizing: border-box; margin: 0; padding: 0; }
    body { background: #0d1117; color: #c9d1d9; font-family: 'Segoe UI', Consolas, monospace; padding: 2rem; }
    h1 { color: #58a6ff; font-size: 1.4rem; margin-bottom: 0.4rem; letter-spacing: 1px; }
    .meta { color: #8b949e; font-size: 0.85rem; margin-bottom: 2rem; }
    .summary { display: flex; gap: 1rem; margin-bottom: 2rem; flex-wrap: wrap; }
    .card { background: #161b22; border: 1px solid #30363d; border-radius: 6px; padding: 1rem 1.5rem; min-width: 110px; }
    .card .label { font-size: 0.7rem; color: #8b949e; text-transform: uppercase; letter-spacing: 1px; }
    .card .value { font-size: 2rem; font-weight: bold; margin-top: 0.2rem; }
    .c-open { color: #f85149; }
    .c-partial { color: #e3b341; }
    .c-closed { color: #3fb950; }
    .c-error { color: #bc8cff; }
    table { width: 100%; border-collapse: collapse; background: #161b22; border: 1px solid #30363d; border-radius: 6px; overflow: hidden; }
    th { background: #21262d; color: #8b949e; font-size: 0.7rem; text-transform: uppercase; letter-spacing: 1px; padding: 0.75rem 1rem; text-align: left; border-bottom: 1px solid #30363d; }
    td { padding: 0.75rem 1rem; border-bottom: 1px solid #21262d; font-size: 0.85rem; vertical-align: top; }
    tr:last-child td { border-bottom: none; }
    .badge { display: inline-block; padding: 0.2rem 0.6rem; border-radius: 12px; font-size: 0.72rem; font-weight: bold; letter-spacing: 0.5px; }
    .badge-open { background: rgba(248,81,73,0.15); color: #f85149; border: 1px solid rgba(248,81,73,0.4); }
    .badge-partial { background: rgba(227,179,65,0.15); color: #e3b341; border: 1px solid rgba(227,179,65,0.4); }
    .badge-closed { background: rgba(63,185,80,0.15); color: #3fb950; border: 1px solid rgba(63,185,80,0.4); }
    .badge-error { background: rgba(188,140,255,0.15); color: #bc8cff; border: 1px solid rgba(188,140,255,0.4); }
    .risk-high { color: #f85149; font-weight: bold; }
    .risk-medium { color: #e3b341; font-weight: bold; }
    .risk-low { color: #3fb950; }
    .mitre { color: #58a6ff; font-family: Consolas, monospace; font-size: 0.8rem; }
    footer { margin-top: 2rem; color: #484f58; font-size: 0.75rem; text-align: center; }
</style>
"@


        $tableRows = foreach ($r in $results) {

             $statusBadge = switch ($r.Status) {

                "OPEN"    { '<span class="badge badge-open">OPEN</span>' }
                "PARTIAL" { '<span class="badge badge-partial">PARTIAL</span>' }
                "CLOSED"  { '<span class="badge badge-closed">CLOSED</span>' }
                "ERROR"   { '<span class="badge badge-error">ERROR</span>' }
                default   { $r.Status }

            }

            $riskClass = switch ($r.Risk) {

                "High"   { "risk-high" }
                "Medium" { "risk-medium" }
                "Low"    { "risk-low" }
                default  { "" }

            }

            "<tr><td class='mitre'>$($r.MITRE_ID)</td><td>$($r.Technique)</td><td>$statusBadge</td><td class='$riskClass'>$($r.Risk)</td><td>$($r.Detail)</td></tr>"

        }

        $htmlContent = @"
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>SnakeMove Report</title>
    $cssStyle
</head>
<body>
    <h1>&#128013; SNAKEMOVE - Lateral Movement Audit</h1>
    <p class="meta">
        Target: $($env:COMPUTERNAME) &nbsp;|&nbsp;
        User: $("$env:USERDOMAIN\$env:USERNAME") &nbsp;|&nbsp;
        Scan Time: $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')
    </p>

    <div class="summary">
        <div class="card"><div class="label">Open</div><div class="value c-open">$openCount</div></div>
        <div class="card"><div class="label">Partial</div><div class="value c-partial">$partialCount</div></div>
        <div class="card"><div class="label">Closed</div><div class="value c-closed">$closedCount</div></div>
        <div class="card"><div class="label">Error</div><div class="value c-error">$errorCount</div></div>
    </div>

    <table>
        <thead>
            <tr>
                <th>MITRE ID</th>
                <th>Technique</th>
                <th>Status</th>
                <th>Risk</th>
                <th>Detail</th>
            </tr>
        </thead>
        <tbody>
            $($tableRows -join "`n ")
        </tbody>
    </table>

    <footer>Generated by SnakeMove v1.0.0 - For authorized penetration testing and educational use only.</footer>
</body>
</html>
"@


        try {

            $htmlContent | Out-File -FilePath $ExportHTML -Encoding UTF8 -ErrorAction Stop

            if (-not $Quiet) {

                Write-Host " [+] HTML results exported to: $ExportHTML" -ForegroundColor Green
            }
        }

        catch {

            Write-Host " [!] HTML Export failed: $($_.Exception.Message)" -ForegroundColor Red
        }

    }

    return $results

}

Export-ModuleMember -Function Get-SnakeMove