Skip to the content.

TLSleuth is intentionally built around small PowerShell functions. The public commands stay focused on orchestration, while private helpers handle option normalization, TCP connection setup, STARTTLS/STLS negotiation, binary protocol stream I/O, TLS handshakes, certificate extraction, result mapping, and cleanup.

That shape matters because TLS inspection has several moving parts. A single call may need to resolve SNI behavior, open a TCP connection, perform protocol-specific plaintext negotiation, upgrade the stream to TLS, capture validation state, extract the certificate, and return stable automation-friendly output. Keeping those responsibilities separate makes the module easier to test and safer to extend.

The Current Helper Call Graph

The diagram below shows the main public command paths and the private helper functions that support them. Local parser helpers inside the SMTP, IMAP, and POP3 negotiation functions are included because they are part of the transport negotiation flow, even though they are not separate files under source/private. The binary protocol helpers are shown as a separate boundary because they are preparation for future transports such as RDP rather than part of the current SMTP/IMAP/POP3 text flow.

flowchart TD
    subgraph PublicCommands["Public commands"]
        GetCert["Get-TLSleuthCertificate"]
        TestProtocol["Test-TLSleuthProtocol"]
    end

    subgraph Setup["Option and runtime setup"]
        ProtocolOptions["ConvertTo-TlsProtocolOptions"]
        RuntimeProtocol["Get-TlsRuntimeProtocol"]
        OperationContext["New-TlsOperationContext"]
        TransportOptions["New-TlsTransportOptionSet"]
    end

    subgraph Connection["Connection setup"]
        ConnectionContext["New-TlsConnectionContext"]
        Retry["Invoke-WithRetry"]
        TcpConnect["Connect-TcpWithTimeout"]
    end

    subgraph Transport["Plaintext transport negotiation"]
        TransportDispatch["Invoke-TlsTransportNegotiation"]
        NoTransport["ImplicitTls: no plaintext negotiation"]
        Smtp["Invoke-SmtpStartTlsNegotiation"]
        Imap["Invoke-ImapStartTlsNegotiation"]
        Pop3["Invoke-Pop3StartTlsNegotiation"]
        StreamTimeout["Invoke-WithStreamTimeout"]
        ReadLine["Read-TextProtocolLine"]
        SendCommand["Send-TextProtocolCommand"]
        ReadSmtp["Read-SmtpResponse local helper"]
        ReadImap["Read-ImapTaggedResponse local helper"]
        Pop3Status["ConvertFrom-Pop3StatusLine local helper"]
        Pop3Multi["Read-Pop3MultilineData local helper"]
        TransportResult["New-TlsTransportNegotiationResult"]
    end

    subgraph BinaryTransport["Binary transport boundary"]
        FutureBinary["Future binary transport helper"]
        ReadBinary["Read-BinaryProtocolData"]
        SendBinary["Send-BinaryProtocolData"]
    end

    subgraph TlsHandshake["TLS handshake and inspection"]
        StartHandshake["Start-TlsHandshake"]
        HandshakeDetails["Get-TlsHandshakeDetails"]
        RemoteCertificate["Get-RemoteCertificate"]
        CertificateValidity["Test-TlsCertificateValidity"]
    end

    subgraph Results["Result mapping"]
        CertificateResult["ConvertTo-TlsCertificateResult"]
        SessionInfo["ConvertTo-TlsSessionInfo"]
        CertOutput["TLSleuth.CertificateResult"]
        ProtocolOutput["TLSleuth.ProtocolTestResult"]
    end

    subgraph Cleanup["Cleanup"]
        CloseResources["Close-NetworkResources"]
    end

    GetCert --> ProtocolOptions
    ProtocolOptions -. "explicit protocol names" .-> RuntimeProtocol
    TestProtocol --> RuntimeProtocol

    GetCert --> OperationContext
    TestProtocol --> OperationContext
    OperationContext --> TransportOptions

    GetCert --> ConnectionContext
    TestProtocol --> ConnectionContext
    ConnectionContext --> Retry
    Retry --> TcpConnect

    GetCert --> TransportDispatch
    TestProtocol --> TransportDispatch
    TransportDispatch -->|ImplicitTls| NoTransport
    TransportDispatch -->|SmtpStartTls| Smtp
    TransportDispatch -->|ImapStartTls| Imap
    TransportDispatch -->|Pop3StartTls| Pop3
    TransportDispatch --> TransportResult
    TransportDispatch -. future binary transport .-> FutureBinary

    Smtp --> StreamTimeout
    Smtp --> ReadSmtp
    Smtp --> SendCommand
    ReadSmtp --> ReadLine

    Imap --> StreamTimeout
    Imap --> ReadLine
    Imap --> SendCommand
    Imap --> ReadImap
    ReadImap --> ReadLine

    Pop3 --> StreamTimeout
    Pop3 --> ReadLine
    Pop3 --> SendCommand
    Pop3 --> Pop3Status
    Pop3 --> Pop3Multi
    Pop3Multi --> ReadLine

    FutureBinary -. read bytes .-> ReadBinary
    FutureBinary -. send bytes .-> SendBinary

    GetCert --> StartHandshake
    TestProtocol --> StartHandshake
    StartHandshake --> HandshakeDetails

    GetCert --> HandshakeDetails
    TestProtocol --> HandshakeDetails
    GetCert --> RemoteCertificate
    GetCert --> CertificateValidity

    GetCert --> CertificateResult
    CertificateResult --> SessionInfo
    CertificateResult --> CertOutput

    TestProtocol --> SessionInfo
    SessionInfo --> ProtocolOutput

    GetCert --> CloseResources
    TestProtocol --> CloseResources

How Get-TLSleuthCertificate Uses The Helpers

Get-TLSleuthCertificate has one job from the user’s point of view: return certificate and TLS session details for a target endpoint.

Internally, it follows this path:

  1. ConvertTo-TlsProtocolOptions converts the user’s -TlsProtocols values into a .NET SslProtocols value. If explicit protocol names are requested, it uses Get-TlsRuntimeProtocol so unavailable runtime values are handled consistently.
  2. New-TlsOperationContext normalizes per-target command state: hostname, port, target host, transport, timeout milliseconds, and transport options.
  3. New-TlsConnectionContext opens the TCP connection. It wraps Connect-TcpWithTimeout in Invoke-WithRetry and makes sure the shared connection object has a place to store the eventual SslStream.
  4. Invoke-TlsTransportNegotiation dispatches any plaintext negotiation needed before TLS starts and returns internal negotiation metadata. Public commands intentionally suppress that internal object so their output contracts remain unchanged.
  5. Start-TlsHandshake creates and authenticates the SslStream.
  6. Get-TlsHandshakeDetails, Get-RemoteCertificate, and Test-TlsCertificateValidity extract TLS session details, the server certificate, and date-based certificate validity.
  7. ConvertTo-TlsCertificateResult builds the public output object and delegates shared TLS/session fields to ConvertTo-TlsSessionInfo.
  8. Close-NetworkResources runs in finally so the connection is cleaned up on success or failure.

The public output contract stays concentrated at the end of the pipeline. The helpers before that point prepare facts; they do not decide the shape of the public object.

How Test-TLSleuthProtocol Uses The Helpers

Test-TLSleuthProtocol is similar, but it repeats the connection and handshake path once per explicit TLS protocol available on the current runtime.

The command starts with Get-TlsRuntimeProtocol, then builds the same operation context with New-TlsOperationContext. For each protocol value, it opens a fresh connection, runs transport negotiation, attempts a handshake with that protocol, extracts handshake details when successful, and maps shared TLS/session fields through ConvertTo-TlsSessionInfo.

Failures are captured per protocol attempt. A failed TLS 1.0 attempt, for example, does not prevent the command from trying TLS 1.2 or TLS 1.3. Cleanup still runs after every attempt through Close-NetworkResources.

Transport Negotiation Is A Dispatcher

Invoke-TlsTransportNegotiation is the boundary between public command orchestration and protocol-specific plaintext behavior.

For ImplicitTls, there is no plaintext negotiation. TLS begins immediately after the TCP connection.

For explicit TLS transports, the dispatcher routes to one protocol helper:

Those transport helpers share lower-level text protocol utilities:

The protocol-specific helpers still own their grammar. SMTP multiline response handling, IMAP tagged responses, and POP3 status/multiline handling stay close to the protocol code that needs them.

Binary transports use a separate boundary. Read-BinaryProtocolData and Send-BinaryProtocolData read and write exact byte sequences without going through the text protocol helpers. That keeps future RDP packet handling out of the SMTP/IMAP/POP3 line-oriented code.

Helper Responsibility Table

Helper Called by Responsibility
Close-NetworkResources Get-TLSleuthCertificate, Test-TLSleuthProtocol Dispose TLS, network stream, and TCP resources safely.
Connect-TcpWithTimeout New-TlsConnectionContext through Invoke-WithRetry Open the initial TCP connection with a bounded timeout.
ConvertTo-TlsCertificateResult Get-TLSleuthCertificate Build the public certificate result object.
ConvertTo-TlsProtocolOptions Get-TLSleuthCertificate Convert user protocol names into SslProtocols.
ConvertTo-TlsSessionInfo ConvertTo-TlsCertificateResult, Test-TLSleuthProtocol Build the shared TLS/session property map.
Get-RemoteCertificate Get-TLSleuthCertificate Extract the remote certificate from the authenticated SslStream.
Get-TlsHandshakeDetails Get-TLSleuthCertificate, Test-TLSleuthProtocol Read negotiated protocol, cipher, stream state, and validation details.
Get-TlsRuntimeProtocol Test-TLSleuthProtocol, ConvertTo-TlsProtocolOptions Discover explicit TLS protocol enum values available on the current runtime.
Invoke-ImapStartTlsNegotiation Invoke-TlsTransportNegotiation Perform IMAP STARTTLS negotiation.
Invoke-Pop3StartTlsNegotiation Invoke-TlsTransportNegotiation Perform POP3 STLS negotiation.
Invoke-SmtpStartTlsNegotiation Invoke-TlsTransportNegotiation Perform SMTP STARTTLS negotiation.
Invoke-TlsTransportNegotiation Get-TLSleuthCertificate, Test-TLSleuthProtocol Dispatch transport-specific plaintext negotiation.
Invoke-WithRetry New-TlsConnectionContext Retry transient connection operations.
Invoke-WithStreamTimeout SMTP, IMAP, and POP3 negotiation helpers Temporarily apply stream timeouts during plaintext negotiation.
New-TlsConnectionContext Get-TLSleuthCertificate, Test-TLSleuthProtocol Open and normalize the shared connection context.
New-TlsOperationContext Get-TLSleuthCertificate, Test-TLSleuthProtocol Normalize per-target command options and timeouts.
New-TlsTransportNegotiationResult Invoke-TlsTransportNegotiation Build internal transport negotiation metadata.
New-TlsTransportOptionSet New-TlsOperationContext Separate shared transport options from protocol-specific options.
Read-BinaryProtocolData Future binary transport helpers Read exact byte counts from binary protocol streams.
Read-TextProtocolLine SMTP, IMAP, and POP3 negotiation helpers Read one CRLF-terminated text protocol line.
Send-BinaryProtocolData Future binary transport helpers Write raw binary protocol bytes to a stream.
Send-TextProtocolCommand SMTP, IMAP, and POP3 negotiation helpers Send one CRLF-terminated text protocol command.
Start-TlsHandshake Get-TLSleuthCertificate, Test-TLSleuthProtocol Create and authenticate the SslStream.
Test-TlsCertificateValidity Get-TLSleuthCertificate Compute date-based certificate validity and days until expiry.

Why This Structure Helps

The main benefit is that the public commands read like workflows instead of protocol implementations. They decide what phase happens next, while the helpers own the details inside each phase.

That separation gives TLSleuth a few practical advantages:

Caveats

This architecture does not turn TLSleuth into a vulnerability scanner. The module still uses the .NET TLS stack and the operating system TLS policy. It reports what can be negotiated and inspected through that runtime.

It also does not enumerate every possible server cipher or craft custom TLS client hellos. The helper split keeps the implementation maintainable, but it stays within TLSleuth’s scope: practical, script-friendly TLS and certificate inspection from PowerShell.

Summary

TLSleuth’s helper architecture follows a simple pattern:

normalize options -> connect -> negotiate transport -> handshake -> inspect -> map result -> cleanup

Get-TLSleuthCertificate and Test-TLSleuthProtocol share most of that path. The differences are concentrated where they belong: certificate retrieval builds a certificate result, while protocol testing loops over runtime-available protocol values and returns one result per attempt.

That shape keeps the public commands stable and makes the next round of protocol support easier to add without scattering transport, timeout, handshake, or result-mapping logic through the module.