From eba4c7257ffea0f5512eedd2dc44a41f55d42eef Mon Sep 17 00:00:00 2001 From: Sam Erde Date: Mon, 26 May 2025 22:05:10 -0400 Subject: [PATCH 01/14] TSS --- Tests/Invoke-TSS.ps1 | 2 +- Tests/TSS Specs.md | 170 ++++++++++++++++++++++++------------------- 2 files changed, 97 insertions(+), 75 deletions(-) diff --git a/Tests/Invoke-TSS.ps1 b/Tests/Invoke-TSS.ps1 index 4f7fad8e..5d7a8b86 100644 --- a/Tests/Invoke-TSS.ps1 +++ b/Tests/Invoke-TSS.ps1 @@ -401,7 +401,7 @@ $ACL.AddAccessRule($AccessRule) Set-Acl "AD:$ESC5WriteOwner" -AclObject $ACL Get-ADObject -Filter 'objectClass -eq "pKIEnrollmentService"' -SearchBase $PKSContainer -Properties * | ForEach-Object { - $ForestGC = $(Get-ADDomainController -Discover -Service GlobalCatalog -ForceDiscover | Select-Object -ExpandProperty Hostname) + ":3268" + $ForestGC = $(Get-ADDomainController -Discover -Service GlobalCatalog -ForceDiscover | Select-Object -ExpandProperty Hostname) + ':3268' [string]$CAFullName = "$($_.dNSHostName)\$($_.Name)" $CAHostname = $_.dNSHostName.split('.')[0] $CAHostFQDN = (Get-ADObject -Filter { (Name -eq $CAHostName) -and (objectclass -eq 'computer') } -Properties DnsHostname -Server $ForestGC).DnsHostname diff --git a/Tests/TSS Specs.md b/Tests/TSS Specs.md index 89934948..22b44443 100644 --- a/Tests/TSS Specs.md +++ b/Tests/TSS Specs.md @@ -1,125 +1,147 @@ # Tactical Speed Square: *the UnLocksmith* ## Auditing + Check if Audit level is set: - - No: leave it - - Yes: Set to 0 + +- No: leave it +- Yes: Set to 0 ## ESC1 + Find: - - Name: ESC1 - - Config: Typical ESC1 - - Principal: Authenticated Users - - Name: ESC1and2 - - Config: "Any purpose" EKU - - Principal: Authenticated Users +- Name: ESC1 +- Config: Typical ESC1 +- Principal: Authenticated Users + +- Name: ESC1and2 +- Config: "Any purpose" EKU +- Principal: Authenticated Users Do Not Find: - - Name: ESC1Filtered - - Config: Typical ESC1 - - Principal: Administrators + +- Name: ESC1Filtered +- Config: Typical ESC1 +- Principal: Administrators ## ESC2 + Find: - - Name: ESC2 - - Config: Typical ESC2 - - Principal: Authenticated Users - - Name: ESC1and2 - - Config: "Any purpose" EKU - - Principal: Authenticated Users +- Name: ESC2 +- Config: Typical ESC2 +- Principal: Authenticated Users + +- Name: ESC1and2 +- Config: "Any purpose" EKU +- Principal: Authenticated Users Do Not Find: - - Name: ESC2Filtered - - Config: Typical ESC2 - - Principal: Administrators + +- Name: ESC2Filtered +- Config: Typical ESC2 +- Principal: Administrators ## ESC3 - Not Complete + Find: - - Name: ESC3Condition1 + +- Name: ESC3Condition1 Find: - - Name: ESC3Condition2 + +- Name: ESC3Condition2 ## ESC4 + Find: - - Name: ESC4GenericAll - - Config: GenericAll - - Principal: Authenticated Users - - Name: ESC4UnsafeOwner - - Config: UnsafeOwner - - Principal: Authenticated Users +- Name: ESC4GenericAll +- Config: GenericAll +- Principal: Authenticated Users + +- Name: ESC4UnsafeOwner +- Config: UnsafeOwner +- Principal: Authenticated Users - - Name: ESC4WriteProperty - - Config: WriteProperty on All Objects - - Principal: Authenticated Users +- Name: ESC4WriteProperty +- Config: WriteProperty on All Objects +- Principal: Authenticated Users - - Name: ESC4WriteOwner - - Config: WriteOwner - - Principal: Authenticated Users +- Name: ESC4WriteOwner +- Config: WriteOwner +- Principal: Authenticated Users Do Not Find: - - Name: ESC4FilteredEnroll - - Config WriteProperty, ExtendedRight on Enroll - - Principal: Domain Users - - Name: ESC4FilteredAutoEnroll - - Config: WriteProperty, ExtendedRight on AutoEnroll - - Principal: Domain Users +- Name: ESC4FilteredEnroll +- Config WriteProperty, ExtendedRight on Enroll +- Principal: Domain Users + +- Name: ESC4FilteredAutoEnroll +- Config: WriteProperty, ExtendedRight on AutoEnroll +- Principal: Domain Users - - Name: ESC4FilteredOwner - - Config: Owner - - Principal: Administrators +- Name: ESC4FilteredOwner +- Config: Owner +- Principal: Administrators - - Name: ESC4FilteredSafeUsers - - Config: GenericAll - - Principal: Administrators +- Name: ESC4FilteredSafeUsers +- Config: GenericAll +- Principal: Administrators ## ESC5 + Find: - - Name: ESC5GenericAll - - Config: GenericAll - - Principal: Authenticated Users - - Name: ESC5UnsafeOwner - - Config: UnsafeOwner - - Principal: Authenticated Users +- Name: ESC5GenericAll +- Config: GenericAll +- Principal: Authenticated Users + +- Name: ESC5UnsafeOwner +- Config: UnsafeOwner +- Principal: Authenticated Users - - Name: ESC5WriteProperty - - Config: WriteProperty on All Objects - - Principal: Authenticated Users +- Name: ESC5WriteProperty +- Config: WriteProperty on All Objects +- Principal: Authenticated Users - - Name: ESC5WriteOwner - - Config: WriteOwner - - Principal: Authenticated Users +- Name: ESC5WriteOwner +- Config: WriteOwner +- Principal: Authenticated Users Do Not Find: - - Name: ESC5FilteredEnroll - - Config WriteProperty, ExtendedRight on Enroll - - Principal: Authenticated Users - - Name: ESC5FilteredAutoEnroll - - Config: WriteProperty, ExtendedRight on AutoEnroll - - Principal: Authenticated Users +- Name: ESC5FilteredEnroll +- Config WriteProperty, ExtendedRight on Enroll +- Principal: Authenticated Users + +- Name: ESC5FilteredAutoEnroll +- Config: WriteProperty, ExtendedRight on AutoEnroll +- Principal: Authenticated Users - - Name: ESC5FilteredOwner - - Config: Owner - - Principal: Administrators +- Name: ESC5FilteredOwner +- Config: Owner +- Principal: Administrators - - Name: ESC5FilteredSafeUsers - - Config: GenericAll - - Principal: Administrators +- Name: ESC5FilteredSafeUsers +- Config: GenericAll +- Principal: Administrators ## ESC6 + Check if dangerous flag exists: - - Yes: leave it - - No: set it + +- Yes: leave it +- No: set it ## ESC8 - Not Complete + Find: - - HTTP Enrollment Endpoint + +- HTTP Enrollment Endpoint Find: - - HTTPS Enrollment Endpoint + +- HTTPS Enrollment Endpoint From 1623985b9b62a85d8473a6d2c3c78c2894c10b72 Mon Sep 17 00:00:00 2001 From: Sam Erde Date: Mon, 26 May 2025 22:09:49 -0400 Subject: [PATCH 02/14] Valid markdown formatting and fixed punctuation. --- .github/ISSUE_TEMPLATE/bug_report.md | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index cd7cf6e7..b645bb57 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -7,20 +7,21 @@ assignees: '' --- -**Describe the Bug** +## Describe the Bug -**Steps To Reproduce** +### Steps To Reproduce -**Expected Behavior** +### Expected Behavior -**Environment** - - Locksmith Version: [e.g. 2024.8] - - OS: [e.g. Windows Server 2019] - - PowerShell Version: [e.g. Windows PowerShell 5.1 or PowerShell 7.4.5] - - PowerShell Host: [e.g. Windows Terminal, PowerShell, PowerShell ISE, VS Code Terminal +### Environment -**Additional Context** +- Locksmith Version: (e.g. 2024.8) +- OS: (e.g. Windows Server 2019) +- PowerShell Version: (e.g. Windows PowerShell 5.1 or PowerShell 7.5.1) +- PowerShell Host: (e.g. Windows Terminal, PowerShell, PowerShell ISE, VS Code Terminal) + +### Additional Context From 4ed33abcd08ba48f40b80fc577325b356e46c7bf Mon Sep 17 00:00:00 2001 From: Sam Erde Date: Mon, 26 May 2025 22:12:40 -0400 Subject: [PATCH 03/14] chore: replace cmdlet aliases with full names; standard auto formatting --- Tests/Compare-ADObjects.ps1 | 18 +++++++++--------- Tests/Show-ADObjectChanges.ps1 | 18 +++++++++--------- 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/Tests/Compare-ADObjects.ps1 b/Tests/Compare-ADObjects.ps1 index d0f604f1..256b7b4b 100644 --- a/Tests/Compare-ADObjects.ps1 +++ b/Tests/Compare-ADObjects.ps1 @@ -1,17 +1,17 @@ # Source: https://learn.microsoft.com/en-us/archive/blogs/janesays/compare-all-properties-of-two-objects-in-windows-powershell param( - [Parameter(Mandatory=$true)] + [Parameter(Mandatory = $true)] [string]$DN1, - [Parameter(Mandatory=$true)] + [Parameter(Mandatory = $true)] [string]$DN2 ) $ReferenceObject = Get-ADObject -Identity $DN1 -Properties * $DifferenceObject = Get-ADObject -Identity $DN2 -Properties * -$ObjectProperties = $ReferenceObject | Get-Member -MemberType Property,NoteProperty | % Name -$ObjectProperties += $DifferenceObject | Get-Member -MemberType Property,NoteProperty | % Name -$ObjectProperties = $ObjectProperties | Sort | Select -Unique +$ObjectProperties = $ReferenceObject | Get-Member -MemberType Property, NoteProperty | ForEach-Object Name +$ObjectProperties += $DifferenceObject | Get-Member -MemberType Property, NoteProperty | ForEach-Object Name +$ObjectProperties = $ObjectProperties | Sort-Object | Select-Object -Unique $Differences = @() foreach ($objectproperty in $ObjectProperties) { @@ -19,14 +19,14 @@ foreach ($objectproperty in $ObjectProperties) { if ($difference) { $differenceproperties = @{ PropertyName = $objectproperty - RefValue = ($difference | ? {$_.SideIndicator -eq '<='} | % $($objectproperty)) - DiffValue = ($difference | ? {$_.SideIndicator -eq '=>'} | % $($objectproperty)) + RefValue = ($difference | Where-Object { $_.SideIndicator -eq '<=' } | ForEach-Object $($objectproperty)) + DiffValue = ($difference | Where-Object { $_.SideIndicator -eq '=>' } | ForEach-Object $($objectproperty)) } $Differences += New-Object PSObject -Property $differenceproperties } } if ($Differences) { return ( - $Differences | Select PropertyName,RefValue,DiffValue + $Differences | Select-Object PropertyName, RefValue, DiffValue ) -} \ No newline at end of file +} diff --git a/Tests/Show-ADObjectChanges.ps1 b/Tests/Show-ADObjectChanges.ps1 index e1a2b36d..d0424bfa 100644 --- a/Tests/Show-ADObjectChanges.ps1 +++ b/Tests/Show-ADObjectChanges.ps1 @@ -1,16 +1,16 @@ # Source: https://learn.microsoft.com/en-us/archive/blogs/janesays/compare-all-properties-of-two-objects-in-windows-powershell param( - [Parameter(Mandatory=$true)] + [Parameter(Mandatory = $true)] [string]$DN ) $ReferenceObject = Get-ADObject -Identity $DN -Properties * -Read-Host "Make your changes and press Enter" +Read-Host 'Make your changes and press Enter' $DifferenceObject = Get-ADObject -Identity $DN -Properties * -$ObjectProperties = $ReferenceObject | Get-Member -MemberType Property,NoteProperty | % Name -$ObjectProperties += $DifferenceObject | Get-Member -MemberType Property,NoteProperty | % Name -$ObjectProperties = $ObjectProperties | Sort | Select -Unique +$ObjectProperties = $ReferenceObject | Get-Member -MemberType Property, NoteProperty | ForEach-Object Name +$ObjectProperties += $DifferenceObject | Get-Member -MemberType Property, NoteProperty | ForEach-Object Name +$ObjectProperties = $ObjectProperties | Sort-Object | Select-Object -Unique $Differences = @() foreach ($objectproperty in $ObjectProperties) { @@ -18,14 +18,14 @@ foreach ($objectproperty in $ObjectProperties) { if ($difference) { $differenceproperties = @{ PropertyName = $objectproperty - RefValue = ($difference | ? {$_.SideIndicator -eq '<='} | % $($objectproperty)) - DiffValue = ($difference | ? {$_.SideIndicator -eq '=>'} | % $($objectproperty)) + RefValue = ($difference | Where-Object { $_.SideIndicator -eq '<=' } | ForEach-Object $($objectproperty)) + DiffValue = ($difference | Where-Object { $_.SideIndicator -eq '=>' } | ForEach-Object $($objectproperty)) } $Differences += New-Object PSObject -Property $differenceproperties } } if ($Differences) { return ( - $Differences | Select PropertyName,RefValue,DiffValue + $Differences | Select-Object PropertyName, RefValue, DiffValue ) -} \ No newline at end of file +} From 8c86a601db7f9a2fee9100fe7644bf40035fa5c6 Mon Sep 17 00:00:00 2001 From: Sam Erde Date: Mon, 26 May 2025 22:17:57 -0400 Subject: [PATCH 04/14] fix: add missing process block --- Private/Test-IsMemberOfProtectedUsers.ps1 | 56 +++++++++++++---------- 1 file changed, 32 insertions(+), 24 deletions(-) diff --git a/Private/Test-IsMemberOfProtectedUsers.ps1 b/Private/Test-IsMemberOfProtectedUsers.ps1 index 15380ccd..d0d8e983 100644 --- a/Private/Test-IsMemberOfProtectedUsers.ps1 +++ b/Private/Test-IsMemberOfProtectedUsers.ps1 @@ -39,36 +39,44 @@ function Test-IsMemberOfProtectedUsers { $User ) - Import-Module ActiveDirectory - - # Use the currently logged in user if none is specified - # Get the user from Active Directory - if (-not($User)) { - # These two are different types. Fixed by referencing $CheckUser.SID later, but should fix here by using one type. - $CurrentUser = ([System.Security.Principal.WindowsIdentity]::GetCurrent().Name).Split('\')[-1] - $CheckUser = Get-ADUser $CurrentUser -Properties primaryGroupID - } else { - $CheckUser = Get-ADUser $User -Properties primaryGroupID + begin { + Import-Module ActiveDirectory } - # Get the Protected Users group by SID instead of by its name to ensure compatibility with any locale or language. - $DomainSID = (Get-ADDomain).DomainSID.Value - $ProtectedUsersSID = "$DomainSID-525" + process { + # Use the currently logged in user if none is specified + # Get the user from Active Directory + if (-not($User)) { + # These two are different types. Fixed by referencing $CheckUser.SID later, but should fix here by using one type. + $CurrentUser = ([System.Security.Principal.WindowsIdentity]::GetCurrent().Name).Split('\')[-1] + $CheckUser = Get-ADUser $CurrentUser -Properties primaryGroupID + } else { + $CheckUser = Get-ADUser $User -Properties primaryGroupID + } + + # Get the Protected Users group by SID instead of by its name to ensure compatibility with any locale or language. + $DomainSID = (Get-ADDomain).DomainSID.Value + $ProtectedUsersSID = "$DomainSID-525" - # Get members of the Protected Users group for the current domain. Recuse in case groups are nested in it. - $ProtectedUsers = Get-ADGroupMember -Identity $ProtectedUsersSID -Recursive | Select-Object -Unique + # Get members of the Protected Users group for the current domain. Recuse in case groups are nested in it. + $ProtectedUsers = Get-ADGroupMember -Identity $ProtectedUsersSID -Recursive | Select-Object -Unique - # Check if the current user is in the 'Protected Users' group - if ($ProtectedUsers.SID.Value -contains $CheckUser.SID) { - Write-Verbose "$($CheckUser.Name) ($($CheckUser.DistinguishedName)) is a member of the Protected Users group." - $true - } else { - # Check if the user's PGID (primary group ID) is set to the Protected Users group RID (525). - if ( $CheckUser.primaryGroupID -eq '525' ) { + # Check if the current user is in the 'Protected Users' group + if ($ProtectedUsers.SID.Value -contains $CheckUser.SID) { + Write-Verbose "$($CheckUser.Name) ($($CheckUser.DistinguishedName)) is a member of the Protected Users group." $true } else { - Write-Verbose "$($CheckUser.Name) ($($CheckUser.DistinguishedName)) is not a member of the Protected Users group." - $false + # Check if the user's PGID (primary group ID) is set to the Protected Users group RID (525). + if ( $CheckUser.primaryGroupID -eq '525' ) { + $true + } else { + Write-Verbose "$($CheckUser.Name) ($($CheckUser.DistinguishedName)) is not a member of the Protected Users group." + $false + } } } + + end { + + } } From a4418ef650a7e4d5de7010c07e355104f07a7326 Mon Sep 17 00:00:00 2001 From: Sam Erde Date: Mon, 26 May 2025 22:19:56 -0400 Subject: [PATCH 05/14] fix: add missing process block --- Private/Set-AdditionalTemplateProperty.ps1 | 24 ++++++++++++---------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/Private/Set-AdditionalTemplateProperty.ps1 b/Private/Set-AdditionalTemplateProperty.ps1 index df659760..1e25345e 100644 --- a/Private/Set-AdditionalTemplateProperty.ps1 +++ b/Private/Set-AdditionalTemplateProperty.ps1 @@ -28,18 +28,20 @@ [Microsoft.ActiveDirectory.Management.ADEntity[]]$ADCSObjects ) - $ADCSObjects | Where-Object objectClass -match 'pKICertificateTemplate' -PipelineVariable template | ForEach-Object { - # Write-Host "[?] Checking if template `"$($template.Name)`" is Enabled on any Certification Authority." -ForegroundColor Blue - $Enabled = $false - $EnabledOn = @() - foreach ($ca in ($ADCSObjects | Where-Object objectClass -eq 'pKIEnrollmentService')) { - if ($ca.certificateTemplates -contains $template.Name) { - $Enabled = $true - $EnabledOn += $ca.Name - } + process { + $ADCSObjects | Where-Object objectClass -Match 'pKICertificateTemplate' -PipelineVariable template | ForEach-Object { + # Write-Host "[?] Checking if template `"$($template.Name)`" is Enabled on any Certification Authority." -ForegroundColor Blue + $Enabled = $false + $EnabledOn = @() + foreach ($ca in ($ADCSObjects | Where-Object objectClass -EQ 'pKIEnrollmentService')) { + if ($ca.certificateTemplates -contains $template.Name) { + $Enabled = $true + $EnabledOn += $ca.Name + } - $template | Add-Member -NotePropertyName Enabled -NotePropertyValue $Enabled -Force - $template | Add-Member -NotePropertyName EnabledOn -NotePropertyValue $EnabledOn -Force + $template | Add-Member -NotePropertyName Enabled -NotePropertyValue $Enabled -Force + $template | Add-Member -NotePropertyName EnabledOn -NotePropertyValue $EnabledOn -Force + } } } } From 6d05decad888445831837b01aeff4bc59037929f Mon Sep 17 00:00:00 2001 From: Sam Erde Date: Mon, 26 May 2025 22:32:56 -0400 Subject: [PATCH 06/14] bump to patched version of jinja --- Docs/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Docs/requirements.txt b/Docs/requirements.txt index a384e051..25f02a7d 100644 --- a/Docs/requirements.txt +++ b/Docs/requirements.txt @@ -1,6 +1,6 @@ # https://github.com/readthedocs-examples/example-mkdocs-basic/blob/main/docs/requirements.txt # requirements.txt -jinja2==3.1.5 #https://pypi.org/project/Jinja2/ +jinja2==3.1.6 #https://pypi.org/project/Jinja2/ mkdocs>=1.6.0 #https://github.com/mkdocs/mkdocs mkdocs-material==9.5.25 #https://github.com/squidfunk/mkdocs-material pygments>=2.18.0 #https://pypi.org/project/Pygments/ From 5071ee325d50337cbdfd61d3fb216f90dcc9332f Mon Sep 17 00:00:00 2001 From: Sam Erde Date: Mon, 26 May 2025 22:36:00 -0400 Subject: [PATCH 07/14] chore: empty catch block --- Private/Set-AdditionalCAProperty.ps1 | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/Private/Set-AdditionalCAProperty.ps1 b/Private/Set-AdditionalCAProperty.ps1 index 16975ccd..7ab8f2cb 100644 --- a/Private/Set-AdditionalCAProperty.ps1 +++ b/Private/Set-AdditionalCAProperty.ps1 @@ -40,7 +40,7 @@ begin { if (-not ([System.Management.Automation.PSTypeName]'TrustAllCertsPolicy') ) { if ($PSVersionTable.PSEdition -eq 'Desktop') { - $code = @" + $code = @' using System.Net; using System.Security.Cryptography.X509Certificates; public class TrustAllCertsPolicy : ICertificatePolicy { @@ -48,11 +48,11 @@ return true; } } -"@ +'@ Add-Type -TypeDefinition $code -Language CSharp [System.Net.ServicePointManager]::CertificatePolicy = New-Object TrustAllCertsPolicy } else { - Add-Type @" + Add-Type @' using System.Net; using System.Security.Cryptography.X509Certificates; using System.Net.Security; @@ -61,7 +61,7 @@ return true; } } -"@ +'@ # Set the ServerCertificateValidationCallback [System.Net.ServicePointManager]::ServerCertificateValidationCallback = [TrustAllCertsPolicy]::TrustAllCerts } @@ -72,7 +72,7 @@ $ADCSObjects | Where-Object objectClass -Match 'pKIEnrollmentService' | ForEach-Object { $CAEnrollmentEndpoint = @() #[array]$CAEnrollmentEndpoint = $_.'msPKI-Enrollment-Servers' | Select-String 'http.*' | ForEach-Object { $_.Matches[0].Value } - foreach ($directory in @("certsrv/", "$($_.Name)_CES_Kerberos/service.svc", "$($_.Name)_CES_Kerberos/service.svc/CES", "ADPolicyProvider_CEP_Kerberos/service.svc", "certsrv/mscep/")) { + foreach ($directory in @('certsrv/', "$($_.Name)_CES_Kerberos/service.svc", "$($_.Name)_CES_Kerberos/service.svc/CES", 'ADPolicyProvider_CEP_Kerberos/service.svc', 'certsrv/mscep/')) { $URL = "://$($_.dNSHostName)/$directory" try { $Auth = 'NTLM' @@ -116,6 +116,7 @@ 'Auth' = $Auth } } catch { + Write-Debug "There may have been an error or something nothing found. $_" } } } From 22477fe50f521a54f05e0922fb530e8c29d64edf Mon Sep 17 00:00:00 2001 From: Sam Erde Date: Mon, 26 May 2025 23:00:58 -0400 Subject: [PATCH 08/14] Trivial markdown rules...or are they? --- README.md | 66 +++++++++++++++++++++++++++++++++++++++---------------- 1 file changed, 47 insertions(+), 19 deletions(-) diff --git a/README.md b/README.md index 2a53ab79..496d688f 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,7 @@ + # Locksmith + ```text _ _____ _______ _ _ _______ _______ _____ _______ _ _ | | | | |____/ |______ | | | | | |_____| @@ -9,6 +11,7 @@ \'-' .---'-''-'-' \'-' .--'--''-'-' \'-' .--'--'-''-' '--' '--' '--' ``` + A small tool built to find and fix common misconfigurations in Active Directory Certificate Services. @@ -20,146 +23,171 @@ A small tool built to find and fix common misconfigurations in Active Directory ![GitHub Actions Workflow Status](https://img.shields.io/github/actions/workflow/status/jakehildreth/Locksmith/powershell.yml?logo=github&label=PSScriptAnalyzer) ![PowerShell Gallery Downloads](https://img.shields.io/powershellgallery/dt/locksmith?logo=powershell&label=PowerShell%20Gallery%20Downloads&color=blue) + ## Contents + 1. [Installation](#Installation) -2. [Run Locksmith](#RunLocksmith) - 1. [Mode 0](#Mode0) - 2. [Mode 1](#Mode1) - 3. [Mode 2](#Mode2) - 4. [Mode 3](#Mode3) - 5. [Mode 4](#Mode4) +2. [Run Locksmith](#Run Locksmith) + 1. [Mode 0](#Mode 0) + 2. [Mode 1](#Mode 1) + 3. [Mode 2](#Mode 2) + 4. [Mode 3](#Mode 3) + 5. [Mode 4](#Mode 4) 6. [Scans](#Scans) - + ## Installation ### Prerequisites + 1. Locksmith must be run on a domain joined system. 2. The ActiveDirectory and ServerManager PowerShell modules must be installed before importing the Locksmith module. 3. Administrative rights may be required for some checks and for remediation. ### Standard Module Installation + Open a PowerShell prompt and install Locksmith from the PowerShell Gallery: + ```powershell Install-Module -Name Locksmith -Scope CurrentUser ``` ### Alternative Installation Methods + 1. Download and Use the Module Without Installing It 1. Download the [latest module version](https://github.com/jakehildreth/Locksmith/releases/latest/download/Locksmith.zip). 2. Open a PowerShell prompt to the location of the extracted file and run: + ```powershell Unblock-File .\Locksmith.zip # if necessary to unblock the download Expand-Archive .\Locksmith.zip Import-Module .\Locksmith\Locksmith.psd1 Invoke-Locksmith ``` + 2. Download the Standalone Script Without Module 1. Download the [latest monolithic (all-in-one) script version](https://github.com/jakehildreth/Locksmith/releases/latest/download/Invoke-Locksmith.zip). 2. Open a PowerShell prompt to the location of the downloaded file and run: + ```powershell Unblock-File .\Invoke-Locksmith.zip Expand-Archive .\Invoke-Locksmith.zip -DestinationPath .\ .\Invoke-Locksmith.ps1 ``` - + ## Run Locksmith -There are several modes you can chose from when running `Invoke-Locksmith`. You can also use the **Scans** parameter to choose which scans you want to invoke. - +There are several modes you can chose from when running `Invoke-Locksmith`. You can also use the **Scans** parameter to choose which scans you want to invoke. + ### Mode 0: Identify Issues, Output to Console (Default) Running `Invoke-Locksmith.ps1` with no parameters or with `-Mode 0` will scan the current Active Directory forest and output all discovered AD CS issues to the console in **Table** format. + ``` powershell # Module Syntax Invoke-Locksmith ``` + ``` powershell # Script Syntax .\Invoke-Locksmith.ps1 ``` -Example Output for Mode 0: - +Example Output for Mode 0: + ### Mode 1: Identify Issues and Fixes, Output to Console + This mode scans the current forest and outputs all discovered AD CS issues and possible fixes to the console in **List** format. ``` powershell # Module Syntax Invoke-Locksmith -Mode 1 ``` + ``` powershell # Script Syntax .\Invoke-Locksmith.ps1 -Mode 1 ``` -Example Output for Mode 1: - +Example Output for Mode 1: + ### Mode 2: Identify Issues, Output to CSV + Locksmith Mode 2 scans the current forest and outputs all discovered AD CS issues to ADCSIssues.CSV in the present working directory. ``` powershell # Module Syntax Invoke-Locksmith -Mode 2 ``` + ``` powershell # Script Syntax .\Invoke-Locksmith.ps1 -Mode 2 ``` -Example Output for Mode 2: - +Example Output for Mode 2: + ### Mode 3: Identify Issues and Fixes, Output to CSV + In Mode 3, Locksmith scans the current forest and outputs all discovered AD CS issues and example fixes to ADCSRemediation.CSV in the present working directory. + ``` powershell # Module Syntax Invoke-Locksmith -Mode 3 ``` + ``` powershell # Script Syntax .\Invoke-Locksmith.ps1 -Mode 3 ``` -Example Output for Mode 3: - +Example Output for Mode 3: + ### Mode 4: Fix All Issues + Mode 4 is the "easy button." Running Locksmith in Mode 4 will identify all misconfigurations and offer to fix each issue. If there is any possible operational impact, Locksmith will warn you. ``` powershell # Module Syntax Invoke-Locksmith -Mode 4 ``` + ``` powershell # Script Syntax .\Invoke-Locksmith.ps1 -Mode 4 ``` + Example Output for Mode 4: +### Scans -### Scans: Select Which Scans to Invoke Use the `-Scans` parameter to choose which vulnerabilities to scan for. Acceptable values include `All`, `Auditing`, `ESC1`, `ESC2`, `ESC3`, `ESC4`, `ESC5`, `ESC6`, `ESC7`, `ESC8`, `ESC11`, `ESC13`, `ESC15`, `EKEUwu`, `ESC16` or `PromptMe`. The `PromptMe` option presents an interactive list allowing you to select one or more scans. ``` powershell # Run all scans Invoke-Locksmith -Scan All ``` + ``` powershell # Prompt the user for a list of scans to select Invoke-Locksmith.ps1 -Scans PromptMe ``` + ``` powershell # Scan for ESC1 vulnerable paths Invoke-Locksmith.ps1 -Scans ESC1 ``` + ``` powershell # Scan for ESC1, ESC2, and ESC8 vulnerable paths Invoke-Locksmith.ps1 -Scans ESC1,ESC2,ESC8 ``` + Thank you for using Locksmith! 💜 From 69bd8e1328130e2f13ef9fe3f5d80c6b81c2cdba Mon Sep 17 00:00:00 2001 From: Sam Erde Date: Mon, 26 May 2025 23:02:25 -0400 Subject: [PATCH 09/14] Fix typo and repo name --- mkdocs.yml | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/mkdocs.yml b/mkdocs.yml index 53df775f..b2ba493e 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -11,10 +11,10 @@ site_name: "Locksmith" site_url: "https://jakehildreth.github.io/Locksmith" repo_url: "https://github.com/jakehildreth/Locksmith" -repo_name: "TrimarcJake/Locksmith" +repo_name: "JakeHildreth/Locksmith" # edit_uri: edit/main/docs/ # edit_uri_template: -site_description: "A small tool to find and fix common misconfigurations in Active Directory Certificate Services." # meta tag to the generated HTML heade +site_description: "A small tool to find and fix common misconfigurations in Active Directory Certificate Services." # meta tag to the generated HTML header site_author: "Jake Hildreth" # meta tag to the generated HTML header copyright: "(c) 2024 Jake Hildreth." # remote_branch: gh-deploy @@ -83,10 +83,3 @@ theme: - navigation.top - toc.follow - toc.integrate - - # favicon: - # icon: - # repo: - # font: - # text: Work Sans - # logo: From 423615716bbbcfdec744ced772a48eb56e6997e4 Mon Sep 17 00:00:00 2001 From: Sam Erde Date: Mon, 26 May 2025 23:04:32 -0400 Subject: [PATCH 10/14] An unused, ungrateful waste of bits and bytes --- CHANGELOG.MD | 2 -- 1 file changed, 2 deletions(-) delete mode 100644 CHANGELOG.MD diff --git a/CHANGELOG.MD b/CHANGELOG.MD deleted file mode 100644 index 4b4ad473..00000000 --- a/CHANGELOG.MD +++ /dev/null @@ -1,2 +0,0 @@ -### 1.0.0 -- Initial release \ No newline at end of file From f99e7cf9c7e6a50c68f1492649030fda46343fba Mon Sep 17 00:00:00 2001 From: Sam Erde Date: Mon, 26 May 2025 23:15:32 -0400 Subject: [PATCH 11/14] Bump version of megalinter flavor and allow on-demand run --- .github/workflows/mega-linter.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/mega-linter.yml b/.github/workflows/mega-linter.yml index 98eb6467..679bdeb9 100644 --- a/.github/workflows/mega-linter.yml +++ b/.github/workflows/mega-linter.yml @@ -13,7 +13,7 @@ on: # branches: # - main # - testing - # workflow_dispatch: + workflow_dispatch: concurrency: group: ${{ github.ref }}-${{ github.workflow }} @@ -45,7 +45,7 @@ jobs: # You can override MegaLinter flavor used to have faster performances # More info at https://megalinter.io/flavors/ # The dotnet flavor includes PowerShell, MD, YAML, JSON, spelling, and more. - uses: oxsecurity/megalinter/flavors/dotnet@v8.3.0 + uses: oxsecurity/megalinter/flavors/dotnet@v8.7.0 id: ml From 24a5075f33ead028d0780903f9f73f1b5c59c5d1 Mon Sep 17 00:00:00 2001 From: Sam Erde Date: Tue, 27 May 2025 09:42:48 -0400 Subject: [PATCH 12/14] Add ESC9 to README -Scans --- README.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 496d688f..0f9ad0bc 100644 --- a/README.md +++ b/README.md @@ -28,11 +28,11 @@ A small tool built to find and fix common misconfigurations in Active Directory 1. [Installation](#Installation) 2. [Run Locksmith](#Run Locksmith) - 1. [Mode 0](#Mode 0) - 2. [Mode 1](#Mode 1) - 3. [Mode 2](#Mode 2) - 4. [Mode 3](#Mode 3) - 5. [Mode 4](#Mode 4) + 1. [Mode 0](#Mode-0) + 2. [Mode 1](#Mode-1) + 3. [Mode 2](#Mode-2) + 4. [Mode 3](#Mode-3) + 5. [Mode 4](#Mode-4) 6. [Scans](#Scans) @@ -168,7 +168,7 @@ Example Output for Mode 4: ### Scans -Use the `-Scans` parameter to choose which vulnerabilities to scan for. Acceptable values include `All`, `Auditing`, `ESC1`, `ESC2`, `ESC3`, `ESC4`, `ESC5`, `ESC6`, `ESC7`, `ESC8`, `ESC11`, `ESC13`, `ESC15`, `EKEUwu`, `ESC16` or `PromptMe`. The `PromptMe` option presents an interactive list allowing you to select one or more scans. +Use the `-Scans` parameter to choose which vulnerabilities to scan for. Acceptable values include `All`, `Auditing`, `ESC1`, `ESC2`, `ESC3`, `ESC4`, `ESC5`, `ESC6`, `ESC7`, `ESC8`, `ESC9`, `ESC11`, `ESC13`, `ESC15`, `EKEUwu`, `ESC16` or `PromptMe`. The `PromptMe` option presents an interactive list allowing you to select one or more scans. ``` powershell # Run all scans From 2e81d285d9293431e2bca3bbb7bf84799f382790 Mon Sep 17 00:00:00 2001 From: Sam Erde Date: Tue, 27 May 2025 09:44:31 -0400 Subject: [PATCH 13/14] Replaced all 'Mandatory = $true' with just 'Mandatory' --- Invoke-Locksmith.ps1 | 56 ++++++++++++++-------------- Private/Get-CAHostObject.ps1 | 2 +- Private/Set-AdditionalCAProperty.ps1 | 2 +- Private/Write-HostColorized.ps1 | 6 +-- Tests/Compare-ADObjects.ps1 | 4 +- Tests/Show-ADObjectChanges.ps1 | 2 +- 6 files changed, 36 insertions(+), 36 deletions(-) diff --git a/Invoke-Locksmith.ps1 b/Invoke-Locksmith.ps1 index dbd8022e..64e6476c 100644 --- a/Invoke-Locksmith.ps1 +++ b/Invoke-Locksmith.ps1 @@ -2043,7 +2043,7 @@ function Get-CAHostObject { [CmdletBinding()] param ( [parameter( - Mandatory = $true, + Mandatory, ValueFromPipeline = $true)] [Microsoft.ActiveDirectory.Management.ADEntity[]]$ADCSObjects, [System.Management.Automation.PSCredential]$Credential, @@ -2053,20 +2053,20 @@ function Get-CAHostObject { if ($Credential) { $ADCSObjects | Where-Object objectClass -Match 'pKIEnrollmentService' | ForEach-Object { if ($_.CAHostDistinguishedName) { - Get-ADObject $_.CAHostDistinguishedName -Properties * -Server $ForestGC -Credential $Credential + Get-ADObject $_.CAHostDistinguishedName -Properties * -Server $ForestGC -Credential $Credential } else { - Write-Warning "Get-CAHostObject: Unable to get information from $($_.DisplayName)" + Write-Warning "Get-CAHostObject: Unable to get information from $($_.DisplayName)" } } } else { $ADCSObjects | Where-Object objectClass -Match 'pKIEnrollmentService' | ForEach-Object { if ($_.CAHostDistinguishedName) { - Get-ADObject -Identity $_.CAHostDistinguishedName -Properties * -Server $ForestGC + Get-ADObject -Identity $_.CAHostDistinguishedName -Properties * -Server $ForestGC } else { - Write-Warning "Get-CAHostObject: Unable to get information from $($_.DisplayName)" + Write-Warning "Get-CAHostObject: Unable to get information from $($_.DisplayName)" } } } @@ -3029,7 +3029,7 @@ function Set-AdditionalCAProperty { [CmdletBinding(SupportsShouldProcess)] param ( [parameter( - Mandatory = $true, + Mandatory, ValueFromPipeline = $true)] [Microsoft.ActiveDirectory.Management.ADEntity[]]$ADCSObjects, [PSCredential]$Credential, @@ -3134,10 +3134,10 @@ function Set-AdditionalCAProperty { $CAHostFQDN = (Get-ADObject -Filter { (Name -eq $CAHostName) -and (objectclass -eq 'computer') } -Properties DnsHostname -Server $ForestGC).DnsHostname } $ping = if ($CAHostFQDN) { - Test-Connection -ComputerName $CAHostFQDN -Count 1 -Quiet + Test-Connection -ComputerName $CAHostFQDN -Count 1 -Quiet } else { - Write-Warning "Unable to resolve $($_.Name) Fully Qualified Domain Name (FQDN)" + Write-Warning "Unable to resolve $($_.Name) Fully Qualified Domain Name (FQDN)" } if ($ping) { try { @@ -3727,23 +3727,23 @@ function Set-RiskRating { switch ($Issue.objectClass) { # Being able to modify Root CA Objects is very bad. 'certificationAuthority' { - $RiskValue += 2; $RiskScoring += 'Root Certification Authority bject: +2' + $RiskValue += 2; $RiskScoring += 'Root Certification Authority bject: +2' } # Being able to modify Issuing CA Objects is also very bad. 'pKIEnrollmentService' { - $RiskValue += 2; $RiskScoring += 'Issuing Certification Authority Object: +2' + $RiskValue += 2; $RiskScoring += 'Issuing Certification Authority Object: +2' } # Being able to modify CA Hosts? Yeah... very bad. 'computer' { - $RiskValue += 2; $RiskScoring += 'Certification Authority Host Computer: +2' + $RiskValue += 2; $RiskScoring += 'Certification Authority Host Computer: +2' } # Being able to modify OIDs could result in ESC13 vulns. 'msPKI-Enterprise-Oid' { - $RiskValue += 1; $RiskScoring += 'OID: +1' + $RiskValue += 1; $RiskScoring += 'OID: +1' } # Being able to modify PKS containers is bad. 'container' { - $RiskValue += 1; $RiskScoring += 'Container: +1' + $RiskValue += 1; $RiskScoring += 'Container: +1' } } } @@ -3764,19 +3764,19 @@ function Set-RiskRating { # Convert Value to Name $RiskName = switch ($RiskValue) { { $_ -le 1 } { - 'Informational' + 'Informational' } 2 { - 'Low' + 'Low' } 3 { - 'Medium' + 'Medium' } 4 { - 'High' + 'High' } { $_ -ge 5 } { - 'Critical' + 'Critical' } } @@ -4335,7 +4335,7 @@ Set-Acl -Path `$Path -AclObject `$ACL "@ } 4 { - break + break } 5 { $Issue.Fix = @" @@ -4670,15 +4670,15 @@ Function Write-HostColorized { # * At least for now, we remain PSv2-COMPATIBLE. # * Thus: # * no `[ordered]`, `::new()`, `[pscustomobject]`, ... - # * No implicit Boolean properties in [CmdletBinding()] and [Parameter()] attributes (`Mandatory = $true` instead of just `Mandatory`) + # * No implicit Boolean properties in [CmdletBinding()] and [Parameter()] attributes (`Mandatory` instead of just `Mandatory`) # === [CmdletBinding(DefaultParameterSetName = 'SingleColor')] param( - [Parameter(ParameterSetName = 'SingleColor', Position = 0, Mandatory = $True)] [string[]] $Pattern, + [Parameter(ParameterSetName = 'SingleColor', Position = 0, Mandatory)] [string[]] $Pattern, [Parameter(ParameterSetName = 'SingleColor', Position = 1)] [ConsoleColor] $ForegroundColor = [ConsoleColor]::Yellow, [Parameter(ParameterSetName = 'SingleColor', Position = 2)] [ConsoleColor] $BackgroundColor, - [Parameter(ParameterSetName = 'PerPatternColor', Position = 0, Mandatory = $True)] [System.Collections.IDictionary] $PatternColorMap, + [Parameter(ParameterSetName = 'PerPatternColor', Position = 0, Mandatory)] [System.Collections.IDictionary] $PatternColorMap, [Parameter(ValueFromPipeline = $True)] $InputObject, [switch] $WholeLine, [switch] $SimpleMatch, @@ -4706,10 +4706,10 @@ Function Write-HostColorized { # We precompile them for better performance with many input objects. [System.Text.RegularExpressions.RegexOptions] $reOpts = if ($CaseSensitive) { - 'Compiled, ExplicitCapture' + 'Compiled, ExplicitCapture' } else { - 'Compiled, ExplicitCapture, IgnoreCase' + 'Compiled, ExplicitCapture, IgnoreCase' } # Transform the dictionary: @@ -4731,10 +4731,10 @@ Function Write-HostColorized { } $colorArgs = @{ } if ($fg) { - $colorArgs['ForegroundColor'] = [ConsoleColor] $fg + $colorArgs['ForegroundColor'] = [ConsoleColor] $fg } if ($bg) { - $colorArgs['BackgroundColor'] = [ConsoleColor] $bg + $colorArgs['BackgroundColor'] = [ConsoleColor] $bg } # Consolidate the patterns into a single pattern with alternation ('|'), @@ -4753,7 +4753,7 @@ Function Write-HostColorized { } } catch { - throw + throw } # Construct the arguments to pass to Out-String. @@ -4776,7 +4776,7 @@ Function Write-HostColorized { foreach ($m in $entry.Key.Matches($_)) { @{ Index = $m.Index; Text = $m.Value; ColorArgs = $entry.Value } if ($WholeLine) { - break patternLoop + break patternLoop } } } diff --git a/Private/Get-CAHostObject.ps1 b/Private/Get-CAHostObject.ps1 index b435074e..079afaee 100644 --- a/Private/Get-CAHostObject.ps1 +++ b/Private/Get-CAHostObject.ps1 @@ -31,7 +31,7 @@ [CmdletBinding()] param ( [parameter( - Mandatory = $true, + Mandatory, ValueFromPipeline = $true)] [Microsoft.ActiveDirectory.Management.ADEntity[]]$ADCSObjects, [System.Management.Automation.PSCredential]$Credential, diff --git a/Private/Set-AdditionalCAProperty.ps1 b/Private/Set-AdditionalCAProperty.ps1 index 7ab8f2cb..4f872ced 100644 --- a/Private/Set-AdditionalCAProperty.ps1 +++ b/Private/Set-AdditionalCAProperty.ps1 @@ -30,7 +30,7 @@ [CmdletBinding(SupportsShouldProcess)] param ( [parameter( - Mandatory = $true, + Mandatory, ValueFromPipeline = $true)] [Microsoft.ActiveDirectory.Management.ADEntity[]]$ADCSObjects, [PSCredential]$Credential, diff --git a/Private/Write-HostColorized.ps1 b/Private/Write-HostColorized.ps1 index cd911a88..492defb0 100644 --- a/Private/Write-HostColorized.ps1 +++ b/Private/Write-HostColorized.ps1 @@ -97,15 +97,15 @@ Function Write-HostColorized { # * At least for now, we remain PSv2-COMPATIBLE. # * Thus: # * no `[ordered]`, `::new()`, `[pscustomobject]`, ... - # * No implicit Boolean properties in [CmdletBinding()] and [Parameter()] attributes (`Mandatory = $true` instead of just `Mandatory`) + # * No implicit Boolean properties in [CmdletBinding()] and [Parameter()] attributes (`Mandatory` instead of just `Mandatory`) # === [CmdletBinding(DefaultParameterSetName = 'SingleColor')] param( - [Parameter(ParameterSetName = 'SingleColor', Position = 0, Mandatory = $True)] [string[]] $Pattern, + [Parameter(ParameterSetName = 'SingleColor', Position = 0, Mandatory)] [string[]] $Pattern, [Parameter(ParameterSetName = 'SingleColor', Position = 1)] [ConsoleColor] $ForegroundColor = [ConsoleColor]::Yellow, [Parameter(ParameterSetName = 'SingleColor', Position = 2)] [ConsoleColor] $BackgroundColor, - [Parameter(ParameterSetName = 'PerPatternColor', Position = 0, Mandatory = $True)] [System.Collections.IDictionary] $PatternColorMap, + [Parameter(ParameterSetName = 'PerPatternColor', Position = 0, Mandatory)] [System.Collections.IDictionary] $PatternColorMap, [Parameter(ValueFromPipeline = $True)] $InputObject, [switch] $WholeLine, [switch] $SimpleMatch, diff --git a/Tests/Compare-ADObjects.ps1 b/Tests/Compare-ADObjects.ps1 index 256b7b4b..03b4699c 100644 --- a/Tests/Compare-ADObjects.ps1 +++ b/Tests/Compare-ADObjects.ps1 @@ -1,8 +1,8 @@ # Source: https://learn.microsoft.com/en-us/archive/blogs/janesays/compare-all-properties-of-two-objects-in-windows-powershell param( - [Parameter(Mandatory = $true)] + [Parameter(Mandatory)] [string]$DN1, - [Parameter(Mandatory = $true)] + [Parameter(Mandatory)] [string]$DN2 ) diff --git a/Tests/Show-ADObjectChanges.ps1 b/Tests/Show-ADObjectChanges.ps1 index d0424bfa..2fa3845e 100644 --- a/Tests/Show-ADObjectChanges.ps1 +++ b/Tests/Show-ADObjectChanges.ps1 @@ -1,6 +1,6 @@ # Source: https://learn.microsoft.com/en-us/archive/blogs/janesays/compare-all-properties-of-two-objects-in-windows-powershell param( - [Parameter(Mandatory = $true)] + [Parameter(Mandatory)] [string]$DN ) From d167204412cfb1d369d1e18f9306ceed7659dc2c Mon Sep 17 00:00:00 2001 From: Sam Erde Date: Tue, 27 May 2025 10:05:00 -0400 Subject: [PATCH 14/14] =?UTF-8?q?valid=20and=20theoretically=20over-compat?= =?UTF-8?q?ible=20syntax=20for=20TOC=20links=20to=20heading=20in=20Markdow?= =?UTF-8?q?n,=20HTML,=20and=20legacy=20HTML=20=F0=9F=98=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 0f9ad0bc..e17f1fbc 100644 --- a/README.md +++ b/README.md @@ -28,12 +28,12 @@ A small tool built to find and fix common misconfigurations in Active Directory 1. [Installation](#Installation) 2. [Run Locksmith](#Run Locksmith) - 1. [Mode 0](#Mode-0) - 2. [Mode 1](#Mode-1) - 3. [Mode 2](#Mode-2) - 4. [Mode 3](#Mode-3) - 5. [Mode 4](#Mode-4) - 6. [Scans](#Scans) + 1. [Mode 0](#mode-0-identify-issues-output-to-console-default) + 2. [Mode 1](#mode-1-identify-issues-and-fixes-output-to-console) + 3. [Mode 2](#mode-2-identify-issues-output-to-csv) + 4. [Mode 3](#mode-3-identify-issues-and-fixes-output-to-csv) + 5. [Mode 4](#mode-4-fix-all-issues) + 6. [Scans](#scans) ## Installation @@ -80,7 +80,7 @@ Install-Module -Name Locksmith -Scope CurrentUser There are several modes you can chose from when running `Invoke-Locksmith`. You can also use the **Scans** parameter to choose which scans you want to invoke. - + ### Mode 0: Identify Issues, Output to Console (Default) Running `Invoke-Locksmith.ps1` with no parameters or with `-Mode 0` will scan the current Active Directory forest and output all discovered AD CS issues to the console in **Table** format. @@ -97,7 +97,7 @@ Invoke-Locksmith Example Output for Mode 0: - + ### Mode 1: Identify Issues and Fixes, Output to Console This mode scans the current forest and outputs all discovered AD CS issues and possible fixes to the console in **List** format. @@ -114,7 +114,7 @@ Invoke-Locksmith -Mode 1 Example Output for Mode 1: - + ### Mode 2: Identify Issues, Output to CSV Locksmith Mode 2 scans the current forest and outputs all discovered AD CS issues to ADCSIssues.CSV in the present working directory. @@ -131,7 +131,7 @@ Invoke-Locksmith -Mode 2 Example Output for Mode 2: - + ### Mode 3: Identify Issues and Fixes, Output to CSV In Mode 3, Locksmith scans the current forest and outputs all discovered AD CS issues and example fixes to ADCSRemediation.CSV in the present working directory. @@ -148,7 +148,7 @@ Invoke-Locksmith -Mode 3 Example Output for Mode 3: - + ### Mode 4: Fix All Issues Mode 4 is the "easy button." Running Locksmith in Mode 4 will identify all misconfigurations and offer to fix each issue. If there is any possible operational impact, Locksmith will warn you.