Federal Modernization Workbook
Chapter 01 — The Loop and the Typed Task
Every step of the loop writes or reads one record. If its shape is wrong, the clock, the owner and the re-read are all wrong with it.
Where examples/servicenow-sailpoint sits in Book 13’s five-step loop, and the record at its center: the identity finding, a typed task with a class, a severity, an owner, a clock and an intended state. You check that record on a laptop with finding_contract.py, which applies the Scripted REST script’s own rules, so a malformed payload fails in a test instead of as a 400 from ServiceNow, and an intended state that gives the re-read nothing to check is flagged before it is stored. Lab 1 runs on a laptop with no tenant; Lab 2 posts one finding to a ServiceNow Personal Developer Instance.
Jump to a slide (17)
Deck · Narrated deck (about 10 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 name which of the loop’s five steps each file in the kit performs, read a finding payload and say what ServiceNow will do with it (accept, reject, de-duplicate, and when it falls due), and run that check in a test.
Audience. Developers who build either side of the integration, and the reviewers who approve their changes. You should read Python and JSON. You don’t need a ServiceNow instance or a SailPoint tenant until Lab 2.
The loop, and where the kit sits
Book 13, Appendix B gives the loop: test, detect drift, raise a typed task, actuate on the native surface, close with evidence. The kit has three builds in Pass 1 that cover four of the steps, and leaves actuation to a later pass, as the implementation example says.
| Loop step | What does it in the kit | Chapter |
|---|---|---|
| 1 Test | identity_drift.py joins the HR extract, Microsoft Graph and ISC on employeeId |
06 |
| 2 Detect drift | ISC event triggers (native change, attribute change) and the drift client | 04, 06 |
| 3 Raise a typed task | The ISC workflows’ HTTP Request step and the ServiceNow Scripted REST resource, which writes u_identity_finding |
01, 04 |
| 4 Actuate | Not in Pass 1: no code writes to Entra ID, Active Directory or ISC | — |
| 5 Close with evidence | The business rule turns any close into Awaiting verification; reread_verification.py closes on a re-read |
05, 07 |
The record those steps share is the identity finding. Its data contract is in the kit’s README: kind, source, drift_class (seven of Book 03’s eight classes; the contract has no contractor credential drift), severity (P1 to P4), system (where the re-read happens), one subject identifier, origin, and an intended_state that says what a re-read must find. Where the fields land in Book 13’s evidence record is in the implementation example’s table. This chapter doesn’t repeat them; it makes them checkable.
The code: examples/servicenow-sailpoint
What exists
The contract is enforced in one place, the Scripted REST resource servicenow/scripted-rest-identity-finding.js, which runs inside ServiceNow. It validates the payload and answers 400 with the reason, maps severity to priority 1 to 4 and a due_date from the program’s tempo (Book 13: P1 immediate, P2 one hour, P3 24 hours, P4 72 hours; program design, not a federal requirement), defaults the severity by class (Book 03 High to P2, Medium to P3, design guidance), de-duplicates against an active finding, and derives an intended state from an ISC event when the payload has none. The node:test suite in tests/servicenow/ runs that script against stubbed Glide APIs. Until now nothing outside ServiceNow applied the same rules, so a Python client learned a payload was wrong only from a 400 on a live instance.
What this chapter adds
clients/python/finding_contract.py:
validate(payload)applies the script’s rules and returns the errors the script would answer 400 to, warnings for what it would accept but the loop can’t use, and the normalized values: severity, priority, system, subject, de-duplication key and response window.- The constants (
DRIFT_CLASSES,DEFAULT_SEVERITY,PRIORITY,WINDOW_SECONDS,SYSTEMS) mirror the script’s. A test reads the JavaScript and fails if they drift apart, so the mirror can’t silently go stale. is_checkable(intended)is true only when an intended state names something the re-read client can compare.{"attributes": {}, "entitlements_absent": []}is a non-empty object, so it looks present, but a re-read finds no difference against it and would close the record on nothing. The check warns; for the three classes the program routes to a person first (HUMAN_DECISION_CLASSES: missing identity, an orphan account and an unauthorized write), the warning says the empty intent is expected until that person sets it. Missing identity and an unauthorized write are Book 13’s identity and provenance drift; an orphan account joins them because its owner decides whether to tie it to a person or disable it, and only then does the disable run as a gated machine action.- The command line checks a payload file, or turns a drift run into the payloads
identity_drift.py --push-to-servicenowwould post and checks each, printing the priority and the due date each would get.
The de-duplication key the module reports is the one Chapter 04 changes: source, subject, class and, when the payload names one, the attribute.
The tests
tests/python/test_finding_contract.py, 26 tests: the constants against the script, every rejection the script makes, the closure-signal rules, the system defaults, the checkable and vacuous intended states, the three human-first classes, the tempo, and every payload the drift client builds from the fixtures accepted. With the rest of the kit, including Chapter 09’s 32, 143 pytest tests pass, plus 15 node:test and 40 Pester tests, with no tenant or network.
The commands below run the kit’s code against fictional fixtures. They show what the code does, not that a ServiceNow instance returns the same responses. Review the code and run Lab 2 on a developer instance before any of it touches production.
Lab 1: fixtures, no tenant
You need Python 3.10 or later with pytest and requests, and Node.js 18 or later for step 3. Work in examples/servicenow-sailpoint.
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 this chapter's tests
python -m pytest tests/python/test_finding_contract.py -q
# Step 2: Check five sample payloads
python clients/python/finding_contract.py --validate tests/fixtures/payloads/samples.json --now 2026-11-01T12:00:00Z
# Step 3: Run the script's own tests
node --test tests/servicenow/servicenow_scripts.test.js
# Step 4: Check what the drift client would post
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 clients/python/finding_contract.py --from-drift out/day1.json --now 2026-11-01T12:00:00Z
# Step 5: Delete the out folder
rm -rf outRun this chapter’s tests:
python -m pytest tests/python/test_finding_contract.py -qWhat the terminal shows, run on the kit’s fixtures (the same output as text) 26 passed.Check five sample payloads:
python clients/python/finding_contract.py --validate tests/fixtures/payloads/samples.json --now 2026-11-01T12:00:00Zsample-1-good: ok stale_access P2 priority 2 due 2026-11-01T13:00:00Z system entra_user sample-2-typo: 400 drift_class must be one of missing_identity, stale_access, orphan_account, attribute_drift, unauthorized_write, credential_drift, schema_drift sample-3-empty-intent: ok unauthorized_write P2 priority 2 due 2026-11-01T13:00:00Z system isc_account sample-3-empty-intent: warning: intended_state names nothing a re-read can check (expected: Book 13 routes this class to a person, who sets it) sample-4-bad-severity: 400 severity must be P1, P2, P3 or P4 sample-5-closure: ok closure_signal (moves matches to Awaiting verification; never closes) 3 accepted, 2 rejected, 0 would de-duplicateWhat the terminal shows, run on the kit’s fixtures (the same output as text) The command exits 1 because two payloads would be rejected. The first sample got no severity, and the class default gave it P2 and a one-hour window.
Run the script’s own tests, the rules the Python mirrors:
node --test tests/servicenow/servicenow_scripts.test.jsWhat the terminal shows, run on the kit’s fixtures (the same output as text) tests 15,pass 15,fail 0.Check what the drift client would post. Run the drift reconciliation over the fixtures, then check its payloads:
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 clients/python/finding_contract.py --from-drift out/day1.json --now 2026-11-01T12:00:00ZWhat the terminal shows, run on the kit’s fixtures (the same output as text) Twelve lines start
ok, three carry the warning (two missing identities and the unauthorized write), and the last line is12 accepted, 0 rejected, 0 would de-duplicate. Erin’s two attribute drifts in Entra ID, department and manager, have their own keys; before Chapter 04’s fix they shared one, and the second would have been folded into the first. The three orphan accounts are human-first too, but their payloads already carry a checkable intent (disabled in ISC,accountEnabledfalse in Entra ID: the fix once the owner confirms), so they don’t warn.Delete the
outfolder when you’re done:rm -rf outWhat the terminal shows, run on the kit’s fixtures (the same output as text)
Expected result: you can say, for any payload, what the Scripted REST resource will do with it.
Lab 2: a ServiceNow developer instance
Not run by the authors. Use a ServiceNow Personal Developer Instance, free to Developer Program members,1 never production. Work in examples/servicenow-sailpoint. You’ll replace: <instance> (your developer instance’s name, as in https://<instance>.service-now.com), <writer-user> and <writer-password> (the integration user you give the finding-writer role in step 2; basic authentication is for developer instances only).
Every command in this lab, in order, from examples/servicenow-sailpoint. Each step below says what it does and what to look for.
# Step 3: Check a payload first, then post it
python clients/python/finding_contract.py --validate tests/fixtures/payloads/valid_finding.json
curl -s -w "\nHTTP %{http_code}\n" -u "<writer-user>:<writer-password>" -H "Content-Type: application/json" -H "Accept: application/json" -X POST --data @tests/fixtures/payloads/valid_finding.json "https://<instance>.service-now.com/api/x_example_idkit/identity_findings/finding"
# Step 3: Check a payload first, then post it
curl -s -w "\nHTTP %{http_code}\n" -u "<writer-user>:<writer-password>" -H "Content-Type: application/json" -H "Accept: application/json" -X POST --data @tests/fixtures/payloads/valid_finding.json "https://<instance>.service-now.com/api/x_example_idkit/identity_findings/finding"
# Step 4: Post samples.json's second payload
python -c "import json; print(json.dumps(json.load(open('tests/fixtures/payloads/samples.json'))[1]))" | curl -s -w "\nHTTP %{http_code}\n" -u "<writer-user>:<writer-password>" -H "Content-Type: application/json" -H "Accept: application/json" -X POST --data @- "https://<instance>.service-now.com/api/x_example_idkit/identity_findings/finding"Build the table and the resource as the kit’s README describes. Creating a Scripted REST API needs the
web_service_adminrole; calling it doesn’t.2- Create the scoped application, and in it
u_identity_findingextending Task, with the fields the README lists and the Awaiting verification state. - Go to System Web Services > Scripted REST APIs > New and create the Scripted REST API
identity_findings. - Give it one resource,
POST /finding, whose script is the kit’s fileservicenow/scripted-rest-identity-finding.js.
- Create the scoped application, and in it
Lock the resource down. Require authentication and add an ACL of type
REST_Endpointgranting a custom role, for examplex_example_idkit.finding_writer, to one integration user.3Check a payload first, then post it as the integration user:
python clients/python/finding_contract.py --validate tests/fixtures/payloads/valid_finding.json curl -s -w "\nHTTP %{http_code}\n" -u "<writer-user>:<writer-password>" -H "Content-Type: application/json" -H "Accept: application/json" -X POST --data @tests/fixtures/payloads/valid_finding.json "https://<instance>.service-now.com/api/x_example_idkit/identity_findings/finding"Expect
201with asys_id,number, priority2, and a due date one hour after the insert. Post the same file again:curl -s -w "\nHTTP %{http_code}\n" -u "<writer-user>:<writer-password>" -H "Content-Type: application/json" -H "Accept: application/json" -X POST --data @tests/fixtures/payloads/valid_finding.json "https://<instance>.service-now.com/api/x_example_idkit/identity_findings/finding"Expect
200withdeduplicated: true.Post
samples.json’s second payload. The first command prints that payload alone, andcurlposts it:python -c "import json; print(json.dumps(json.load(open('tests/fixtures/payloads/samples.json'))[1]))" | curl -s -w "\nHTTP %{http_code}\n" -u "<writer-user>:<writer-password>" -H "Content-Type: application/json" -H "Accept: application/json" -X POST --data @- "https://<instance>.service-now.com/api/x_example_idkit/identity_findings/finding"Expect
400with the same reason Lab 1 printed.Back it out. Delete the test records, the ACL and the role, and the Scripted REST API (System Web Services > Scripted REST APIs); delete the scoped application if you made it for the lab. A developer instance returns to the pool after a period without use, and is reset when it does.4
The verified facts
| Item | Value | Source |
|---|---|---|
| Scripted REST API URL | https://<instance>/api/<name_space>/<version>/<api_id>/<relative_path>; the version is optional |
5 |
| Scripted REST API access | Authentication through API access policies; ACLs of type REST_Endpoint, one of which must be satisfied; resources inherit the API’s ACLs unless they set their own |
6 |
| Role to create one | web_service_admin, not needed to call one |
7 |
| SailPoint event triggers the kit uses | Identity Attributes Changed (idn:identity-attributes-changed) and Provisioning Action Completed (idn:post-provisioning), both fire-and-forget |
8 |
| ServiceNow Government Community Cloud | FedRAMP Certified, High, JAB | 9 |
| SailPoint Identity Security Cloud | FedRAMP Certified, Moderate, agency path | 10 |
| Personal Developer Instance | Free to Developer Program members; returned to the pool and reset after ten days unused, hibernates when idle (page dated January 30, 2025) | 11 |
What couldn’t be verified
- Which identity
gs.getUserName()returns for an OAuth client-credentials call, which the business rule relies on (Chapter 05); kit README verify list, item 10. - The current developer-instance reclamation rule. ServiceNow’s page says ten days unused; community posts from July 2026 describe a rule that also considers the instance’s age. Check the Developer Site before relying on an instance for more than a few days (research gaps, row 1).
Federal requirements and evidence
A typed finding is the “owned, clocked” half of the record Book 13 says an assessor reads: who owns the drift, by when, and against what intended state. The approval and the re-read are added in later chapters.
| What the output proves | Requirement it supports | KSI | Who keeps it |
|---|---|---|---|
| Every drift becomes one record with a class, a severity, an owner and a due date, rejected at the door if malformed | NIST SP 800-53 CM-3 (the record of configuration-controlled changes) and CA-7 (continuous monitoring), both in the Moderate baseline12 | KSI-CMT-LMC; KSI-MLA-EVC | The ServiceNow record; the contract tests in the repository |
| The response window per severity is applied by code, not chosen per ticket | The program’s tempo (Book 13, design guidance); an external clock, such as a CISA directive’s, overrides it when shorter | None: program design | The record’s due_date; Chapter 07’s window adherence |
| Lab 1, steps 1 and 3: the Python mirror and the Scripted REST script apply the same rules, tested before a change merges | CM-3(2) (testing, validation and documentation of changes) and SA-11 (developer testing), at Moderate13 | KSI-CMT-VTD; KSI-SCR-MIT | The pytest tests/python/test_finding_contract.py and node --test tests/servicenow/servicenow_scripts.test.js runs, in the pull request’s checks |
| Lab 1, step 2: a malformed payload is refused with its reason, and an accepted one gets its priority and due date | CM-3 | KSI-CMT-LMC | The finding_contract.py --validate output, kept with the pull request that changes a client |
| Lab 1, step 4: every payload the drift client builds is accepted before it reaches ServiceNow | CM-3(2); CA-7 | KSI-CMT-VTD; KSI-MLA-EVC | The finding_contract.py --from-drift output. out/day1.json and out/day1.csv are fixture output; the live run’s files are Chapter 06’s |
| Lab 1, step 5: fixture output isn’t kept | None | None | Nobody: rm -rf out deletes fictional data |
Lab 2, steps 1 and 2: the Scripted REST API requires authentication and a REST_Endpoint ACL that grants one custom role to one integration user |
AC-3 (access enforcement), AC-6 (least privilege) and AC-2 (account management), at Moderate14 | KSI-IAM-ELP; KSI-IAM-SNU | The ACL, the role and the user, in the scoped application’s update set |
Lab 2, step 3: a checked payload creates one u_identity_finding record with priority and due date, and a repeat de-duplicates |
CM-3; CA-7 | KSI-CMT-LMC; KSI-MLA-EVC | The record on the instance; the 201 and then 200 with deduplicated: true |
| Lab 2, step 4: the instance refuses the payload Lab 1 refused, for the same reason | CM-3 | KSI-CMT-LMC | The 400 response, beside Lab 1’s output |
| Lab 2, step 5: no test record, ACL, role or API is left on the instance | AC-2 | KSI-IAM-SNU | The instance’s deletion history; a developer instance holds no agency data |
| Production runs on the FedRAMP Certified offerings in the verified facts, not on a developer instance | FedRAMP authorization of the cloud services used; SA-9 (external system services) at Moderate15 | KSI-SCR-MIT; KSI-SCR-MON | The ISSO, with each offering’s authorization package and the Marketplace listing with its read date |
The KSI column names the FedRAMP 20x Key Security Indicators whose related SP 800-53 controls include the row’s control.16 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.
The control-by-control mapping is Book 13’s Appendix A; the federal sources are in the federal framework crosswalk.
Dangers
| Danger | What happens | The guard |
|---|---|---|
| The Python mirror drifts from the script | A payload passes locally and fails in ServiceNow, or the reverse | test_constants_match_the_scripted_rest_script reads the JavaScript |
| A typo in a drift class | The finding is rejected and the drift goes unrecorded | 400 locally first; the CLI exits 1 |
| An intended state with nothing to check | A re-read finds no difference and closes the record on nothing | is_checkable warns; Chapter 05’s guard skips the record |
| Severity chosen per ticket | Windows stop meaning anything | Class defaults in one table; a payload may only raise or lower it explicitly |
| A test instance treated as permanent | Work is lost when the instance is reclaimed | Keep the scoped application in source control or an update set |
Checklist
Footnotes
ServiceNow, “Understanding PDIs” (Yokohama, last updated January 30, 2025), https://www.servicenow.com/docs/r/SmYvGPN0OygSQs53fsexIQ/yG_RdLKb649OcfnDiGEkwQ (read November 1, 2026).↩︎
ServiceNow, “Scripted REST APIs” (Zurich API reference: URL format, API access policies,
REST_EndpointACLs, theweb_service_adminrole), https://www.servicenow.com/docs/bundle/zurich-api-reference/page/integrate/custom-web-services/concept/c_CustomWebServices.html (read November 1, 2026).↩︎ServiceNow, “Scripted REST APIs” (Zurich API reference: URL format, API access policies,
REST_EndpointACLs, theweb_service_adminrole), https://www.servicenow.com/docs/bundle/zurich-api-reference/page/integrate/custom-web-services/concept/c_CustomWebServices.html (read November 1, 2026).↩︎ServiceNow, “Understanding PDIs” (Yokohama, last updated January 30, 2025), https://www.servicenow.com/docs/r/SmYvGPN0OygSQs53fsexIQ/yG_RdLKb649OcfnDiGEkwQ (read November 1, 2026).↩︎
ServiceNow, “Scripted REST APIs” (Zurich API reference: URL format, API access policies,
REST_EndpointACLs, theweb_service_adminrole), https://www.servicenow.com/docs/bundle/zurich-api-reference/page/integrate/custom-web-services/concept/c_CustomWebServices.html (read November 1, 2026).↩︎ServiceNow, “Scripted REST APIs” (Zurich API reference: URL format, API access policies,
REST_EndpointACLs, theweb_service_adminrole), https://www.servicenow.com/docs/bundle/zurich-api-reference/page/integrate/custom-web-services/concept/c_CustomWebServices.html (read November 1, 2026).↩︎ServiceNow, “Scripted REST APIs” (Zurich API reference: URL format, API access policies,
REST_EndpointACLs, theweb_service_adminrole), https://www.servicenow.com/docs/bundle/zurich-api-reference/page/integrate/custom-web-services/concept/c_CustomWebServices.html (read November 1, 2026).↩︎SailPoint Developer, “Triggers,” Available Event Triggers table, https://developer.sailpoint.com/docs/api/triggers (read November 1, 2026).↩︎
FedRAMP Marketplace data, https://www.fedramp.gov/marketplace/products.json (last change November 1, 2026): ServiceNow Government Community Cloud, F1305072116; SailPoint Identity Security Cloud, FR2001938710A (read November 1, 2026).↩︎
FedRAMP Marketplace data, https://www.fedramp.gov/marketplace/products.json (last change November 1, 2026): ServiceNow Government Community Cloud, F1305072116; SailPoint Identity Security Cloud, FR2001938710A (read November 1, 2026).↩︎
ServiceNow, “Understanding PDIs” (Yokohama, last updated January 30, 2025), https://www.servicenow.com/docs/r/SmYvGPN0OygSQs53fsexIQ/yG_RdLKb649OcfnDiGEkwQ (read November 1, 2026).↩︎
National Institute of Standards and Technology, SP 800-53B Moderate baseline, release 5.2.0 OSCAL profile (includes AC-2, AC-3, AC-6, AU-9, CA-7, CM-3, CM-3(2), CM-6, SA-9, 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).↩︎
National Institute of Standards and Technology, SP 800-53B Moderate baseline, release 5.2.0 OSCAL profile (includes AC-2, AC-3, AC-6, AU-9, CA-7, CM-3, CM-3(2), CM-6, SA-9, 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).↩︎
National Institute of Standards and Technology, SP 800-53B Moderate baseline, release 5.2.0 OSCAL profile (includes AC-2, AC-3, AC-6, AU-9, CA-7, CM-3, CM-3(2), CM-6, SA-9, 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).↩︎
National Institute of Standards and Technology, SP 800-53B Moderate baseline, release 5.2.0 OSCAL profile (includes AC-2, AC-3, AC-6, AU-9, CA-7, CM-3, CM-3(2), CM-6, SA-9, 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).↩︎
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).↩︎




