In this section

0.1 Why Defenders Use PowerShell

Module 0

Introduction

This course teaches PowerShell as a security analyst uses it: to collect evidence from Windows machines, to read logs, to hunt across many hosts, to call security APIs, and to build tools a team can share. This first lesson answers the question a newcomer reasonably asks first, which is why PowerShell at all, when Windows already has a command prompt and many analysts already know Bash.

The answer is one idea, and the rest of the course depends on it: PowerShell commands pass objects, not text. That sounds abstract, so the lesson shows it on evidence. The same question about the same laptop is asked of a command prompt's text output and of PowerShell's objects, and the difference in what each can tell you, and how reliably, is the reason defenders use it.

Why defenders use PowerShell CMD and Bash commands pass text PowerShell commands pass objects Columns by position parse, split, hope Properties by name Owner, CommandLine, ParentProcessId An answer you can keep filter, count, export, report The same question, asked of the same laptop, answered from text or from objects.

By the end of the lesson you can explain what it means for commands to pass text or objects, read a command prompt tool's output and see what it leaves out, parse text correctly when you have to, filter and count PowerShell objects by property name, compare how CMD, Bash and PowerShell answer the same question, and say what PowerShell reaches that makes it the analyst's shell on Windows.

The evidence is one laptop at one moment: NE-SHARMA-LT at 13:00 UTC on 9 March 2026, during the intrusion this course investigates. It comes twice, as the command prompt's tasklist tool would print it and as PowerShell exports the same processes, along with two more laptops for comparison. Every command in this lesson runs live against those files.

1

The First Question

what is running, and as whom?

Almost every investigation of a computer starts with the same question: what is running on it, and as whom? On Windows, the command prompt answers with tasklist. Here is the start of tasklist's answer for NE-SHARMA-LT, in its CSV format with the verbose columns, and its length:

PS C:\Evidence> Get-Content ./tasklist-sharma.csv -TotalCount 6
"Image Name","PID","Session Name","Session#","Mem Usage","Status","User Name","CPU Time","Window Title"
"System Idle Process","0","Services","0","800 K","Unknown","N/A","0:00:00","N/A"
"System","4","Services","0","816 K","Unknown","NT AUTHORITY\SYSTEM","0:00:04","N/A"
"Registry","112","Services","0","1,248 K","Unknown","NT AUTHORITY\SYSTEM","0:00:52","N/A"
"smss.exe","388","Services","0","2,352 K","Unknown","NT AUTHORITY\SYSTEM","0:00:28","N/A"
"csrss.exe","540","Services","0","2,960 K","Unknown","NT AUTHORITY\SYSTEM","0:00:00","N/A"
PS C:\Evidence> @(Get-Content ./tasklist-sharma.csv).Count
29

Twenty-eight processes, one line each, plus a header: the image name, the process ID, the session, its memory, its status, the user it runs as, the processor time it has used, and its window title. It is readable, and for one look at one machine, it is enough.

But it is text. Every value, the process ID included, is characters in quotes. A person can read it; a program has to cut it apart to use it, and has to know where one value ends and the next begins. What tasklist chose to print is also all there is: if a column you need is not in the text, no amount of cutting will find it.

That limitation is the theme of this lesson. Text output is a report written for a person. It is the end of a process, not a step in one, and an investigation is a long chain of steps, each one using the answer of the last.

Text also loses information silently. tasklist's memory column says 10,752 K, rounded to kilobytes and formatted with a comma for a reader. The process's real working set is a number of bytes, and the rounding, the separator and the unit are all choices the tool made on your behalf. A reader barely notices; a script comparing two snapshots has to undo every one of them.

2

Searching Text

what findstr and grep can do

The classic text approach is to search the output for a word. The command prompt has findstr, Bash has grep, and PowerShell's Select-String does the same job. Searching tasklist's output for rundll32:

PS C:\Evidence> Get-Content ./tasklist-sharma.csv | Select-String rundll32
"rundll32.exe","7488","Console","1","10,752 K","Running","NE\p.sharma","0:00:48","N/A"
"rundll32.exe","8012","Services","0","12,848 K","Unknown","NT AUTHORITY\SYSTEM","0:00:32","N/A"

Two lines, two rundll32 processes, one as the user p.sharma and one as SYSTEM, the account Windows itself runs as. That is a real lead already: rundll32 is a Windows program that runs code from a DLL, and two copies under different accounts is worth a look.

A DLL is a library of code that a program loads; on its own it cannot run, so Windows provides rundll32 to call a function inside one. Legitimate software uses it every day, and so do attackers, because a signed Windows program running their code is less conspicuous than an unknown executable. Module 4 returns to this pair in detail.

What the lines cannot say is which DLL each one runs, what started them, or when. tasklist does not print command lines or parent processes, so the evidence that would explain the two lines is simply not in the text. An analyst using only text tools would now go and find another command that does print those things, and parse its text too.

Searching text also matches anywhere in the line. A user named rundll32test, or a window title mentioning rundll32, would match the same way. The search finds characters, not meaning, which is fine for a quick look and a weak foundation for anything that has to be right.

Text searches are still worth knowing, and this course uses Select-String often: on log files written as text, on scripts, on command lines, and on anything else that arrives as characters. The point is to use text tools on text and object tools on objects, and not to turn objects into text just to search them.

3

Cutting Text Apart

and how it goes wrong

To use a value from text, a script has to extract it. The obvious way to read the memory column of a CSV line is to split the line at its commas and take the fifth piece:

First trysplit the text
PS> Get-Content ./tasklist-sharma.csv | Select-String rundll32 | ForEach-Object { $_.Line.Split(',')[4] }
"10 and "12: half of each memory figure.
The memory column is written "10,752 K", with a comma inside the quotes. Splitting on commas cuts it in two, and every column after it shifts by one. Nothing errors; the answer is just wrong.
Refinedparse it once, then use names
PS> Import-Csv ./tasklist-sharma.csv | Where-Object 'Image Name' -eq 'rundll32.exe'
10,752 K and 12,848 K, each in its own named column.

The refined command uses Import-Csv, which understands quoting, so the comma inside "10,752 K" stays inside its value. Run in full:

PS C:\Evidence> $t = Import-Csv ./tasklist-sharma.csv
PS C:\Evidence> $t | Where-Object 'Image Name' -eq 'rundll32.exe' | Format-Table 'Image Name', PID, 'Mem Usage', 'User Name'
Image Name   PID  Mem Usage User Name
----------   ---  --------- ---------
rundll32.exe 7488 10,752 K  NE\p.sharma
rundll32.exe 8012 12,848 K  NT AUTHORITY\SYSTEM

Both processes with their memory, each value in a column with a name, and the commas back where they belong. Import-Csv has turned text into objects, and from here on the work uses names rather than positions.

Notice that Import-Csv took its property names from the header line, spaces included, which is why 'Image Name' and 'Mem Usage' need quotes. The values are still text, though: Mem Usage is the characters 10,752 K, not a number. Parsing gives structure, not meaning; turning that column into a number would take one more deliberate step.

The split looked correct, ran without error and returned something plausible. That is the characteristic failure of text processing: it breaks silently when the data does something the parser did not expect, such as a comma in a number, a space in a name or a new column in a new version of the tool. Every text pipeline in an investigation is a place where this can happen.

Real tools make this worse in ways that are hard to predict. tasklist's columns change with the /v switch, its number formats follow the machine's regional settings, so a German Windows writes 10.752 K, and its column headers are translated on non-English systems. A parser written on one analyst's machine can be wrong on the next. Objects carry none of those choices, because they never stopped being data.

4

Objects

the same question, answered with properties

PowerShell asks Windows for processes and gets back objects: each process is one thing with named properties, such as its ID, its parent's ID, its owner and its full command line. The same moment on NE-SHARMA-LT, exported from PowerShell, asked the same question:

PS C:\Evidence> $p = Get-Content ./procs/proc-sharma.json -Raw | ConvertFrom-Json
PS C:\Evidence> $p | Where-Object Name -eq rundll32.exe | Format-List ProcessId, ParentProcessId, Owner, CommandLine
ProcessId       : 7488
ParentProcessId : 7316
Owner           : NE\p.sharma
CommandLine     : "C:\Windows\System32\rundll32.exe" C:\Users\p.sharma\AppData\Local\Temp\upd.dll,Start

ProcessId       : 8012
ParentProcessId : 1192
Owner           : NT AUTHORITY\SYSTEM
CommandLine     : "C:\Windows\System32\rundll32.exe" C:\Users\p.sharma\AppData\Local\Temp\upd.dll,Start

The same two processes, and now the answer that matters. Both run the same DLL, upd.dll, from the user's Temp folder. The user's copy was started by process 7316; the SYSTEM copy by 1192. Two copies of one payload, under two accounts, started by two different parents, is the beginning of the intrusion this course investigates.

Nothing was parsed. Where-Object compared each process's Name property with rundll32.exe, and Format-List showed four properties by name. If the export had a new property tomorrow, this command would still work, because it never depended on where anything was in a line.

In this lesson the objects came from a JSON export written on the laptop, but on a live Windows machine the command is Get-CimInstance Win32_Process, which asks Windows Management Instrumentation for the same objects. Module 4 explains the difference between that and Get-Process, which returns different objects about the same processes, and why an analyst usually wants the CIM version for its command lines and parents.

Objects are also typed. ProcessId is a number, so it sorts as a number and compares as one; a date is a date. In text, "10" sorts before "9" and a date is whatever format the tool happened to print. Many quiet errors in text-based investigations come from comparing values as text that were never meant to be text.

The objects in this lesson came from a file, so their properties are whatever the export wrote. On a live machine, PowerShell's process objects come straight from Windows and carry far more: dozens of properties for each process, from its priority to its handle count. Lesson 0.6 shows how to list every property an object has, which is how an analyst finds the one that answers today's question.

5

Three Shells, One Question

CMD, Bash and PowerShell side by side

The same question, in each of the three shells an analyst is likely to meet during a career in security operations, written the way each is usually written. This is for reading, not running: the CMD and PowerShell lines need a Windows machine, the Bash line a Linux one:

REM CMD: text, no command lines, no parents
tasklist /v /fo csv | findstr /i rundll32

# Bash on a Linux server: text again; columns by position
ps -eo pid,ppid,user,args | grep -i rundll32

# PowerShell: objects; properties by name
Get-CimInstance Win32_Process -Filter "Name = 'rundll32.exe'" |
    Select-Object ProcessId, ParentProcessId, CommandLine

The command prompt's line finds two text lines with no command line or parent. Bash's ps can print command lines on Linux, and its output is text again, read by column position, with the command line as the last column precisely because it may contain spaces. PowerShell's line returns objects with exactly the properties asked for.

None of this makes CMD or Bash bad. Bash is the right shell on a Linux server, and text tools are fast and universal. The point is what happens next: an analyst rarely stops at one command. The answer gets filtered, joined with other evidence, counted, exported and written into a report, and every one of those steps is simpler and safer when the answer is objects.

PowerShell also runs on Linux and macOS, as PowerShell 7, and this course's own build runs it on Linux. On a Linux server, though, the evidence still comes from Linux's own tools and files, which speak text. PowerShell earns its place most clearly on Windows, where the operating system itself hands it objects.

For an analyst who already knows Bash, most habits carry over: a pipeline of small commands, each doing one thing, read from left to right. What changes is what flows through the pipe. In Bash it is lines of characters, and every command parses them again. In PowerShell it is objects, parsed once, if at all, and every command after that reads properties by name.

6

Counting, Grouping, Comparing

questions text makes hard

Investigations need numbers: how many, how many of each kind, how many more than usual. A count is often the first thing a lead or an incident manager asks for, and the first thing a reviewer checks, so it has to be right and repeatable.

With objects, a count is one more command in the pipeline. How many processes are on the laptop, how many run as SYSTEM, and the split between the two:

PS C:\Evidence> $p = Get-Content ./procs/proc-sharma.json -Raw | ConvertFrom-Json
PS C:\Evidence> @($p).Count
28
PS C:\Evidence> @($p | Where-Object Owner -like '*SYSTEM').Count
16
PS C:\Evidence> $p | Group-Object { $_.Owner -like '*SYSTEM' } | Select-Object Count, Name
Count Name
----- ----
   12 False
   16 True

Twenty-eight processes, sixteen of them SYSTEM, grouped into the two counts by a condition written once. The condition is a small script block, run once for each process, and Group-Object sorts the processes by its answer, True or False.

The same pattern groups by anything you can compute from an object, which makes it one of the most reused lines in this course. Doing the same from tasklist's text would mean parsing every line, finding the user column, and hoping no value contains a comma, a quote or a space in the wrong place.

Sixteen SYSTEM processes on an ordinary laptop is also a useful reminder about judgment. Most of what runs as SYSTEM is Windows itself, so "runs as SYSTEM" is a fact about a process, not a finding about it. The SYSTEM rundll32 matters because of what it runs and where from, which only the full object shows. Counting is where an investigation starts asking questions, not where it answers them.

Group-Object, used here with a condition, is one of the commands that make PowerShell useful for evidence. Grouping by owner, by parent, by host or by hour turns a list of hundreds of items into a few lines that show where the unusual ones are. Module 1 teaches it properly; here it is enough to see that the question took one line.

7

Answers You Can Keep

objects out, not just in

An investigation's results have to leave the shell: into a case file, a ticket, a report or another tool. Objects convert to structured formats directly, keeping every property and its name. The user-owned rundll32 as JSON:

PS C:\Evidence> $p = Get-Content ./procs/proc-sharma.json -Raw | ConvertFrom-Json
PS C:\Evidence> $p | Where-Object ProcessId -eq 7488 | Select-Object ProcessId, Name, ParentProcessId, Owner, CommandLine | ConvertTo-Json
{
  "ProcessId": 7488,
  "Name": "rundll32.exe",
  "ParentProcessId": 7316,
  "Owner": "NE\\p.sharma",
  "CommandLine": "\"C:\\Windows\\System32\\rundll32.exe\" C:\\Users\\p.sharma\\AppData\\Local\\Temp\\upd.dll,Start"
}

Five properties, each with its name, and the command line intact, ready for a case file or for another analyst's script to read back as the same object. Text output would have to be copied, and whatever the copy lost, the reader never sees.

The backslashes are doubled in the JSON because the format uses a backslash as its escape character; reading it back with ConvertFrom-Json restores single ones. That is the kind of detail a structured format handles for you and a hand-written text format gets wrong, usually in exactly the paths and command lines an investigation cares about.

That round trip is how this lesson's evidence was made. The process exports were written on the laptops by PowerShell, as JSON, and read back here with ConvertFrom-Json. The objects that come back have the same properties they had on the laptop, which is why commands written against a live machine and commands written against evidence look the same in this course.

The same is true of CSV, which spreadsheets open, and of the reports Module 10 builds. Keeping results as structured data until the last moment, and turning them into text only for the person who reads them, is a habit this course will keep returning to.

It matters most when evidence is challenged. A finding that says "the SYSTEM rundll32 ran upd.dll from Temp" is stronger when the record it came from can be produced unchanged, with every property it had when it was collected. A screenshot of console text cannot be filtered, recounted or checked by someone else; a JSON export can, months later.

What one shell reaches, and where this course uses it

Every source below answers with objects, so the same filtering, counting and exporting work on all of them.

The live host

Processes, connections, services, accounts, scheduled tasks, files and the registry. Modules 3 to 5.

Logs

Windows event logs, Sysmon, and PowerShell's own logging. Modules 6 and 7.

Defenses

Defender's settings, detections and exclusions, and the places attackers persist. Module 8.

Many hosts at once

Remoting, fan-out collection and JEA. Module 9.

Cloud and intelligence

Microsoft Graph, Entra ID sign-ins, Defender XDR and threat-intelligence APIs. Module 10.

Your own tools

Functions, modules and signed toolkits the team shares. Module 11.

Each row of that card is a module of this course, and each is a source that answers with objects. The skills of this lesson, filtering by property, counting, grouping and exporting, work the same on a process, an event, a sign-in record or an API response. Learning them once on processes is learning them for all of it.

8

One Command, Every Host

from one laptop to a fleet

An object-based command does not care how many machines its input came from. The three laptops' exports, read together and asked the first question of this lesson:

PS C:\Evidence> Get-ChildItem ./procs/*.json | ForEach-Object { Get-Content $_ -Raw | ConvertFrom-Json } |
>>     Where-Object Name -eq rundll32.exe | Format-Table ProcessId, Owner, CreationDate
ProcessId Owner               CreationDate
--------- -----               ------------
     7488 NE\p.sharma         03/09/2026 10:31:23
     8012 NT AUTHORITY\SYSTEM 03/09/2026 10:31:58
PS C:\Evidence> @(Get-ChildItem ./procs/*.json).Count
3

Three exports read, and both rundll32 processes found on NE-SHARMA-LT alone, with their owners and start times. NE-PATEL-LT and NE-ZHANG-LT have none, which is itself a finding: on this evidence, the payload was running on one laptop of the three.

The command is the one from Section 4 with Get-ChildItem in front of it. Module 9 does the same across a fleet, collecting live, and it is still the same idea: the question is written once and the objects come from wherever the evidence is.

That is the case for PowerShell in one sentence: one shell, present on every Windows machine, that reaches the host, its logs, its defenses, its neighbors and the cloud services behind it, and hands back objects that can be filtered, counted, kept and reported without being cut apart. The rest of this module shows how it is built and how to find your way around it.

Evidence handled as text
findstr or grep for a name
Where-Object on the Name propertycharacters match anywhere; properties match exactly
split a line at its commas
Import-Csv, then column namesquoted values contain commas
copy console output into the report
export objects as JSON or CSVtext cannot be refiltered or checked
Objects from PowerShell, text parsed once with a real parser, and structure kept until a person reads it.

Nothing in this lesson required knowing PowerShell's syntax in depth. Where-Object, Format-List, Group-Object and ConvertTo-Json were used by name, and each did what its name says. That readability is entirely deliberate in PowerShell's design, and Lesson 0.5 explains the naming rule behind it. The modules after this one teach each command properly; this one shows why they are worth learning.

Practice

Do this Ask with objects
  1. Prefer objects. When a PowerShell command returns the information, use it rather than parsing a text tool's output.
  2. Parse once. When text is all you have, parse it once with a real parser such as Import-Csv, then work with names.
  3. Filter by name. Select processes, events and records by property, not by searching for characters in a line.
  4. Keep structure. Export results as JSON or CSV, and turn them into text only for the person reading them.
What you should end up with: answers you can rely on, count and hand to someone else unchanged.

Then, on a Windows machine of your own, run tasklist /v /fo csv and Get-CimInstance Win32_Process, and find one fact each gives that the other does not.

Next: the two different PowerShells installed on every Windows machine, and which one this course uses.