diff --git a/Private/Convert/Convert-IdentityReferenceToSid.ps1 b/Private/Convert/Convert-IdentityReferenceToSid.ps1 index 6f3d744..049398f 100644 --- a/Private/Convert/Convert-IdentityReferenceToSid.ps1 +++ b/Private/Convert/Convert-IdentityReferenceToSid.ps1 @@ -13,16 +13,9 @@ function Convert-IdentityReferenceToSid { Supports forest-wide searches using Global Catalog when RootDSE is provided, enabling resolution of principals from child domains and trusted domains within the forest. - .PARAMETER Principal + .PARAMETER IdentityReference The IdentityReference object to convert. Typically from ACL IdentityReference properties. - .PARAMETER Credential - PSCredential for authenticating to Active Directory. Required when running from non-domain joined computers. - - .PARAMETER RootDSE - A DirectoryEntry object for the RootDSE. Used to determine the domain context for LDAP queries. - If not specified, attempts to derive the domain from the NTAccount name. - .INPUTS System.Security.Principal.IdentityReference Accepts IdentityReference objects via the pipeline. diff --git a/Private/Convert/Resolve-Principal.ps1 b/Private/Convert/Resolve-Principal.ps1 index c824894..6af1ff9 100644 --- a/Private/Convert/Resolve-Principal.ps1 +++ b/Private/Convert/Resolve-Principal.ps1 @@ -17,13 +17,6 @@ function Resolve-Principal { .PARAMETER IdentityReference The IdentityReference object to convert. Can be either NTAccount or SecurityIdentifier. - .PARAMETER Credential - PSCredential for authenticating to Active Directory. Required for LDAP queries. - - .PARAMETER RootDSE - A DirectoryEntry object for the RootDSE. Used to determine the domain context for LDAP queries. - If not specified, attempts to query without specific domain context. - .INPUTS System.Security.Principal.IdentityReference Accepts IdentityReference objects (NTAccount or SecurityIdentifier) via the pipeline. diff --git a/Private/Get/Get-RootDSE.ps1 b/Private/Get/Get-RootDSE.ps1 index 69c97f1..eedd46d 100644 Binary files a/Private/Get/Get-RootDSE.ps1 and b/Private/Get/Get-RootDSE.ps1 differ diff --git a/Private/Get/Get-WebEnrollmentEndpointStatus.ps1 b/Private/Get/Get-WebEnrollmentEndpointStatus.ps1 index 42d80b4..f3aeacf 100644 --- a/Private/Get/Get-WebEnrollmentEndpointStatus.ps1 +++ b/Private/Get/Get-WebEnrollmentEndpointStatus.ps1 @@ -26,6 +26,11 @@ function Get-WebEnrollmentEndpointStatus { PSCustomObject with properties URL, NtlmOffered, EpaNotRequired. Returns $null when the endpoint is unreachable or times out. + .EXAMPLE + Get-WebEnrollmentEndpointStatus -Url 'http://ca.contoso.com/certsrv/' + Checks whether the certsrv web enrollment endpoint at the given URL requires NTLM + authentication and whether Extended Protection for Authentication (EPA) is enforced. + .NOTES Intentionally has no unit tests - HttpClient cannot be mocked in Pester. Use integration tests (Get-WebEnrollmentEndpointStatus.Integration.Tests.ps1) diff --git a/Private/Initialize/Initialize-LS2Scan.ps1 b/Private/Initialize/Initialize-LS2Scan.ps1 index ff70f6a..4a123c8 100644 --- a/Private/Initialize/Initialize-LS2Scan.ps1 +++ b/Private/Initialize/Initialize-LS2Scan.ps1 @@ -42,6 +42,11 @@ function Initialize-LS2Scan { The full vulnerability scan only runs once per session. Subsequent calls to any Find-LS2* function will use the cached IssueStore data. + + .EXAMPLE + Initialize-LS2Scan -Forest 'contoso.com' + Initializes the Locksmith2 scan context for the contoso.com forest using the current user's + credentials. Returns $true on success. #> [CmdletBinding()] [OutputType([bool])] diff --git a/Private/Set/Set-CADisableExtensionList.ps1 b/Private/Set/Set-CADisableExtensionList.ps1 index e43dbb9..d3f15d9 100644 --- a/Private/Set/Set-CADisableExtensionList.ps1 +++ b/Private/Set/Set-CADisableExtensionList.ps1 @@ -12,8 +12,8 @@ function Set-CADisableExtensionList { - DisableExtensionList: Array of disabled extension OIDs - SecurityExtensionDisabled: Boolean indicating if the security extension (1.3.6.1.4.1.311.25.2) is disabled - .PARAMETER InputObject - Pipeline input from previous Set-CA* functions. Must contain DistinguishedName and CAFullName properties. + .PARAMETER AdcsObject + Pipeline input of LS2AdcsObject instances. Must contain DistinguishedName and CAFullName properties. .OUTPUTS PSCustomObject with DistinguishedName and CAFullName properties for pipeline continuation. diff --git a/Private/Set/Set-DangerousEnrollee.ps1 b/Private/Set/Set-DangerousEnrollee.ps1 index 3e72f50..bf0119f 100644 Binary files a/Private/Set/Set-DangerousEnrollee.ps1 and b/Private/Set/Set-DangerousEnrollee.ps1 differ diff --git a/Private/Set/Set-LowPrivilegeEnrollee.ps1 b/Private/Set/Set-LowPrivilegeEnrollee.ps1 index 5e03293..f385ace 100644 Binary files a/Private/Set/Set-LowPrivilegeEnrollee.ps1 and b/Private/Set/Set-LowPrivilegeEnrollee.ps1 differ diff --git a/Private/UI/Show-Logo.ps1 b/Private/UI/Show-Logo.ps1 index 48fd373..7b6008b 100644 --- a/Private/UI/Show-Logo.ps1 +++ b/Private/UI/Show-Logo.ps1 @@ -35,6 +35,31 @@ $LowerHalfBlock = [char]0x2584 $UpperHalfBlock = [char]0x2580 function ConvertTo-ConsoleColor { + <# + .SYNOPSIS + Maps an RGB color value to the nearest System.ConsoleColor. + + .DESCRIPTION + Finds the closest match to the given RGB values from the 16 standard console colors + using squared Euclidean distance in RGB space. + + .PARAMETER R + Red channel value (0-255). + + .PARAMETER G + Green channel value (0-255). + + .PARAMETER B + Blue channel value (0-255). + + .OUTPUTS + System.ConsoleColor + The nearest ConsoleColor to the given RGB values. + + .EXAMPLE + ConvertTo-ConsoleColor -R 0 -G 128 -B 255 + Returns the ConsoleColor closest to a medium blue. + #> param([int]$R, [int]$G, [int]$B) $colorMap = @( @{ Color = [System.ConsoleColor]::Black; R = 0; G = 0; B = 0 } @@ -70,11 +95,61 @@ function ConvertTo-ConsoleColor { } function Get-TrueColorFg { + <# + .SYNOPSIS + Returns an ANSI escape sequence for a true-color (24-bit) foreground color. + + .DESCRIPTION + Builds the ANSI CSI SGR escape sequence for setting the terminal foreground to the + specified 24-bit RGB color. Requires a terminal with VT/ANSI true-color support. + + .PARAMETER R + Red channel value (0-255). + + .PARAMETER G + Green channel value (0-255). + + .PARAMETER B + Blue channel value (0-255). + + .OUTPUTS + System.String + The ANSI escape sequence string for the requested foreground color. + + .EXAMPLE + Get-TrueColorFg -R 0 -G 200 -B 255 + Returns the ANSI sequence to set the foreground to a cyan-blue. + #> param([int]$R, [int]$G, [int]$B) return "$ESC[38;2;${R};${G};${B}m" } function Get-TrueColorBg { + <# + .SYNOPSIS + Returns an ANSI escape sequence for a true-color (24-bit) background color. + + .DESCRIPTION + Builds the ANSI CSI SGR escape sequence for setting the terminal background to the + specified 24-bit RGB color. Requires a terminal with VT/ANSI true-color support. + + .PARAMETER R + Red channel value (0-255). + + .PARAMETER G + Green channel value (0-255). + + .PARAMETER B + Blue channel value (0-255). + + .OUTPUTS + System.String + The ANSI escape sequence string for the requested background color. + + .EXAMPLE + Get-TrueColorBg -R 30 -G 30 -B 46 + Returns the ANSI sequence to set the background to a dark navy. + #> param([int]$R, [int]$G, [int]$B) return "$ESC[48;2;${R};${G};${B}m" } diff --git a/Public/Find-LS2VulnerableCA.ps1 b/Public/Find-LS2VulnerableCA.ps1 index 01b234d..0623922 100644 --- a/Public/Find-LS2VulnerableCA.ps1 +++ b/Public/Find-LS2VulnerableCA.ps1 @@ -17,6 +17,20 @@ function Find-LS2VulnerableCA { .PARAMETER Technique ESC technique name to scan for (e.g., 'ESC6', 'ESC7a', 'ESC7m', 'ESC8', 'ESC11', 'ESC16') + .PARAMETER Forest + Fully qualified domain name of the target AD forest. If not specified, uses the value already + set in module scope or auto-detected by Resolve-LS2ConnectionContext. + + .PARAMETER Credential + PSCredential for authenticating to Active Directory. If not specified, uses the credential + already set in module scope or the current user's identity. + + .PARAMETER ExpandGroups + When specified, expands group principals in discovered issues into individual per-member issues. + + .PARAMETER Rescan + Forces a fresh vulnerability scan even if the IssueStore is already populated. + .EXAMPLE Find-LS2VulnerableCA -Technique ESC6 Checks for CAs with EDITF_ATTRIBUTESUBJECTALTNAME2 enabled. diff --git a/Public/Find-LS2VulnerableObject.ps1 b/Public/Find-LS2VulnerableObject.ps1 index 220de10..fa7405b 100644 --- a/Public/Find-LS2VulnerableObject.ps1 +++ b/Public/Find-LS2VulnerableObject.ps1 @@ -15,7 +15,21 @@ function Find-LS2VulnerableObject { by focusing on the supporting infrastructure objects. .PARAMETER Technique - ESC technique name to scan for. Currently supports 'ESC5'. + ESC technique name to scan for. Currently supports 'ESC5a' and 'ESC5o'. + + .PARAMETER Forest + Fully qualified domain name of the target AD forest. If not specified, uses the value already + set in module scope or auto-detected by Resolve-LS2ConnectionContext. + + .PARAMETER Credential + PSCredential for authenticating to Active Directory. If not specified, uses the credential + already set in module scope or the current user's identity. + + .PARAMETER ExpandGroups + When specified, expands group principals in discovered issues into individual per-member issues. + + .PARAMETER Rescan + Forces a fresh vulnerability scan even if the IssueStore is already populated. .EXAMPLE Find-LS2VulnerableObject -Technique ESC5o diff --git a/Public/Find-LS2VulnerableTemplate.ps1 b/Public/Find-LS2VulnerableTemplate.ps1 index e7f169a..fb43003 100644 --- a/Public/Find-LS2VulnerableTemplate.ps1 +++ b/Public/Find-LS2VulnerableTemplate.ps1 @@ -10,6 +10,20 @@ function Find-LS2VulnerableTemplate { .PARAMETER Technique ESC technique name to scan for (e.g., 'ESC1', 'ESC2', 'ESC3c1', 'ESC3c2') + .PARAMETER Forest + Fully qualified domain name of the target AD forest. If not specified, uses the value already + set in module scope or auto-detected by Resolve-LS2ConnectionContext. + + .PARAMETER Credential + PSCredential for authenticating to Active Directory. If not specified, uses the credential + already set in module scope or the current user's identity. + + .PARAMETER ExpandGroups + When specified, expands group principals in discovered issues into individual per-member issues. + + .PARAMETER Rescan + Forces a fresh vulnerability scan even if the IssueStore is already populated. + .EXAMPLE Find-LS2VulnerableTemplate -Technique ESC1 Scans for templates vulnerable to ESC1 (misconfigured certificate templates). diff --git a/Public/Get-LS2Stores.ps1 b/Public/Get-LS2Stores.ps1 index 79e10e6..156fcea 100644 --- a/Public/Get-LS2Stores.ps1 +++ b/Public/Get-LS2Stores.ps1 @@ -31,18 +31,6 @@ function Get-LS2Stores { These stores are populated during the execution of Invoke-Locksmith2 and persist for the duration of the PowerShell session. - .PARAMETER Name - Optional. Name of a specific store to retrieve. Valid values: - - PrincipalStore - - AdcsObjectStore - - DomainStore - - IssueStore - - SafePrincipals - - DangerousPrincipals - - StandardOwners - - If not specified, returns an object containing all stores. - .INPUTS None. This function does not accept pipeline input. diff --git a/Tests/Locksmith2.CBHCoverage.Tests.ps1 b/Tests/Locksmith2.CBHCoverage.Tests.ps1 new file mode 100644 index 0000000..0aa3c2d --- /dev/null +++ b/Tests/Locksmith2.CBHCoverage.Tests.ps1 @@ -0,0 +1,116 @@ +#requires -Version 5.1 +BeforeDiscovery { + $ModuleRoot = Split-Path $PSScriptRoot -Parent + $allFunctionCases = [System.Collections.Generic.List[hashtable]]::new() + + $sourceFiles = Get-ChildItem -Recurse -Include '*.ps1' -Path @( + (Join-Path $ModuleRoot 'Private'), + (Join-Path $ModuleRoot 'Public') + ) | Sort-Object FullName + + foreach ($file in $sourceFiles) { + # Handle UTF-16LE (BOM: FF FE) + $rawBytes = [System.IO.File]::ReadAllBytes($file.FullName) + $encoding = if ($rawBytes.Length -ge 2 -and $rawBytes[0] -eq 0xFF -and $rawBytes[1] -eq 0xFE) { + [System.Text.Encoding]::Unicode + } else { + [System.Text.Encoding]::UTF8 + } + $content = [System.IO.File]::ReadAllText($file.FullName, $encoding) + + $parseErrors = $null + $ast = [System.Management.Automation.Language.Parser]::ParseInput( + $content, $file.FullName, [ref]$null, [ref]$parseErrors + ) + if ($parseErrors) { continue } + + $functions = $ast.FindAll({ + $args[0] -is [System.Management.Automation.Language.FunctionDefinitionAst] + }, $true) + + foreach ($func in $functions) { + $help = $func.GetHelpContent() + + # Actual parameter names extracted from the AST param() block + $paramNames = @() + if ($func.Body.ParamBlock -and $func.Body.ParamBlock.Parameters.Count -gt 0) { + $paramNames = @($func.Body.ParamBlock.Parameters | ForEach-Object { + $_.Name.VariablePath.UserPath + }) + } + + # Parameter names documented in .PARAMETER blocks (lowercased for comparison) + $docParamNamesLower = @() + if ($help -and $help.Parameters -and $help.Parameters.Count -gt 0) { + $docParamNamesLower = @($help.Parameters.Keys | ForEach-Object { $_.ToLower() }) + } + + $codeParamNamesLower = @($paramNames | ForEach-Object { $_.ToLower() }) + + # In param() but missing from CBH + $missingParams = @($paramNames | Where-Object { $_.ToLower() -notin $docParamNamesLower }) + + # In CBH .PARAMETER but not in param() — phantom entries + $phantomParams = @($docParamNamesLower | Where-Object { $_ -notin $codeParamNamesLower }) + + # [OutputType(...)] attribute declared on the param block + $hasOutputType = $false + if ($func.Body.ParamBlock -and $func.Body.ParamBlock.Attributes) { + $hasOutputType = [bool]($func.Body.ParamBlock.Attributes | + Where-Object { $_.TypeName.Name -eq 'OutputType' }) + } + + $allFunctionCases.Add(@{ + FunctionName = $func.Name + FileName = $file.Name + HasSynopsis = $help -and -not [string]::IsNullOrWhiteSpace($help.Synopsis) + HasDescription = $help -and -not [string]::IsNullOrWhiteSpace($help.Description) + ParamCount = $paramNames.Count + MissingParams = $missingParams + PhantomParams = $phantomParams + HasExample = $help -and $help.Examples -and $help.Examples.Count -gt 0 + HasOutputType = $hasOutputType + HasOutputsDoc = $help -and $help.Outputs -and $help.Outputs.Count -gt 0 + }) + } + } + + $casesWithParams = @($allFunctionCases | Where-Object { $_.ParamCount -gt 0 }) + $casesWithOutputType = @($allFunctionCases | Where-Object { $_.HasOutputType }) +} + +Describe 'CBH Synopsis' -Tag 'Unit', 'CBH' { + It ' in should have a non-empty .SYNOPSIS' -ForEach $allFunctionCases { + $HasSynopsis | Should -BeTrue + } +} + +Describe 'CBH Description' -Tag 'Unit', 'CBH' { + It ' in should have a non-empty .DESCRIPTION' -ForEach $allFunctionCases { + $HasDescription | Should -BeTrue + } +} + +Describe 'CBH Parameter Coverage' -Tag 'Unit', 'CBH' { + It ' in should document all parameters in CBH' -ForEach $casesWithParams { + $MissingParams | Should -BeNullOrEmpty -Because "these parameters lack a .PARAMETER doc block: $($MissingParams -join ', ')" + } +} + +Describe 'CBH No Phantom Parameters' -Tag 'Unit', 'CBH' { + It ' in should not document non-existent parameters in CBH' -ForEach $allFunctionCases { + $PhantomParams | Should -BeNullOrEmpty -Because "these .PARAMETER entries do not match any param() variable: $($PhantomParams -join ', ')" + } +} + +Describe 'CBH Example' -Tag 'Unit', 'CBH' { + It ' in should have at least one .EXAMPLE' -ForEach $allFunctionCases { + $HasExample | Should -BeTrue + } +} + +Describe 'CBH Outputs Coverage' -Tag 'Unit', 'CBH' { + It ' in should document .OUTPUTS when [OutputType] is declared' -ForEach $casesWithOutputType { + $HasOutputsDoc | Should -BeTrue -Because '[OutputType] is declared but .OUTPUTS is missing from CBH' + } +}