├── .gitattributes ├── .gitignore ├── Create-TestLaps.ps1 ├── LAPS Audit Dashboard.pbix ├── LAPSParsingTask.xml ├── LAPSQuery.ps1 ├── LAPSQueryTask.xml ├── LAPSReporting.ps1 ├── LAPSServerSetup.ps1 ├── LAPSSubscription.xml └── README.md /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | 4 | # Custom for Visual Studio 5 | *.cs diff=csharp 6 | 7 | # Standard to msysgit 8 | *.doc diff=astextplain 9 | *.DOC diff=astextplain 10 | *.docx diff=astextplain 11 | *.DOCX diff=astextplain 12 | *.dot diff=astextplain 13 | *.DOT diff=astextplain 14 | *.pdf diff=astextplain 15 | *.PDF diff=astextplain 16 | *.rtf diff=astextplain 17 | *.RTF diff=astextplain 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Windows image file caches 2 | Thumbs.db 3 | ehthumbs.db 4 | 5 | # Folder config file 6 | Desktop.ini 7 | 8 | # Recycle Bin used on file shares 9 | $RECYCLE.BIN/ 10 | 11 | # Windows Installer files 12 | *.cab 13 | *.msi 14 | *.msm 15 | *.msp 16 | 17 | # Windows shortcuts 18 | *.lnk 19 | 20 | # ========================= 21 | # Operating System Files 22 | # ========================= 23 | 24 | # OSX 25 | # ========================= 26 | 27 | .DS_Store 28 | .AppleDouble 29 | .LSOverride 30 | 31 | # Thumbnails 32 | ._* 33 | 34 | # Files that might appear on external disk 35 | .Spotlight-V100 36 | .Trashes 37 | 38 | # Directories potentially created on remote AFP share 39 | .AppleDB 40 | .AppleDesktop 41 | Network Trash Folder 42 | Temporary Items 43 | .apdisk 44 | -------------------------------------------------------------------------------- /Create-TestLaps.ps1: -------------------------------------------------------------------------------- 1 | # Use to create a series of test OU's with computer objects, set the password attribute to something on a number of them and then perform a read on the password attributes 2 | # Useful for testing auditing as well as a querying of set passwords in an environment 3 | 4 | for($i=100; $i -le 120; $i++){ 5 | 6 | New-ADOrganizationalUnit -Path "dc=contoso,dc=com" -Name LAPS$i 7 | for($c=1; $c -le 100; $c++){ 8 | new-ADComputer -path "ou=laps$i,dc=contoso,dc=com" -Name LAP$i$c 9 | } 10 | for($b=1; $b -le 50; $b++){ 11 | {SET-ADcomputer LAPS$i$b –replace @{'ms-Mcs-AdmPwd'=”STUFF”} 12 | Get-ADComputer LAPS$i$b -Properties ms-Mcs-AdmPwd} 13 | } 14 | } 15 | 16 | <# 17 | Use the following to delete this test environment if needed 18 | 19 | for($i=100; $i -le 120; $i++){ 20 | 21 | Set-ADObject -ProtectedFromAccidentalDeletion $false -Identity "OU=LAPS$i,dc=contoso,dc=com" 22 | 23 | remove-ADOrganizationalUnit -Identity "OU=LAPS$i,dc=contoso,dc=com" -Recursive -Confirm:$false 24 | 25 | } 26 | #> -------------------------------------------------------------------------------- /LAPS Audit Dashboard.pbix: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kurtfalde/LAPS-Reporting/297ee01e65b534059c0e312c8510de721d4bf5d7/LAPS Audit Dashboard.pbix -------------------------------------------------------------------------------- /LAPSParsingTask.xml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kurtfalde/LAPS-Reporting/297ee01e65b534059c0e312c8510de721d4bf5d7/LAPSParsingTask.xml -------------------------------------------------------------------------------- /LAPSQuery.ps1: -------------------------------------------------------------------------------- 1 |  2 | <# 3 | 4 | Script to query various data points for the LAPS password attributes in Active Directory. 5 | By default this queries for all computers in the domain the script is ran from 6 | 7 | 8 | 9 | #> 10 | 11 | cd C:\laps 12 | del .\lapsquery.csv 13 | 14 | $computers = Get-ADComputer -Filter * -Properties Name,ms-Mcs-AdmPwdExpirationTime,msDS-ReplAttributeMetaData 15 | 16 | Foreach ($computer in $computers){ 17 | 18 | 19 | $CompObj = New-Object psobject 20 | 21 | $CompObj | Add-Member noteproperty Computername $computer.Name 22 | 23 | If($computer.'ms-Mcs-AdmPwdExpirationTime' -eq $null){$ExpirationDate = $computer.'ms-Mcs-AdmPwdExpirationTime'} 24 | Else {$ExpirationDate = [DateTime]::FromFileTime($computer.'ms-Mcs-AdmPwdExpirationTime')} 25 | $CompObj | Add-Member noteproperty NextPasswordExpiration $ExpirationDate 26 | 27 | 28 | $xmlAttribute = $computer."msDS-ReplAttributeMetaData" 29 | $xmlAttribute = “” + $xmlAttribute + “” 30 | $xmlAttribute = $xmlAttribute.Replace([char]0,” ”) 31 | $xmlattribute =[xml]$xmlattribute 32 | 33 | $OriginatingDCPwdLastSet = $null 34 | $OriginatingDCSiteLastSet = $null 35 | $LastTimePwdModified = $null 36 | 37 | foreach ($attribute in $xmlAttribute.root.DS_REPL_ATTR_META_DATA | Where-Object {$_.pszAttributeName -eq "ms-Mcs-AdmPwd"}){ 38 | 39 | $OriginatingDCPwdLastSet = ($attribute.pszLastOriginatingDsaDN.Split(','))[1] -replace '^cn=' 40 | $OriginatingDCSiteLastSet = ($attribute.pszLastOriginatingDsaDN.Split(','))[3] -replace '^cn=' 41 | $LastTimePwdModified = $attribute.ftimeLastOriginatingChange 42 | 43 | 44 | } 45 | 46 | $CompObj | Add-Member noteproperty OriginatingDCLastPwdSet $OriginatingDCPwdLastSet 47 | $CompObj | Add-Member noteproperty OriginatingDCSiteLastSet $OriginatingDCSiteLastSet 48 | $CompObj | Add-Member noteproperty LastTimePwdModified $LastTimePwdModified 49 | 50 | $ComputerOU = $computer.DistinguishedName -creplace "^[^,]*,","" 51 | $CompObj | Add-Member NoteProperty ComputerOU $ComputerOU 52 | 53 | $CompObj | Write-Host 54 | $compobj | Export-Csv .\lapsquery.csv -Append -NoTypeInformation 55 | 56 | } 57 | -------------------------------------------------------------------------------- /LAPSQueryTask.xml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kurtfalde/LAPS-Reporting/297ee01e65b534059c0e312c8510de721d4bf5d7/LAPSQueryTask.xml -------------------------------------------------------------------------------- /LAPSReporting.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | This Sample Code is provided for the purpose of illustration only and is not 3 | intended to be used in a production environment. THIS SAMPLE CODE AND ANY 4 | RELATED INFORMATION ARE PROVIDED "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER 5 | EXPRESSED OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE IMPLIED WARRANTIES OF 6 | MERCHANTABILITY AND/OR FITNESS FOR A PARTICULAR PURPOSE. We grant You a 7 | nonexclusive, royalty-free right to use and modify the Sample Code and to 8 | reproduce and distribute the object code form of the Sample Code, provided 9 | that You agree: 10 | (i) to not use Our name, logo, or trademarks to market Your 11 | software product in which the Sample Code is embedded; 12 | (ii) to include a valid copyright notice on Your software product 13 | in which the Sample Code is embedded; and 14 | (iii) to indemnify, hold harmless, and defend Us and Our suppliers 15 | from and against any claims or lawsuits, including attorneys’ 16 | fees, that arise or result from the use or distribution of 17 | the Sample Code. 18 | 19 | 20 | Originally written by Kurt Falde Using PSEventLogWatcher from http://pseventlogwatcher.codeplex.com/ 21 | New version for parsing LAPS Audit Log entries 9/20/2015 Kurt Falde MSFT Services 22 | 23 | #> 24 | 25 | 26 | 27 | $workingdir = "c:\laps" 28 | 29 | cd $workingdir 30 | 31 | #The following line was needed on a 2008 R2 setup at one customer It shouldn't hurt having it here however this can be removed in some cases 32 | Add-Type -AssemblyName System.Core 33 | 34 | #Following added to unregister any existing SLAMWatcher Event in case script has already been ran 35 | Unregister-Event LAPSWatcher -ErrorAction SilentlyContinue 36 | Remove-Job LAPSWatcher -ErrorAction SilentlyContinue 37 | 38 | 39 | Import-Module .\EventLogWatcher.psm1 40 | 41 | #Verify Bookmark currently exists prior to setting as start point 42 | $TestStream = $null 43 | $ECName = (((Get-Content .\bookmark.stream)[1]) -split "'")[1] 44 | $ERId = (((Get-Content .\bookmark.stream)[1]) -split "'")[3] 45 | $TestStream = Get-WinEvent -LogName $ECName -FilterXPath "*[System[(EventRecordID=$ERID)]]" 46 | If ($TestStream -eq $null) {Remove-Item .\bookmark.stream} 47 | 48 | $BookmarkToStartFrom = Get-BookmarkToStartFrom 49 | 50 | # The type of events that you want to parse.. in most cases we are parsing the Forwarded Events log with a specific xpath query for the events we want. 51 | 52 | $XpathQuery = "*[System[EventID=4662]] and *[EventData[Data[@Name='ObjectType'] and (Data='%{bf967a86-0de6-11d0-a285-00aa003049e2}')]]" 53 | $EventLogQuery = New-EventLogQuery "ForwardedEvents" -Query $XpathQuery 54 | 55 | 56 | $EventLogWatcher = New-EventLogWatcher $EventLogQuery $BookmarkToStartFrom 57 | 58 | $action = { 59 | 60 | #Following is debug line while developing as it will let you enter the nested prompt/session where you 61 | #can actually query the $EventRecord / $EventRecordXML etc for troubleshooting 62 | #$host.EnterNestedPrompt() 63 | $outfile = "c:\laps\laps.csv" 64 | 65 | 66 | $EventObj = New-Object psobject 67 | $EventObj | Add-Member noteproperty DomainController $EventRecord.MachineName 68 | $EventObj | Add-Member noteproperty User $EventRecordXml.SelectSingleNode("//*[@Name='SubjectUserName']")."#text" 69 | 70 | #Adding Date in Short Format to .csv object (Time is nice but it gets very messy when you start putting in Excel/PowerBI to aggregate down to hours/days etc) 71 | $EventObj | Add-Member noteproperty EventDate $EventRecord.TimeCreated.ToShortDateString() 72 | 73 | #Adding EventRecordID field to .csv object for a unique identifier 74 | $EventObj | Add-Member noteproperty EventRecordID $EventRecordXML.Event.System.EventRecordID 75 | 76 | #Adding Target Computer Object after resolving GUID to name 77 | $ComputerGUID = ($EventRecordXML.SelectSingleNode("//*[@Name='ObjectName']")."#text") -replace "%", "" -replace "{", "" -replace "}", "" 78 | $ComputerName = $null 79 | Try 80 | { 81 | $ComputerName = (get-adobject -id $ComputerGUID).Name 82 | } 83 | Catch 84 | { 85 | $ComputerName = $ComputerGUID 86 | } 87 | $EventObj | Add-Member noteproperty TargetComputer $ComputerName 88 | 89 | 90 | If ($Outfile -ne $Null) 91 | { 92 | write-host $EventObj 93 | $EventObj | Convertto-CSV -Outvariable OutData -NoTypeInformation 94 | 95 | $OutPath = $Outfile 96 | If (Test-Path $OutPath) 97 | { 98 | $Outdata[1..($Outdata.count - 1)] | ForEach-Object {Out-File -InputObject $_ $OutPath -append default} 99 | } else { 100 | Out-File -InputObject $Outdata $OutPath -Encoding default 101 | } 102 | } 103 | 104 | 105 | } 106 | 107 | Register-EventRecordWrittenEvent $EventLogWatcher -action $action -SourceIdentifier LAPSWatcher 108 | 109 | $EventLogWatcher.Enabled = $True -------------------------------------------------------------------------------- /LAPSServerSetup.ps1: -------------------------------------------------------------------------------- 1 | <# Setup script for Subscription Server for LAPS Audit Events and reporting 2 | 3 | Microsoft Services - Kurt Falde 4 | 5 | #> 6 | 7 | #Please change the $DCTOQUERY value to the DC that has been used for Audit Testing 8 | $DCTOQUERY = "ADDC" 9 | cd C:\LAPS 10 | 11 | Import-Module ServerManager 12 | If ((Get-WindowsFeature -Name RSAT-AD-PowerShell).Installed -eq $false){ 13 | write-host "This needs the AD Powershell commandlets to proceed please run Install-WindowsFeature -Name RSAT-AD_PowerShell" 14 | Exit 15 | } 16 | 17 | $rootdse = Get-ADRootDSE 18 | $Guids = Get-ADObject -SearchBase ($rootdse.SchemaNamingContext) -LDAPFilter "(schemaidguid=*)" -Properties lDAPDisplayName,schemaIDGUID 19 | ForEach ($Guid in $Guids){ 20 | If ($guid.lDAPDisplayName -Like "*ms-mcs-admpwd"){ 21 | $SGuid = ([System.GUID]$guid.SchemaIDGuid).Guid 22 | Write-host -ForegroundColor Cyan "The SchemaIDGuide for ms-mcs-admpwd in this forest is"([System.GUID]$guid.SchemaIDGuid) 23 | } 24 | } 25 | 26 | $xpath = "*[EventData[Data[@Name='AccessMask'] and (Data='0x100')]] and *[System[EventID=4662]] and *[EventData[Data[@Name='ObjectType'] and (Data='%{bf967a86-0de6-11d0-a285-00aa003049e2}')]]" 27 | $LAPSAuditEvents = Get-WinEvent -ComputerName $DCTOQUERY -LogName Security -FilterXPath "$xpath" -MaxEvents 200 28 | 29 | $xpathdata = @() 30 | :Outer Foreach($LAPSAuditevent in $LAPSAuditEvents){ 31 | [xml]$LAPSAuditEventXML = $LAPSAuditEvent.ToXml() 32 | $EventXMLNames = $LAPSAuditEventXML.Event.EventData.Data 33 | 34 | #loop through the "Properties" XML Node looking for Events that contain the LAPS ms-mscadmpwd SchemaIDGuid 35 | #Add all hits to a new Array $xpathdata 36 | 37 | Foreach ($EventXMLName in $EventXMLNames){ 38 | If (($EventXMLName.name -eq "Properties") -and ($EventXMLName.'#text' -match "$SGuid")){ 39 | $LAPSProp = $EventXMLName.'#text' 40 | If ($xpathdata.count -eq 0){$xpathdata += $LAPSProp} 41 | ElseIf (($LAPSProp | Select-String -AllMatches $xpathdata) -eq $null){$xpathdata += $LAPSProp } 42 | } 43 | } 44 | } 45 | 46 | #Parsing through the $xpathdata array to create the right text needed for the xpath filter 47 | $xpathguid = $null 48 | If ($xpathdata.Count -le 1) { 49 | Write-host -ForegroundColor Cyan "Either a Single or No Xpath Filters created are you sure you tested LAPS UI and using ADUC at a minium on the DC you are testing? That or change the get-winevent line to a larger number of events" 50 | $xpathguid = "(Data='$xpathdata')" 51 | } 52 | 53 | ElseIf ($xpathdata.Count -gt 1) { 54 | Write-host -ForegroundColor Green "More than a single xpath filter found creating full xpath query output" 55 | Foreach ($xpaththing in $xpathdata){ 56 | If ($xpaththing -ne $xpathdata[-1]){ 57 | $xpathguid += "(Data='$xpaththing') or " 58 | } 59 | Else{$xpathguid += "(Data='$xpaththing')"} 60 | } 61 | } 62 | 63 | #FYI by default we are filtering out local queries to this from the "SYSTEM" account on a DC as this appears to happen with a DC reading it's own account in AD and creates noise 64 | #$xpathlaps = "" 65 | #$xpathlaps = $xpathlaps.Replace("`n", " ").Replace("`t", " ") 66 | 67 | #(gc C:\laps\LAPSSubscription.xml).replace('CHANGETHIS', $xpathlaps) | sc C:\laps\LAPSSubscription.xml -Encoding UTF8 68 | 69 | #Setting WinRM Service to automatic start and running quickconfig 70 | Set-Service -Name winrm -StartupType Automatic 71 | winrm quickconfig -quiet 72 | 73 | #Set the size of the forwarded events log to 500MB 74 | wevtutil sl forwardedevents /ms:500000000 75 | 76 | 77 | #Running quickconfig for subscription service 78 | wecutil qc -quiet 79 | 80 | #Creating Applocker Subscription from XML files FYI we do delete any existing ones and recreate 81 | If ((wecutil gs "LAPS Audit Events") -ne $NULL) { 82 | wecutil ds "LAPS Audit Events" 83 | wecutil cs .\LAPSSubscription.xml 84 | } 85 | Else {wecutil cs .\LAPSSubscription.xml} 86 | 87 | #Fix up the query in the registry with the proper text with carriage returns replaced.. carriage returns in the xml file imported via wecutil do not make it to the registry key 88 | $xpathguid = $xpathguid.Replace("`n","`r`n") 89 | $xpathlaps = "" 90 | Set-ItemProperty "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\EventCollector\Subscriptions\LAPS Audit Events" -Name "Query" -Value $xpathlaps 91 | wecutil rs "LAPS Audit Events" 92 | 93 | 94 | #FYI if you need to export Subscriptions to fix SIDS or anything in an environment 95 | #use wecutil gs "%subscriptionname%" /f:xml >>"C:\Temp\%subscriptionname%.xml" 96 | 97 | #Creating Task Scheduler Item to restart LAPS parsing script on reboot of system. 98 | schtasks.exe /delete /tn "LAPS Parsing Task" /F 99 | schtasks.exe /create /tn "LAPS Parsing Task" /xml LAPSParsingTask.xml 100 | schtasks.exe /run /tn "LAPS Parsing Task" 101 | 102 | #Creating Task Scheduler Item to run LAPS query script on a daily basis. 103 | schtasks.exe /delete /tn "LAPS Query Task" /F 104 | schtasks.exe /create /tn "LAPS Query Task" /xml LAPSQueryTask.xml 105 | schtasks.exe /run /tn "LAPS Query Task" -------------------------------------------------------------------------------- /LAPSSubscription.xml: -------------------------------------------------------------------------------- 1 |  2 | LAPS Audit Events 3 | SourceInitiated 4 | LAPS Audit Events 5 | true 6 | http://schemas.microsoft.com/wbem/wsman/1/windows/EventLog 7 | 8 | 9 | Custom 10 | 11 | 12 | 13 | 1 14 | 1000 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | true 26 | http 27 | RenderedText 28 | 29 | ForwardedEvents 30 | 31 | O:NSG:BAD:P(A;;GA;;;DD)S: 32 | 33 | 34 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # LAPS-Reporting 2 | Solution for Auditing LAPS usage in an Active Directory environment. 3 | 4 | Please see http://blogs.technet.com/b/kfalde/archive/2015/11/18/laps-audit-reporting-via-wef-posh-and-powerbi.aspx for 5 | an overview of how to configure this project to gather LAPS Local Administrator Password Solution data from an Active Directory 6 | environment. 7 | --------------------------------------------------------------------------------