Skip to content

Lab 04 — Windows Security Automation with PowerShell

Difficulty: Intermediate
Estimated Time: 120–150 minutes
Primary Language: PowerShell
Security Domain: Windows Security / SOC / Endpoint Security / System Hardening
Environment: Windows lab system
Automation Type: Defensive Security Assessment

Your task is to build a PowerShell-based Windows security assessment tool for a Windows system that you own or are explicitly authorized to administer.

The script will automatically collect:

SYSTEM INFORMATION
CURRENT USER CONTEXT
LOCAL USERS
LOCAL ADMINISTRATORS
RUNNING PROCESSES
WINDOWS SERVICES
NETWORK CONFIGURATION
LISTENING CONNECTIONS
WINDOWS EVENT LOGS
FAILED LOGINS
SUCCESSFUL LOGINS
MICROSOFT DEFENDER STATUS
WINDOWS FIREWALL STATUS
SCHEDULED TASKS
SELECTED SECURITY SETTINGS

The final workflow will be:

WINDOWS HOST
POWERSHELL
COLLECT
FILTER
NORMALIZE
ANALYZE
EXPORT
SECURITY REPORT

Windows remains one of the most common enterprise operating systems.

Security professionals regularly investigate:

WORKSTATIONS
DOMAIN MEMBERS
APPLICATION SERVERS
ADMINISTRATIVE SERVERS
JUMP HOSTS
SECURITY WORKSTATIONS
CLOUD WINDOWS VMs

During an assessment or incident, you often need answers such as:

Which Windows version is running?
Who is currently logged in?
Which local accounts exist?
Who belongs to Administrators?
Which processes are running?
Which services are active?
Which ports are listening?
What failed authentication events occurred?
Is Defender running?
Are firewall profiles enabled?
Which scheduled tasks exist?

PowerShell makes these questions highly automatable.

By completing this lab, you should be able to:

BUILD A POWERSHELL SCRIPT
USE FUNCTIONS
USE PARAMETERS
USE OBJECT PIPELINES
QUERY CIM
QUERY LOCAL USERS
QUERY LOCAL GROUPS
QUERY PROCESSES
QUERY SERVICES
QUERY TCP CONNECTIONS
QUERY WINDOWS EVENT LOGS
ANALYZE AUTHENTICATION EVENTS
CHECK DEFENDER
CHECK FIREWALL
CHECK SCHEDULED TASKS
EXPORT CSV
EXPORT JSON
GENERATE HTML REPORTS
IMPLEMENT LOGGING
HANDLE ERRORS
BUILD REUSABLE WINDOWS SECURITY AUTOMATION
WINDOWS HOST
┌─────────────┐
│ POWERSHELL │
└──────┬──────┘
┌─────────────┼─────────────┐
↓ ↓ ↓
USERS PROCESSES SERVICES
↓ ↓ ↓
LOCAL ADMINS NETWORK EVENT LOGS
↓ ↓ ↓
└─────────────┼─────────────┘
DEFENDER / FIREWALL
SECURITY DATA
┌───────────┼───────────┐
↓ ↓ ↓
CSV JSON HTML

Use this lab only on:

YOUR OWN WINDOWS SYSTEM
WINDOWS TRAINING VM
AUTHORIZED ENTERPRISE SYSTEM
CONTROLLED SECURITY LAB

The lab focuses on:

READ-ONLY INVENTORY
LOG ANALYSIS
SECURITY CONFIGURATION REVIEW
REPORTING

Do not disable:

MICROSOFT DEFENDER
WINDOWS FIREWALL
AUDITING
LOGGING
SECURITY CONTROLS

as part of this lab.

Recommended systems:

Windows 10
Windows 11
Windows Server 2019+
Windows Server 2022+
Windows Server 2025

Use:

Windows PowerShell 5.1

or preferably:

PowerShell 7+

where your cmdlets support it.

Check:

Terminal window
$PSVersionTable

Review:

PSVersion
PSEdition
OS
Platform

Create:

Terminal window
New-Item `
-ItemType Directory `
-Path "C:\Labs\WindowsSecurityAutomation" `
-Force

Enter:

Terminal window
Set-Location "C:\Labs\WindowsSecurityAutomation"

Create:

Terminal window
New-Item `
-ItemType Directory `
-Path ".\Reports" `
-Force
New-Item `
-ItemType Directory `
-Path ".\Logs" `
-Force
New-Item `
-ItemType Directory `
-Path ".\Src" `
-Force

Expected:

WindowsSecurityAutomation
|
+-- Logs
|
+-- Reports
|
+-- Src

Create:

Src\Windows-Security-Audit.ps1

Open it in:

VS Code
PowerShell ISE
Notepad

Start with:

Terminal window
<#
.SYNOPSIS
Windows Security Assessment Lab
.DESCRIPTION
Collects defensive Windows security
inventory and generates local reports.
.NOTES
Use only on systems you own or are
explicitly authorized to administer.
#>

Add:

Terminal window
$ScriptVersion = "1.0.0"

Add:

Terminal window
Set-StrictMode -Version Latest

This helps detect:

UNDEFINED VARIABLES
PROPERTY ERRORS
SOME SCRIPTING MISTAKES

Add:

Terminal window
$ErrorActionPreference = "Stop"

For individual commands where failure is expected or non-critical, you can override this carefully.

Add:

Terminal window
$BaseDirectory = Split-Path `
-Parent `
(Split-Path -Parent $PSCommandPath)
$ReportDirectory = Join-Path `
$BaseDirectory `
"Reports"
$LogDirectory = Join-Path `
$BaseDirectory `
"Logs"

Add:

Terminal window
New-Item `
-ItemType Directory `
-Path $ReportDirectory `
-Force `
| Out-Null
New-Item `
-ItemType Directory `
-Path $LogDirectory `
-Force `
| Out-Null

Add:

Terminal window
$RunTimestamp = Get-Date `
-Format "yyyyMMdd-HHmmss"
$AssessmentId = "WIN-$RunTimestamp"

Add:

Terminal window
$LogFile = Join-Path `
$LogDirectory `
"windows-security-audit-$RunTimestamp.log"
$JsonReport = Join-Path `
$ReportDirectory `
"windows-security-report-$RunTimestamp.json"
$HtmlReport = Join-Path `
$ReportDirectory `
"windows-security-report-$RunTimestamp.html"

Add:

Terminal window
function Write-AuditLog {
param(
[Parameter(Mandatory)]
[string]$Message,
[ValidateSet(
"INFO",
"WARNING",
"ERROR"
)]
[string]$Level = "INFO"
)
$Timestamp = Get-Date `
-Format "yyyy-MM-ddTHH:mm:ssK"
$Line = "$Timestamp [$Level] $Message"
Write-Host $Line
Add-Content `
-Path $LogFile `
-Value $Line
}

Temporarily:

Terminal window
Write-AuditLog `
-Message "Windows security audit test"

Run:

Terminal window
.\Src\Windows-Security-Audit.ps1

Confirm a log file appears.

Then remove the temporary test line.

Security scripts should not completely fail because one optional cmdlet is unavailable.

Create:

Terminal window
function Invoke-SafeCollection {
param(
[Parameter(Mandatory)]
[string]$Name,
[Parameter(Mandatory)]
[scriptblock]$ScriptBlock
)
try {
Write-AuditLog `
-Message "Collecting: $Name"
& $ScriptBlock
}
catch {
Write-AuditLog `
-Level "WARNING" `
-Message (
"Unable to collect $Name. " +
$_.Exception.Message
)
return $null
}
}

Create:

Terminal window
function Get-SystemSecurityInformation {
$OperatingSystem = Get-CimInstance `
-ClassName Win32_OperatingSystem
$ComputerSystem = Get-CimInstance `
-ClassName Win32_ComputerSystem
$Bios = Get-CimInstance `
-ClassName Win32_BIOS
[PSCustomObject]@{
ComputerName = $env:COMPUTERNAME
Caption = $OperatingSystem.Caption
Version = $OperatingSystem.Version
BuildNumber = $OperatingSystem.BuildNumber
Architecture = $OperatingSystem.OSArchitecture
Manufacturer = $ComputerSystem.Manufacturer
Model = $ComputerSystem.Model
Domain = $ComputerSystem.Domain
LastBootTime = $OperatingSystem.LastBootUpTime
BiosVersion = $Bios.SMBIOSBIOSVersion
}
}

Run:

Terminal window
Get-SystemSecurityInformation |
Format-List

Always determine:

COMPUTER NAME
WINDOWS EDITION
BUILD NUMBER
DOMAIN MEMBERSHIP
BOOT TIME
HARDWARE CONTEXT

before interpreting security events.

Create:

Terminal window
function Get-CurrentIdentityContext {
$Identity = [System.Security.Principal.WindowsIdentity]::GetCurrent()
$Principal = New-Object `
System.Security.Principal.WindowsPrincipal(
$Identity
)
[PSCustomObject]@{
User = $Identity.Name
AuthenticationType = $Identity.AuthenticationType
IsAuthenticated = $Identity.IsAuthenticated
IsAdministrator = $Principal.IsInRole(
[System.Security.Principal.WindowsBuiltInRole]::Administrator
)
}
}

Ask:

WHO IS RUNNING THE SCRIPT?
IS THE SESSION ELEVATED?
WHICH DATA MAY BE INACCESSIBLE?

Create:

Terminal window
function Get-LocalUserInventory {
if (
Get-Command `
Get-LocalUser `
-ErrorAction SilentlyContinue
) {
Get-LocalUser |
Select-Object `
Name,
Enabled,
LastLogon,
PasswordRequired,
PasswordExpires,
UserMayChangePassword
}
else {
Get-CimInstance `
-ClassName Win32_UserAccount `
-Filter "LocalAccount=True" |
Select-Object `
Name,
Disabled,
Lockout,
PasswordRequired
}
}

22 — Security Questions for Local Accounts

Section titled “22 — Security Questions for Local Accounts”

Review:

IS THE ACCOUNT REQUIRED?
IS IT ENABLED?
WHEN WAS IT LAST USED?
IS IT A SERVICE ACCOUNT?
DOES IT HAVE ADMIN ACCESS?
WHO OWNS IT?

Create:

Terminal window
function Get-LocalAdministratorInventory {
if (
Get-Command `
Get-LocalGroupMember `
-ErrorAction SilentlyContinue
) {
Get-LocalGroupMember `
-Group "Administrators" |
Select-Object `
Name,
ObjectClass,
PrincipalSource
}
else {
Get-CimInstance `
-ClassName Win32_GroupUser |
Where-Object {
$_.GroupComponent -match `
'Name="Administrators"'
}
}
}

Local administrative access provides powerful control over a system.

Every administrator should have:

BUSINESS REQUIREMENT
DOCUMENTED OWNER
APPROPRIATE APPROVAL
REGULAR REVIEW

Later you will use:

Terminal window
$LocalUsers |
Export-Csv `
-Path (
Join-Path `
$ReportDirectory `
"local-users-$RunTimestamp.csv"
) `
-NoTypeInformation
Terminal window
$LocalAdministrators |
Export-Csv `
-Path (
Join-Path `
$ReportDirectory `
"local-admins-$RunTimestamp.csv"
) `
-NoTypeInformation

Create:

Terminal window
function Get-ProcessInventory {
Get-Process |
Select-Object `
Id,
ProcessName,
CPU,
WorkingSet64,
Path `
| Sort-Object `
WorkingSet64 `
-Descending
}

For some processes:

Path

may be inaccessible unless appropriate privileges are available.

This should not cause you to conclude:

NO PATH
=
MALICIOUS PROCESS

Ask:

IS THE PROCESS EXPECTED?
WHO INSTALLED IT?
WHERE DOES IT RUN FROM?
IS IT SIGNED?
WHAT ACCOUNT RUNS IT?
DOES IT HAVE NETWORK CONNECTIONS?

For richer context:

Terminal window
Get-CimInstance `
-ClassName Win32_Process |
Select-Object `
ProcessId,
Name,
ExecutablePath,
CommandLine

Command lines can contain:

TOKENS
PASSWORDS
CONNECTION STRINGS
API KEYS

Do not unnecessarily publish or centrally store full command-line data.

Create:

Terminal window
function Get-ServiceInventory {
Get-CimInstance `
-ClassName Win32_Service |
Select-Object `
Name,
DisplayName,
State,
StartMode,
StartName,
PathName
}

The field:

StartName

indicates the identity under which the service runs.

Pay special attention to:

LOCAL SYSTEM
PRIVILEGED DOMAIN ACCOUNTS
CUSTOM SERVICE ACCOUNTS

Ask:

IS THE SERVICE REQUIRED?
WHO OWNS IT?
WHAT ACCOUNT RUNS IT?
WHERE IS THE EXECUTABLE?
DOES IT START AUTOMATICALLY?
IS THE SERVICE PATCHED?

Example:

Terminal window
Get-Service |
Where-Object {
$_.Status -eq "Running"
} |
Sort-Object DisplayName

Create:

Terminal window
function Get-NetworkConfiguration {
if (
Get-Command `
Get-NetIPConfiguration `
-ErrorAction SilentlyContinue
) {
Get-NetIPConfiguration |
Select-Object `
InterfaceAlias,
InterfaceDescription,
IPv4Address,
IPv6Address,
IPv4DefaultGateway,
DNSServer
}
else {
Get-CimInstance `
Win32_NetworkAdapterConfiguration |
Where-Object {
$_.IPEnabled
} |
Select-Object `
Description,
IPAddress,
DefaultIPGateway,
DNSServerSearchOrder
}
}

Create:

Terminal window
function Get-TcpConnectionInventory {
if (
Get-Command `
Get-NetTCPConnection `
-ErrorAction SilentlyContinue
) {
Get-NetTCPConnection |
Select-Object `
LocalAddress,
LocalPort,
RemoteAddress,
RemotePort,
State,
OwningProcess
}
else {
netstat -ano
}
}

For systems supporting:

Get-NetTCPConnection

use:

Terminal window
Get-NetTCPConnection `
-State Listen |
Select-Object `
LocalAddress,
LocalPort,
OwningProcess

Example:

Terminal window
Get-NetTCPConnection `
-State Listen |
ForEach-Object {
$Process = Get-Process `
-Id $_.OwningProcess `
-ErrorAction SilentlyContinue
[PSCustomObject]@{
Address = $_.LocalAddress
Port = $_.LocalPort
ProcessId = $_.OwningProcess
Process = $Process.ProcessName
}
}

A port is much more useful when correlated with:

PROCESS
SERVICE
BIND ADDRESS
FIREWALL POLICY
BUSINESS PURPOSE

For each listener:

WHY IS THIS PORT OPEN?
IS IT EXPECTED?
IS IT BOUND TO LOCALHOST?
IS IT BOUND TO ALL INTERFACES?
WHICH PROCESS OWNS IT?
IS THE SERVICE AUTHENTICATED?
IS THE FIREWALL RESTRICTING ACCESS?

Windows records many security events in:

Security
System
Application
PowerShell
Microsoft-Windows-Windows Defender/Operational

Start with:

Terminal window
Get-WinEvent `
-LogName Security `
-MaxEvents 10

Useful Windows Security Event IDs include:

4624
Successful Logon
4625
Failed Logon
4634
Logoff
4648
Explicit Credentials Used
4672
Special Privileges Assigned
4740
Account Locked Out

Examples:

4720
User Account Created
4726
User Account Deleted
4732
Member Added to Local Security Group
4733
Member Removed from Local Security Group

Examples include:

4698
Scheduled Task Created
4702
Scheduled Task Updated

Interpret availability according to your audit policy.

Create:

Terminal window
function Get-FailedLoginEvents {
Get-WinEvent `
-FilterHashtable @{
LogName = "Security"
Id = 4625
} `
-MaxEvents 100 `
-ErrorAction Stop |
Select-Object `
TimeCreated,
Id,
MachineName,
Message
}
Terminal window
$FailedLogins |
Export-Csv `
-Path (
Join-Path `
$ReportDirectory `
"failed-logins-$RunTimestamp.csv"
) `
-NoTypeInformation

The:

Message

field is human-readable but not ideal for structured analytics.

A more advanced workflow should parse:

XML EVENT DATA

instead.

Example:

Terminal window
$Event = Get-WinEvent `
-FilterHashtable @{
LogName = "Security"
Id = 4625
} `
-MaxEvents 1
$Event.ToXml()

Structured event data allows you to extract:

TARGET USER
SOURCE IP
LOGON TYPE
WORKSTATION
STATUS

without depending on localized human-readable messages.

Create:

Terminal window
function Convert-WinEventToData {
param(
[Parameter(Mandatory)]
$Event
)
$Xml = [xml]$Event.ToXml()
$Data = @{}
foreach (
$Item in $Xml.Event.EventData.Data
) {
$Data[
[string]$Item.Name
] = [string]$Item.'#text'
}
return $Data
}

Create:

Terminal window
function Get-StructuredFailedLogins {
$Events = Get-WinEvent `
-FilterHashtable @{
LogName = "Security"
Id = 4625
} `
-MaxEvents 100
foreach ($Event in $Events) {
$Data = Convert-WinEventToData `
-Event $Event
[PSCustomObject]@{
TimeCreated = $Event.TimeCreated
User = $Data["TargetUserName"]
Domain = $Data["TargetDomainName"]
SourceIp = $Data["IpAddress"]
SourcePort = $Data["IpPort"]
Workstation = $Data["WorkstationName"]
LogonType = $Data["LogonType"]
Status = $Data["Status"]
SubStatus = $Data["SubStatus"]
}
}
}

Now you can perform:

COUNT BY USER
COUNT BY SOURCE IP
COUNT BY LOGON TYPE
TIME-BASED ANALYSIS

without manually reading event messages.

Example:

Terminal window
$StructuredFailedLogins |
Group-Object User |
Sort-Object Count -Descending |
Select-Object `
Count,
Name
Terminal window
$StructuredFailedLogins |
Where-Object {
$_.SourceIp -and
$_.SourceIp -ne "-"
} |
Group-Object SourceIp |
Sort-Object Count -Descending |
Select-Object `
Count,
Name

Repeated failures may result from:

BAD PASSWORD
STALE CREDENTIAL
SERVICE MISCONFIGURATION
USER ERROR
VPN ISSUE
AUTOMATED SYSTEM
ATTACK ACTIVITY

Automation identifies the:

PATTERN

not the final cause.

Create:

Terminal window
function Get-SuccessfulLoginEvents {
Get-WinEvent `
-FilterHashtable @{
LogName = "Security"
Id = 4624
} `
-MaxEvents 100 `
-ErrorAction Stop |
Select-Object `
TimeCreated,
Id,
MachineName,
Message
}

Important examples include:

2
Interactive
3
Network
10
Remote Interactive

There are additional logon types.

Always interpret them in context.

Create:

Terminal window
function Get-AccountLockoutEvents {
Get-WinEvent `
-FilterHashtable @{
LogName = "Security"
Id = 4740
} `
-MaxEvents 50 `
-ErrorAction Stop |
Select-Object `
TimeCreated,
Id,
Message
}

Create:

Terminal window
function Get-PrivilegedLogonEvents {
Get-WinEvent `
-FilterHashtable @{
LogName = "Security"
Id = 4672
} `
-MaxEvents 50 `
-ErrorAction Stop |
Select-Object `
TimeCreated,
Id,
Message
}

Create:

Terminal window
function Get-AccountCreationEvents {
Get-WinEvent `
-FilterHashtable @{
LogName = "Security"
Id = 4720
} `
-MaxEvents 50 `
-ErrorAction Stop |
Select-Object `
TimeCreated,
Message
}

Create:

Terminal window
function Get-GroupChangeEvents {
Get-WinEvent `
-FilterHashtable @{
LogName = "Security"
Id = 4732,4733
} `
-MaxEvents 100 `
-ErrorAction Stop |
Select-Object `
TimeCreated,
Id,
Message
}

You may correlate:

ACCOUNT CREATED
ADDED TO ADMINISTRATORS
PRIVILEGED LOGON

This sequence can deserve investigation.

Do not automatically conclude malicious activity without context.

Create:

Terminal window
function Get-DefenderStatus {
if (
Get-Command `
Get-MpComputerStatus `
-ErrorAction SilentlyContinue
) {
Get-MpComputerStatus |
Select-Object `
AMServiceEnabled,
AntivirusEnabled,
AntispywareEnabled,
BehaviorMonitorEnabled,
IoavProtectionEnabled,
NISEnabled,
RealTimeProtectionEnabled,
AntivirusSignatureLastUpdated,
QuickScanAge,
FullScanAge
}
else {
[PSCustomObject]@{
Status = "Get-MpComputerStatus unavailable"
}
}
}

Look for:

ANTIVIRUS ENABLED
REAL-TIME PROTECTION
BEHAVIOR MONITORING
SIGNATURE UPDATE TIME

Do not run commands that disable:

REAL-TIME PROTECTION
ANTIVIRUS
BEHAVIOR MONITORING

The purpose of this lab is:

SECURITY VISIBILITY

not control bypass.

Create:

Terminal window
function Get-FirewallStatus {
if (
Get-Command `
Get-NetFirewallProfile `
-ErrorAction SilentlyContinue
) {
Get-NetFirewallProfile |
Select-Object `
Name,
Enabled,
DefaultInboundAction,
DefaultOutboundAction,
LogAllowed,
LogBlocked,
LogFileName
}
else {
[PSCustomObject]@{
Status = "Get-NetFirewallProfile unavailable"
}
}
}

Typical profiles:

DOMAIN
PRIVATE
PUBLIC

Ask:

IS EACH PROFILE ENABLED?
WHAT IS THE DEFAULT INBOUND POLICY?
IS BLOCKED TRAFFIC LOGGED?

70 — Do Not Automatically Change Firewall Rules

Section titled “70 — Do Not Automatically Change Firewall Rules”

This lab performs:

READ
ASSESS
REPORT

not:

AUTOMATIC FIREWALL MODIFICATION

Create:

Terminal window
function Get-ScheduledTaskInventory {
if (
Get-Command `
Get-ScheduledTask `
-ErrorAction SilentlyContinue
) {
Get-ScheduledTask |
Select-Object `
TaskPath,
TaskName,
State,
Author
}
else {
schtasks.exe /Query /FO CSV /V
}
}

Scheduled tasks may be used for legitimate:

MAINTENANCE
BACKUPS
SOFTWARE UPDATES
MONITORING

Every task should still have understandable:

OWNER
PURPOSE
COMMAND
TRIGGER
EXECUTION ACCOUNT

During security review, ask:

DOES A HIGH-PRIVILEGE SERVICE
EXECUTE A FILE FROM
A USER-WRITABLE LOCATION?

Do not attempt to exploit such a condition in this defensive lab.

Document it for authorized remediation review.

PowerShell itself has useful event logs.

Check:

Terminal window
Get-WinEvent `
-ListLog "*PowerShell*" |
Select-Object `
LogName,
RecordCount,
IsEnabled

If available:

Terminal window
Get-WinEvent `
-LogName `
"Microsoft-Windows-PowerShell/Operational" `
-MaxEvents 50

It may provide visibility into:

SCRIPT EXECUTION
COMMAND ACTIVITY
ENGINE EVENTS
OPERATIONAL ERRORS

depending on configured logging policies.

Collect:

Terminal window
auditpol.exe /get /category:*

Store the output for review.

Create:

Terminal window
function Get-AuditPolicyStatus {
& auditpol.exe `
/get `
/category:* `
2>&1
}

If relevant audit categories are disabled:

IMPORTANT EVENTS
MAY NEVER BE RECORDED

A security analyst must understand both:

WHAT THE LOG SHOWS

and:

WHAT THE SYSTEM WAS CONFIGURED TO LOG

For basic system context, you can record:

Terminal window
Get-HotFix |
Sort-Object InstalledOn -Descending |
Select-Object -First 20

Create:

Terminal window
function Get-HotfixInventory {
Get-HotFix |
Sort-Object `
InstalledOn `
-Descending |
Select-Object `
-First 20 `
HotFixID,
Description,
InstalledOn
}

A list of installed hotfixes alone does not prove:

FULL PATCH COMPLIANCE

Enterprise patch assessment should compare:

CURRENT STATE
EXPECTED BASELINE
VULNERABILITY DATA
VENDOR GUIDANCE

Create:

Terminal window
function Get-DiskInventory {
Get-CimInstance `
-ClassName Win32_LogicalDisk |
Where-Object {
$_.DriveType -eq 3
} |
Select-Object `
DeviceID,
VolumeName,
@{
Name = "SizeGB"
Expression = {
[math]::Round(
$_.Size / 1GB,
2
)
}
},
@{
Name = "FreeGB"
Expression = {
[math]::Round(
$_.FreeSpace / 1GB,
2
)
}
}
}

84 — Why Disk Capacity Is Security Relevant

Section titled “84 — Why Disk Capacity Is Security Relevant”

A full system drive can cause:

SECURITY LOG FAILURE
APPLICATION FAILURE
UPDATE FAILURE
MONITORING FAILURE

Create:

Terminal window
function Invoke-WindowsSecurityAssessment {
Write-AuditLog `
-Message "Assessment started"
$SystemInformation = Invoke-SafeCollection `
-Name "System Information" `
-ScriptBlock {
Get-SystemSecurityInformation
}
$Identity = Invoke-SafeCollection `
-Name "Current Identity" `
-ScriptBlock {
Get-CurrentIdentityContext
}
$LocalUsers = Invoke-SafeCollection `
-Name "Local Users" `
-ScriptBlock {
@(Get-LocalUserInventory)
}
$LocalAdministrators = Invoke-SafeCollection `
-Name "Local Administrators" `
-ScriptBlock {
@(Get-LocalAdministratorInventory)
}
$Processes = Invoke-SafeCollection `
-Name "Processes" `
-ScriptBlock {
@(Get-ProcessInventory)
}
$Services = Invoke-SafeCollection `
-Name "Services" `
-ScriptBlock {
@(Get-ServiceInventory)
}
$NetworkConfiguration = Invoke-SafeCollection `
-Name "Network Configuration" `
-ScriptBlock {
@(Get-NetworkConfiguration)
}
$TcpConnections = Invoke-SafeCollection `
-Name "TCP Connections" `
-ScriptBlock {
@(Get-TcpConnectionInventory)
}
$FailedLogins = Invoke-SafeCollection `
-Name "Failed Logins" `
-ScriptBlock {
@(Get-StructuredFailedLogins)
}
$Defender = Invoke-SafeCollection `
-Name "Microsoft Defender" `
-ScriptBlock {
Get-DefenderStatus
}
$Firewall = Invoke-SafeCollection `
-Name "Windows Firewall" `
-ScriptBlock {
@(Get-FirewallStatus)
}
$ScheduledTasks = Invoke-SafeCollection `
-Name "Scheduled Tasks" `
-ScriptBlock {
@(Get-ScheduledTaskInventory)
}
$Hotfixes = Invoke-SafeCollection `
-Name "Hotfixes" `
-ScriptBlock {
@(Get-HotfixInventory)
}
$Disks = Invoke-SafeCollection `
-Name "Disk Information" `
-ScriptBlock {
@(Get-DiskInventory)
}
[PSCustomObject]@{
AssessmentId = $AssessmentId
CollectionTime = Get-Date
ScriptVersion = $ScriptVersion
System = $SystemInformation
Identity = $Identity
LocalUsers = $LocalUsers
LocalAdministrators = $LocalAdministrators
Processes = $Processes
Services = $Services
NetworkConfiguration = $NetworkConfiguration
TcpConnections = $TcpConnections
FailedLogins = $FailedLogins
Defender = $Defender
Firewall = $Firewall
ScheduledTasks = $ScheduledTasks
Hotfixes = $Hotfixes
Disks = $Disks
}
}

At the bottom:

Terminal window
$Assessment = Invoke-WindowsSecurityAssessment

Add:

Terminal window
$Assessment |
ConvertTo-Json `
-Depth 8 |
Set-Content `
-Path $JsonReport `
-Encoding UTF8

JSON provides:

STRUCTURED OUTPUT
MACHINE READABILITY
API INTEGRATION
FUTURE AUTOMATION
ARCHIVING

Add:

Terminal window
if ($Assessment.LocalUsers) {
$Assessment.LocalUsers |
Export-Csv `
-Path (
Join-Path `
$ReportDirectory `
"local-users-$RunTimestamp.csv"
) `
-NoTypeInformation
}

Repeat for:

LOCAL ADMINS
SERVICES
PROCESSES
TCP CONNECTIONS
FAILED LOGINS
Terminal window
if ($Assessment.LocalAdministrators) {
$Assessment.LocalAdministrators |
Export-Csv `
-Path (
Join-Path `
$ReportDirectory `
"local-admins-$RunTimestamp.csv"
) `
-NoTypeInformation
}
Terminal window
if ($Assessment.Processes) {
$Assessment.Processes |
Export-Csv `
-Path (
Join-Path `
$ReportDirectory `
"processes-$RunTimestamp.csv"
) `
-NoTypeInformation
}
Terminal window
if ($Assessment.Services) {
$Assessment.Services |
Export-Csv `
-Path (
Join-Path `
$ReportDirectory `
"services-$RunTimestamp.csv"
) `
-NoTypeInformation
}
Terminal window
if ($Assessment.TcpConnections) {
$Assessment.TcpConnections |
Export-Csv `
-Path (
Join-Path `
$ReportDirectory `
"tcp-connections-$RunTimestamp.csv"
) `
-NoTypeInformation
}
Terminal window
if ($Assessment.FailedLogins) {
$Assessment.FailedLogins |
Export-Csv `
-Path (
Join-Path `
$ReportDirectory `
"failed-logins-$RunTimestamp.csv"
) `
-NoTypeInformation
}

Create:

Terminal window
function New-HtmlSecurityReport {
param(
[Parameter(Mandatory)]
$Assessment
)
$Body = @()
$Body += "<h1>Windows Security Assessment</h1>"
$Body += "<p>Assessment ID: $($Assessment.AssessmentId)</p>"
$Body += "<p>Generated: $($Assessment.CollectionTime)</p>"
$Body += "<h2>System Information</h2>"
$Body += (
$Assessment.System |
ConvertTo-Html `
-Fragment
)
$Body += "<h2>Current Identity</h2>"
$Body += (
$Assessment.Identity |
ConvertTo-Html `
-Fragment
)
$Body += "<h2>Local Administrators</h2>"
$Body += (
$Assessment.LocalAdministrators |
ConvertTo-Html `
-Fragment
)
$Body += "<h2>Defender</h2>"
$Body += (
$Assessment.Defender |
ConvertTo-Html `
-Fragment
)
$Body += "<h2>Firewall Profiles</h2>"
$Body += (
$Assessment.Firewall |
ConvertTo-Html `
-Fragment
)
$Body += "<h2>Recent Failed Logins</h2>"
$Body += (
$Assessment.FailedLogins |
Select-Object `
-First 25 |
ConvertTo-Html `
-Fragment
)
ConvertTo-Html `
-Title "Windows Security Assessment" `
-Body $Body |
Set-Content `
-Path $HtmlReport `
-Encoding UTF8
}

Run:

Terminal window
New-HtmlSecurityReport `
-Assessment $Assessment

Run:

Terminal window
Invoke-Item $HtmlReport

Your browser should display the assessment.

At the end:

Terminal window
Write-AuditLog `
-Message "Assessment completed"
Write-Host ""
Write-Host "JSON Report:"
Write-Host $JsonReport
Write-Host ""
Write-Host "HTML Report:"
Write-Host $HtmlReport

You should now have:

Reports/
|
+-- windows-security-report-<timestamp>.json
|
+-- windows-security-report-<timestamp>.html
|
+-- local-users-<timestamp>.csv
|
+-- local-admins-<timestamp>.csv
|
+-- processes-<timestamp>.csv
|
+-- services-<timestamp>.csv
|
+-- tcp-connections-<timestamp>.csv
|
+-- failed-logins-<timestamp>.csv

100 — Analyst Review: Local Administrators

Section titled “100 — Analyst Review: Local Administrators”

Open:

local-admins-*.csv

Ask:

DO I RECOGNIZE EVERY ADMIN?
IS EACH ACCOUNT REQUIRED?
ARE DOMAIN GROUPS PRESENT?
IS ACCESS APPROVED?
IS ANY ACCOUNT STALE?

Look for:

UNEXPECTED PROCESS NAMES
UNEXPECTED EXECUTABLE PATHS
HIGH RESOURCE USAGE
UNUSUAL USER LOCATIONS

Do not judge based solely on process name.

Ask:

WHICH SERVICES RUN AUTOMATICALLY?
WHICH RUN WITH HIGH PRIVILEGE?
ARE SERVICE EXECUTABLE PATHS EXPECTED?
ARE CUSTOM SERVICE ACCOUNTS REQUIRED?

Look for:

LISTENING PORTS
REMOTE CONNECTIONS
UNKNOWN PROCESSES
UNEXPECTED BIND ADDRESSES

Then correlate with:

PROCESS
SERVICE
FIREWALL
BUSINESS PURPOSE

104 — Analyst Review: Authentication Failures

Section titled “104 — Analyst Review: Authentication Failures”

Group:

Terminal window
$Assessment.FailedLogins |
Group-Object User |
Sort-Object Count -Descending |
Select-Object `
Count,
Name

Then:

Terminal window
$Assessment.FailedLogins |
Group-Object SourceIp |
Sort-Object Count -Descending |
Select-Object `
Count,
Name
FAILED LOGIN
WHO?
FROM WHERE?
HOW MANY TIMES?
WHICH LOGON TYPE?
WAS THERE LATER SUCCESS?
WHAT HAPPENED NEXT?

106 — Create a Simple Authentication Summary

Section titled “106 — Create a Simple Authentication Summary”

Example:

Terminal window
$FailedByUser = $Assessment.FailedLogins |
Group-Object User |
Sort-Object Count -Descending
$FailedByIp = $Assessment.FailedLogins |
Group-Object SourceIp |
Sort-Object Count -Descending

Important checks:

AntivirusEnabled
RealTimeProtectionEnabled
BehaviorMonitorEnabled
AntivirusSignatureLastUpdated

Confirm:

DOMAIN PROFILE
PRIVATE PROFILE
PUBLIC PROFILE

according to your lab baseline.

Finding Example Condition Review
Unexpected local admin Unknown account in Administrators High
Defender inactive Expected control unavailable High
Firewall profile disabled Baseline expects enabled High
Repeated failed logins Multiple failures Review
Unexpected listener Unknown service/port Review
Stale local account Unused account remains enabled Review

These are lab review levels, not universal enterprise severity ratings.

Create a basic function:

Terminal window
function Get-AssessmentFindings {
param(
[Parameter(Mandatory)]
$Assessment
)
$Findings = @()
if (
$Assessment.Defender -and
$Assessment.Defender.PSObject.Properties.Name -contains
"RealTimeProtectionEnabled" -and
-not $Assessment.Defender.RealTimeProtectionEnabled
) {
$Findings += [PSCustomObject]@{
Finding = "Defender real-time protection not enabled"
ReviewLevel = "High"
Recommendation = "Validate endpoint protection configuration."
}
}
foreach (
$Profile in @(
$Assessment.Firewall
)
) {
if (
$null -ne $Profile.Enabled -and
-not $Profile.Enabled
) {
$Findings += [PSCustomObject]@{
Finding = "Firewall profile disabled: $($Profile.Name)"
ReviewLevel = "High"
Recommendation = "Validate against approved firewall baseline."
}
}
}
return $Findings
}

Do not write automation that immediately changes:

DEFENDER
FIREWALL
LOCAL ADMINS
SERVICES

based solely on these findings.

Use:

DETECT
VALIDATE
REVIEW
APPROVE
REMEDIATE

Create:

Terminal window
Get-FileHash `
-Path $JsonReport `
-Algorithm SHA256

Save:

Terminal window
Get-FileHash `
-Path $JsonReport `
-Algorithm SHA256 |
Export-Csv `
-Path "$JsonReport.sha256.csv" `
-NoTypeInformation

A hash can provide:

REPORT INTEGRITY REFERENCE

for:

AUDIT
INCIDENT RESPONSE
EVIDENCE MANAGEMENT

Also record:

Terminal window
Get-FileHash `
-Path $PSCommandPath `
-Algorithm SHA256

This documents which script file produced the report.

Your final report should ideally include:

ASSESSMENT ID
COMPUTER NAME
COLLECTION USER
SCRIPT VERSION
COLLECTION TIME
SCRIPT HASH

A more professional script can start:

Terminal window
param(
[string]$OutputDirectory
)

If not supplied:

Terminal window
if (-not $OutputDirectory) {
$OutputDirectory = $ReportDirectory
}

Example:

Terminal window
param(
[switch]$IncludeEventLogs
)

Then collect high-volume event data only when requested.

Parameters make automation:

REUSABLE
CONTROLLED
PREDICTABLE

instead of requiring source-code edits every time.

Use PowerShell parsing:

Terminal window
$Errors = $null
[System.Management.Automation.Language.Parser]::ParseFile(
".\Src\Windows-Security-Audit.ps1",
[ref]$null,
[ref]$Errors
)
$Errors

No errors should be returned.

First run:

Terminal window
.\Src\Windows-Security-Audit.ps1

without elevation.

Document which sections succeed.

On your authorized lab machine, reopen PowerShell:

Run as Administrator

Run again.

Compare:

STANDARD USER OUTPUT
ADMINISTRATOR OUTPUT

Do not assume the script should always run as:

ADMINISTRATOR

Ask:

WHICH DATA REQUIRES ELEVATION?
CAN MOST COLLECTION RUN
WITHOUT IT?

Temporarily call your safe wrapper with:

Terminal window
Invoke-SafeCollection `
-Name "Test Missing Command" `
-ScriptBlock {
Get-NotARealCommand
}

Confirm the script:

LOGS THE FAILURE
CONTINUES SAFELY

Then remove the test.

Some systems may not provide Security log access to your current account.

The script should:

REPORT THE COLLECTION FAILURE

instead of pretending:

NO EVENTS EXIST

Examples:

NO 4625 EVENTS
NO ACCOUNT LOCKOUTS
NO CUSTOM LOCAL USERS

The report should still generate successfully.

Keep:

-MaxEvents

during lab development.

Avoid loading an entire enterprise Security log when you only need recent records.

Prefer:

Terminal window
Get-WinEvent -FilterHashtable

over collecting every event and filtering afterward.

Filter as early as possible.

Do not export everything simply because PowerShell can collect it.

Ask:

DO I NEED THIS DATA?
IS IT SENSITIVE?
WHO WILL RECEIVE THE REPORT?
HOW LONG WILL IT BE STORED?

Your generated reports may contain:

USERNAMES
ADMINISTRATIVE GROUPS
NETWORK INFORMATION
SERVICE DETAILS
PROCESS INFORMATION
SECURITY CONFIGURATION

Treat the output as sensitive security information.

This assessment should never attempt to collect:

PASSWORDS
PASSWORD HASHES
AUTHENTICATION TOKENS
BROWSER COOKIES
PRIVATE KEYS

Do not reduce:

SECURITY AUDITING
POWERSHELL LOGGING
DEFENDER LOGGING

to make testing easier.

Visibility is part of the security control.

Understand:

EXECUTION POLICY

but do not treat it as a complete security boundary.

For the lab, use your organization’s approved PowerShell execution configuration.

133 — Do Not Blindly Run Downloaded Scripts

Section titled “133 — Do Not Blindly Run Downloaded Scripts”

Before executing PowerShell from another source:

READ IT
UNDERSTAND IT
VERIFY THE SOURCE
TEST IT IN A LAB
REVIEW REQUIRED PRIVILEGES

Production organizations may sign PowerShell scripts to help establish:

PUBLISHER IDENTITY
SCRIPT INTEGRITY
CHANGE CONTROL

Your project should eventually look like:

WindowsSecurityAutomation/
|
+-- Src/
| +-- Windows-Security-Audit.ps1
|
+-- Reports/
|
+-- Logs/
|
+-- Tests/
|
+-- README.md
|
+-- architecture.md

Include:

PROJECT PURPOSE
AUTHORIZED USE
REQUIREMENTS
HOW TO RUN
COLLECTED DATA
OUTPUT FILES
SECURITY CONTROLS
LIMITATIONS
TROUBLESHOOTING
FUTURE IMPROVEMENTS

Use:

This project performs defensive Windows
security inventory and assessment.
It does not disable security controls,
extract credentials, create persistence,
or perform exploitation.
Run only against systems you own or are
explicitly authorized to administer.

Your tool does not provide:

FULL MALWARE ANALYSIS
MEMORY FORENSICS
FULL CIS BENCHMARK AUDIT
VULNERABILITY SCANNING
ACTIVE DIRECTORY FOREST ASSESSMENT
ENDPOINT DETECTION REPLACEMENT
EDR REPLACEMENT

139 — Future Enhancement: Active Directory Context

Section titled “139 — Future Enhancement: Active Directory Context”

For an authorized domain environment, future versions could collect:

DOMAIN MEMBERSHIP
DOMAIN USERS
DOMAIN GROUPS
PRIVILEGED GROUP MEMBERSHIP
COMPUTER INVENTORY

Keep this separate from the basic local-host lab.

140 — Future Enhancement: Remote Collection

Section titled “140 — Future Enhancement: Remote Collection”

PowerShell supports remote administration.

However, remote collection should require:

EXPLICIT AUTHORIZATION
APPROVED CREDENTIALS
APPROVED REMOTING CONFIGURATION
TARGET SCOPE

This lab does not require lateral movement or unauthorized remote access.

141 — Future Enhancement: Baseline Comparison

Section titled “141 — Future Enhancement: Baseline Comparison”

A powerful defensive feature is:

CURRENT SYSTEM
COMPARE
KNOWN GOOD BASELINE

Example:

EXPECTED ADMINS
vs
CURRENT ADMINS

Conceptually:

Terminal window
$ExpectedAdmins = @(
"BUILTIN\Administrators",
"CONTOSO\Endpoint Admins"
)
$CurrentAdmins = (
$Assessment.LocalAdministrators
).Name
Compare-Object `
-ReferenceObject $ExpectedAdmins `
-DifferenceObject $CurrentAdmins

Use only your own authorized baseline values.

143 — Future Enhancement: Change Detection

Section titled “143 — Future Enhancement: Change Detection”

Run the audit daily and compare:

YESTERDAY
TODAY

Identify:

NEW LOCAL ADMIN
NEW SERVICE
NEW LISTENER
NEW SCHEDULED TASK
FIREWALL CHANGE
DEFENDER CHANGE
KNOWN GOOD STATE
TIME PASSES
SYSTEM CHANGES
CURRENT STATE
COMPARE
SECURITY DRIFT

145 — Future Enhancement: Centralized Reporting

Section titled “145 — Future Enhancement: Centralized Reporting”

Architecture:

WINDOWS HOSTS
POWERSHELL COLLECTION
JSON
CENTRAL PROCESSOR
SQL
DASHBOARD

This connects directly with the other Programming Labs.

146 — Common PowerShell Security Automation Mistakes

Section titled “146 — Common PowerShell Security Automation Mistakes”

Avoid:

RUNNING EVERYTHING AS ADMIN
NO ERROR HANDLING
NO LOGGING
HARDCODED CREDENTIALS
EXPORTING SENSITIVE DATA NEEDLESSLY
USING MESSAGE TEXT WHEN STRUCTURED XML EXISTS
LOADING HUGE EVENT LOGS WITHOUT FILTERING
AUTOMATICALLY REMEDIATING FINDINGS
DISABLING SECURITY CONTROLS
NO BASELINE
NO REPORT VERSION
NO SCRIPT HASH
NO DOCUMENTATION

Confirm:

  • PowerShell version reviewed
  • Lab workspace created
  • Script created
  • Strict mode configured
  • Error handling configured
  • Logging implemented
  • Assessment ID created
  • System information collected
  • Current identity collected
  • Local users inventoried
  • Local administrators inventoried
  • Processes inventoried
  • Services inventoried
  • Network configuration collected
  • TCP connections collected
  • Listening ports reviewed
  • Failed logins collected
  • Structured event parsing tested
  • Failed logins grouped by user
  • Failed logins grouped by IP
  • Successful logins understood
  • Account lockouts reviewed
  • Privileged logons understood
  • Account creation events understood
  • Group membership changes understood
  • Defender status collected
  • Firewall status collected
  • Scheduled tasks inventoried
  • Audit policy reviewed
  • Hotfix information collected
  • Disk information collected
  • CSV reports exported
  • JSON report exported
  • HTML report generated
  • Reports hashed
  • Script tested as standard user
  • Script tested with authorized elevation
  • Missing-data behavior tested
  • No credentials collected
  • No security controls disabled
  • Findings manually reviewed

You started with individual PowerShell cmdlets:

Get-CimInstance
Get-LocalUser
Get-LocalGroupMember
Get-Process
Get-Service
Get-NetTCPConnection
Get-WinEvent
Get-MpComputerStatus
Get-NetFirewallProfile
Get-ScheduledTask

You converted them into:

ONE REPEATABLE
WINDOWS SECURITY
ASSESSMENT WORKFLOW

Your PowerShell tool now collects:

SYSTEM CONTEXT
LOCAL IDENTITIES
PRIVILEGED ACCESS
PROCESS INFORMATION
SERVICE INFORMATION
NETWORK INFORMATION
AUTHENTICATION EVENTS
DEFENDER STATUS
FIREWALL STATUS
TASK INFORMATION
PATCH CONTEXT

and produces:

CSV
JSON
HTML
EXECUTION LOGS

The central lesson is:

WINDOWS SECURITY
IS ABOUT
CORRELATING OBJECTS

A failed login becomes more valuable when connected to:

USER
+
SOURCE IP
+
LOGON TYPE
+
TIMESTAMP
+
ASSET
+
LATER ACTIVITY

A listening port becomes more useful when connected to:

PROCESS
+
SERVICE
+
FIREWALL
+
BUSINESS PURPOSE

And an administrator account becomes meaningful when connected to:

OWNER
+
APPROVAL
+
USAGE
+
BUSINESS NEED

Whenever you assess a Windows system, think:

WHAT SYSTEM IS THIS?
WHO AM I?
WHO CAN LOG IN?
WHO HAS ADMIN ACCESS?
WHAT PROCESSES ARE RUNNING?
WHAT SERVICES ARE RUNNING?
WHAT IS LISTENING?
WHAT AUTHENTICATION EVENTS OCCURRED?
ARE SECURITY CONTROLS ACTIVE?
WHAT REQUIRES HUMAN REVIEW?

PowerShell’s greatest security advantage is:

WINDOWS DATA
STRUCTURED OBJECTS
PIPELINE
FILTER
CORRELATE
EXPORT
SECURITY DECISION

➡️ Lab 05 — SQL Security Analytics Lab

The next lab moves from operating-system security automation into enterprise security data analysis.

You will build a synthetic security database containing:

USERS
ASSETS
LOGIN EVENTS
VULNERABILITIES
INCIDENTS

Then use SQL to answer investigation questions such as:

WHO HAS THE MOST FAILED LOGINS?
WHICH SOURCE IP TARGETED
THE MOST ACCOUNTS?
WHICH ADMINISTRATORS
DO NOT HAVE MFA?
WHICH CRITICAL ASSETS
HAVE OPEN CRITICAL FINDINGS?
WHICH ASSETS
HAVE NO OWNER?
WHICH INCIDENTS
REMAIN UNASSIGNED?

The workflow will become:

SECURITY QUESTION
SQL
FILTER
JOIN
AGGREGATE
CORRELATE
ANALYST FINDING