Reading width
Wide uses the full column for everything, text, diagrams, code, and exercises. Narrow keeps the standard reading width.
Text size
Scales the body text. Headings and code blocks keep their size.
In this section
0.5 Cmdlets, Functions, Scripts and Modules
Introduction
Every word you type at the start of a PowerShell command names something to run, and that something is one of four kinds: a cmdlet, a function, a script or a native program.
A fifth kind, the alias, is not really a command at all but another name for one, and Section 6 treats it separately. They look alike when you type them, and most of the time the difference does not matter. When it does, it matters a great deal, because it decides what actually runs.
This lesson explains each kind, the naming rule that makes PowerShell's commands readable, how modules package them, and what happens when two things share a name. That last point is where an apparently dry subject becomes a security one: attackers who can define a function or an alias can change what a familiar command does, and Module 8 hunts for exactly that.
By the end of the lesson you can tell which kind of command a name runs with Get-Command, read and use the Verb-Noun naming rule, write a small function and a small script, find which module a command comes from, recognize aliases and their platform differences, and explain command precedence and how to defeat shadowing by naming a command fully.
The evidence is NE-SHARMA-LT's process export from Lesson 0.1, used as the input for a function and a script built during the lesson. Everything runs live in this course's PowerShell 7.6, which runs on Linux; where a fact differs on Windows, the lesson says so.
Four Kinds of Command
and how to tell them apartGet-Command takes a name and says exactly what it is and where it comes from. Three commands this course has already used:
PS C:\Evidence> Get-Command Get-Process, ConvertFrom-Json, Where-Object | Format-Table Name, CommandType, Source
Name CommandType Source
---- ----------- ------
Get-Process Cmdlet Microsoft.PowerShell.Management
ConvertFrom-Json Cmdlet Microsoft.PowerShell.Utility
Where-Object Cmdlet Microsoft.PowerShell.Core
All three are cmdlets: compiled .NET code, shipped in modules, here Microsoft.PowerShell.Management, Microsoft.PowerShell.Utility and Microsoft.PowerShell.Core. Those three modules, with a few others, ship inside PowerShell itself, so they are present on every machine where PowerShell runs and need no installation.
Cmdlets are the bulk of PowerShell; they are fast, consistent and documented, and almost every command in this course is one. Their consistency is not an accident: Microsoft's guidelines for writing cmdlets fix how they name parameters, how they handle the pipeline and how they report errors, so learning one teaches you a great deal about the rest.
The other three kinds are functions, which are PowerShell code given a name; scripts, which are .ps1 files run by their path; and native commands, which are programs outside PowerShell, such as ipconfig.exe on Windows. Native commands take text arguments and return text, the world Lesson 0.1 contrasted with objects, though PowerShell can still capture their output and parse it.
Native commands are not second-class. Some of the most useful tools on a Windows host are executables: whoami /all for a token's groups and privileges, netstat for connections, schtasks, wevtutil and the Sysinternals tools.
PowerShell runs them like any command and captures their text. Many have PowerShell equivalents that return objects, and this course prefers those, but knowing which native tool a cmdlet replaces helps when reading older scripts and runbooks.
Get-Command is the first tool to reach for whenever a name behaves unexpectedly. It also works on things you have not run yet: before using an unfamiliar command from a blog post, Get-Command confirms it exists in your session, which module provides it, and whether that module is the one you expected.
It answers in one line what kind of command a name is and where it comes from, which settles most confusion before it turns into a long search.
Verb-Noun
the naming ruleCmdlets and well-written functions are named Verb-Noun: an approved verb, a hyphen, and a singular noun saying what the command acts on. PowerShell lists the approved verbs, grouped by purpose:
PS C:\Evidence> (Get-Verb).Count
100
PS C:\Evidence> (Get-Verb | Group-Object Group | ForEach-Object { "$($_.Name) $($_.Count)" }) -join ', '
Common 34, Communications 6, Data 24, Diagnostic 7, Lifecycle 22, Other 1, Security 6
PS C:\Evidence> (Get-Command -Noun Process).Name -join ', '
Debug-Process, Get-Process, Start-Process, Stop-Process, Switch-Process, Wait-Process
One hundred verbs in seven groups, from Common, with verbs like Get, Set, New and Remove, to Security, with verbs like Grant, Revoke and Protect. Every command that acts on processes shares the noun Process: Debug-Process, Get-Process, Start-Process, Stop-Process, Switch-Process and Wait-Process.
The rule makes commands guessable. It is also why searching is easy: Get-Command -Noun Service lists everything PowerShell can do to a service, and Get-Command -Verb Remove lists everything that deletes something, two searches Lesson 0.6 builds on.
If you know Get-Process, you can guess Stop-Process exists, and that Get-Service and Stop-Service work the same way for services. It also makes them readable in evidence: a script block log that contains Remove-Item and Unregister-ScheduledTask says what the script did in its own words, without any need to look the commands up.
Verbs also carry a promise. Get never changes anything; Set, Remove and Stop do. The promise holds for Microsoft's commands and for well-written ones; it is a convention, not a guarantee, so a function from elsewhere called Get-Something still deserves a read before you trust that it only reads.
A reviewer can scan a script for the verbs that change things before reading anything else, and Lesson 11.8's review tool did exactly that. Lesson 11.1 holds your own functions to the same list, with PSScriptAnalyzer checking every name.
Nouns follow conventions too. They are singular, Get-Process rather than Get-Processes, because a command that returns one or many uses the same name. Modules often add a short prefix to their nouns to avoid clashing with others, which is why Microsoft Graph's commands are Get-MgUser and Get-MgGroup, and why Lesson 11.6 discussed a prefix for the Northgate toolkit.
Functions
your own named commandsA function is a block of PowerShell code with a name, defined in the session, a script or a module, and then called like any other command. A small one, of the kind an analyst writes in a minute, that reads a process export and returns its rundll32 processes:
PS C:\Evidence> function Get-RundllProcess {
>> param([Parameter(Mandatory)][string]$Path)
>> Get-Content $Path -Raw | ConvertFrom-Json | Where-Object Name -eq 'rundll32.exe'
>> }
PS C:\Evidence> Get-Command Get-RundllProcess | Format-Table Name, CommandType
Name CommandType
---- -----------
Get-RundllProcess Function
PS C:\Evidence> @(Get-RundllProcess -Path ./procs/proc-sharma.json).Count
2
Get-Command reports it as a Function, and it returned the two rundll32 processes from the export. The function did its work with three cmdlets, Get-Content, ConvertFrom-Json and Where-Object, which is typical: most useful functions are a few cmdlets in a pipeline, given a name and a parameter. It has a parameter, -Path, declared in its param block and marked mandatory, so PowerShell asks for it if it is missing.
Leaving it out at an interactive prompt makes PowerShell ask for a value; in a non-interactive session, such as a scheduled task, it fails at once. From outside, it is indistinguishable from a cmdlet: same naming, same parameters, objects out.
Internally it is very different: a cmdlet is compiled ahead of time from C# or another .NET language, while a function is PowerShell source code that the engine runs. That makes functions easy to read and change, and it is why script block logging, in Module 7, can record a function's full text when it runs, but only a cmdlet's name.
That is the point of functions. Turning a habit into a definition is also how a team agrees on a method: the function's code is the method, written down where anyone can read it, rather than a step each analyst remembers slightly differently.
A check you type three times becomes a command with a name, which you, a colleague and a scheduled task can all run the same way. Module 11 builds a whole toolkit this way, starting with exactly this kind of small function and adding validation, help and tests.
A function defined at the prompt lasts until the session ends. Close the console and it is gone, which is fine for an experiment and a trap for anything you meant to keep.
To keep one, put it in a script, a profile or, best, a module, which Section 5 covers. Defined functions are visible on the Function: drive, one of the providers Lesson 0.3 listed, and that drive is also where Module 8 looks for functions an attacker defined in a profile.
Scripts
files of commands, run by pathA script is an ordinary text file with a .ps1 extension, readable in any editor. It can take parameters just like a function and is run by its path, as Lesson 0.4 showed. A two-line script that counts processes by owner:
PS C:\Evidence> Set-Content ./Count-Owner.ps1 'param([string]$Path, [string]$Owner) @(Get-Content $Path -Raw | ConvertFrom-Json | Where-Object Owner -like "*$Owner").Count'
PS C:\Evidence> ./Count-Owner.ps1 -Path ./procs/proc-sharma.json -Owner SYSTEM
16
PS C:\Evidence> (Get-Command ./Count-Owner.ps1).CommandType
ExternalScript
PS C:\Evidence> Remove-Item ./Count-Owner.ps1
The script took -Path and -Owner, read the export and returned 16, the processes owned by SYSTEM on NE-SHARMA-LT. Its param block works exactly like a function's: named parameters, types and defaults, so the call reads like any other command, and Tab completes the parameter names. Get-Command called it an ExternalScript, PowerShell's name for a script file. Unlike a function, nothing about it stays in the session after it runs.
Variables it creates disappear with it, unless the script is dot-sourced, run with a dot and a space in front of its path, which runs it in the caller's scope instead. Dot-sourcing is how a script can define functions for the session to keep using, and it is also why Lesson 11.4 warned against dot-sourcing settings files you did not write.
Scripts are the unit you share, carry to another machine and keep: a collection script carried to a host, a report generator kept beside the evidence, the Project's investigation script. They are also the unit attackers drop on disk, which is why the script block logging of Module 7 records their full text when they run, and why Lesson 11.8 reviews every script you did not write before running it.
The line between a function and a script is mostly about lifetime and sharing. In evidence, the difference shows in the logs: a script run from disk has a path recorded with its script blocks, while a function defined at the prompt or inside another script does not, which helps Module 7 trace where code came from.
A script is a file that does one job when run; a function is a named command that stays available to call. Many scripts simply define functions and call them, and a module, next, is the tidy way to share a set of functions.
Modules
how commands shipA module is a package of commands, usually functions and cmdlets, with a name and a version, installed in a folder PowerShell knows to search. Every cmdlet belongs to one. Where ConvertFrom-Json comes from, how many commands that module provides, and which modules this session has loaded:
PS C:\Evidence> (Get-Command ConvertFrom-Json).Source
Microsoft.PowerShell.Utility
PS C:\Evidence> (Get-Command -Module Microsoft.PowerShell.Utility).Count
114
PS C:\Evidence> Get-Module | Select-Object -ExpandProperty Name | Sort-Object
Microsoft.PowerShell.Utility
ConvertFrom-Json is one of 114 commands in Microsoft.PowerShell.Utility here. The count differs a little between platforms and versions, which is a reason to ask Get-Command rather than remember a number. The session has loaded only the modules it has actually needed so far: PowerShell imports a module automatically the first time one of its commands is used, so there is rarely any need to import the built-in ones by hand.
Modules are how almost everything in this course arrives, from Microsoft or anyone else. Microsoft's own commands, the Microsoft Graph SDK of Module 10, PSScriptAnalyzer from Lesson 11.8, and the NEAnalyst toolkit Lesson 11.6 builds are all modules.
Get-Module -ListAvailable lists what is installed, and Get-Command -Module lists what one provides, which is the quickest way to learn a new module's surface. Import-Module loads one explicitly, which scripts sometimes do at the top so that a missing module fails at once with a clear message rather than halfway through.
Because modules load automatically from known folders, those folders matter. A module placed in a folder on the module path is loaded whenever one of its command names is typed, which makes the module path a place to check for persistence, alongside the profiles of Lesson 0.4.
The module path itself is an environment variable, $env:PSModulePath, a list of folders separated by semicolons on Windows and colons elsewhere. The user's own modules folder comes first, so a module installed there wins over one with the same name installed for all users. That ordering is convenient for testing a new version of a tool, and exactly what an attacker would exploit to replace one.
Aliases
short names, and platform differencesAn alias is a short alternative name for a command, defined once and resolved every time it is typed: gci for Get-ChildItem, gps for Get-Process, % for ForEach-Object. They save typing at the prompt and appear constantly in attacker scripts, which favor short, cryptic commands. But they are not the same on every platform:
PS C:\Evidence> Get-Command ls | Format-Table Name, CommandType, Source
Name CommandType Source
---- ----------- ------
ls Application /usr/bin/ls
PS C:\Evidence> (Get-Alias -Definition Get-ChildItem).Name -join ', '
dir, gci
On this build, which runs on Linux, ls is not an alias at all: it is the native Linux program, an Application. Its Source is the program's path, /usr/bin/ls, which is how Get-Command reports any native program: by where the operating system found it. On Windows, ls is an alias for Get-ChildItem.
PowerShell 7 removed aliases such as ls, cat and ps on Linux and macOS so as not to hide the system's own commands, and kept them on Windows. Windows keeps them because, there, no native ls exists to hide, and users coming from other shells expect it to work.
That difference is a small trap with a large lesson. A script that uses ls behaves one way on Windows and another on Linux, returning objects in one place and text in the other.
Scripts you share should use full command names, which is what PSScriptAnalyzer's alias rule enforces; aliases are for typing, not for keeping. Full names cost a few characters and remove a whole class of surprises, which is a trade worth making in anything that will run on a machine you have not seen.
Aliases in evidence deserve expansion before judgment. Attackers chain them to make one line unreadable at a glance, which is the point: obfuscation works on people, not on Get-Alias.
Lesson 11.8's review tool expanded every alias to its command before checking it, because iex looks harmless to an eye that has not learned it is Invoke-Expression. Get-Alias does the same expansion at the prompt for any name you do not recognize.
Precedence and Shadowing
which one runs when names collideWhen two commands share a name, PowerShell resolves it in a fixed order: an alias first, then a function, then a cmdlet, then a native program. A function named like a cmdlet therefore replaces it, silently:
PS> Get-DatePS> Microsoft.PowerShell.Utility\Get-DateShown in full, with every command that answers to the name:
PS C:\Evidence> function Get-Date { 'shadowed' }
PS C:\Evidence> Get-Date
shadowed
PS C:\Evidence> (Microsoft.PowerShell.Utility\Get-Date -Year 2026 -Month 3 -Day 9).DayOfWeek
Monday
PS C:\Evidence> Get-Command Get-Date -All | Format-Table Name, CommandType, Source
Name CommandType Source
---- ----------- ------
Get-Date Function
Get-Date Cmdlet Microsoft.PowerShell.Utility
PS C:\Evidence> Remove-Item Function:Get-Date
Plain Get-Date returned the function's output. Nothing on screen said that anything unusual had happened; the output was simply wrong, which is what makes shadowing effective for an attacker and confusing for everyone else. The module-qualified name, Microsoft.PowerShell.Utility\Get-Date, ran the real cmdlet and reported that 9 March 2026 was a Monday.
Get-Command -All listed both, the function first, because it wins. The function's Source is empty because it was defined at the prompt rather than in a module, which is itself a clue: a command that should come from a module but has no source was defined by something in the session.
This is a real technique. An attacker who controls a profile can define a function named Get-Process that hides their own process, or an alias that points a familiar command elsewhere, and every analyst who starts that shell sees the lie.
The investigator's own tools are the target: the commands most likely to be shadowed are the ones a responder runs first, such as Get-Process, Get-Service and Get-ScheduledTask. Module 8 hunts for exactly these definitions. For your own scripts, module-qualified names and -NoProfile are the defenses: one ensures the command is the one you meant, the other ensures nothing redefined it first.
Shadowing is not only malicious. A colleague's profile might define a convenient Get-Process wrapper that adds columns, and a script that assumes the real cmdlet behaves oddly on that machine alone. The diagnosis is the same either way: Get-Command -All on the name, and the answer is usually obvious within a line.
Commands, Checked
five factsThe facts of this lesson as checks: Get-Process is a cmdlet, the function defined here is a function, ConvertFrom-Json comes from Microsoft.PowerShell.Utility, Get is an approved verb, and the function returns two processes:
PS C:\Evidence> function Get-RundllProcess {
>> param([Parameter(Mandatory)][string]$Path)
>> Get-Content $Path -Raw | ConvertFrom-Json | Where-Object Name -eq 'rundll32.exe'
>> }
PS C:\Evidence> @(
>> (Get-Command Get-Process).CommandType -eq "Cmdlet"
>> (Get-Command Get-RundllProcess).CommandType -eq "Function"
>> (Get-Command ConvertFrom-Json).Source -eq "Microsoft.PowerShell.Utility"
>> (Get-Verb).Verb -contains "Get"
>> @(Get-RundllProcess -Path ./procs/proc-sharma.json).Count -eq 2
>> )
True
True
True
True
True
Five True. Every one of the five facts was obtained by asking PowerShell, not by reading documentation, so they are true of the PowerShell actually running here.
The fifth is the function from Section 3, still defined in the session, still returning the two rundll32 processes: a named command is as dependable as a cmdlet once it exists. Each check asks Get-Command or runs the command, rather than assuming, which is the habit this lesson is really about: when a name matters, ask PowerShell what it runs before trusting it.
The next lesson finishes the set of tools for finding your way around: Get-Help for what a command does, Get-Member for what an object holds, and Get-Command and Get-Alias used for discovery rather than identification.
Four kinds of command, one naming rule, modules to ship and load them, and a fixed precedence order to settle collisions between names: that is the whole of how PowerShell decides what to run. The rest of the course adds commands, never new kinds.
Practice
- Identify. Ask Get-Command what kind of command a name is and which module it comes from.
- Name well. Name your functions and scripts Verb-Noun with approved verbs.
- Write it out. Use full command names in scripts; keep aliases for the prompt.
- Qualify. Use module-qualified names and -NoProfile when you need to be sure what runs.
Then run Get-Command on five commands you have used before, including one native program, and note which of the four kinds each one is and where it comes from.
Next: finding your way around with Get-Help, Get-Command, Get-Member and Get-Alias.