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 06 — Drift Reconciliation

Build 3 of the kit: a read-only client joins the HR extract, Microsoft Graph and SailPoint ISC on employeeId and reports Book 03’s drift classes with stable finding IDs. Run it twice, a day apart, and compare the runs by finding ID into new, persisting and resolved findings, refusing to compare runs that computed different classes, so a class one run skipped can’t look fixed; resolved findings are leads for the re-read, not closures; with labs against fixtures and a test tenant.

Events tell you what changed. A reconciliation tells you what is wrong now, including what changed while nobody was listening.

NoteIn this chapter

The kit’s Build 3, the loop’s steps 1 and 2: a read-only join of the HR extract, Microsoft Graph and ISC on employeeId that measures Book 03’s drift classes. Events (Chapter 04) catch changes as they happen; the reconciliation catches the state, including drift whose event was missed. This chapter adds drift_compare.py, which compares two runs by their stable finding IDs, so a scheduled run raises only what is new, and shows what has stopped being measured. Lab 1 runs a two-day story on a laptop; Lab 2 runs the client against a test tenant and sandbox.

TipNarrated tour
Building Ticket-to-Machine, Chapter 06 — Drift Reconciliation: narrated tour · 9 minutes · captions from the player's CC button · Download MP4 · All videos
Jump to a slide (15)

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

Goal and audience

Goal. By the end of this chapter you can run the reconciliation over saved exports, read its findings, compare two runs, and explain why a resolved finding doesn’t close anything by itself.

Audience. Developers and identity engineers who schedule the reconciliation, and the identity data steward who reads its output. You should read Python and CSV. You don’t need a tenant until Lab 2.

The code: examples/servicenow-sailpoint

What exists

clients/python/identity_drift.py, described in the implementation example’s Build 3:

  • Inputs. An HR extract (CSV, employeeId and status required); Graph users with employeeId, accountEnabled, department and the manager’s employeeId; ISC identities from Search (accounts nested) and uncorrelated accounts from the Accounts API; and, optionally, the Entra directory audit. Each can be a saved export, which is how the labs run.
  • Classes. Missing identity, stale access, orphan account and attribute drift (department and manager), High or Medium as Book 03, Chapter 11 sets them; an unauthorized write only when the audit is read. Credential and schema drift are always reported as not computed, because they need the card system, Intune and the provisioning middleware’s output.
  • Finding IDs. Each finding’s ID is a hash of its class, system, employee ID, object ID and attribute, so the same drift gets the same ID on every run. The PowerShell client produces the same IDs from the same fixtures.
  • Outputs. JSON (the findings, and which classes were and weren’t computed) and CSV. --push-to-servicenow posts the findings to the Scripted REST resource, and still writes nothing without --apply.

The client measured one moment. A scheduled run that pushed every finding every night would lean on the Scripted REST resource’s de-duplication to avoid duplicates, and nothing reported what had disappeared.

What this chapter adds

  • clients/python/drift_compare.py compares two runs by finding ID: new (raise it), persisting (already raised; the de-duplication adds a counter) and resolved (no longer measured). It refuses to compare runs that computed different classes, one with the audit and one without, because every finding of the class the second run skipped would look resolved. A resolved finding is printed with a reminder that its record still closes only on Chapter 05’s re-read.
  • tests/fixtures/hr_extract_day2.csv, the second day of the fictional estate: HR has corrected Erin’s department from HR to Finance, the value someone had already typed into Entra ID by hand. Graph now agrees with HR; ISC, not yet re-aggregated, doesn’t.
  • tests/fixtures/config.fixtures.json, the fixture settings for an offline run (the authoritative HR source and the provisioning service’s app ID), with no secrets.
  • Output folders. The client now creates the folder for --out-json and --out-csv if it doesn’t exist.

The tests

tests/python/test_drift_compare.py, 4 tests: the day-two story, identical runs, the refusal and the command line. The kit’s original 13 drift tests pass unchanged, including the shared finding IDs with the PowerShell client.

ImportantIllustrative code

The commands below run over fictional exports. They show the join and the comparison, not that your HR extract, Graph or ISC return these shapes; the manager expansion and the ISC employee ID attribute in particular are on the verify list. Expect a burst of High findings in the first month of a real run: Book 03 calls that the measurement working.

Lab 1: fixtures, no tenant

Work in examples/servicenow-sailpoint, with Python 3.10 or later, pytest and requests.

NoteEvery command in Lab 1

Every command in this lab, in order, from examples/servicenow-sailpoint. Each step below says what it does and what to look for.

# Step 1: Run the drift tests
python -m pytest tests/python/test_identity_drift.py tests/python/test_drift_compare.py -q

# Step 2: Day one
python clients/python/identity_drift.py --hr-csv tests/fixtures/hr_extract.csv --config tests/fixtures/config.fixtures.json --graph-users-json tests/fixtures/graph_users.json --isc-identities-json tests/fixtures/isc_identities.json --isc-accounts-json tests/fixtures/isc_uncorrelated_accounts.json --entra-audit-json tests/fixtures/entra_audit.json --out-json out/day1.json --out-csv out/day1.csv
python -c "import csv, json; [print(r['drift_class'], r['system'], r['employee_id'], r['attribute'], r['expected'], '->', r['observed'], json.loads(r['evidence']).get('initiated_by', '')) for r in csv.DictReader(open('out/day1.csv', encoding='utf-8')) if r['employee_id'] in ('E1002', 'E1004')]"

# Step 3: Day two
python clients/python/identity_drift.py --hr-csv tests/fixtures/hr_extract_day2.csv --config tests/fixtures/config.fixtures.json --graph-users-json tests/fixtures/graph_users.json --isc-identities-json tests/fixtures/isc_identities.json --isc-accounts-json tests/fixtures/isc_uncorrelated_accounts.json --entra-audit-json tests/fixtures/entra_audit.json --out-json out/day2.json --out-csv out/day2.csv

# Step 4: Compare the two days
python clients/python/drift_compare.py --previous out/day1.json --current out/day2.json

# Step 5: See the refusal
python clients/python/identity_drift.py --hr-csv tests/fixtures/hr_extract_day2.csv --config tests/fixtures/config.fixtures.json --graph-users-json tests/fixtures/graph_users.json --isc-identities-json tests/fixtures/isc_identities.json --isc-accounts-json tests/fixtures/isc_uncorrelated_accounts.json --out-json out/day2-noaudit.json --out-csv out/day2-noaudit.csv
python clients/python/drift_compare.py --previous out/day1.json --current out/day2-noaudit.json
  1. Run the drift tests:

    python -m pytest tests/python/test_identity_drift.py tests/python/test_drift_compare.py -q

    Terminal window showing python -m pytest tests/python/test_identity_drift.py tests/python/test_drift_… and what it printed

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

    17 passed.

  2. Day one, then Erin’s and Carol’s rows from out/day1.csv:

    python clients/python/identity_drift.py --hr-csv tests/fixtures/hr_extract.csv --config tests/fixtures/config.fixtures.json --graph-users-json tests/fixtures/graph_users.json --isc-identities-json tests/fixtures/isc_identities.json --isc-accounts-json tests/fixtures/isc_uncorrelated_accounts.json --entra-audit-json tests/fixtures/entra_audit.json --out-json out/day1.json --out-csv out/day1.csv
    python -c "import csv, json; [print(r['drift_class'], r['system'], r['employee_id'], r['attribute'], r['expected'], '->', r['observed'], json.loads(r['evidence']).get('initiated_by', '')) for r in csv.DictReader(open('out/day1.csv', encoding='utf-8')) if r['employee_id'] in ('E1002', 'E1004')]"

    Terminal window showing python clients/python/identity_drift.py --hr-csv tests/fixtures/hr_extract.cs… and what it printed

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

    The summary counts attribute_drift 4, missing_identity 2, orphan_account 3, stale_access 2, unauthorized_write 1, and lists credential and schema drift under not_computed. In the rows from out/day1.csv, Erin’s Entra ID department is Finance where HR says HR, and the audit shows a help desk administrator made that change. Carol, terminated, is still enabled in Entra ID and in Active Directory.

  3. Day two, after HR’s correction:

    python clients/python/identity_drift.py --hr-csv tests/fixtures/hr_extract_day2.csv --config tests/fixtures/config.fixtures.json --graph-users-json tests/fixtures/graph_users.json --isc-identities-json tests/fixtures/isc_identities.json --isc-accounts-json tests/fixtures/isc_uncorrelated_accounts.json --entra-audit-json tests/fixtures/entra_audit.json --out-json out/day2.json --out-csv out/day2.csv

    Terminal window showing python clients/python/identity_drift.py --hr-csv tests/fixtures/hr_extract_da… and what it printed

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

    The same counts. The totals hide what moved.

  4. Compare the two days:

    python clients/python/drift_compare.py --previous out/day1.json --current out/day2.json
    1 new, 11 persisting, 1 resolved
      new       6456e1f12a60e6be attribute_drift isc E1004 department: expected 'Finance', observed 'HR'
      resolved  1f0e34ebf049f163 attribute_drift entra E1004 department: expected 'HR', observed 'Finance'
    Resolved findings are leads for the re-read; their records close only on reread_verification.py.

    Terminal window showing python clients/python/drift_compare.py --previous out/day1.json --current out… and what it printed

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

    Entra ID now matches HR, so that finding is resolved; ISC still holds the old value until the next aggregation, so a new one opens. The unauthorized write persists: HR agreeing afterwards doesn’t make the write authorized, and a person decides that finding.

  5. See the refusal. Run day two without the audit, then compare:

    python clients/python/identity_drift.py --hr-csv tests/fixtures/hr_extract_day2.csv --config tests/fixtures/config.fixtures.json --graph-users-json tests/fixtures/graph_users.json --isc-identities-json tests/fixtures/isc_identities.json --isc-accounts-json tests/fixtures/isc_uncorrelated_accounts.json --out-json out/day2-noaudit.json --out-csv out/day2-noaudit.csv
    python clients/python/drift_compare.py --previous out/day1.json --current out/day2-noaudit.json

    Terminal window showing python clients/python/identity_drift.py --hr-csv tests/fixtures/hr_extract_da… and what it printed

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

    The first command lists unauthorized_write under not_computed. The second prints error: The runs computed different drift classes (...) and exits 2: without the refusal, the unauthorized write would have shown as resolved.

Lab 2: a test tenant and a SailPoint sandbox

Not run by the authors. Use a test Entra tenant in the worldwide or GCC cloud and a SailPoint sandbox, with synthetic HR data like the fixtures, never production. Work in examples/servicenow-sailpoint. You’ll replace: <graph-permissions> and <isc-scopes> (what the app registration and the API client hold, comma-separated), <tenant> (your sandbox tenant’s name), <reader-client-id> and <reader-client-secret> (the sandbox API client), <graph-tenant-id>, <graph-client-id> and <graph-cert.pem> (the test app registration and the PEM file holding its certificate and key), <employee-id-attribute> (from Chapter 03‘s Lab 2), <hr-source-name> (the sandbox’s HR source), <provisioning-app-id> (the appId allowed to write HR-mastered attributes), <active-employee-id>, <terminated-employee-id> and <mover-employee-id> (three test users’ employeeId values), <active-user-department> and <terminated-user-department> (the first two users’ departments in Entra ID), <department-not-in-entra> (any other department, for the third user), <new-department> (step 4), and <since-day1> and <since-day2> (midnight UTC on the day before each run, for example 2026-10-31T00:00:00Z).

NoteEvery command in Lab 2

Every command in this lab, in order, from examples/servicenow-sailpoint. Each step below says what it does and what to look for.

# Step 1: Grant the reads
python clients/python/kit_calls.py --task drift --system graph --granted "<graph-permissions>"
python clients/python/kit_calls.py --task drift --system isc --granted "<isc-scopes>"

# Step 2: Write a synthetic HR extract
cat > hr_synthetic.csv <<'EOF'
employeeId,status,department,managerEmployeeId,displayName,email
<active-employee-id>,Active,<active-user-department>,,Test User One,
<terminated-employee-id>,Terminated,<terminated-user-department>,,Test User Two,
<mover-employee-id>,Active,<department-not-in-entra>,,Test User Three,
EOF

# Step 3: Run the client live
export SAILPOINT_API_BASE="https://<tenant>.api.identitynow.com"
export SAILPOINT_CLIENT_ID="<reader-client-id>"
export SAILPOINT_CLIENT_SECRET="<reader-client-secret>"
export GRAPH_TENANT_ID="<graph-tenant-id>"
export GRAPH_CLIENT_ID="<graph-client-id>"
export GRAPH_CERT_PATH="<graph-cert.pem>"
export GRAPH_CLOUD="Global"
export ISC_EMPLOYEE_ID_ATTRIBUTE="<employee-id-attribute>"
export ISC_AUTHORITATIVE_SOURCES="<hr-source-name>"
export ALLOWED_WRITER_APP_IDS="<provisioning-app-id>"
python clients/python/identity_drift.py --hr-csv hr_synthetic.csv --config config/config.example.json --include-audit "<since-day1>" --out-json out/day1.json --out-csv out/day1.csv

# Step 4: Run it again the next day
sed -i 's/^<mover-employee-id>,Active,[^,]*,/<mover-employee-id>,Active,<new-department>,/' hr_synthetic.csv
python clients/python/identity_drift.py --hr-csv hr_synthetic.csv --config config/config.example.json --include-audit "<since-day2>" --out-json out/day2.json --out-csv out/day2.csv
python clients/python/drift_compare.py --previous out/day1.json --current out/day2.json

# Step 5: Back it out
rm -rf out hr_synthetic.csv
unset SAILPOINT_API_BASE SAILPOINT_CLIENT_ID SAILPOINT_CLIENT_SECRET GRAPH_TENANT_ID GRAPH_CLIENT_ID GRAPH_CERT_PATH GRAPH_CLOUD ISC_EMPLOYEE_ID_ATTRIBUTE ISC_AUTHORITATIVE_SOURCES ALLOWED_WRITER_APP_IDS
  1. Grant the reads from Chapter 03: User.Read.All (and AuditLog.Read.All if you read the audit) on a test app registration with a certificate; sp:search:read and idn:accounts:read on a sandbox API client. Check both:

    python clients/python/kit_calls.py --task drift --system graph --granted "<graph-permissions>"
    python clients/python/kit_calls.py --task drift --system isc --granted "<isc-scopes>"
  2. Write a synthetic HR extract whose employeeId values match test users, with one terminated user still enabled and one department that differs. This writes hr_synthetic.csv with three rows: an active user, a terminated user who is still enabled, and an active user whose department differs from the one in Entra ID.

    cat > hr_synthetic.csv <<'EOF'
    employeeId,status,department,managerEmployeeId,displayName,email
    <active-employee-id>,Active,<active-user-department>,,Test User One,
    <terminated-employee-id>,Terminated,<terminated-user-department>,,Test User Two,
    <mover-employee-id>,Active,<department-not-in-entra>,,Test User Three,
    EOF
  3. Run the client live with the kit’s example configuration and your values in environment variables (they win over the file), reading the audit since yesterday. Certificate authentication needs the optional cryptography package.

    export SAILPOINT_API_BASE="https://<tenant>.api.identitynow.com"
    export SAILPOINT_CLIENT_ID="<reader-client-id>"
    export SAILPOINT_CLIENT_SECRET="<reader-client-secret>"
    export GRAPH_TENANT_ID="<graph-tenant-id>"
    export GRAPH_CLIENT_ID="<graph-client-id>"
    export GRAPH_CERT_PATH="<graph-cert.pem>"
    export GRAPH_CLOUD="Global"
    export ISC_EMPLOYEE_ID_ATTRIBUTE="<employee-id-attribute>"
    export ISC_AUTHORITATIVE_SOURCES="<hr-source-name>"
    export ALLOWED_WRITER_APP_IDS="<provisioning-app-id>"
    python clients/python/identity_drift.py --hr-csv hr_synthetic.csv --config config/config.example.json --include-audit "<since-day1>" --out-json out/day1.json --out-csv out/day1.csv

    Compare its counts with a hand count of the extract (the implementation example’s proof step 4). If Graph rejects $expand=manager under the application permission, record it (verify list, item 14).

  4. Run it again the next day after changing one department in the test HR extract, and compare the runs. The first command changes the third user’s department in hr_synthetic.csv.

    sed -i 's/^<mover-employee-id>,Active,[^,]*,/<mover-employee-id>,Active,<new-department>,/' hr_synthetic.csv
    python clients/python/identity_drift.py --hr-csv hr_synthetic.csv --config config/config.example.json --include-audit "<since-day2>" --out-json out/day2.json --out-csv out/day2.csv
    python clients/python/drift_compare.py --previous out/day1.json --current out/day2.json

    Push nothing: these commands leave out --push-to-servicenow, and that flag without --apply only lists what it would send.

  5. Back it out. Delete out, the synthetic extract, the app registration’s permissions and certificate, and the sandbox API client:

    rm -rf out hr_synthetic.csv
    unset SAILPOINT_API_BASE SAILPOINT_CLIENT_ID SAILPOINT_CLIENT_SECRET GRAPH_TENANT_ID GRAPH_CLIENT_ID GRAPH_CERT_PATH GRAPH_CLOUD ISC_EMPLOYEE_ID_ATTRIBUTE ISC_AUTHORITATIVE_SOURCES ALLOWED_WRITER_APP_IDS

The verified facts

Item Value Source
List users GET /v1.0/users, User.Read.All (application) 1
Manager GET /users?$expand=manager documented; application permissions not supported on GET /users/{id}/manager 2
Directory audit GET /v1.0/auditLogs/directoryAudits, AuditLog.Read.All (application); available in US Government L4 and L5 3
ISC identities POST /v3/search, identities index, sp:search:read; searchAfter past 10,000 4
Uncorrelated accounts GET /v3/accounts with the uncorrelated filter (eq), idn:accounts:read; limit at most 250 5
GCC endpoints Microsoft 365 GCC uses the worldwide graph.microsoft.com 6

What couldn’t be verified

  • $expand=manager under an application permission. Verify list, item 14.
  • Which ISC identity attribute holds the HR key (employeeNumber or attributes.employeeId). Verify list, item 9.
  • The Entra audit’s property display names the client watches (Department, EmployeeId and others). Verify list, item 15.

Federal requirements and evidence

What the output proves Requirement it supports KSI Who keeps it
Every active HR record has its accounts, every terminated one has none enabled, and every enabled account has a person NIST SP 800-53 AC-2 (account management) and AC-2(3) (disable accounts) at Moderate;7 Book 03’s leaver and orphan measures KSI-IAM-SUS; KSI-IAM-AAM The drift JSON and CSV of each run
Drift is measured on a schedule, and what changed between runs is reported CA-7 (continuous monitoring) and SI-4 at Moderate8 KSI-MLA-EVC; KSI-MLA-RVL The comparison output, kept with each run
What wasn’t measured is said, not implied CA-7; Book 13’s detection coverage measure KSI-MLA-EVC not_computed in every run; the comparison’s refusal
Lab 1, step 1: the reconciliation and the comparison hold, tested before a change merges SA-11 (developer testing) at Moderate9 KSI-SCR-MIT The pytest tests/python/test_identity_drift.py tests/python/test_drift_compare.py run, in the pull request’s checks
Lab 1, step 2: one run finds each drift by class, with expected and observed values and, for a write, who made it AC-2; AC-2(3); SI-4 KSI-IAM-SUS; KSI-IAM-AAM; KSI-MLA-RVL identity_drift.py --out-json out/day1.json --out-csv out/day1.csv; the fixture run’s files are deleted, a live run’s are kept
Lab 1, step 3: a second run on the next day’s HR extract CA-7 KSI-MLA-EVC out/day2.json and out/day2.csv
Lab 1, step 4: what was resolved, what persists and what is new between the runs CA-7; SI-4 KSI-MLA-EVC; KSI-MLA-RVL The drift_compare.py --previous --current output, kept with the second run
Lab 1, step 5: a run without the audit says so, and the comparison refuses to call the write resolved CA-7 KSI-MLA-EVC not_computed in out/day2-noaudit.json; the comparison’s exit 2
Lab 2, step 1: the Graph permissions and ISC scopes are reads only AC-6 (least privilege) and AC-2 at Moderate10 KSI-IAM-ELP; KSI-IAM-SNU The two kit_calls.py --granted outputs, kept with the approvals
Lab 2, step 2: the test uses a synthetic HR extract, never real HR records (design guidance) None None Nobody; hr_synthetic.csv is deleted in step 5
Lab 2, step 3: a live run finds the terminated user still enabled and the department that differs AC-2; AC-2(3); SI-4 KSI-IAM-SUS; KSI-IAM-AAM; KSI-MLA-RVL out/day1.json and out/day1.csv from the live run, with the hand count of the extract
Lab 2, step 4: the next day’s live run, compared with the first; nothing is pushed CA-7; SI-4 KSI-MLA-EVC; KSI-MLA-RVL out/day2.json, out/day2.csv and the drift_compare.py output
Lab 2, step 5: no test output, extract, permission, certificate or API client is left AC-2 KSI-IAM-SNU The app registration and API client lists; rm -rf out hr_synthetic.csv
Not run in the labs: identity_drift.py --push-to-servicenow --apply posts each finding to the Scripted REST resource CM-3 (configuration change control) at Moderate11 KSI-CMT-LMC The u_identity_finding records (Chapter 01)

The KSI column names the FedRAMP 20x Key Security Indicators whose related SP 800-53 controls include the row’s control.12 KSIs are indicators for cloud service providers: the agency authorizes its own system and lines its own evidence up with what its providers report against them; it doesn’t report KSIs itself.

Dangers

Danger What happens The guard
A class one run skipped looks resolved Unauthorized writes “disappear” the night the audit read fails drift_compare.py refuses runs with different computed classes
A resolved finding closes the record Drift that left the measurement, not the estate, is called fixed Resolved findings are leads; only the re-read closes
Pushing every finding every night The tracker fills with repeats Push only the comparison’s new findings; the de-duplication counts the rest
The first month’s burst read as failure The pipeline is turned off before it is tuned Book 03 expects it; triage by class and severity
A guest or the HR source’s own accounts flagged Noise hides real drift Guests are excluded; ISC_AUTHORITATIVE_SOURCES and ALLOWED_WRITER_APP_IDS
An HR extract with names in a shared folder Personal data spreads The run folder is evidence; keep it where the HR data is allowed, and delete lab copies

Checklist

Back to top

Footnotes

  1. Microsoft Learn, “List users,” permissions, https://learn.microsoft.com/graph/api/user-list?view=graph-rest-1.0 (read November 1, 2026).↩︎

  2. Microsoft Learn, “List manager,” permissions and HTTP request, https://learn.microsoft.com/graph/api/user-list-manager?view=graph-rest-1.0 (read November 1, 2026).↩︎

  3. Microsoft Learn, “List directoryAudits,” https://learn.microsoft.com/graph/api/directoryaudit-list?view=graph-rest-1.0 (read November 1, 2026).↩︎

  4. SailPoint, Identity Security Cloud OpenAPI specification, v3, searchPost, https://github.com/sailpoint-oss/api-specs/blob/main/idn/v3/paths/search-post.yaml (read November 1, 2026).↩︎

  5. SailPoint, Identity Security Cloud OpenAPI specification, v3, listAccounts and the limit parameter, https://github.com/sailpoint-oss/api-specs/blob/main/idn/v3/paths/accounts.yaml and https://github.com/sailpoint-oss/api-specs/blob/main/idn/v3/parameters/limit.yaml (read November 1, 2026).↩︎

  6. Microsoft Learn, “Microsoft Graph national cloud deployments,” https://learn.microsoft.com/graph/deployments (read November 1, 2026).↩︎

  7. National Institute of Standards and Technology, SP 800-53B Moderate baseline, release 5.2.0 OSCAL profile (includes AC-2, AC-2(3), AC-6, CA-7, CM-3, SA-11 and SI-4), 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 1, 2026).↩︎

  8. National Institute of Standards and Technology, SP 800-53B Moderate baseline, release 5.2.0 OSCAL profile (includes AC-2, AC-2(3), AC-6, CA-7, CM-3, SA-11 and SI-4), 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 1, 2026).↩︎

  9. National Institute of Standards and Technology, SP 800-53B Moderate baseline, release 5.2.0 OSCAL profile (includes AC-2, AC-2(3), AC-6, CA-7, CM-3, SA-11 and SI-4), 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 1, 2026).↩︎

  10. National Institute of Standards and Technology, SP 800-53B Moderate baseline, release 5.2.0 OSCAL profile (includes AC-2, AC-2(3), AC-6, CA-7, CM-3, SA-11 and SI-4), 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 1, 2026).↩︎

  11. National Institute of Standards and Technology, SP 800-53B Moderate baseline, release 5.2.0 OSCAL profile (includes AC-2, AC-2(3), AC-6, CA-7, CM-3, SA-11 and SI-4), 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 1, 2026).↩︎

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