Skip to main content
Version: 3.0.0-preview

Run configuration reference

The run configuration tells Maester which tests to run, with which values, and how to report them. It is the maester-config.json format you know from 2.x, grown with new sections. Every 2.x maester-config.json, custom/maester-config.json and maester-config.<tenantId>.json loads without changes, and an empty file is an empty configuration.

Since 3.0 the module no longer ships a row per test: severities come from the tests themselves, so your config only needs what you want to change. See the overview for a short introduction.

Where the configuration comes from​

Maester uses exactly one source, in this order:

  1. -Config on Invoke-Maester (or Invoke-MtTest): a path to a JSON file, a configuration object (hashtable or PSCustomObject), or an array of paths and objects merged from left to right. The run is hermetic: config files next to your tests are not read.

    Invoke-Maester -Config ./policy.json, @{ Metadata = @{ RunId = 'nightly-42' } }
  2. The MAESTER_CONFIG environment variable: a path to a config file. Also hermetic.

  3. The files Maester finds, starting from -Path (or the current folder when -Path is not given). Each is an optional layer, and each merges over the one before:

    LayerFound at
    maester-config.json-Path, then -Path/tests, then up to five parent folders. The first match wins.
    custom/maester-config.jsonThe custom folder next to that root file, or under -Path or -Path/tests when there is no root file.
    maester-config.<tenantId>.jsonSearched like the root file, using the tenant ID of the Graph connection. Every section applies, including Selection. Without a Graph connection the file is not looked for.

Under every source sits a small defaults layer (the default values of the global settings). Command-line parameters sit on top of every source.

Two changes from 2.x:

  • The tenant file is merged, not substituted. In 2.x maester-config.<tenantId>.json was loaded instead of maester-config.json. In 3.0 it is merged over maester-config.json and custom/maester-config.json, and wins over both. A tenant now inherits base settings it does not set itself; Maester warns once and names the global settings it inherited. Set them in the tenant file to override them.
  • Custom/maester-config.json works on its own. In 2.x it was ignored without a root file beside it.

If a maester-config.json looks like a copy of the file Maester 2.x shipped (300 or more TestSettings rows that all have a Title), Maester warns: its rows override the severities the tests now carry. Keep only the rows you changed.

A config file found in a parent folder applies to every run below it. When such a file (outside -Path) sets Selection, Environment or Output, Maester warns, so that a stray file cannot quietly change what runs. Pass the config with -Config if that is what you want. -Verbose logs the full path of every config file used.

ConfigSource in the result's MaesterConfig names the sources that were used.

Merge rules​

  • TestSettings rows merge per Id (case-insensitive) and per property: a higher layer can change one property of a row without repeating the others.
  • Every other object merges per key, recursively.
  • Arrays and plain values replace. An empty array in a higher layer clears the lower one.
  • On the command line, -Tag and -TestId replace the config's Selection.Tag and Selection.TestId; -ExcludeTag and -ExcludeTestId add to the config's lists; -IncludePreview and -IncludeLongRunning turn the setting on when either source does.

Keys Maester does not know are kept and passed through, so they do no harm. The 2.x keys ModuleVersion and ConfigVersion are ignored.

A complete example​

{
"Metadata": { "RunId": "2026-10-06-nightly", "RequestedBy": "security-pipeline" },
"GlobalSettings": {
"EmergencyAccessAccounts": [
{ "Type": "User", "UserPrincipalName": "[email protected]" },
{ "Type": "Group", "Id": "00000000-0000-0000-0000-000000000000" }
]
},
"Selection": {
"BuiltIn": "All",
"DefaultAction": "Run",
"Product": [],
"Feature": [],
"Area": [],
"Severity": [],
"Suite": [],
"Tag": [],
"ExcludeTag": [],
"TestId": [],
"ExcludeTestId": ["MT.1024.*"],
"IncludePreview": false,
"IncludeLongRunning": false,
"OnUnknownId": "Warn"
},
"TestSettings": [
{ "Id": "MT.1005", "Severity": "Critical" },
{ "Id": "MT.1089", "Enabled": false, "Reason": "Not relevant for this tenant" },
{ "Id": "MT.1198", "Parameters": { "MaximumValidityDays": 180 }, "TimeoutSeconds": 600 }
],
"Environment": {
"TenantType": "Auto",
"Cloud": "GCC",
"Licenses": "Auto",
"Services": "Auto",
"Enforce": { "Service": true, "License": true, "TenantType": false, "Cloud": false }
},
"Execution": { "TestTimeoutSeconds": 0, "LongRunningTimeoutSeconds": 0 },
"Output": {
"ErrorsAsFailures": false,
"TestResult": { "Format": "NUnitXml", "Path": "./test-results/maester.xml" }
}
}

Every section is optional.

Metadata​

Free-form values about the run, echoed in the result as RunMetadata. Use it to tag a run, for example with a RunId shared by the parts of a split run.

GlobalSettings​

Tenant-wide settings that tests read with Get-MtSetting. Maester knows these:

SettingTypeDefaultUsed by
EmergencyAccessAccountsarraynoneConditional Access checks. See Emergency access accounts.
DataverseEnvironmentUrlstringCopilot Studio checks.
GitHubOrganizationstringGitHub checks.
GitHubApiBaseUristringhttps://api.github.comGitHub checks.
GitHubApiVersionstring2022-11-28GitHub checks.
XspmExternalDataUrisobjectbuilt-in sourcesXSPM checks. See the overview.

Any other key is passed through. Custom tests and test packs should use a Namespace.Key name, for example Contoso.SiteUrl.

Selection​

Which tests run. Every key has a command-line equivalent.

KeyValuesDefaultEffect
BuiltInAll, NoneAllNone runs only your custom tests, like -SkipBuiltIn.
DefaultActionRun, SkipRunSkip is allow-list mode: a test runs only if a TestSettings row for its ID sets "Enabled": true. Others are NotRun with reason NotListed.
Productarray[]Run only tests of these products (like -Product): keys such as entra or exchange, or an alias such as Entra ID. Matches a test's Product or any of its RelatedProduct values.
Featurearray[]Run only tests about these features (like -Feature), for example conditional-access.
Areaarray[]Run only tests of these security areas (like -Area), for example authentication.
Severityarray[]Run only tests with these severities (like -Severity). The severity a TestSettings row sets is the one that counts.
Suitearray[]Run only tests of these suites (like -Suite), for example Maester, CISA, CIS, EIDSCA, ORCA or AD.
Tagarray[]Run only tests that any of these tags selects (like -Tag).
ExcludeTagarray[]Do not run tests that any of these tags selects (like -ExcludeTag).
TestIdarray[]Run only these IDs (like -TestId).
ExcludeTestIdarray[]Do not run these IDs (like -ExcludeTestId). Wins over TestId.
IncludePreviewboolfalseRun preview tests (like -IncludePreview).
IncludeLongRunningboolfalseRun long-running tests (like -IncludeLongRunning).
OnUnknownIdWarn, Ignore, ErrorWarnWhat to do when an ID you named matches no test. Error stops the run before any test runs.

Rules:

  • Product, Feature, Area, Severity and Suite select by the attributes of a test. The values of one selector are alternatives, and different selectors must all match: -Product entra, exchange -Severity High runs the high-severity tests of either product. They combine the same way with Tag and TestId. Values are compared case-insensitively and * wildcards are allowed. The keys are listed in MaesterTaxonomy.psd1, and each result names the ones it uses under Taxonomy.
  • A Pester-format test has no product, feature or area, so a run that selects by one of them leaves it out. Its suite is that of its folder, and its severity counts only when a TestSettings row sets it.
  • A command-line value replaces the list of the same name in the config.
  • Tags and IDs are compared case-insensitively. Tag lists match any tag; * wildcards are allowed.
  • Test IDs are exact or use * wildcards (CISA.MS.AAD.3.*). An ID named exactly runs even if the test is preview or long-running; a wildcard match does not lift those exclusions.
  • Preview tests (Lifecycle = 'Preview') are left out unless IncludePreview is set or any Tag is given. Long-running tests are left out unless IncludeLongRunning is set or Tag contains LongRunning or CAWhatIf. Selecting by product, feature, area, severity or suite does not lift either exclusion.
  • The tags All and Full were removed in 3.0 (in 2.x they were deprecated and selected nothing). Using them stops the run; use IncludePreview and IncludeLongRunning instead.
  • Tests that need Active Directory run only after Connect-Maester -Service ActiveDirectory, whatever the selection says.
  • In DefaultAction: Skip mode, a row with only Severity or Parameters does not admit a test, and admission is only the first gate: the selectors, tags and the preview and long-running rules still apply. A row on a family's parent ID or on any of its instance IDs admits the family. With no "Enabled": true rows at all, nothing runs.
  • Unknown IDs are listed in the result under Selection.UnknownIds. An ID under a declared family's parent ID is never unknown, so you can keep a row for an instance that does not exist in this run.

Tags​

A tag selects a test that carries it in the Tag of its [MaesterTest] attribute, or whose folder's suite.json lists it under Tags. Tags are for your own tests. The tests that ship with Maester no longer carry tags for their suite, product, topic or severity, because each of those is an attribute.

The tags of Maester 2.x keep working. -Tag CA, -Tag CIS, -ExcludeTag MS.AAD.3.5 and every other 2.x tag select the built-in tests that carried the tag in 2.x, and the ID of a built-in test is still one of its tags, so -Tag MT.1068 selects it. A run that selects through a 2.x tag writes one warning that names the tag and, where there is one, the selector that replaces it. Tests added after Maester 3.0 do not carry 2.x tags, so move to the selectors when you can:

2.x tagExampleNow
SuiteCISA, Maester-Suite cisa
Product or serviceEXO, Entra-Product exchange
TopicCA, Privileged-Feature conditional-access, -Area privileged-access
Benchmark control IDMS.AAD.3.5-TestId CISA.MS.AAD.3.5
SeveritySeverity:High-Severity High
A test's IDMT.1068-TestId MT.1068

The 2.x tags and the test ID are used for selection only. A result row's Tag lists the tags written in the test, and its Product, Feature, Area, Suite and Severity fields say the rest.

Deselected and disabled tests still appear in the result as NotRun rows with a reason code. See Applicability and reason codes.

TestSettings​

One row per test you want to change, built-in or custom, native or Pester-format.

KeyTypeEffect
IdstringThe test ID, or a family instance ID (MT.1024.<suffix>). Required.
SeveritystringCritical, High, Medium, Low or Info. Overrides the test's own severity.
Enabledboolfalse turns the test off: it is not run and appears as NotRun with reason DisabledByConfig. true admits it in DefaultAction: Skip mode.
ReasonstringShown as the reason of a disabled test.
ParametersobjectValues for the test's parameters, by name. Native tests only.
TimeoutSecondsintA timeout for this test, overriding Execution. Native tests only.

For a family, a row on an instance ID wins over a row on the parent ID. "Enabled": false on the parent ID turns off the whole family; on an instance ID it turns off only that instance, and the others still run. Parameters and TimeoutSeconds on an instance ID apply to that instance only: a parameter the instance row does not name keeps the value of the family's row, and "TimeoutSeconds": 0 turns the timeout off for that instance.

Parameter values are checked against the test's param() block before it runs. Use Get-MtTest <ID> to see a test's parameters, their types, defaults, allowed values and descriptions.

Parameter typeAccepted JSON value
intA whole number in the 32-bit range
bool, switchtrue or false
stringA string, or { "Id": "...", "DisplayName": "..." } (the test receives the Id)
string[]A string or an array of strings (or of { "Id": ... } objects)

A name the test does not have, a fraction, a value of the wrong type, a value outside the parameter's ValidateRange or ValidateSet, a value that does not match its kind, a common parameter name, or an engine-owned name (Instance, Mt*) gives that test an Error row with reason InvalidConfiguration. The rest of the run carries on.

Environment​

The facts Maester uses to decide whether a test applies, and which of those checks are enforced. Each fact is Auto (detect it, the default) or a forced value.

KeyValuesEffect
TenantTypeAuto, Workforce, ExternalForces the tenant type.
CloudAuto, Commercial, GCC, GCCHigh, DoD, China, Bleu, Delos, GovSGForces the cloud. Set GCC for a GCC tenant: Graph cannot tell GCC from Commercial.
LicensesAuto, or an array of service plan namesForces the tenant's licences.
ServicesAuto, or an object such as { "Teams": false, "Graph": true }Forces the connection state of the services named. Others are still detected.
EnforceobjectWhich checks skip tests: Service (default true), License (default true), TenantType (default false), Cloud (default false).

Forcing a service to true does not connect it: tests that need it run and fail if it is not really connected. Forcing is meant for runs split across processes, where every part should reach the same decisions. The detected and forced values are recorded in the result's TenantContext. See Applicability.

Execution​

KeyTypeDefaultEffect
TestTimeoutSecondsint0 (none)Stops a native test that runs longer and reports it as Error with reason Timeout.
LongRunningTimeoutSecondsint0 (use TestTimeoutSeconds)The timeout for tests marked LongRunning.
MaxParallelint1Reserved for parallel execution. Maester 3.0 runs tests one at a time and ignores it.

A test's own TestSettings[].TimeoutSeconds wins over LongRunningTimeoutSeconds, which wins over TestTimeoutSeconds; a more specific value of 0 turns the broader timeout off for those tests. Timeouts are off by default. On a host that stops a run after a period without console output, keep every timeout below that period: a test writes nothing while it runs.

Output​

KeyTypeDefaultEffect
ErrorsAsFailuresboolfalseWhen true, a test that threw (TestError, Timeout, InvalidReturn) fails the run's Result and is a failure in the XML file.
TestResult.PathstringnoneWrites NUnit or JUnit XML for native and Pester-format tests to this path. From a config file found by folder discovery, only a relative path inside the current folder is accepted; -Config and MAESTER_CONFIG may name any path.
TestResult.FormatstringNUnitXmlNUnitXml or JUnitXml.
ConsoleModestringAutoHow the console output is written: Interactive (a live status line with the result counts and the running test), Stream (lines only, for CI and redirected output), Plain (lines without colour or symbols) or Auto (Interactive on a terminal, Stream otherwise). -OutputMode and the MAESTER_OUTPUT_MODE environment variable win over it.
DashboardPanelsstring[]all of themThe panels of the full-screen dashboard of an interactive run, in order: Tenant (tenant name, primary domain and account), Totals (the results so far as a bar split by result, with the counts), Connections (the services of the run and which of them are connected), Failed (failed tests by severity), Slowest (the tests that took the longest), Drift (changes against the newest earlier results file in the output folder), FixFirst (failed tests of severity Critical or High whose fix has a low user impact and a low cost), Blog (the newest post on maester.dev), Version (not a panel: a newer Maester on the PowerShell Gallery is mentioned under the logo, as a link), Tips, Contributor (one of the people who built Maester, with a link to their page), Sponsor (one line with a link to the sponsor page), Pace (a graph of how long each test took), Results (one block per test) and the insight panels WeakestAreas (the areas with the lowest share of passed tests), AttackTactics (failed tests by MITRE ATT&CK tactic), FailedTests (one dot per failed test, by the user impact and the cost of its fix), NistFamilies (the NIST 800-53 control families with the lowest share of passed tests), PassRate (the share of passed tests from the first test to now) and EveryTest (how long each test took, in the order the tests finished). Pace, Results and the insight panels are in the main column (the graph at the top, next to the logo; the blocks under the product lanes; the insight panels under the list of tests, in rows of three when the console has the rows for them, and only in a run of 25 tests or more); the others show in a column on the right when the console is about 140 columns or wider, with Blog, Tips and Contributor at its bottom. Blog and Version each make one web request in the background, and are left out with -SkipVersionCheck. An empty list turns the panels off.
CIAnnotationsbooltrueOn GitHub Actions and Azure Pipelines, writes the first 20 Failed (as warnings) and Error (as errors) rows as annotations.

See CI test results for how each result maps to an XML outcome.

Checking what a configuration does​

# Which tests would run, and why the others would not
$r = Invoke-Maester -DryRun -PassThru
$r.MaesterConfig.ConfigSource
$r.Tests | Where-Object Result -EQ 'NotRun' | Group-Object ReasonCode

A -DryRun runs no native test and sends no mail or Teams message, but still writes the report files, so you can open the report and look at the plan. Pester-format custom tests still go through Pester discovery, which runs their BeforeDiscovery and Describe-level code.