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 01 — The Loop and the Typed Task

Where the kit sits in Book 13’s five-step loop, and the one record every step writes or reads: the typed identity finding. Check the data contract on a laptop before anything reaches ServiceNow, with the drift classes, severities, systems, subject rules, tempo and de-duplication key mirrored from the Scripted REST script by a test, and a warning when an intended state gives the re-read nothing to check; with labs against fixtures and a ServiceNow developer instance.

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.

NoteIn this chapter

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.

TipNarrated tour
Building Ticket-to-Machine, Chapter 01 — The Loop and the Typed Task: narrated tour · 10 minutes · captions from the player's CC button · Download MP4 · All videos
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-servicenow would 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.

ImportantIllustrative code

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.

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 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 out
  1. Run this chapter’s tests:

    python -m pytest tests/python/test_finding_contract.py -q

    Terminal window showing python -m pytest tests/python/test_finding_contract.py -q and what it printed

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

    26 passed.

  2. Check five sample payloads:

    python clients/python/finding_contract.py --validate tests/fixtures/payloads/samples.json --now 2026-11-01T12:00:00Z
    sample-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-duplicate

    Terminal window showing python clients/python/finding_contract.py --validate tests/fixtures/payloads/… and what it printed

    What 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.

  3. Run the script’s own tests, the rules the Python mirrors:

    node --test tests/servicenow/servicenow_scripts.test.js

    Terminal window showing node --test tests/servicenow/servicenow_scripts.test.js and what it printed

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

    tests 15, pass 15, fail 0.

  4. 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:00Z

    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)

    Twelve lines start ok, three carry the warning (two missing identities and the unauthorized write), and the last line is 12 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, accountEnabled false in Entra ID: the fix once the owner confirms), so they don’t warn.

  5. Delete the out folder when you’re done:

    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)

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).

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 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"
  1. Build the table and the resource as the kit’s README describes. Creating a Scripted REST API needs the web_service_admin role; calling it doesn’t.2

    1. Create the scoped application, and in it u_identity_finding extending Task, with the fields the README lists and the Awaiting verification state.
    2. Go to System Web Services > Scripted REST APIs > New and create the Scripted REST API identity_findings.
    3. Give it one resource, POST /finding, whose script is the kit’s file servicenow/scripted-rest-identity-finding.js.
  2. Lock the resource down. Require authentication and add an ACL of type REST_Endpoint granting a custom role, for example x_example_idkit.finding_writer, to one integration user.3

  3. Check 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 201 with a sys_id, number, priority 2, 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 200 with deduplicated: true.

  4. Post samples.json’s second payload. The first command prints that payload alone, and curl posts 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 400 with the same reason Lab 1 printed.

  5. 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

Back to top

Footnotes

  1. ServiceNow, “Understanding PDIs” (Yokohama, last updated January 30, 2025), https://www.servicenow.com/docs/r/SmYvGPN0OygSQs53fsexIQ/yG_RdLKb649OcfnDiGEkwQ (read November 1, 2026).↩︎

  2. ServiceNow, “Scripted REST APIs” (Zurich API reference: URL format, API access policies, REST_Endpoint ACLs, the web_service_admin role), https://www.servicenow.com/docs/bundle/zurich-api-reference/page/integrate/custom-web-services/concept/c_CustomWebServices.html (read November 1, 2026).↩︎

  3. ServiceNow, “Scripted REST APIs” (Zurich API reference: URL format, API access policies, REST_Endpoint ACLs, the web_service_admin role), https://www.servicenow.com/docs/bundle/zurich-api-reference/page/integrate/custom-web-services/concept/c_CustomWebServices.html (read November 1, 2026).↩︎

  4. ServiceNow, “Understanding PDIs” (Yokohama, last updated January 30, 2025), https://www.servicenow.com/docs/r/SmYvGPN0OygSQs53fsexIQ/yG_RdLKb649OcfnDiGEkwQ (read November 1, 2026).↩︎

  5. ServiceNow, “Scripted REST APIs” (Zurich API reference: URL format, API access policies, REST_Endpoint ACLs, the web_service_admin role), https://www.servicenow.com/docs/bundle/zurich-api-reference/page/integrate/custom-web-services/concept/c_CustomWebServices.html (read November 1, 2026).↩︎

  6. ServiceNow, “Scripted REST APIs” (Zurich API reference: URL format, API access policies, REST_Endpoint ACLs, the web_service_admin role), https://www.servicenow.com/docs/bundle/zurich-api-reference/page/integrate/custom-web-services/concept/c_CustomWebServices.html (read November 1, 2026).↩︎

  7. ServiceNow, “Scripted REST APIs” (Zurich API reference: URL format, API access policies, REST_Endpoint ACLs, the web_service_admin role), https://www.servicenow.com/docs/bundle/zurich-api-reference/page/integrate/custom-web-services/concept/c_CustomWebServices.html (read November 1, 2026).↩︎

  8. SailPoint Developer, “Triggers,” Available Event Triggers table, https://developer.sailpoint.com/docs/api/triggers (read November 1, 2026).↩︎

  9. 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).↩︎

  10. 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).↩︎

  11. ServiceNow, “Understanding PDIs” (Yokohama, last updated January 30, 2025), https://www.servicenow.com/docs/r/SmYvGPN0OygSQs53fsexIQ/yG_RdLKb649OcfnDiGEkwQ (read November 1, 2026).↩︎

  12. 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).↩︎

  13. 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).↩︎

  14. 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).↩︎

  15. 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).↩︎

  16. 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).↩︎