Pack security
A pack runs PowerShell code in your Maester session, with every connection in it, your environment and your files. Maester is not a sandbox. What it does is make sure that the code that runs is the code you approved, that you saw what it reaches before approving it, and that your organisation can set limits. Read-only Graph scopes limit what a pack can change in your tenant; they do not stop it reading local files or sending data out.
What is checked, and whenโ
| When | Check |
|---|---|
| Install | The pack is read without running any of it: manifest, every file, and an AST scan of every script. Errors stop the install. |
| Install | The release is older than the cooldown, the pack policy allows it, and it is not on the blocklist. |
| Install | Its modules are saved at their exact versions into the pack store, and the digest of each module folder is recorded. |
| Install | You approve the prompt. A High risk pack, or one newer than the cooldown, needs its code typed. |
| Every load | The stored files match the digest in the lock file, the policy still allows the pack, it is not blocked, and every module still matches its digest. Only then is any file loaded. |
Every path that loads pack code goes through that last check: Invoke-Maester, Invoke-MtTest,
Connect-Maester, Disconnect-Maester, Test-MtConnection and Get-MtTenantContext.
The lock fileโ
maester-packs.lock.json pins each pack to a commit and a digest: SHA-256 over the sorted list of every file's
path and SHA-256, with Authenticode signature blocks left out so that signing a pack does not change it. The pack
store keeps the files read-only under that digest. A changed file stops the pack with PackUnavailable until
you run Restore-MtPack (to get the approved files back) or Install-MtPack again (to review the change).
Only a complete signature block of PowerShell comments at the end of a file is left out. Marker text inside
strings, and a trailer that is incomplete or followed by code, stay part of the digest. Pack archives refuse paths
that contain : on every platform, so a pack from GitHub can't carry Windows alternate data streams.
Windows alternate data streams on files already on your computer, such as the Zone.Identifier mark that
downloads get or the stream Microsoft Defender Endpoint DLP adds, are not part of the digest and don't stop a
pack. PowerShell never runs a stream's content, a pack's code can read one only with a call you can see when you
review it, and only someone who can already change the stored files could add one.
A module that the pack runs in a separate process from your own install ("Install": "User") cannot be pinned:
it is shown as High risk, and AllowExternalEnvironments: false refuses the pack.
Each module a pack uses is pinned the same way, by the digest of its folder. A module you installed yourself
(used when the pack policy's AllowExternalEnvironments is on) is pinned by its digest at approval, raises the
pack to High risk, and needs approving again when it changes.
Cooldownโ
A new release is not installed until it is 72 hours old, so a compromised release has time to be noticed and blocked. The age comes from the content, never from a date a publisher can set:
- an immutable release counts from when it was published, because its tag cannot be moved;
- anything else counts from when this computer first saw that commit of that repository, so a tag moved to new code starts a new cooldown.
-IncludeRecent installs a newer release; at a console you also type the pack's code. PackPolicy.CooldownHours
changes the length.
Pack policyโ
The PackPolicy section of maester-config.json, the MAESTER_PACK_POLICY environment variable (a path to a
JSON file) and a machine file (%ProgramData%\Maester\pack-policy.json or /etc/maester/pack-policy.json) are
combined, and the strictest value of each setting wins:
| Setting | Effect |
|---|---|
MaximumRisk | Low, Medium or High: the highest risk a pack may have. |
AllowedPublishers | Only packs from these GitHub owners. An empty list denies all non-built-in packs; an omitted or null value adds no restriction. |
TrustedSources | Only packs from these sources (owner/repo). |
AllowUnlisted | false: only packs listed in the Maester Packs index. |
RequireBadge | Only packs with this index badge. |
AllowExternalEnvironments | false: a pack may use only modules that Maester saved. |
AllowedRuntimes | The pack runtimes allowed. |
CooldownHours | The cooldown, in hours. |
RequireFreshBlocklist | true: no pack runs when the blocklist cannot be fetched. |
Packs that ship with Maester are always allowed.
Blocklistโ
Maester fetches a list of blocked packs, signed with an ECDSA P-256 key that ships with the module, and caches it
for an hour. Only a list with a valid signature, a serial at least as high as the last one seen, and an unexpired
date is used. A blocked pack does not install and, when already installed, does not run (PackBlocked). When
the list cannot be fetched, the cached list is used with a warning, unless RequireFreshBlocklist is set.
Strict freshness also rejects missing trust keys and cached failures. A verified, unexpired list fetched
within the cache interval can be reused, but reading it from disk or memory does not restart that interval.
Both its original fetch time and signed expiry are checked on every call. After an hour, a refresh is attempted;
if that fails, only permissive mode can use an unexpired cached list, with a warning. When several entries
match a pack, any blocking entry takes precedence over a warning regardless of their order in the list.
Repairing stored packsโ
Restore-MtPack and reinstalling replace a stored pack whose files changed. The new files and the copy made of
them must both match the approved digest before the old copy is removed, so a failed download or a copy that
doesn't match leaves the stored pack as it was, and Restore-MtPack reports that pack as failed.
Reporting a packโ
Report a malicious or vulnerable pack to the Maester maintainers through GitHub security advisories.