Computer Audit with Powershell

Shaun VermaakCOG Lead Engineer
CERTIFIED EXPERT
My name is Shaun Vermaak and I have always been fascinated with technology and how we use it to enhance our lives and business.
Published:
Updated:
This article details my method of auditing computers by querying WMI class, serializing it to JSON and saving it is a central location, ready to be deserialized again and pulled into a report

Introduction


When auditing Windows computers the WMI classes are a goldmine of information. 


For example, if I wanted to audit computer processors, I could utilize the WIN32_Processor WMI class

https://docs.microsoft.com/en-us/windows/desktop/cimwin32prov/win32-processor


This script in this article allows you to easily add additional WMI classes which are populated during the audit process and saved as a JSON file. This information is readily available to write a report against by just deserializing these JSON files.


These JSON files are just a textual representation of the WMI object which, when deserialized, can be used as the original WMI object.


For more information regarding the JSON format, see https://www.w3schools.com/js/js_json_intro.asp


The Script (AuditComputer.ps1)


# Define a computer class to hold the audit information
class Computer
{
    [System.Object[]]$WMI
}

# Instantiate a instance of the computer class
$computer = [Computer]::New();

# Load WMI classes into the computer class. Add the ones you need
$computer.WMI += (Get-WMIObject Win32_OperatingSystem);
$computer.WMI += (Get-WMIObject Win32_Volume);
$computer.WMI += (Get-WMIObject Win32_ComputerSystem);
$computer.WMI += (Get-WMIObject Win32_Service);
$computer.WMI += (Get-WMIObject Win32_Processor);

# Serialize the class variable
ConvertTo-Json -Depth 10 $computer


Even though it is a deceptively simple script, it is extremely powerful. It allows you to pull all information, whether you are using it or not, to JSON format. This means that if you later need to generate a report of information that you didn't expect to use, you will already have it if you included the appropriate WMI class in the audit.


Execution


Below is an example auditing of all computers in a domain.


$Computers = Get-ADComputer -Filter *;
   
ForEach ($Computer in $Computers)
{
    Invoke-Command -computer $Computer.Name -FilePath .\AuditComputer.ps1 | Out-File -FilePath ".\$($Computer.Name)-$(get-date -f yyyyMMddHHmmss).json"
}


Below is an example of deserializing JSON files back to WMI class objects.


$fullpath = "SomeComputer-20190305182712.json";

$computerAudit      = Get-Content $fullpath | ConvertFrom-Json;
$computerSystem = $computerAudit.WMI | Where-Object {$_.__CLASS -eq "Win32_ComputerSystem"};
$operatingSystem = $latestComputerAudit.WMI | Where-Object {$_.__CLASS -eq "Win32_OperatingSystem"};
$drives = $latestComputerAudit.WMI | Where-Object {$_.__CLASS -eq "Win32_Volume" -and $_.DriveType -eq "3" -and $_.SystemVolume -ne "True"} | Sort("Caption");
$Processor = $latestComputerAudit.WMI | Where-Object {$_.__CLASS -eq "Win32_Processor"};
$W32TimeService = $latestComputerAudit.WMI | Where-Object {$_.__CLASS -eq "Win32_Service" -and $_.Name -eq "W32Time"};


Sample Report


Below is a sample audit script/report


$servicesToCheck = "Netlogon","NTDS","DNS","W32Time";
$subject = "Active Directory Report - Generated on $(Get-Date)";
$smtpsettings = @{
To =  "shaun.vermaak@somedomain.com"
From = "ADHealthCheck@somedomain.com"
Subject = $subject
SmtpServer = "smtpserver01"
}

$DomainControllers = Get-ADDomainController -Filter * | Sort("Name");

ForEach ($DomainController in $DomainControllers)
{
Invoke-Command -computer $DomainController.Name -FilePath .\AuditComputer.ps1 | Out-File ".\$($DomainController.Name)-$(get-date -f yyyyMMddHHmmss).json"
}

# Replication Status
$AllReplOut = repadmin /csv /showrepl *;

# $DomainControllers = Get-ADDomainController -Filter *;

$html = "<!DOCTYPE html><html><head><style>";
$html = $html + "table {border: 1px solid black; border-collapse: collapse;}";
$html = $html + "td, th {border: 1px solid black; padding: 15px;}";
$html = $html + "th {background: #DDDDDD}";
$html = $html + "td.good {background:#C6EFCE;color:#2C6153;}";
$html = $html + "td.bad {background:#FFC7CE;color:#AD0055;}";
$html = $html + "td.neutral {background:#FFEB9C;color:#ADA585;}";
$html = $html + "</style></head><body>";

$allServerFlags = $hash = @{};
$allServerFlags.Add("Schema Master","$($(Get-ADForest).SchemaMaster.ToUpper())");
$allServerFlags.Add("Domain Naming Master", "$($(Get-ADForest).SchemaMaster.ToUpper())");
$allServerFlags.Add("RID Master", "$($(Get-ADDomain).RIDMaster.ToUpper())");
$allServerFlags.Add("PDC Emulator", "$($(Get-ADDomain).PDCEmulator.ToUpper())");
$allServerFlags.Add("Infrastructure Master", "$($(Get-ADDomain).InfrastructureMaster.ToUpper())");

$html = $html + "<h2>$($subject)</h2>"

$computerSummary = "<table>";

$serviceTH = "";
foreach ($serviceToCheck in $servicesToCheck)
{
$serviceTH = "$($serviceTH)<th>$($serviceToCheck)</th>"
}

$computerSummary = $computerSummary + "<tr><th>Date</th><th>Name</th><th>Replication</th><th>Operating System</th><th>Build Number</th><th>Number of Logical Processors</th><th>Total Physical Memory</th><th>Drives</th>$($serviceTH)<th>DNS Test</th></tr>";      
foreach ($DomainController in $DomainControllers)
{
$fullpath = "$($DomainController.Name)-*.json"
$oldestFile = dir $fullpath | sort lastwritetime | select -First 1
$latestFile = dir $fullpath | sort lastwritetime | select -Last 1

$oldestComputerAudit      = Get-Content $oldestFile | ConvertFrom-Json
$latestComputerAudit      = Get-Content $latestFile | ConvertFrom-Json

$computerSystem           = $latestComputerAudit.WMI | Where-Object {$_.__CLASS -eq "Win32_ComputerSystem"};
$operatingSystem          = $latestComputerAudit.WMI | Where-Object {$_.__CLASS -eq "Win32_OperatingSystem"};
$oldestDrives             = $oldestComputerAudit.WMI | Where-Object {$_.__CLASS -eq "Win32_Volume" -and $_.DriveType -eq "3" -and $_.SystemVolume -ne "True"} | Sort("Caption");
$latestDrives             = $latestComputerAudit.WMI | Where-Object {$_.__CLASS -eq "Win32_Volume" -and $_.DriveType -eq "3" -and $_.SystemVolume -ne "True"} | Sort("Caption");
$Processor                = $latestComputerAudit.WMI | Where-Object {$_.__CLASS -eq "Win32_Processor"};

$serviceTD = "";
foreach ($serviceToCheck in $servicesToCheck)
{
$service = $latestComputerAudit.WMI | Where-Object {$_.__CLASS -eq "Win32_Service" -and $_.Name -eq "$($serviceToCheck)"}
if ($service.State -eq "Running")
{
$serviceTD = "$($serviceTD)<td class='good'>$($service.State)</td>";
}
else
{
$serviceTD = "$($serviceTD)<td class='bad'>$($service.State)</td>";
}
}

$computerFQDN             = "$($computerSystem.DNSHostName).$($computerSystem.Domain)".ToUpper();
$serverFlags              = $allServerFlags.GetEnumerator() | ? {$_.Value -eq "$($computerFQDN)"};
$serverFlagsJoined        = "";

$serverReplication        = ConvertFrom-Csv -InputObject $AllReplOut | where {$_.'Destination DSA' -eq $DomainController.Name -and $_.'Number of Failures' -ge 1};
if ($serverReplication -ne $null)
{
$serverReplicationSummary = $serverReplication | select "Source DSA", "Destination DSA", "Naming Context" ,"Number of Failures", "Last Failure Time", "Last Success Time", "Last Failure Status" | ConvertTo-Html;
}
else
{
$serverReplicationSummary = "No replication issues";
}

foreach ($serverFlag in $serverFlags)
{
$serverFlagsJoined = "$($serverFlagsJoined) ($($serverFlag.Name.Trim()))";
}

$dnsTest = (Test-DnsServer -IPAddress $DomainController.IPv4Address).Result;
$dnsTestSummary = "";
if ($dnsTest -eq "Success")
{
$dnsTestSummary = "<td class='good'>Success</td>";
}
else
{
$dnsTestSummary = "<td class='bad'>Failed</td>";
}

# Drive summary table
$driveSummary = "<table>";
$driveSummary = $driveSummary + "<tr><th>Drive</th><th>Capacity</th><th>Free</th><th>Growth</th></tr>";
foreach ($drive in $latestDrives)
{
$oldDrive = $oldestDrives | ? {$_.SerialNumber -eq $drive.SerialNumber};
$driveFreePercentage = $([math]::Round((($drive.FreeSpace/$drive.Capacity)*100),3));
$driveGrowthPercentage = $([math]::Round(((($drive.FreeSpace - $oldDrive.FreeSpace)/$drive.Capacity)*100),3));

$driveSummary = $DriveSummary + "<tr><td>$($drive.Caption)</td>";
$driveSummary = $DriveSummary + "<td>$([math]::Round($drive.Capacity/1GB,3)) GB</td>";

if ($driveFreePercentage -lt 16)
{
$driveSummary = $DriveSummary + "<td class='bad'>$([math]::Round($drive.FreeSpace/1GB,3)) GB ($($driveFreePercentage) %)</td>";
}
else
{
$driveSummary = $DriveSummary + "<td class='good'>$([math]::Round($drive.FreeSpace/1GB,3)) GB ($($driveFreePercentage) %)</td>";
}

if ($driveGrowthPercentage -gt 6)
{
$driveSummary = $DriveSummary + "<td class='bad'>$([math]::Round(($drive.FreeSpace - $oldDrive.FreeSpace)/1GB,3)) GB ($driveGrowthPercentage %)</td></tr>";
}
else
{
$driveSummary = $DriveSummary + "<td class='neutral'>$([math]::Round(($drive.FreeSpace - $oldDrive.FreeSpace)/1GB,3)) GB ($driveGrowthPercentage %)</td></tr>";
}
}
$driveSummary = $DriveSummary + "</table>";

$computerSummary = $computerSummary + "<tr><td>$((Get-Item $latestFile).LastWriteTime)</td><td>$($computerFQDN)$($serverFlagsJoined)</td><td>$($serverReplicationSummary)</td><td>$($operatingSystem.Caption)</td><td>$($operatingSystem.BuildNumber)</td><td>$($Processor.Count)</td><td>$([int]($computerSystem.TotalPhysicalMemory/1GB)) GB</td><td>$($driveSummary)</td>$($serviceTD)$($dnsTestSummary)</tr>";
}
$computerSummary = $computerSummary + "</table>";

$html = $html +  $computerSummary;

$html = $html + "</body></html>";
$html;

Send-MailMessage @smtpsettings -Body $html -BodyAsHtml -Encoding ([System.Text.Encoding]::UTF8)



Conclusion


I hope you found this tutorial useful. You are encouraged to ask questions, report any bugs or make any other comments about it below.

 

Note: If you need further "Support" about this topic, please consider using the Ask a Question feature of Experts Exchange. I monitor questions asked and would be pleased to provide any additional support required in questions asked in this manner, along with other EE experts...  

 

Please do not forget to press the "Thumbs Up" button if you think this article was helpful and valuable for EE members.


It also provides me with positive feedback. Thank you!

5
3,146 Views
Shaun VermaakCOG Lead Engineer
CERTIFIED EXPERT
My name is Shaun Vermaak and I have always been fascinated with technology and how we use it to enhance our lives and business.

Comments (4)

Albert WidjajaIT Professional
CERTIFIED EXPERT

Commented:
Thanks for sharing this great script Shaun.

However, when I execute it, I got this error from the AUDITCOMPUTER.PS1 for every Domain Controllers:

At line:20 char:1
+ class Computer {
+ ~~~~~
The 'class' keyword is not supported in this version of the language.
    + CategoryInfo          : ParserError: (:) [], ParseException
    + FullyQualifiedErrorId : ReservedKeywordNotAllowed
    + PSComputerName        : DC011-VM

Open in new window

Shaun VermaakCOG Lead Engineer
CERTIFIED EXPERT
Awarded 2017
Distinguished Expert 2019

Author

Commented:
You need Powershell 5 to use classes. Upgrade one and give it a go
Albert WidjajaIT Professional
CERTIFIED EXPERT

Commented:
Yes, I already have it on my workstation:

PS C:\> $PSVersionTable

Name                           Value
----                           -----
PSVersion                      5.1.17134.590
PSEdition                      Desktop
PSCompatibleVersions           {1.0, 2.0, 3.0, 4.0...}
BuildVersion                   10.0.17134.590
CLRVersion                     4.0.30319.42000
WSManStackVersion              3.0
PSRemotingProtocolVersion      2.3
SerializationVersion           1.1.0.1

Open in new window


is it because some of my Domain Controllers are on Windows Server 2012 R2?
Shaun VermaakCOG Lead Engineer
CERTIFIED EXPERT
Awarded 2017
Distinguished Expert 2019

Author

Commented:
It runs remotely so you need PS 5 on DC

Have a question about something in this article? You can receive help directly from the article author. Sign up for a free trial to get started.