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:
ConvertTo-TlsProtocolOptionsconverts the user’s-TlsProtocolsvalues into a .NETSslProtocolsvalue. If explicit protocol names are requested, it usesGet-TlsRuntimeProtocolso unavailable runtime values are handled consistently.New-TlsOperationContextnormalizes per-target command state: hostname, port, target host, transport, timeout milliseconds, and transport options.New-TlsConnectionContextopens the TCP connection. It wrapsConnect-TcpWithTimeoutinInvoke-WithRetryand makes sure the shared connection object has a place to store the eventualSslStream.Invoke-TlsTransportNegotiationdispatches 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.Start-TlsHandshakecreates and authenticates theSslStream.Get-TlsHandshakeDetails,Get-RemoteCertificate, andTest-TlsCertificateValidityextract TLS session details, the server certificate, and date-based certificate validity.ConvertTo-TlsCertificateResultbuilds the public output object and delegates shared TLS/session fields toConvertTo-TlsSessionInfo.Close-NetworkResourcesruns infinallyso 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:
Invoke-SmtpStartTlsNegotiationreads the SMTP banner, sendsEHLO, checks forSTARTTLS, then sendsSTARTTLS.Invoke-ImapStartTlsNegotiationreads the IMAP greeting, sendsCAPABILITY, checks forSTARTTLS, then sendsSTARTTLS.Invoke-Pop3StartTlsNegotiationreads the POP3 greeting, sendsCAPA, checks forSTLS, then sendsSTLS.
Those transport helpers share lower-level text protocol utilities:
Invoke-WithStreamTimeouttemporarily applies read/write timeouts around plaintext negotiation.Read-TextProtocolLinereads CRLF-terminated protocol lines.Send-TextProtocolCommandwrites ASCII protocol commands.
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:
- New transports can plug into
Invoke-TlsTransportNegotiationwithout adding bulky branching to public commands. - Runtime TLS capability checks live in one place instead of being repeated across commands.
- Shared output fields are mapped once through
ConvertTo-TlsSessionInfo, which reduces drift betweenTLSleuth.CertificateResultandTLSleuth.ProtocolTestResult. - TCP connection setup, retry behavior, TLS handshakes, and cleanup can be tested separately.
- STARTTLS/STLS parsing remains protocol-specific where the protocol rules actually differ.
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.