Skip to main content
This is not an official U.S. federal government website. The Federal Modernization Workbook is independent technical analysis. It does not represent the position of, and has not been reviewed or endorsed by, any federal agency.

Federal Modernization Workbook

Chapter 10 — Drift and Evidence for the ISSO

Run Chapter 01’s register and Chapter 02’s ownership check on a schedule, compare the register with the last approved baseline by fingerprint, and raise the drift the plan names (ownerless, unused or expiring registrations, passwords that never expire, credentials added outside the pipeline, new consent grants), starting from the Sentinel rule Learn links for added credentials (pinned in the repository) and run offline; then read the review cycle’s records from Chapters 07 and 09, take Chapters 03 to 09’s findings through a small documented contract, compare the AI use-case inventory vintages, and package each run with a SHA-256 per file, the tenant ID and a hash chain into the period evidence for AC-2, AC-6(5), IA-4, IA-5, IA-9 and M-25-21, with labs against fixtures and a test tenant.

A register approved once proves the day it was approved. The ISSO needs to know what changed since, who changed it, and that nobody edited the record afterwards.

NoteIn this chapter

The evidence behind the project plan’s tasks NP7.1, NP7.2 and NP7.4 and its Gate 7, “steady state.” You start with the Sentinel analytics rule that Learn’s security operations guide for applications links for credentials added to existing applications, which the repository keeps as a pinned copy, and run its query offline over fictional audit rows to see what it catches and what it misses. Then you build the drift run in examples/non-person-identity: Chapter 01‘s register and Chapter 02’s ownership check, run together, compared with the last approved baseline by fingerprint, with the credential and consent events from Microsoft Graph’s directory audit log and the service principal sign-in activity report. You read the review cycle’s records and the other chapters’ findings through a small contract, compare one AI use-case inventory vintage with the last, and extend the first workbook’s examples/drift-pipeline with new files that package each run in a hash chain and assemble a period for the ISSO. Lab 1 runs on a laptop with no tenant; Lab 2 uses a test tenant.

TipNarrated tour
Building Non-Person Identity, Chapter 10 — Drift and Evidence for the ISSO: narrated tour · 11 minutes · captions from the player's CC button · Download MP4 · All videos
Jump to a slide (17)

Deck · Narrated deck (about 11 minutes, captions) · Script · Captions. Recorded on November 2, 2026 from the approved chapter.

Goal and audience

Goal. By the end of this chapter you can run the plan’s register drift check (NP7.1: “ownerless, unused or expiring registrations, passwords that never expire, credentials added outside the pipeline”) on a schedule against a baseline someone approved, show the review cycle (NP7.2) from the records the reviewing chapters write, and hand the ISSO a package and a period summary for AC-2, AC-6(5), IA-4, IA-5, IA-9 and M-25-21 that they can check with sha256sum alone (NP7.4), with the annual inventory vintage comparison beside it. For each rule you can say whether it is Learn’s, Microsoft’s sample, the first two workbooks’ pipeline, or the program’s design.

Audience. Developers and pipeline engineers who own the scheduled checks, the identity governance officer who owns NP7.2 and NP7.4, and the ISSO who receives the evidence and signs Gate 7. You should read PowerShell 7 and Python and know Chapters 01 and 02. You don’t need a tenant until Lab 2.

What the evidence has to show

Four pages already say what this evidence is for, and this chapter builds it without repeating them.

  • The plan. NP7.1 schedules the register drift; NP7.2 asks for “a full review cycle: owners’ quarterly permission review, sponsors’ attestation, expiry-based recertification of agent access”; NP7.4 asks for “evidence to the ISSO for AC-2, AC-6(5), IA-4, IA-5 and IA-9, and for M-25-21; the annual inventory vintage comparison scheduled.” Gate 7 asks that every service account and service principal has an owner and sponsor, a reason and a target, that none holds a password that never expires and none is orphaned, that drift checks and review cycles run, and that every remaining gMSA and dMSA is moved or in the dated Kerberos island (project plan).
  • Book 02, Chapter 10 sets the pattern: inventories committed as a baseline, each later run compared with it, new objects, removed objects and attribute changes as drift, and each finding closed at the source, never by editing the copy (Book 02, Chapter 10).
  • The companion guide’s federal requirements map the non-person identity track to AC-2, AC-6(5), IA-4, IA-5 and IA-9, and M-25-21 for AI, as the program’s reading; the assessor and the authorizing official decide each control (companion guide).
  • Book 17 sets the AI side: expiry is the agent’s review (Chapter 05), and each new vintage of the AI use-case inventory is compared with the last by use case ID for four kinds of change (Chapter 09).

Learn gives the facts the design turns on. Its security operations guide for applications rates credentials added to existing applications as high risk, says any credential added outside expected processes could be a malicious actor’s, and says application and service principal updates are logged as two entries.1 Its activity list names the events: Update application - Certificates and secrets management, Add service principal credentials, Consent to application, Add delegated permission grant and Add app role assignment to service principal, all logged by Core Directory in the ApplicationManagement category.2 Audit logs are kept 30 days with Microsoft Entra ID P1 or P2, and seven on Free.3 Learn’s recommendations call an application credential expiring when it ends within the next 30 days, and an application unused when it hasn’t been used for over 90 days.4 5

So the run has the same three parts as the first two workbooks’ evidence runs. Chapters 01 and 02 describe the state: what exists, who owns it, what it holds. The audit read describes the events in a window: who added a credential or a grant, and whether the pipeline did. A comparison with the approved baseline turns the state into drift, and a chain of windows with no gap between them is what “drift checks running” means in code. Design guidance.

Chapters 03 to 09 are written, and their outputs are fixed in each chapter. This chapter reads Chapter 01’s and Chapter 02’s run folders directly, and defines a small contract that the other chapters’ outputs can be written to: a findings file with id, rule, severity, chapter, objectKey and fingerprint, review records, and the rotation pipeline’s credential log (the contract). No chapter’s kit writes these files yet: the files in the repository are fixtures, and only powershell/NpiDrift/NpiDrift.psm1 reads them. Until a chapter writes its file, the run says so, and the period marks the controls that depend on it as incomplete.

Which cloud, for this read

The audit and sign-in reads call Microsoft Graph: graph.microsoft.com for GCC, which uses the worldwide endpoints; GCC High uses graph.microsoft.us and DoD dod-graph.microsoft.us.6 List directoryAudits is available in the global service, US Government L4 and US Government L5.7 The service principal sign-in activity report is beta and is listed for the global service only, not L4 or L5,8 so a GCC tenant can read it on the worldwide endpoint, and a GCC High or DoD run doesn’t send the call and records the unused check as not read (research gaps, row 93). The run takes the cloud as a required parameter with no default and uses Chapter 01’s reader, which sends the token only to that cloud’s host.

Start with Microsoft’s sample

What Microsoft gives you

Learn’s Microsoft Entra security operations guide for applications is Microsoft’s procedure for this chapter. Its “Application credentials” table says what to monitor (credentials added to existing applications, high risk; credentials with lifetimes longer than policy allows, medium), where (the audit log, filtered to Core Directory and ApplicationManagement), and when to alert (“outside of normal business hours or workflows”), and it links a pre-built Microsoft Sentinel rule, New access credential added to Application or Service Principal.9 Its “Application permissions” and “End-user consent” tables name the consent and grant activities to watch.10 Learn’s List directoryAudits gives the Graph read and its filters,11 and the recommendations for expiring credentials and unused applications give the thresholds.12 13

The pinned copy

The query lives in Microsoft’s Azure-Sentinel repository, so the repository pins the rule, unmodified, in examples/third_party/sentinel-app-credential-rule: Solutions/Microsoft Entra ID/Analytic Rules/NewAppOrServicePrincipalCredential.yaml at commit 2985095 (October 30, 2026), retrieved October 30, 2026, with the repository’s MIT license.14 Learn links the rule’s earlier path, which upstream now keeps as a stub pointing to this one. snapshot.json records each file’s SHA-256 and upstream git blob hash, checked with gh api before the manifest was written; a unit test checks the hashes, and the weekly source watch reports new commits to the rule.

tests/SentinelRuleHarness.ps1 reads the query from the pinned YAML at test time, splits it into its 20 pipeline stages, and evaluates each one it recognizes by its exact text over tests/fixtures/ch10/audit.json, ten fictional audit items shaped as the AuditLogs table’s columns. A stage it doesn’t recognize throws, so a new upstream query fails the tests instead of being half-run. It isn’t a KQL engine, and nothing is sent to a workspace.

What it leaves to you

Each row is pinned by a test: in the “as published” block of tests/NpiDrift.Tests.ps1 for the sample, and in the rest of that file or in examples/drift-pipeline/tests/test_npi_evidence.py for the code.

Microsoft’s rule, as published Why it matters What the code adds
Finds the client secret a person added to HR Connector, an app that already had a certificate — Kept as the model: the activity names and the KeyDescription key list, read from the same events (NPI-DRF-07)
Requires the initiator to carry an “@”: a user principal name, or an app whose display name has one A credential added by an app, whether the rotation pipeline or an app holding Application.ReadWrite.All (here Mail Automation adding a secret to Payroll Export), is never seen Every initiator is read, user or app; the pipeline is named in a reviewed list, not inferred
Keeps only events whose old key list isn’t empty The first secret on an app that had none, the riskiest kind, is dropped Every added key ID, compared old against new
Keeps only targets of type Application, even when a person adds the credential A credential added to a service principal (Add service principal credentials) is dropped; Learn notes such updates are logged as separate entries15 Service principal targets resolved to the app registration’s register row
Alerts on the pipeline’s own rotation when a person runs it It knows no pipeline and no change record, so every rotation alerts and the alert is ignored Inside the pipeline only when the key is in the credential log (Chapter 04) and the initiator is the person the log records as its operator
Parses only the first of several credentials added in one event A second key added at the same moment is missed Every key in the difference
Carries the initiator’s user principal name into the alert, split into Name and UPNSuffix Names travel wherever the alert goes Events kept as object IDs and key IDs; names dropped
Looks back one hour, in a Sentinel workspace, with no link to the register, the owners, the expiry or the consent grants Needs the workspace and its diagnostic setting before any evidence exists, and says nothing about the rest of NP7.1 The Graph audit read with AuditLog.Read.All, refused past the 30 days the log keeps,16 joined to the register, the baseline and Chapter 02’s ownership check

The verdict. Use Learn’s activity names and the rule’s KeyDescription comparison as the model for the credential read. Don’t use the rule as the NP7.1 check: it misses app-initiated and first credentials and service principal targets, can’t tell the pipeline from an attacker, and covers one item of five.

The code: what the drift run builds on and adds

The run is two pieces, each in its own files. examples/non-person-identity gains Invoke-NpiDriftRun.ps1, Invoke-NpiVintage.ps1, the module powershell/NpiDrift/NpiDrift.psm1, the call list config/calls-ch10.json, the thresholds config/drift-ch10.json, the contract notes docs/ch10-contract.md and its tests. examples/drift-pipeline gains python/npi_evidence.py, workflow/npi-evidence.yml, tests/test_npi_evidence.py and two fixture folders, as new files: drift_pipeline.py, pa_evidence.py and their tests are unchanged, and npi_evidence.py imports their hashing, JSON, fingerprint, refusal, row-reading and time helpers. Chapter 01’s and Chapter 02’s modules, scripts and tests are unchanged too.

The run

Invoke-NpiDriftRun.ps1 -Cloud gcc -TenantId <id> writes one folder per run, named by its time and mode, such as 2026-10-30T0600Z-fixture, as Building Privileged Access, Chapter 08 does:

  • inventory/, written by Chapter 01’s Invoke-NpiInventory.ps1, unchanged;
  • ownership/, written by Chapter 02’s Invoke-NpiOwnership.ps1, unchanged (its orphan plan is written and nothing is sent);
  • drift/, this chapter’s: events.json, signin-activity.json, findings.json, chapter-findings.json, review-cycle.json, summary.json, baseline-candidate.json and a manifest.json in the same shape as the other two;
  • run.json: the run ID, the as-of time, the cloud, the tenant ID, the mode, the audit window, the approved baseline’s run and SHA-256, the permissions, and each task’s status with the SHA-256 of its manifest.

The cloud and the tenant ID are required. In a live run the tenant ID must match the Graph session’s. A task that fails is recorded as Error in run.json and leaves no folder; the drift task needs the inventory, so it fails with it. The audit window runs from -Since (by default 24 hours before -AsOf) to -AsOf; the sample workflow starts each window where the last package’s ended. Design guidance.

The approved baseline

A run without -BaselinePath compares with nothing and writes drift/baseline-candidate.json: each identity’s fingerprint, a SHA-256 over 20 governance columns of its register row (owners, record, credentials and their next expiry, federated subjects, Graph permissions, grants, tenant-wide consent, privileged groups, delegation, SPNs, enabled), with the tenant, cloud, run and register hash. Last logon, password age and the stale lead are left out: they move every day and aren’t drift. A reviewer approves a candidate by passing its SHA-256 to the next run as -ApprovedBaselineSha256, as Chapter 02’s write path approves a plan; the run refuses a baseline whose hash differs, or that is for another tenant or cloud. Design guidance, following Book 02’s committed baseline.

The audit and sign-in reads

Get-NpiAuditEvent reads GET /v1.0/auditLogs/directoryAudits once for each activity name in config/drift-ch10.json, filtered by activityDateTime (ge, le) and activityDisplayName (eq), the filters Learn’s directoryAudit resource page lists.17 It goes through Chapter 01’s reader, so the call must be declared in config/calls-ch10.json, is a GET, goes only to the cloud’s Graph host, and follows every page. It refuses a window that starts more than 30 days before the run.18 Each event is kept as IDs: the activity and its class, the result, the initiator (user ID or app service principal ID), the target, and the key IDs, types and usages added or removed. Names and key display names are dropped. Design guidance.

Get-NpiSignInActivity reads GET /beta/reports/servicePrincipalSignInActivities for each app’s last sign-in, in clouds where Learn lists it.19

Task Calls Least-privileged permission Delegated role
drift (read) List directoryAudits; List servicePrincipalSignInActivities (beta) AuditLog.Read.All20 21 Reports Reader, Security Reader or Security Administrator22

With Chapter 01’s inventory and Chapter 02’s ownership tasks, the run needs four Graph application permissions (Application.Read.All, AuditLog.Read.All, Directory.Read.All, User.Read.All), none a write, Reader on the user-assigned identities, and the default Active Directory read. Directory.Read.All is Chapter 01’s, for delegated permission grants (Chapter 01’s research gaps, row 5); this chapter adds nothing broader than AuditLog.Read.All.

The register drift findings

Rule Severity What it flags Source
NPI-DRF-01 Warn An identity new since the approved baseline Book 02, Chapter 10: new objects are drift
NPI-DRF-02 Warn; Info when removed An identity whose fingerprint changed, with the columns that changed; one in the baseline that is gone Book 02, Chapter 10: attribute changes and removed objects
NPI-DRF-03 Finding Ownerless: an orphan candidate in Chapter 02’s run. The orphan rule decides; this code disables nothing Plan NP7.1; Chapter 02
NPI-DRF-04 Warn An app registration or legacy service principal with no sign-in for over 90 days, or none at all; one new since the baseline isn’t judged Learn: unused for over 90 days23
NPI-DRF-05 Warn A credential that expires within 30 days Learn: expiring within the next 30 days24
NPI-DRF-06 Finding An enabled AD account whose password never expires, except one found only that way (Chapter 02, NPI-OWN-09) Plan NP7.1 and Gate 7: none
NPI-DRF-07 Finding A credential added outside the pipeline, one per key Learn: credentials added outside expected processes, high25
NPI-DRF-08 Finding when tenant-wide or a high-privilege Graph permission; Warn otherwise A new consent grant or application permission, from the baseline and the audit events Learn: grants of highly privileged permissions, high26
NPI-DRF-09 Warn or Info An evidence gap: a contract file refused, a chapter that sent nothing, the sign-in report unavailable in the cloud, the ownership check failed Design guidance

Every finding has the stable ID Chapters 01 and 02 use, <rule>:<objectKey>, and a fingerprint: the first 16 hex characters of SHA-256 over <rule>|<objectKey>|, the first workbook’s fingerprint() with an empty issue, so the fingerprint stays the same however the issue text changes. The contract asks Chapters 03 to 09 to compute it the same way, and npi_evidence.py checks every one it is given.

Inside the pipeline. A credential counts as the pipeline’s only when two independent records agree: the target and key ID are in the credential log Chapter 04’s rotation writes, and the event’s initiator is the delegated person the log records as the operator who sent the rotation (the user’s object ID in operator). Every write is a person’s, so a service principal never counts, listed or not; a person adding a key the log lists under someone else, or a key it doesn’t list, is still NPI-DRF-07 (research gaps, row 96). Design guidance.

The review cycle

NP7.2 is the identity governance officer’s. Chapters 07 and 09 build the reviews; this chapter reads only their records, through the contract’s fmp-npi-review/1 files (no kit module writes them yet, and Chapter 09 has no sponsor attestation or recertification step, so those records are fixtures until one does), and writes review-cycle.json: for each app registration, legacy service principal, agent identity and blueprint, the last owners’ permission review, the last sponsor attestation, and the agent’s access assignments with their next expiry.

Rule Severity What it flags Source
NPI-REV-01 Finding An app registration holding Graph permissions with no owners’ permission review in the last 92 days Plan NP7.2: quarterly (Design guidance for the days)
NPI-REV-02 Finding An agent identity or blueprint with no sponsor attestation in the last 365 days Learn: sponsors attest every six to twelve months27
NPI-REV-03 Finding An agent access assignment with no end date, an end more than 365 days away, or recorded as active after it Book 17, Chapter 05: expiry is the review; no agent access without an end date (research gaps, row 97)
NPI-REV-04 Warn A kind of review record not received this run Design guidance: a missing record isn’t a passed review

The annual vintage comparison

Invoke-NpiVintage.ps1 compares this year’s AI use-case inventory vintage with last year’s by use case ID, Book 17, Chapter 09’s four changes, and joins both to the register through a use-case map (UseCaseId, RecordId) and Chapter 02’s service records. It writes vintage-<year>/ with its findings, the changes and a manifest holding the register’s and each input’s SHA-256.

Rule Severity What it flags
NPI-VIN-01 Finding; Info when matched A new piloted or deployed entry with no identity in the register
NPI-VIN-02 Finding An entry retired whose identities are still enabled: start the leaver process
NPI-VIN-03 Warn An authorization answer changed
NPI-VIN-04 Finding An entry that left the inventory without being reported as retired
NPI-VIN-05 Warn An agent identity, blueprint or AI record whose record maps to no use case in this vintage

The first four are Book 17, Chapter 09’s; the fifth is its “identity with no inventory row” check, run against the register.

The package and the chain

npi_evidence.py package writes <run-id>/ with the run as written (run/), a summary.md with rule IDs, object keys and fingerprints but no names, an EVIDENCE-MANIFEST.json and a SHA256SUMS that sha256sum -c reads. It checks the run first, as the first two workbooks’ pipelines do: run.json, each task manifest by its hash, every file against its manifest, nothing extra, every recorded request a GET or a Get-AD* read, one tenant, cloud and mode throughout, and every finding’s fingerprint. The manifest records the run, the cloud, the tenant ID, the mode, the audit window, the approved baseline, each task’s status, the run.json hash, the GitHub run identifiers when there are any, every open finding from every chapter by fingerprint, what opened and what closed since the previous package, a SHA-256 and size for every file, and the previous package’s run ID and manifest hash. That last hash makes the packages a chain: a replaced package or a missing day shows up as a break. The same inputs give the same bytes. verify is what the ISSO runs.

The packager doesn’t sign, for the reason the first workbook gives: the hashes show nothing changed after packaging, and a container with a locked time-based retention policy keeps blobs from being modified or deleted until the interval ends.28 Signing stays the ISSO’s choice (research gaps, row 99).

What the ISSO receives

npi_evidence.py period --since <start> --until <end> [--vintage <dir>] is the plan’s NP7.4. It verifies every package whose window falls in the period, checks the chain and that the windows cover the period with no gap, lists the findings opened and closed by fingerprint, and writes PERIOD-MANIFEST.json, period-summary.md and SHA256SUMS. The packages stay where they are, cited by their manifest hashes.

Control Rules and chapters that give its evidence (Design guidance)
AC-2 Account Management NPI-INV-02, -03; NPI-OWN-01, -02, -08; NPI-DRF-01 to -04; NPI-REV-02; Chapter 03
AC-6(5) Privileged Accounts NPI-INV-01, -06; NPI-DRF-08; NPI-REV-01; Chapter 07
IA-4 Identifier Management NPI-OWN-06, -07; NPI-INV-09; NPI-DRF-01; NPI-VIN-01, -05
IA-5 Authenticator Management NPI-INV-04, -05; NPI-DRF-05 to -07; Chapters 04 and 06
IA-9 Service Identification and Authentication NPI-DRF-07; Chapters 05 and 08
M-25-21 NPI-REV-02, -03; NPI-VIN-01 to -05; Chapter 09; the vintage comparison

For each control the period gives the open Findings and Warns at the period’s end and the sources it didn’t receive; its status is “evidence present; the assessor decides” or “evidence incomplete.” The mapping is the program’s reading, from the companion guide; it decides nothing about the control (research gaps, row 98). For Gate 7 it gives:

Gate 7 criterion What the period shows Who decides
Every service account and service principal has an owner and sponsor, a reason and a target NPI-DRF-03, NPI-REV-02, and whether Chapter 03’s decisions arrived The ISSO; Chapter 03’s dispositions
None holds a password that never expires NPI-DRF-06 at period end The ISSO
None is orphaned NPI-DRF-03 at period end The ISSO; the orphan rule (D-NP2)
Drift checks and review cycles running Packages, coverage gaps, chain breaks, unverified task runs; NPI-REV-01 to -04 The ISSO, from the period
Every remaining gMSA and dMSA moved or in the dated Kerberos island Chapter 06’s findings, when they arrive Identity engineering (NP7.3)

The schedule

workflow/npi-evidence.yml is a sample GitHub Actions workflow, not active in this repository. It follows the first workbook’s: Learn’s Azure Login sign-in with a federated credential and no secret, pinned actions, and a job that can request a token but write nothing else.29 It runs daily, starts each audit window where the last package’s ended, fetches the approved baseline and its SHA-256 from repository settings, and on the first of the month assembles the previous month’s period. It skips the Active Directory reads, which need a runner inside the domain. The first workbook’s workflow_lint.py checks it with no findings. The plan doesn’t set a frequency for NP7.1; daily is the sample’s choice. Design guidance.

The tests

Pester 5 under PowerShell 7: tests/NpiDrift.Tests.ps1, 51 tests, 11 of them Microsoft’s rule as published, beside the other chapters’ tests, unchanged. pytest: tests/test_npi_evidence.py, 21 tests over the two fixture runs and the vintage comparison; with the other workbooks’ tests, 142 in examples/drift-pipeline. A Pester test reruns the drift run and the vintage comparison from the fixtures and checks that their data files match the fixtures the Python tests read. None of them needs a tenant or a network.

ImportantIllustrative code

The commands below run Microsoft’s pinned Sentinel rule over fictional audit rows, and the example against a fictional estate served by fakes. The sample workflow has been parsed and linted, not run. They show what the code does, not that a tenant returns the same shapes. Review the code, and run Lab 2 in a test tenant, before it produces evidence anyone relies on.

Lab 1: fixtures, no tenant

You need PowerShell 7.1 or later with Pester 5, Python 3.10 or later with pytest and PyYAML, and a shell with sha256sum (Linux, macOS or Git Bash). Steps 1 to 6 run in examples/non-person-identity in PowerShell; steps 7 to 10 in examples/drift-pipeline in a POSIX shell; step 11 in both.

NoteEvery command in Lab 1

Every command in this lab, in order. Each step below says what it does and what to look for.

In PowerShell 7:

# Step 1: Run Microsoft's rule over the fixture
Invoke-Pester -Path tests/NpiDrift.Tests.ps1 -FullNameFilter '*as published*' -Output None -PassThru |
    Select-Object PassedCount, FailedCount, NotRunCount

# Step 2: List what the run needs
Import-Module ./powershell/NpiDrift/NpiDrift.psm1
(Get-NpiDriftPermission -Task drift-run).GraphPermissions -join ', '

# Step 3: Run the first day, with no baseline
$lab = @{ Cloud = 'gcc'; TenantId = 'c0ffee00-0000-4000-8000-000000000001'; OutputPath = 'out/runs'; InputPath = 'tests/fixtures/inputs'
    AuditFixturePath = 'tests/fixtures/ch10/audit.json'; SignInFixturePath = 'tests/fixtures/ch10/signin-activity.json'
    ContractPath = 'tests/fixtures/ch10/contract'; PipelineIdentitiesPath = 'tests/fixtures/ch10/inputs/pipeline-identities.csv' }
./Invoke-NpiDriftRun.ps1 @lab -FixturePath tests/fixtures/ch10/estate-day1.json -AsOf 2026-10-29T06:00:00Z

# Step 4: Approve the baseline and run the second day
$base = 'out/runs/2026-10-29T0600Z-fixture/drift/baseline-candidate.json'
$sha = (Get-FileHash $base -Algorithm SHA256).Hash.ToLower()
./Invoke-NpiDriftRun.ps1 @lab -FixturePath tests/fixtures/ch10/estate-day2.json -AsOf 2026-10-30T06:00:00Z -BaselinePath $base -ApprovedBaselineSha256 $sha

# Step 5: Read the drift
Get-Content out/runs/2026-10-30T0600Z-fixture/drift/findings.json -Raw | ConvertFrom-Json | Group-Object Rule, Severity | Sort-Object Name | Format-Table Name, Count

# Step 6: Compare the AI use-case inventory vintages
./Invoke-NpiVintage.ps1 -RegisterRun out/runs/2026-10-30T0600Z-fixture/inventory -Vintage 2026 `
    -CurrentPath tests/fixtures/ch10/vintage/ai-inventory-2026.csv -PriorPath tests/fixtures/ch10/vintage/ai-inventory-2025.csv `
    -UseCaseMapPath tests/fixtures/ch10/vintage/ai-use-cases.csv -ServiceRecordsPath tests/fixtures/inputs/service-records.csv -OutputPath out/vintage

In a POSIX shell (Git Bash on Windows):

# Step 7: Package both days, chained
DAY1=tests/fixtures/npi-runs/2026-10-29T0600Z-fixture
DAY2=tests/fixtures/npi-runs/2026-10-30T0600Z-fixture
python python/npi_evidence.py package --run $DAY1 --out out/evidence
python python/npi_evidence.py package --run $DAY2 \
  --previous-manifest out/evidence/2026-10-29T0600Z-fixture/EVIDENCE-MANIFEST.json --out out/evidence

# Step 8: Verify the second package
python python/npi_evidence.py verify out/evidence/2026-10-30T0600Z-fixture
(cd out/evidence/2026-10-30T0600Z-fixture && sha256sum -c --quiet SHA256SUMS)
grep -c contoso out/evidence/2026-10-30T0600Z-fixture/summary.md

# Step 9: Assemble the period for the ISSO, with the vintage
python python/npi_evidence.py period --evidence out/evidence --since 2026-10-28T06:00:00Z --until 2026-10-30T06:00:00Z \
  --vintage tests/fixtures/npi-vintage/vintage-2026 --out out/periods
cat out/periods/period-2026-10-28_2026-10-30/period-summary.md

# Step 10: Break it on purpose
echo edited >> out/evidence/2026-10-30T0600Z-fixture/summary.md
python python/npi_evidence.py verify out/evidence/2026-10-30T0600Z-fixture
python python/npi_evidence.py package --run $DAY1 --previous-manifest out/evidence/2026-10-30T0600Z-fixture/EVIDENCE-MANIFEST.json --out out/swap
python python/workflow_lint.py workflow/npi-evidence.yml

In PowerShell 7:

# Step 11: Run the tests
Invoke-Pester -Path tests

In a POSIX shell (Git Bash on Windows):

# Step 11: Run the tests
python -m pytest tests -q

In PowerShell 7:

# After the steps
Remove-Item -Recurse -Force out

In a POSIX shell (Git Bash on Windows):

# After the steps
rm -rf out
  1. Run Microsoft’s rule over the fixture:

    Invoke-Pester -Path tests/NpiDrift.Tests.ps1 -FullNameFilter '*as published*' -Output None -PassThru |
        Select-Object PassedCount, FailedCount, NotRunCount

    Terminal window showing Invoke-Pester -Path tests/NpiDrift.Tests.ps1 -FullNameFilter '*as published*'… and what it printed

    What the terminal shows, run on the kit’s fixtures (the same output as text)

    PassedCount 11, FailedCount 0, NotRunCount 40. Read the third and fifth tests: of the four credentials added in the second day’s window, the rule finds one.

  2. List what the run needs:

    Import-Module ./powershell/NpiDrift/NpiDrift.psm1
    (Get-NpiDriftPermission -Task drift-run).GraphPermissions -join ', '

    Terminal window showing Import-Module ./powershell/NpiDrift/NpiDrift.psm1 and what it printed

    What the terminal shows, run on the kit’s fixtures (the same output as text)

    Application.Read.All, AuditLog.Read.All, Directory.Read.All, User.Read.All. The drift task alone needs AuditLog.Read.All. Nothing ends in ReadWrite.

  3. Run the first day, with no baseline:

    $lab = @{ Cloud = 'gcc'; TenantId = 'c0ffee00-0000-4000-8000-000000000001'; OutputPath = 'out/runs'; InputPath = 'tests/fixtures/inputs'
        AuditFixturePath = 'tests/fixtures/ch10/audit.json'; SignInFixturePath = 'tests/fixtures/ch10/signin-activity.json'
        ContractPath = 'tests/fixtures/ch10/contract'; PipelineIdentitiesPath = 'tests/fixtures/ch10/inputs/pipeline-identities.csv' }
    ./Invoke-NpiDriftRun.ps1 @lab -FixturePath tests/fixtures/ch10/estate-day1.json -AsOf 2026-10-29T06:00:00Z
    Run 2026-10-29T0600Z-fixture (Fixture, gcc): inventory OK, ownership OK, drift OK
    Audit window 2026-10-28T06:00:00Z to 2026-10-29T06:00:00Z; baseline none: this run is a baseline candidate
    Drift findings: 14 Finding, 4 Warn, 5 Info; baseline candidate SHA-256 604ee088ae43a964072671234833aeb71bccd082fa9ce4032a55d2fc4b146cc8
    Written to <path>/out/runs/2026-10-29T0600Z-fixture (run.json lists each task's manifest hash)

    Terminal window showing $lab = @{ Cloud = 'gcc'; TenantId = 'c0ffee00-0000-4000-8000-000000000001'; O… and what it printed

    What the terminal shows, run on the kit’s fixtures (the same output as text)
  4. Approve the baseline and run the second day:

    $base = 'out/runs/2026-10-29T0600Z-fixture/drift/baseline-candidate.json'
    $sha = (Get-FileHash $base -Algorithm SHA256).Hash.ToLower()
    ./Invoke-NpiDriftRun.ps1 @lab -FixturePath tests/fixtures/ch10/estate-day2.json -AsOf 2026-10-30T06:00:00Z -BaselinePath $base -ApprovedBaselineSha256 $sha
    Run 2026-10-30T0600Z-fixture (Fixture, gcc): inventory OK, ownership OK, drift OK
    Audit window 2026-10-29T06:00:00Z to 2026-10-30T06:00:00Z; baseline 2026-10-29T0600Z-fixture (604ee088ae43…)
    Drift findings: 18 Finding, 11 Warn, 5 Info; baseline candidate SHA-256 fbe346457c070d6bb7de0fbb43f35d54315b42d46e6ba26b245d33efa6dc840c
    Written to <path>/out/runs/2026-10-30T0600Z-fixture (run.json lists each task's manifest hash)

    Terminal window showing $base = 'out/runs/2026-10-29T0600Z-fixture/drift/baseline-candidate.json' and what it printed

    What the terminal shows, run on the kit’s fixtures (the same output as text)

    In a real run a second person reviews the candidate and passes you its hash. The candidate records the register’s SHA-256, and every chapter writes LF line endings on every platform, so the two hashes are the same on Windows, Linux and macOS.

  5. Read the drift:

    Get-Content out/runs/2026-10-30T0600Z-fixture/drift/findings.json -Raw | ConvertFrom-Json | Group-Object Rule, Severity | Sort-Object Name | Format-Table Name, Count
    Name                Count
    ----                -----
    NPI-DRF-01, Warn        2
    NPI-DRF-02, Warn        6
    NPI-DRF-03, Finding     7
    NPI-DRF-04, Warn        2
    NPI-DRF-06, Finding     3
    NPI-DRF-07, Finding     3
    NPI-DRF-08, Finding     1
    NPI-DRF-09, Info        5
    NPI-DRF-09, Warn        1
    NPI-REV-01, Finding     2
    NPI-REV-02, Finding     1
    NPI-REV-03, Finding     1

    Terminal window showing Get-Content out/runs/2026-10-30T0600Z-fixture/drift/findings.json -Raw | Conv… and what it printed

    What the terminal shows, run on the kit’s fixtures (the same output as text)

    NPI-DRF-07 holds the three credentials added outside the pipeline: a secret a person added to HR Connector, a secret Mail Automation added to Payroll Export’s service principal, and the first secret on Build Pipeline. The pipeline’s two certificate rotations, on Deploy Bot and Vendor Portal, aren’t there: both are by the person the credential log names as the operator, and in the log. Vendor Portal’s expiring certificate (NPI-DRF-05 on day 1) is gone. NPI-DRF-08 is Mail Automation’s new grant for all users, with the two audit events behind it. NPI-DRF-04 is Legacy Reports (last sign-in May 1) and Old SAML Connector (none); Survey Tool, new since the baseline, isn’t judged. NPI-DRF-09’s Warn is the sample Chapter 05 file, refused because its fingerprint is wrong; its Infos are the five chapters that sent nothing. Open events.json: object IDs and key IDs, no names. Open review-cycle.json: Payroll Export’s last owners’ review is 2026-09-15; Mail Automation’s, 2026-05-01, is too old (NPI-REV-01).

  6. Compare the AI use-case inventory vintages:

    ./Invoke-NpiVintage.ps1 -RegisterRun out/runs/2026-10-30T0600Z-fixture/inventory -Vintage 2026 `
        -CurrentPath tests/fixtures/ch10/vintage/ai-inventory-2026.csv -PriorPath tests/fixtures/ch10/vintage/ai-inventory-2025.csv `
        -UseCaseMapPath tests/fixtures/ch10/vintage/ai-use-cases.csv -ServiceRecordsPath tests/fixtures/inputs/service-records.csv -OutputPath out/vintage

    Terminal window showing ./Invoke-NpiVintage.ps1 -RegisterRun out/runs/2026-10-30T0600Z-fixture/invent… and what it printed

    What the terminal shows, run on the kit’s fixtures (the same output as text)

    Vintage 2026: 5 changes (1 AuthorizationChanged, 1 Left, 2 New, 1 Retired); findings 3 Finding, 2 Warn, 1 Info. UC-005 is a new deployed agentic entry with no identity, UC-002 was retired while Mail Automation is still enabled, UC-004 left without being retired, and the Benefits Q&A agent identity maps to no use case. Delete out when you’re done; the runs in step 7 are the repository’s copies of these two, which a Pester test checks against a fresh run.

  7. Package both days, chained. In examples/drift-pipeline:

    DAY1=tests/fixtures/npi-runs/2026-10-29T0600Z-fixture
    DAY2=tests/fixtures/npi-runs/2026-10-30T0600Z-fixture
    python python/npi_evidence.py package --run $DAY1 --out out/evidence
    python python/npi_evidence.py package --run $DAY2 \
      --previous-manifest out/evidence/2026-10-29T0600Z-fixture/EVIDENCE-MANIFEST.json --out out/evidence
    {"evidence_manifest_sha256": "12532bfb598dc4303f1d60aa7dba1c401b055d36aa666b5893890a2666f09e32", "new": 62, "open": 62, "package": "2026-10-29T0600Z-fixture", "resolved": 0}
    {"evidence_manifest_sha256": "7676170f011129acd75a888fe8625acdc32297cf87c49ffbbd6a4e558375fc80", "new": 19, "open": 80, "package": "2026-10-30T0600Z-fixture", "resolved": 1}

    Terminal window showing DAY1=tests/fixtures/npi-runs/2026-10-29T0600Z-fixture and what it printed

    What the terminal shows, run on the kit’s fixtures (the same output as text)

    Every chapter’s open findings are counted, Chapters 01 and 02’s with them, and compared by fingerprint: 19 opened on day 2, one closed (Vendor Portal’s NPI-DRF-05).

  8. Verify the second package:

    python python/npi_evidence.py verify out/evidence/2026-10-30T0600Z-fixture
    (cd out/evidence/2026-10-30T0600Z-fixture && sha256sum -c --quiet SHA256SUMS)
    grep -c contoso out/evidence/2026-10-30T0600Z-fixture/summary.md

    Terminal window showing python python/npi_evidence.py verify out/evidence/2026-10-30T0600Z-fixture and what it printed

    What the terminal shows, run on the kit’s fixtures (the same output as text)

    verify reports 21 files and the manifest hash 7676170f…, sha256sum exits 0, and grep prints 0: no user principal name in the summary. In EVIDENCE-MANIFEST.json, previous.evidence_manifest_sha256 is day 1’s hash and tenant_id is the fixture tenant’s.

  9. Assemble the period for the ISSO, with the vintage:

    python python/npi_evidence.py period --evidence out/evidence --since 2026-10-28T06:00:00Z --until 2026-10-30T06:00:00Z \
      --vintage tests/fixtures/npi-vintage/vintage-2026 --out out/periods
    cat out/periods/period-2026-10-28_2026-10-30/period-summary.md

    Terminal window showing python python/npi_evidence.py period --evidence out/evidence --since 2026-10-… and what it printed

    What the terminal shows, run on the kit’s fixtures (the same output as text)

    Exit 2: {"chain_breaks": 0, "closed": 1, "complete": false, "coverage_gaps": 0, "opened": 19, "packages": 2, ...}. The control table shows AC-2 incomplete without Chapter 03, IA-5 without Chapters 04 and 06, IA-9 without Chapters 05 and 08, and AC-6(5), IA-4 and M-25-21 with evidence present. In the Gate 7 table, “None holds a password that never expires” (three accounts) and “None is orphaned” (seven candidates) are not met, the review cycle is not met (four review findings), and the owner-and-reason and Kerberos island criteria are “not decided by this code” until Chapters 03 and 06 send their findings.

  10. Break it on purpose:

    echo edited >> out/evidence/2026-10-30T0600Z-fixture/summary.md
    python python/npi_evidence.py verify out/evidence/2026-10-30T0600Z-fixture
    python python/npi_evidence.py package --run $DAY1 --previous-manifest out/evidence/2026-10-30T0600Z-fixture/EVIDENCE-MANIFEST.json --out out/swap
    python python/workflow_lint.py workflow/npi-evidence.yml

    Terminal window showing echo edited >> out/evidence/2026-10-30T0600Z-fixture/summary.md and what it printed

    What the terminal shows, run on the kit’s fixtures (the same output as text)

    verify exits 1: summary.md doesn't match its SHA-256. The swapped package exits 1: this run (2026-10-29T06:00:00Z) isn't later than the previous package (2026-10-30T06:00:00Z). The workflow lints with 0 finding(s), 0 warning(s).

  11. Run the tests. In examples/non-person-identity, in PowerShell:

    Invoke-Pester -Path tests

    Terminal window showing Invoke-Pester -Path tests and what it printed

    What the terminal shows, run on the kit’s fixtures (the same output as text)

    In examples/drift-pipeline, in the POSIX shell:

    python -m pytest tests -q

    Terminal window showing python -m pytest tests -q and what it printed

    What the terminal shows, run on the kit’s fixtures (the same output as text)

Expected result: the Pester run passes 513 tests, and the pytest run passes 142. Delete out in both folders when you’re done. In examples/non-person-identity:

Remove-Item -Recurse -Force out

Terminal window showing Remove-Item -Recurse -Force out and what it printed

What the terminal shows, run on the kit’s fixtures (the same output as text)

In examples/drift-pipeline:

rm -rf out

Terminal window showing rm -rf out and what it printed

What the terminal shows, run on the kit’s fixtures (the same output as text)

Lab 2: test tenant

Use a test tenant in the worldwide or GCC cloud with Microsoft Entra ID P1 or P2, never production, with fictional people. The run only reads; the changes in step 2 are made by hand in the test tenant. Steps 1 to 5 run in examples/non-person-identity in PowerShell on Chapter 01’s test host; step 6 in examples/drift-pipeline in a POSIX shell. You’ll replace: <tenant-id> (the test tenant’s ID), <app-id> and <thumbprint> (the reader app you register in step 1, and its certificate), <test-domain> (the test domain’s DNS name), <inputs> (Chapter 02’s Lab 2 inputs folder), <pipeline-identities.csv> (your pipeline identities, in the format of tests/fixtures/ch10/inputs/pipeline-identities.csv), <baseline-run> and <drift-run> (the folders the runs in steps 1 and 3 write under out/runs-test), and <sha> (the baseline candidate’s SHA-256, as the reviewer passes it to you).

NoteEvery command in Lab 2

Every command in this lab, in order. Each step below says what it does and what to look for.

In PowerShell 7:

# Step 1: Register the reader app
Connect-MgGraph -Environment Global -TenantId <tenant-id> -ClientId <app-id> -CertificateThumbprint <thumbprint>
Connect-AzAccount -ServicePrincipal -Environment AzureCloud -Tenant <tenant-id> -ApplicationId <app-id> -CertificateThumbprint <thumbprint>
./Invoke-NpiDriftRun.ps1 -Cloud gcc -TenantId <tenant-id> -Domain <test-domain> -InputPath <inputs> -OutputPath out/runs-test -PipelineIdentitiesPath <pipeline-identities.csv>

# Step 1: Register the reader app
(Get-FileHash out/runs-test/<baseline-run>/drift/baseline-candidate.json -Algorithm SHA256).Hash.ToLower()

# Step 3: Run the drift run
Connect-MgGraph -Environment Global -TenantId <tenant-id> -ClientId <app-id> -CertificateThumbprint <thumbprint>
Connect-AzAccount -ServicePrincipal -Environment AzureCloud -Tenant <tenant-id> -ApplicationId <app-id> -CertificateThumbprint <thumbprint>
./Invoke-NpiDriftRun.ps1 -Cloud gcc -TenantId <tenant-id> -Domain <test-domain> -InputPath <inputs> -OutputPath out/runs-test `
    -BaselinePath out/runs-test/<baseline-run>/drift/baseline-candidate.json -ApprovedBaselineSha256 <sha> -PipelineIdentitiesPath <pipeline-identities.csv>

# Step 4: Check what the fixture assumed
Get-Content out/runs-test/<drift-run>/drift/events.json -Raw | ConvertFrom-Json | Format-Table ActivityDateTime, Activity, TargetType, TargetId, @{ n = 'KeysAdded'; e = { $_.KeysAdded.KeyId -join ' ' } } -Wrap
Get-Content out/runs-test/<drift-run>/drift/findings.json -Raw | ConvertFrom-Json | Where-Object Rule -in 'NPI-DRF-07', 'NPI-DRF-08' | Format-Table Rule, Object, Issue -Wrap
Get-Content out/runs-test/<drift-run>/drift/signin-activity.json -Raw | ConvertFrom-Json | ConvertTo-Json -Depth 5

In a POSIX shell (Git Bash on Windows):

# Step 6: Package and verify locally
RUN1=../non-person-identity/out/runs-test/<baseline-run>
RUN2=../non-person-identity/out/runs-test/<drift-run>
python python/npi_evidence.py package --run $RUN1 --out out/evidence-test
python python/npi_evidence.py package --run $RUN2 \
  --previous-manifest out/evidence-test/<baseline-run>/EVIDENCE-MANIFEST.json --out out/evidence-test
python python/npi_evidence.py verify out/evidence-test/<drift-run>
(cd out/evidence-test/<drift-run> && sha256sum -c --quiet SHA256SUMS)

In PowerShell 7:

# Step 7: Back it out
Remove-Item -Recurse -Force out/runs-test

In a POSIX shell (Git Bash on Windows):

# Step 7: Back it out
rm -rf out/evidence-test
  1. Register the reader app in the tenant’s own cloud (for GCC, the worldwide Microsoft Entra admin center) with a certificate. Grant the four application permissions from Lab 1, step 2, with admin consent, and nothing else; grant Reader on the test subscription. With Chapter 02’s Lab 2 inputs in place, run step 3’s command once without -BaselinePath and -ApprovedBaselineSha256, and have a second person review that run’s drift/baseline-candidate.json and pass you its SHA-256.

    1. Entra ID > App registrations > New registration; name it and Register. Its Application (client) ID is <app-id>.

    2. Certificates & secrets > Certificates > Upload certificate, upload a test certificate’s public part, and Add. Its Thumbprint is <thumbprint>.

    3. API permissions > Add a permission > Microsoft Graph > Application permissions; select Application.Read.All, AuditLog.Read.All, Directory.Read.All and User.Read.All, then Add permissions and Grant admin consent for your tenant.

    4. In the Azure portal, on the test subscription, Access control (IAM) > Add > Add role assignment; select Reader, Next, Select members, the app, Select, and Review + assign.

    5. On the test host, the first run, with no baseline:

      Connect-MgGraph -Environment Global -TenantId <tenant-id> -ClientId <app-id> -CertificateThumbprint <thumbprint>
      Connect-AzAccount -ServicePrincipal -Environment AzureCloud -Tenant <tenant-id> -ApplicationId <app-id> -CertificateThumbprint <thumbprint>
      ./Invoke-NpiDriftRun.ps1 -Cloud gcc -TenantId <tenant-id> -Domain <test-domain> -InputPath <inputs> -OutputPath out/runs-test -PipelineIdentitiesPath <pipeline-identities.csv>

      The reviewer reads out/runs-test/<baseline-run>/drift/baseline-candidate.json and computes the hash they pass you:

      (Get-FileHash out/runs-test/<baseline-run>/drift/baseline-candidate.json -Algorithm SHA256).Hash.ToLower()
  2. Make three changes to see. As a test administrator: add a client secret to a test app registration that has a certificate; add the first secret to a second test app that has no credential; and grant admin consent for a delegated Graph permission to a third test app.

    1. On the first test app, Certificates & secrets > Client secrets > New client secret, then Add.
    2. On the second test app, the same.
    3. On the third test app, API permissions > Add a permission > Microsoft Graph > Delegated permissions; select one permission, Add permissions, and Grant admin consent for your tenant.
  3. Run the drift run with your own inputs and the approved baseline:

    Connect-MgGraph -Environment Global -TenantId <tenant-id> -ClientId <app-id> -CertificateThumbprint <thumbprint>
    Connect-AzAccount -ServicePrincipal -Environment AzureCloud -Tenant <tenant-id> -ApplicationId <app-id> -CertificateThumbprint <thumbprint>
    ./Invoke-NpiDriftRun.ps1 -Cloud gcc -TenantId <tenant-id> -Domain <test-domain> -InputPath <inputs> -OutputPath out/runs-test `
        -BaselinePath out/runs-test/<baseline-run>/drift/baseline-candidate.json -ApprovedBaselineSha256 <sha> -PipelineIdentitiesPath <pipeline-identities.csv>
  4. Check what the fixture assumed. In drift/events.json, record the exact activityDisplayName of each credential event, hyphen or dash (research gaps, row 91); whether a secret added to the service principal is logged as Add service principal credentials, Update service principal, or both (row 92); and whether KeysAdded holds the key IDs, which means the KeyDescription format is the one Microsoft’s rule parses. NPI-DRF-07 should list both secrets, NPI-DRF-08 the consent. In drift/signin-activity.json, record whether the beta report returned rows (row 93). The three files, in that order:

    Get-Content out/runs-test/<drift-run>/drift/events.json -Raw | ConvertFrom-Json | Format-Table ActivityDateTime, Activity, TargetType, TargetId, @{ n = 'KeysAdded'; e = { $_.KeysAdded.KeyId -join ' ' } } -Wrap
    Get-Content out/runs-test/<drift-run>/drift/findings.json -Raw | ConvertFrom-Json | Where-Object Rule -in 'NPI-DRF-07', 'NPI-DRF-08' | Format-Table Rule, Object, Issue -Wrap
    Get-Content out/runs-test/<drift-run>/drift/signin-activity.json -Raw | ConvertFrom-Json | ConvertTo-Json -Depth 5
  5. Cross-check against Learn’s procedure. In the admin center, open Monitoring & health > Audit logs, filter on the ApplicationManagement category and the window, as Learn’s security operations guide describes:30 every credential and consent event there should be in events.json. If the tenant sends audit logs to a Log Analytics workspace, run Microsoft’s rule there: it should return the first secret’s event and not the second.

  6. Package and verify locally. Run Lab 1, steps 7 and 8, on the test run. In examples/drift-pipeline, package both runs, chained, and verify the second:

    RUN1=../non-person-identity/out/runs-test/<baseline-run>
    RUN2=../non-person-identity/out/runs-test/<drift-run>
    python python/npi_evidence.py package --run $RUN1 --out out/evidence-test
    python python/npi_evidence.py package --run $RUN2 \
      --previous-manifest out/evidence-test/<baseline-run>/EVIDENCE-MANIFEST.json --out out/evidence-test
    python python/npi_evidence.py verify out/evidence-test/<drift-run>
    (cd out/evidence-test/<drift-run> && sha256sum -c --quiet SHA256SUMS)
  7. Back it out. Remove both secrets and the consent grant from the test apps, remove the four permissions and the Reader assignment, delete the reader app’s registration and certificate, and delete out/runs-test and the packages: they hold the tenant’s identities and owners. If you ran the sample workflow in a test repository, follow the first workbook’s Chapter 09, Lab 2 for its federated credential, an unlocked retention policy, and its back-out. The folders, in examples/non-person-identity:

    Remove-Item -Recurse -Force out/runs-test

    And in examples/drift-pipeline:

    rm -rf out/evidence-test

In production, lock the evidence container’s retention policy once Lab 2 passes; Learn says an unlocked policy is for testing only.31

Verified facts

Item Value US Government clouds Stage Source
List directoryAudits GET /auditLogs/directoryAudits; AuditLog.Read.All (higher: Directory.Read.All); delegated roles Reports Reader, Security Administrator, Security Reader; $filter (eq, ge, le, startswith), $top, $orderby, skiptoken L4 and L5 v1.0 32
directoryAudit filters activityDateTime eq, ge, le; activityDisplayName eq, startswith; result success, failure, timeout — v1.0 33
Activity names Update application - Certificates and secrets management, Add service principal credentials, Remove service principal credentials, Consent to application, Add delegated permission grant, Add app role assignment to service principal; Core Directory, ApplicationManagement — — 34
Monitoring application credentials and consent Credentials added to existing applications, high; application and service principal updates logged as two entries; alert outside normal workflows; grants of highly privileged permissions, high; end-user consent, low — — 35
List servicePrincipalSignInActivities GET /reports/servicePrincipalSignInActivities; AuditLog.Read.All (higher: Directory.Read.All); Reports Reader, Security Reader, Security Administrator Global service only; L4 and L5 not available Beta 36
Expiring application credentials On an app registration and expiring within the next 30 days — Preview 37
Unused applications Not used for over 90 days; managed identities and apps created within 90 days exempt; Learn’s action plan soft-deletes, then permanently deletes after 15 days — Preview 38
Audit log retention Free seven days; P1 and P2 30 days — — 39
Agent sponsors Attest every six to twelve months that each agent is still needed; quarterly review for missing sponsors, outdated metadata or no recent activity — Agent ID: see below 40 41
Graph hosts GCC graph.microsoft.com; GCC High graph.microsoft.us; DoD dod-graph.microsoft.us — — 42
Sign-in from GitHub Actions Azure Login with a federated credential; the job needs id-token: write; US Government audience api://AzureADTokenExchangeUSGov Azure Government environment Learn’s samples use azure/login@v2; the sample workflow pins v3.1.0 (Design guidance) 43
Immutable evidence Time-based retention; a locked policy can’t be deleted or shortened; unlocked is for testing — GA 44

Where Learn’s pages differ, recorded so a later edit doesn’t loosen the code:

  • The credential activity’s name. The activity list writes Update application - Certificates and secrets management;45 the security operations guide writes Update Application-Certificates and secrets management.46 The code filters on the activity list’s name, the one Microsoft’s rule also uses, and normalizes a dash; Lab 2 records the live value (research gaps, row 91).
  • Service principal credentials. The activity list has Add service principal credentials;47 the security operations guide’s filter for added credentials names Update Service principal/Update Application.48 The code reads the activity list’s name; Lab 2 records which a tenant logs (row 92).
  • Unused applications. The recommendation’s action plan deletes an unused app and permanently deletes it after 15 days;49 Learn’s governance guidance for service accounts says to disable an account, not delete it, until the review is complete.50 The code follows the stricter: unused is a Warn for the owner, and the orphan rule disables (row 95).
  • Agent ID’s stage. Learn’s release notes list the Agent ID platform as generally available in April 2026, and the current Agent ID overview has no preview notice; the older overview address that showed one now opens it.51 This chapter only reads the review records Chapter 09 writes; the stage of each agent feature is Chapter 09’s to record.
  • The linked rule’s path. The security operations guide links Detections/AuditLogs/NewAppOrServicePrincipalCredential.yaml,52 which upstream now keeps as a stub pointing to the solution path pinned here.53

Federal requirements and evidence

The drift run and its packages are the evidence behind the guide’s G6 Lifecycle gate and its Evidence section: checks on the continuous monitoring cadence, filed with the date, the tenant and the version. What the ISSO receives maps the rules to controls; this table maps each Lab step and each file the code writes. The NIST SP 800-53 Rev. 5 controls are in the Moderate baseline, except IA-9, which is in none.54

What the output proves Requirement it supports KSI it aligns to Who keeps it
Microsoft’s analytics rule for credentials added to applications, run as published over the fixture, is the model the credential check keeps (Lab 1, step 1: NpiDrift.Tests.ps1, 11 tests) SI-4 System Monitoring KSI-IAM-SUS, KSI-MLA-RVL, KSI-SVC-EIS The repository’s tests; the pinned copy
The run needs four Graph read permissions, Reader and the default AD read, none a write (Lab 1, step 2, and Lab 2, step 1: Get-NpiDriftPermission -Task drift-run) AC-6 Least Privilege KSI-IAM-ELP, KSI-IAM-JIT Identity engineering; the ISSO keeps the consent record
A fingerprint over 20 governance columns of every identity, offered as the baseline, and used only when a reviewer approves its SHA-256 (Lab 1, steps 3 and 4: drift/baseline-candidate.json; -ApprovedBaselineSha256) CM-2 Baseline Configuration; CM-3 Configuration Change Control KSI-CMT-RMV, KSI-MLA-EVC, KSI-SVC-ACM (CM-2); KSI-CMT-RVP (CM-3) The reviewer who approves the hash
What changed since the baseline: new, changed and removed identities, ownerless ones, expiring credentials, passwords that never expire (Lab 1, step 5, and Lab 2, step 3: drift/findings.json, NPI-DRF-01 to -06) CA-7 Continuous Monitoring; AC-2 Account Management KSI-MLA-EVC (CA-7); KSI-IAM-SNU (AC-2) The identity governance office
A credential added outside the pipeline, and a new consent grant or application permission, from the directory audit log (drift/events.json; drift/findings.json, NPI-DRF-07 and -08) AC-2(4) Automated Audit Actions; AU-6 Audit Record Review, Analysis, and Reporting; SI-4 KSI-MLA-LET, KSI-MLA-RVL (AC-2(4), AU-6); KSI-IAM-SUS (SI-4) Security operations
Each app’s last sign-in, where the cloud has the report, and the apps unused for over 90 days (drift/signin-activity.json; NPI-DRF-04) AC-2(3) Disable Accounts (inactive accounts) KSI-IAM-AAM The identity governance office
The last owners’ permission review, the last sponsor attestation and each agent assignment’s end date (drift/review-cycle.json; NPI-REV-01 to -04) AC-6(7) Review of User Privileges; AC-2 KSI-IAM-AAM, KSI-IAM-JIT The identity governance officer (NP7.2)
The other chapters’ findings, read through the contract, and any evidence gap (drift/chapter-findings.json; NPI-DRF-09) CA-7 KSI-MLA-EVC The ISSO
The run’s counts, and each task’s status with the SHA-256 of its manifest, one tenant, cloud and mode throughout (drift/summary.json; run.json; drift/manifest.json) CA-7; AU-9 Protection of Audit Information KSI-MLA-EVC; KSI-MLA-OSM The ISSO
The year’s AI use-case inventory compared with last year’s and joined to the register (Lab 1, step 6: findings.json and changes.json in vintage-<year>/; NPI-VIN-01 to -05) OMB M-25-21, section 3(b)(v), as Book 17, Appendix A maps it; CM-8 System Component Inventory KSI-PIY-GIV (CM-8) The Chief AI Officer’s staff; the identity governance office
Each run is packaged with a SHA-256 for every file, chained to the previous package, and a summary with no names (Lab 1, step 7: EVIDENCE-MANIFEST.json, SHA256SUMS, summary.md) AU-9; SI-7 Software, Firmware, and Information Integrity KSI-MLA-OSM (AU-9); KSI-SVC-VRI (SI-7) The ISSO
A package can be checked with sha256sum alone, and its summary names no one (Lab 1, step 8: npi_evidence.py verify; sha256sum -c --quiet SHA256SUMS; grep -c contoso returns 0) AU-9; SI-7 KSI-MLA-OSM; KSI-SVC-VRI The ISSO
The period’s packages are verified, the chain is whole, the windows leave no gap, and findings opened and closed are listed per control (Lab 1, step 9: PERIOD-MANIFEST.json, period-summary.md, SHA256SUMS) CA-7 (status reporting); AU-9 KSI-MLA-EVC; KSI-MLA-OSM The ISSO (NP7.4)
An edited file and a package chained out of order are each refused, and the sample workflow lints clean (Lab 1, step 10: verify exits 1; the chained package exits 1; workflow_lint.py workflow/npi-evidence.yml reports 0 findings) SI-7; CM-3(2) Testing, Validation, and Documentation of Changes KSI-SVC-VRI; KSI-CMT-VTD The repository’s tests
The code passes its tests in both kits (Lab 1, step 11: Invoke-Pester -Path tests; python -m pytest tests -q) CM-3(2) KSI-CMT-VTD The repository; the pull request’s checks
The test tenant’s three changes are found: two secrets added outside the pipeline and one consent grant (Lab 2, steps 2 and 3: NPI-DRF-07 and -08 in drift/findings.json) AC-2(4); SI-4 KSI-MLA-LET, KSI-MLA-RVL; KSI-IAM-SUS Security operations
The audit log’s activity names are recorded as the live tenant writes them (Lab 2, step 4: activityDisplayName in drift/events.json) AU-2 Event Logging; AU-12 Audit Record Generation KSI-MLA-LET Security operations
Every credential change the admin center’s audit log shows is in the run (Lab 2, step 5: Audit logs, ApplicationManagement, compared with drift/events.json) AU-6 KSI-MLA-RVL Security operations
The test runs package and verify like the fixture runs (Lab 2, step 6: the two packages and verify on the second) AU-9; SI-7 KSI-MLA-OSM; KSI-SVC-VRI The ISSO
The lab’s secrets, grant, permissions and reader app are removed when the lab ends (Lab 2, step 7) AC-2 (remove accounts no longer needed); IA-5 Authenticator Management KSI-IAM-JIT; KSI-IAM-SNU Identity engineering

KSIs are FedRAMP 20x indicators for cloud service providers. The KSI column names the indicators whose related controls, as FedRAMP lists them, include the row’s control or one of its enhancements; which output supports which indicator is the program’s mapping (design guidance), and the M-25-21 row is mapped through CM-8.55 The agency authorizes its own system and lines this evidence up with what its providers report against KSIs; it doesn’t report KSIs itself (Book 00, Appendix B). Lab steps that only delete out have no row.

Dangers

Danger What happens The guard
A baseline nobody approved Drift is measured against whatever the last run happened to see, and a malicious change becomes the new normal The baseline is compared only with an approved SHA-256, for this tenant and cloud
A failed read reported as a fix A 403 makes yesterday’s findings disappear A failed task is Error with no folder; a missing contract file or review kind is NPI-DRF-09 or NPI-REV-04, never a pass
A day nobody read A credential added in the gap has no evidence, and Gate 7’s “drift checks running” passes on the days that were read Each window starts where the last ended; a gap is a coverage gap in the period
A window older than the log The read returns part of the window and looks complete Refused past 30 days; read the gap from the workspace and attach it
Trusting Microsoft’s rule as the check App-initiated, first and service principal credentials go unseen The Graph read of every initiator and target, every added key; tests pin what the rule misses
Every rotation alerts The pipeline’s own rotations bury the one that matters, and the alert is ignored Inside the pipeline needs both the credential log entry and its recorded operator as the initiator
An operator’s or pipeline’s identity misused An attacker with a credential adds a key A key not in the credential log, or added by anyone but its logged operator, is still NPI-DRF-07
Names in the tracker or the summary Owners and initiators are listed for everyone who can read it Events keep IDs; summaries carry rule IDs, object keys and fingerprints; a test searches them for every name in the fixtures
A finding’s text changes and it looks new Every date in an issue reopens it The fingerprint is the rule and object key only
Another chapter’s file accepted as written A wrong tenant, a forged fingerprint or a misnamed rule enters the evidence Each contract file is checked whole and refused with the reason
A run edited, or swapped with another tenant’s The evidence describes a run that never happened Every file checked against its manifest, each manifest against run.json; tenant, cloud and mode must match through the chain
A replaced package A bad day is quietly rewritten The hash chain; period reports the break; a locked retention policy
Unused treated as delete A seasonal app is removed NPI-DRF-04 is a Warn; nothing here writes; retirement is the decommission process
The wrong cloud A GCC High run reads graph.microsoft.com, or sends a beta call the cloud lacks Required cloud, no default; the sign-in report isn’t sent where Learn doesn’t list it

Checklist

Back to top

Footnotes

  1. Microsoft Learn, “Microsoft Entra security operations guide for applications,” application credentials; application permissions; end-user consent, https://learn.microsoft.com/entra/architecture/security-operations-applications (accessed Oct 30, 2026).↩︎

  2. Microsoft Learn, “Microsoft Entra audit log categories and activities,” Core Directory, https://learn.microsoft.com/entra/identity/monitoring-health/reference-audit-activities (accessed Oct 30, 2026; source checked in MicrosoftDocs/entra-docs).↩︎

  3. Microsoft Learn, “Microsoft Entra data retention,” activity reports, https://learn.microsoft.com/entra/identity/monitoring-health/reference-reports-data-retention (accessed Oct 30, 2026).↩︎

  4. Microsoft Learn, “Microsoft Entra recommendation: Renew expiring application credentials (preview),” description, https://learn.microsoft.com/entra/identity/monitoring-health/recommendation-renew-expiring-application-credential (accessed Oct 30, 2026).↩︎

  5. Microsoft Learn, “Microsoft Entra recommendation: Remove unused applications (preview),” description and action plan, https://learn.microsoft.com/entra/identity/monitoring-health/recommendation-remove-unused-apps (accessed Oct 30, 2026).↩︎

  6. Microsoft Learn, “Microsoft Graph national cloud deployments,” https://learn.microsoft.com/graph/deployments (accessed Oct 30, 2026; read for Chapter 01).↩︎

  7. Microsoft Learn, “List directoryAudits,” https://learn.microsoft.com/graph/api/directoryaudit-list?view=graph-rest-1.0 (accessed Oct 30, 2026).↩︎

  8. Microsoft Learn, “List servicePrincipalSignInActivities,” https://learn.microsoft.com/graph/api/reportroot-list-serviceprincipalsigninactivities?view=graph-rest-beta (accessed Oct 30, 2026).↩︎

  9. Microsoft Learn, “Microsoft Entra security operations guide for applications,” application credentials; application permissions; end-user consent, https://learn.microsoft.com/entra/architecture/security-operations-applications (accessed Oct 30, 2026).↩︎

  10. Microsoft Learn, “Microsoft Entra security operations guide for applications,” application credentials; application permissions; end-user consent, https://learn.microsoft.com/entra/architecture/security-operations-applications (accessed Oct 30, 2026).↩︎

  11. Microsoft Learn, “List directoryAudits,” https://learn.microsoft.com/graph/api/directoryaudit-list?view=graph-rest-1.0 (accessed Oct 30, 2026).↩︎

  12. Microsoft Learn, “Microsoft Entra recommendation: Renew expiring application credentials (preview),” description, https://learn.microsoft.com/entra/identity/monitoring-health/recommendation-renew-expiring-application-credential (accessed Oct 30, 2026).↩︎

  13. Microsoft Learn, “Microsoft Entra recommendation: Remove unused applications (preview),” description and action plan, https://learn.microsoft.com/entra/identity/monitoring-health/recommendation-remove-unused-apps (accessed Oct 30, 2026).↩︎

  14. Microsoft, Azure/Azure-Sentinel, Solutions/Microsoft Entra ID/Analytic Rules/NewAppOrServicePrincipalCredential.yaml at commit 2985095e602f32c493e43998856ac9bcf1382afd (October 30, 2026; the rule last changed January 8, 2024), MIT License, https://github.com/Azure/Azure-Sentinel/blob/2985095e602f32c493e43998856ac9bcf1382afd/Solutions/Microsoft%20Entra%20ID/Analytic%20Rules/NewAppOrServicePrincipalCredential.yaml (accessed Oct 30, 2026); pinned copy in examples/third_party/sentinel-app-credential-rule.↩︎

  15. Microsoft Learn, “Microsoft Entra security operations guide for applications,” application credentials; application permissions; end-user consent, https://learn.microsoft.com/entra/architecture/security-operations-applications (accessed Oct 30, 2026).↩︎

  16. Microsoft Learn, “Microsoft Entra data retention,” activity reports, https://learn.microsoft.com/entra/identity/monitoring-health/reference-reports-data-retention (accessed Oct 30, 2026).↩︎

  17. Microsoft Learn, “directoryAudit resource type,” properties, https://learn.microsoft.com/graph/api/resources/directoryaudit?view=graph-rest-1.0 (accessed Oct 30, 2026).↩︎

  18. Microsoft Learn, “Microsoft Entra data retention,” activity reports, https://learn.microsoft.com/entra/identity/monitoring-health/reference-reports-data-retention (accessed Oct 30, 2026).↩︎

  19. Microsoft Learn, “List servicePrincipalSignInActivities,” https://learn.microsoft.com/graph/api/reportroot-list-serviceprincipalsigninactivities?view=graph-rest-beta (accessed Oct 30, 2026).↩︎

  20. Microsoft Learn, “List directoryAudits,” https://learn.microsoft.com/graph/api/directoryaudit-list?view=graph-rest-1.0 (accessed Oct 30, 2026).↩︎

  21. Microsoft Learn, “List servicePrincipalSignInActivities,” https://learn.microsoft.com/graph/api/reportroot-list-serviceprincipalsigninactivities?view=graph-rest-beta (accessed Oct 30, 2026).↩︎

  22. Microsoft Learn, “List directoryAudits,” https://learn.microsoft.com/graph/api/directoryaudit-list?view=graph-rest-1.0 (accessed Oct 30, 2026).↩︎

  23. Microsoft Learn, “Microsoft Entra recommendation: Remove unused applications (preview),” description and action plan, https://learn.microsoft.com/entra/identity/monitoring-health/recommendation-remove-unused-apps (accessed Oct 30, 2026).↩︎

  24. Microsoft Learn, “Microsoft Entra recommendation: Renew expiring application credentials (preview),” description, https://learn.microsoft.com/entra/identity/monitoring-health/recommendation-renew-expiring-application-credential (accessed Oct 30, 2026).↩︎

  25. Microsoft Learn, “Microsoft Entra security operations guide for applications,” application credentials; application permissions; end-user consent, https://learn.microsoft.com/entra/architecture/security-operations-applications (accessed Oct 30, 2026).↩︎

  26. Microsoft Learn, “Microsoft Entra security operations guide for applications,” application credentials; application permissions; end-user consent, https://learn.microsoft.com/entra/architecture/security-operations-applications (accessed Oct 30, 2026).↩︎

  27. Microsoft Learn, “Best practices for Microsoft Entra Agent ID,” govern the agent lifecycle, https://learn.microsoft.com/entra/agent-id/best-practices-agent-id (accessed Oct 30, 2026).↩︎

  28. Microsoft Learn, “Store business-critical blob data with immutable storage in a write once, read many (WORM) state,” https://learn.microsoft.com/azure/storage/blobs/immutable-storage-overview (accessed Oct 30, 2026; read for the first workbook’s Chapter 09).↩︎

  29. Microsoft Learn, “Use the Azure Login action with OpenID Connect,” https://learn.microsoft.com/azure/developer/github/connect-from-azure-openid-connect (accessed Oct 31, 2026).↩︎

  30. Microsoft Learn, “Microsoft Entra security operations guide for applications,” application credentials; application permissions; end-user consent, https://learn.microsoft.com/entra/architecture/security-operations-applications (accessed Oct 30, 2026).↩︎

  31. Microsoft Learn, “Store business-critical blob data with immutable storage in a write once, read many (WORM) state,” https://learn.microsoft.com/azure/storage/blobs/immutable-storage-overview (accessed Oct 30, 2026; read for the first workbook’s Chapter 09).↩︎

  32. Microsoft Learn, “List directoryAudits,” https://learn.microsoft.com/graph/api/directoryaudit-list?view=graph-rest-1.0 (accessed Oct 30, 2026).↩︎

  33. Microsoft Learn, “directoryAudit resource type,” properties, https://learn.microsoft.com/graph/api/resources/directoryaudit?view=graph-rest-1.0 (accessed Oct 30, 2026).↩︎

  34. Microsoft Learn, “Microsoft Entra audit log categories and activities,” Core Directory, https://learn.microsoft.com/entra/identity/monitoring-health/reference-audit-activities (accessed Oct 30, 2026; source checked in MicrosoftDocs/entra-docs).↩︎

  35. Microsoft Learn, “Microsoft Entra security operations guide for applications,” application credentials; application permissions; end-user consent, https://learn.microsoft.com/entra/architecture/security-operations-applications (accessed Oct 30, 2026).↩︎

  36. Microsoft Learn, “List servicePrincipalSignInActivities,” https://learn.microsoft.com/graph/api/reportroot-list-serviceprincipalsigninactivities?view=graph-rest-beta (accessed Oct 30, 2026).↩︎

  37. Microsoft Learn, “Microsoft Entra recommendation: Renew expiring application credentials (preview),” description, https://learn.microsoft.com/entra/identity/monitoring-health/recommendation-renew-expiring-application-credential (accessed Oct 30, 2026).↩︎

  38. Microsoft Learn, “Microsoft Entra recommendation: Remove unused applications (preview),” description and action plan, https://learn.microsoft.com/entra/identity/monitoring-health/recommendation-remove-unused-apps (accessed Oct 30, 2026).↩︎

  39. Microsoft Learn, “Microsoft Entra data retention,” activity reports, https://learn.microsoft.com/entra/identity/monitoring-health/reference-reports-data-retention (accessed Oct 30, 2026).↩︎

  40. Microsoft Learn, “Best practices for Microsoft Entra Agent ID,” govern the agent lifecycle, https://learn.microsoft.com/entra/agent-id/best-practices-agent-id (accessed Oct 30, 2026).↩︎

  41. Microsoft Learn, “Microsoft Entra releases and announcements,” April 2026, https://learn.microsoft.com/entra/fundamentals/whats-new#april-2026, and “What is Microsoft Entra Agent ID?,” https://learn.microsoft.com/entra/agent-id/what-is-microsoft-entra-agent-id (both accessed Oct 31, 2026).↩︎

  42. Microsoft Learn, “Microsoft Graph national cloud deployments,” https://learn.microsoft.com/graph/deployments (accessed Oct 30, 2026; read for Chapter 01).↩︎

  43. Microsoft Learn, “Use the Azure Login action with OpenID Connect,” https://learn.microsoft.com/azure/developer/github/connect-from-azure-openid-connect (accessed Oct 31, 2026).↩︎

  44. Microsoft Learn, “Store business-critical blob data with immutable storage in a write once, read many (WORM) state,” https://learn.microsoft.com/azure/storage/blobs/immutable-storage-overview (accessed Oct 30, 2026; read for the first workbook’s Chapter 09).↩︎

  45. Microsoft Learn, “Microsoft Entra audit log categories and activities,” Core Directory, https://learn.microsoft.com/entra/identity/monitoring-health/reference-audit-activities (accessed Oct 30, 2026; source checked in MicrosoftDocs/entra-docs).↩︎

  46. Microsoft Learn, “Microsoft Entra security operations guide for applications,” application credentials; application permissions; end-user consent, https://learn.microsoft.com/entra/architecture/security-operations-applications (accessed Oct 30, 2026).↩︎

  47. Microsoft Learn, “Microsoft Entra audit log categories and activities,” Core Directory, https://learn.microsoft.com/entra/identity/monitoring-health/reference-audit-activities (accessed Oct 30, 2026; source checked in MicrosoftDocs/entra-docs).↩︎

  48. Microsoft Learn, “Microsoft Entra security operations guide for applications,” application credentials; application permissions; end-user consent, https://learn.microsoft.com/entra/architecture/security-operations-applications (accessed Oct 30, 2026).↩︎

  49. Microsoft Learn, “Microsoft Entra recommendation: Remove unused applications (preview),” description and action plan, https://learn.microsoft.com/entra/identity/monitoring-health/recommendation-remove-unused-apps (accessed Oct 30, 2026).↩︎

  50. Microsoft Learn, “Governing Microsoft Entra service accounts,” build a lifecycle process, https://learn.microsoft.com/entra/architecture/govern-service-accounts (accessed Oct 30, 2026; read for Chapter 02).↩︎

  51. Microsoft Learn, “Microsoft Entra releases and announcements,” April 2026, https://learn.microsoft.com/entra/fundamentals/whats-new#april-2026, and “What is Microsoft Entra Agent ID?,” https://learn.microsoft.com/entra/agent-id/what-is-microsoft-entra-agent-id (both accessed Oct 31, 2026).↩︎

  52. Microsoft Learn, “Microsoft Entra security operations guide for applications,” application credentials; application permissions; end-user consent, https://learn.microsoft.com/entra/architecture/security-operations-applications (accessed Oct 30, 2026).↩︎

  53. Microsoft, Azure/Azure-Sentinel, Solutions/Microsoft Entra ID/Analytic Rules/NewAppOrServicePrincipalCredential.yaml at commit 2985095e602f32c493e43998856ac9bcf1382afd (October 30, 2026; the rule last changed January 8, 2024), MIT License, https://github.com/Azure/Azure-Sentinel/blob/2985095e602f32c493e43998856ac9bcf1382afd/Solutions/Microsoft%20Entra%20ID/Analytic%20Rules/NewAppOrServicePrincipalCredential.yaml (accessed Oct 30, 2026); pinned copy in examples/third_party/sentinel-app-credential-rule.↩︎

  54. National Institute of Standards and Technology, SP 800-53B Moderate baseline, release 5.2.0 OSCAL profile (includes AC-2, AC-2(3), AC-2(4), AC-6, AC-6(7), AU-2, AU-6, AU-9, AU-12, CA-7, CM-2, CM-3, CM-3(2), CM-8, IA-5, SI-4 and SI-7; IA-9 is not in it), https://github.com/usnistgov/oscal-content/blob/main/nist.gov/SP800-53/rev5/json/NIST_SP-800-53_rev5_MODERATE-baseline_profile.json (read November 2, 2026).↩︎

  55. FedRAMP, “Key Security Indicators,” FedRAMP Consolidated Rules for 2026, indicator statements and “Related SP 800-53 Controls,” https://www.fedramp.gov/2026/reference/key-security-indicators/ (read November 2, 2026).↩︎