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 05 — The Re-read Rule

Build 2 of the kit and its answer to Gap 1: a ServiceNow business rule turns any close into Awaiting verification, and a client re-reads the account, identity or user from its system of record and closes the finding only with the evidence, or reopens it. Run the same decisions offline from saved records and saved reads, judge ISC’s aggregation lag against a fixed clock, and add the guard that an intended state naming nothing a re-read can check is skipped, never verified; with labs against fixtures and a ServiceNow developer instance.

A success response is not closure. Neither is a ticket status, and neither is a comparison against nothing.

NoteIn this chapter

The kit’s Build 2, the loop’s step 5 and Book 13’s fourth clause: a finding closes only when a re-read of the target shows the intended state. The business rule enforces it inside ServiceNow; reread_verification.py does the re-reading. This chapter adds an offline mode that runs the client’s decisions over saved records and saved reads, so the rule can be taught and tested with no tenant, and a guard: an intended state that names nothing a re-read can check is skipped, because comparing against it always “matches.” Lab 1 runs on a laptop; Lab 2 closes one finding on a developer instance.

TipNarrated tour
Building Ticket-to-Machine, Chapter 05 — The Re-read Rule: narrated tour · 10 minutes · captions from the player's CC button · Download MP4 · All videos
Jump to a slide (16)

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 predict, for any waiting finding and any read of its subject, whether the client will verify it, reopen it, leave it pending or skip it, and show that no path closes a finding without a matching re-read.

Audience. Developers who own the re-read client or the business rule, ServiceNow administrators who configure the rule, and the ISSO, for whom this is the closure evidence. You should read Python and a little JavaScript. You don’t need a tenant until Lab 2.

The code: examples/servicenow-sailpoint

What exists

The implementation example’s Build 2 gives the design. In brief:

  • The business rule (servicenow/business-rule-await-verification.js, before update, on the state field) moves any close on u_identity_finding, and on tickets SailPoint’s Service Desk integration opened, to Awaiting verification, unless the same update carries a new evidence hash and comes from the verifier account. A named exception approver may still cancel with a close note: the declared-exception lane. This is the answer to Gap 1, where SailPoint’s Service Desk best practice treats the mapped ServiceNow status as completion.1
  • The re-read client (clients/python/reread_verification.py) lists waiting findings through the Table API, re-reads each subject by its u_system (GET /v3/accounts/{id}, a Search for one identity, or GET /v1.0/users/{id} in Graph), compares with u_intended_state, and then closes the record with the evidence (query, time, a SHA-256 of the response, the intended state, the checker) and attaches the evidence as JSON, or reopens it with the differences. An ISC read reflects the last aggregation, so a read older than the time the finding started waiting is pending for a grace period, 24 hours by default (design guidance). It writes only with --apply.

Two things were missing. The client could run only against a live instance and tenant, so its decisions could be seen only in mocked unit tests. And it skipped a record whose intended state was empty, {}, but not one whose intended state was a non-empty object naming nothing: {"attributes": {}, "entitlements_absent": []} is what the script stores for a native change the workflow didn’t describe (Chapter 04), and comparing a read against it finds no difference. The client would have closed that record, with evidence, on nothing.

What this chapter adds

  • The guard. Before reading anything, the client checks the intended state with finding_contract.is_checkable (Chapter 01). A record that names nothing checkable is reported as skipped with the reason, and is never verified.
  • The offline mode. --records-json takes saved waiting records in the Table API’s result shape; --observations-json takes saved reads keyed by subject ID, with null for a read that found nothing; --now fixes the clock for the grace period. The same verify_records makes the same decisions; only the reader is swapped. It is a dry run only: --apply is refused, and a subject with no saved read is an error, because a missing read isn’t a match.
  • Six fictional waiting records in tests/fixtures/reread/, one per outcome: a reverted native change that verifies; a terminated user still enabled in Entra ID (mismatch); an ISC account read before it was re-aggregated (pending, then mismatch after the grace period); an attribute change that verifies; a native change with an empty intended state (skipped); and an orphan account disabled (verified).

The tests

tests/python/test_reread_offline.py, 6 tests: the six outcomes, the grace period expiring, the guard never calling the reader, a missing saved read as an error, a null read as not found, and the refusals. The kit’s original nine re-read tests, and the business rule’s six node:test tests, pass unchanged.

ImportantIllustrative code

The offline mode runs the client’s logic over saved data. It shows the decisions, not that ISC or Graph returns the same shapes, and not that the business rule recognizes the verifier on your release (see what couldn’t be verified). Run Lab 2 before relying on either.

Lab 1: fixtures, no tenant

Work in examples/servicenow-sailpoint, with Python 3.10 or later and Node.js 18 or later.

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 re-read tests
python -m pytest tests/python/test_reread_verification.py tests/python/test_reread_offline.py -q

# Step 2: Re-read six waiting findings at noon on November 1
python clients/python/reread_verification.py --records-json tests/fixtures/reread/awaiting_records.json --observations-json tests/fixtures/reread/observations.json --now 2026-11-01T12:00:00Z

# Step 3: Run the same records a day later
python clients/python/reread_verification.py --records-json tests/fixtures/reread/awaiting_records.json --observations-json tests/fixtures/reread/observations.json --now 2026-11-02T12:00:00Z --out-json out/reread.json

# Step 4: Run the business rule's tests
node --test --test-name-pattern="redirected|verifier|exception approver|evidence alone|non-closing|SDIM" tests/servicenow/servicenow_scripts.test.js
  1. Run the re-read tests:

    python -m pytest tests/python/test_reread_verification.py tests/python/test_reread_offline.py -q

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

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

    15 passed.

  2. Re-read six waiting findings at noon on November 1:

    python clients/python/reread_verification.py --records-json tests/fixtures/reread/awaiting_records.json --observations-json tests/fixtures/reread/observations.json --now 2026-11-01T12:00:00Z
    [dry-run] IDF0001001: verified; would PATCH state=3
    [dry-run] IDF0001002: mismatch; would PATCH state=1 (accountEnabled: intended False, observed True)
    IDF0001003: pending, the ISC copy predates the change; re-run after the next aggregation
    [dry-run] IDF0001004: verified; would PATCH state=3
    IDF0001005: skipped, the intended state names nothing a re-read can check
    [dry-run] IDF0001006: verified; would PATCH state=3
    1 mismatch, 1 pending, 1 skipped, 3 verified

    Terminal window showing python clients/python/reread_verification.py --records-json tests/fixtures/re… and what it printed

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

    IDF0001003’s account was last modified at 22:00 on October 31; the finding started waiting at 06:00 on November 1. The read predates the change, so it waits. IDF0001005 is the case the guard exists for.

  3. Run the same records a day later:

    python clients/python/reread_verification.py --records-json tests/fixtures/reread/awaiting_records.json --observations-json tests/fixtures/reread/observations.json --now 2026-11-02T12:00:00Z --out-json out/reread.json

    Terminal window showing python clients/python/reread_verification.py --records-json tests/fixtures/re… and what it printed

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

    IDF0001003 is now mismatch; would PATCH state=1 (disabled: intended True, observed False), and the last line is 2 mismatch, 1 skipped, 3 verified. After the grace period a stale read is a mismatch, and the record reopens. out/reread.json holds each result with its evidence hash.

  4. Run the business rule’s tests:

    node --test --test-name-pattern="redirected|verifier|exception approver|evidence alone|non-closing|SDIM" tests/servicenow/servicenow_scripts.test.js

    Terminal window showing node --test --test-name-pattern='redirected|verifier|exception approver|evide… and what it printed

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

    tests 6, pass 6: a person’s close is redirected; the verifier with fresh evidence may close; evidence alone, or the verifier alone, isn’t enough; the exception approver may cancel with a note; other changes are untouched; on incident, only tickets the integration opened are governed.

Lab 2: a ServiceNow developer instance

Not run by the authors. Use the developer instance from Chapters 01 and 02, never production, and the verifier user from Chapter 02. Work in examples/servicenow-sailpoint. You’ll replace: <instance> (your developer instance’s name), <verifier-client-id> and <verifier-client-secret> (the verifier’s OAuth integration), <test-sys_id> (the sys_id of a test finding), and <graph-tenant-id>, <graph-client-id> and <graph-cert.pem> (a test app registration with User.Read.All, and the PEM file holding its certificate and key).

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: Confirm who the verifier is
export SN_ACCESS_TOKEN=$(curl -s -X POST "https://<instance>.service-now.com/oauth_token.do" -d grant_type=client_credentials -d client_id="<verifier-client-id>" -d client_secret="<verifier-client-secret>" | python -c "import json,sys; print(json.load(sys.stdin)['access_token'])")
curl -s -w "\nHTTP %{http_code}\n" -X PATCH -H "Authorization: Bearer $SN_ACCESS_TOKEN" -H "Content-Type: application/json" -H "Accept: application/json" --data '{"work_notes": "lab: which user does ServiceNow record for the verifier?"}' "https://<instance>.service-now.com/api/now/table/u_identity_finding/<test-sys_id>"

# Step 4: Close it with evidence
curl -s -w "\nHTTP %{http_code}\n" -X PATCH -H "Authorization: Bearer $SN_ACCESS_TOKEN" -H "Content-Type: application/json" -H "Accept: application/json" --data '{"u_intended_state": "{\"accountEnabled\": false}"}' "https://<instance>.service-now.com/api/now/table/u_identity_finding/<test-sys_id>"

# Step 4: Close it with evidence
unset SN_ACCESS_TOKEN
export SN_INSTANCE="<instance>"
export SN_CLIENT_ID="<verifier-client-id>"
export SN_CLIENT_SECRET="<verifier-client-secret>"
export GRAPH_TENANT_ID="<graph-tenant-id>"
export GRAPH_CLIENT_ID="<graph-client-id>"
export GRAPH_CERT_PATH="<graph-cert.pem>"
python clients/python/reread_verification.py --table u_identity_finding

# Step 4: Close it with evidence
python clients/python/reread_verification.py --apply --table u_identity_finding

# Step 5: Back it out
unset SN_INSTANCE SN_CLIENT_ID SN_CLIENT_SECRET GRAPH_TENANT_ID GRAPH_CLIENT_ID GRAPH_CERT_PATH
  1. Configure the rule on u_identity_finding as the kit’s README says (before, update, order 1000, condition current.state.changes(), script servicenow/business-rule-await-verification.js), and the system properties: the Awaiting verification value, the verifier’s user_name, the exception role.

  2. Close a finding as a person. Set a test finding’s state to Closed Complete as an ordinary user. Expect it to land in Awaiting verification, with a work note.

  3. Confirm who the verifier is. As the verifier’s OAuth client, request a token and make one PATCH to a test record’s work notes:

    export SN_ACCESS_TOKEN=$(curl -s -X POST "https://<instance>.service-now.com/oauth_token.do" -d grant_type=client_credentials -d client_id="<verifier-client-id>" -d client_secret="<verifier-client-secret>" | python -c "import json,sys; print(json.load(sys.stdin)['access_token'])")
    curl -s -w "\nHTTP %{http_code}\n" -X PATCH -H "Authorization: Bearer $SN_ACCESS_TOKEN" -H "Content-Type: application/json" -H "Accept: application/json" --data '{"work_notes": "lab: which user does ServiceNow record for the verifier?"}' "https://<instance>.service-now.com/api/now/table/u_identity_finding/<test-sys_id>"

    Look at the record’s history for the user ServiceNow recorded. If it isn’t the user_name in x_example_idkit.verifier_user, the rule will refuse every close (verify list, item 10).

  4. Close it with evidence. Set the test finding’s u_intended_state to something a Graph read of a test user will show; this sets {"accountEnabled": false}, for a finding about a disabled test user:

    curl -s -w "\nHTTP %{http_code}\n" -X PATCH -H "Authorization: Bearer $SN_ACCESS_TOKEN" -H "Content-Type: application/json" -H "Accept: application/json" --data '{"u_intended_state": "{\"accountEnabled\": false}"}' "https://<instance>.service-now.com/api/now/table/u_identity_finding/<test-sys_id>"

    Run the client as the verifier, first as a dry run. Certificate authentication needs the optional cryptography package.

    unset SN_ACCESS_TOKEN
    export SN_INSTANCE="<instance>"
    export SN_CLIENT_ID="<verifier-client-id>"
    export SN_CLIENT_SECRET="<verifier-client-secret>"
    export GRAPH_TENANT_ID="<graph-tenant-id>"
    export GRAPH_CLIENT_ID="<graph-client-id>"
    export GRAPH_CERT_PATH="<graph-cert.pem>"
    python clients/python/reread_verification.py --table u_identity_finding

    Then with --apply:

    python clients/python/reread_verification.py --apply --table u_identity_finding

    Expect the record closed with u_evidence, u_evidence_hash, u_verified_at, a close note and a JSON attachment.23

  5. Back it out. Delete the test findings and their attachments, deactivate the business rule if you built it for the lab, and delete the system properties. Clear the credentials from your shell:

    unset SN_INSTANCE SN_CLIENT_ID SN_CLIENT_SECRET GRAPH_TENANT_ID GRAPH_CLIENT_ID GRAPH_CERT_PATH

The verified facts

Item Value Source
Table API GET with sysparm_query, sysparm_fields, sysparm_limit, sysparm_offset; PATCH on a record 4
Attachment API POST /api/now/attachment/file with table_name, table_sys_id, file_name; Content-Type mandatory 5
Re-read of an account GET /v3/accounts/{id}, idn:accounts:read 6
Re-read of an identity POST /v3/search, identities index, sp:search:read 7
Re-read of an Entra user GET /v1.0/users/{id}, User.Read.All (application); 404 Not Found when the user doesn’t exist 8
Service Desk integration status mapping The mapped ServiceNow status decides when SailPoint treats the provisioning task as complete (Gap 1) 9

What couldn’t be verified

  • The identity gs.getUserName() returns for an OAuth client-credentials call, which the rule compares with the verifier property. Verify list, item 10; Lab 2, step 3.
  • How long an ISC account read lags the source. It depends on the aggregation schedule; tune --grace-hours to it. Verify list, item 17.
  • How to recognize a ticket the Service Desk integration opened. The rule assumes opened_by; a configuration may use a correlation field instead. Verify list, item 12.

Federal requirements and evidence

What the output proves Requirement it supports KSI Who keeps it
A finding closed only after a re-read showed the intended state, with the query, the time, a hash of the response and the checker Book 13’s clause 4, “a success response is not closure”; NIST SP 800-53 CM-3 (documented, verified changes) and CA-7 at Moderate10 KSI-CMT-LMC; KSI-MLA-EVC u_evidence, u_evidence_hash and the attachment on each record; Chapter 07 exports them
No person can close a finding by setting its state; cancellation is a named exception with a note AC-5 (separation of duties) at Moderate;11 Book 13’s declared-exception lane KSI-IAM-JIT The business rule and its properties; the work notes
A read that predates the change doesn’t count CA-7: monitoring results that reflect the current state KSI-MLA-EVC The client’s pending outcome and grace period
An empty intended state never closes a record CM-3: a verified change, not an asserted one KSI-CMT-VTD The guard and its test
Lab 1, step 1: the re-read decisions hold, online and offline, tested before a change merges CM-3(2) (testing changes); SA-11 (developer testing), at Moderate12 KSI-CMT-VTD; KSI-SCR-MIT The pytest tests/python/test_reread_verification.py tests/python/test_reread_offline.py run, in the pull request’s checks
Lab 1, step 2: each waiting finding is verified, left pending or skipped, and nothing is written CM-4(2) (verification of controls) at Moderate;13 CA-7 KSI-CMT-VTD; KSI-MLA-EVC The dry-run output of reread_verification.py --records-json
Lab 1, step 3: after the grace period a stale read is a mismatch and the record would reopen CM-4(2); CA-7 KSI-CMT-VTD; KSI-MLA-EVC out/reread.json: each record’s outcome, differences and evidence hash. A live run’s --out-json file is kept with the run
Lab 1, step 4: a person’s close is redirected; only the verifier with fresh evidence closes AC-5; CM-3(2) KSI-IAM-JIT; KSI-CMT-VTD The node --test --test-name-pattern run, in the pull request’s checks
Lab 2, steps 1 and 2: a person’s close lands in Awaiting verification, with a work note AC-5; CM-3 KSI-IAM-JIT; KSI-CMT-LMC The test record’s history and work note
Lab 2, step 3: the user ServiceNow records for the verifier’s OAuth client AU-12 (audit record generation) at Moderate;14 AC-5 KSI-MLA-LET; KSI-IAM-JIT The record’s history, kept with the x_example_idkit.verifier_user property
Lab 2, step 4: reread_verification.py --apply closes the record only after a Graph read shows the intended state CM-3; CM-4(2); CA-7 KSI-CMT-LMC; KSI-CMT-VTD; KSI-MLA-EVC The record’s u_evidence, u_evidence_hash, u_verified_at and close note, and the attachment reread-evidence-<number>.json; a mismatch reopens the record with a work note
Lab 2, step 5: no test finding, attachment, rule, property or credential is left None: lab clean-up None Nobody; the unset command clears the shell

The KSI column names the FedRAMP 20x Key Security Indicators whose related SP 800-53 controls include the row’s control.15 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
Closure by ticket status The Service Desk integration, or a person, marks the work done with nothing checked The business rule redirects every close without fresh evidence from the verifier
An intended state that names nothing A re-read “matches” and closes the record on nothing is_checkable; such records are skipped, with the reason
A stale ISC read A finding closes on the state before the fix, or reopens on it Pending inside the grace period, mismatch after it
A missing read treated as a match A subject nobody read is closed The offline reader raises on a missing subject; the live reader raises on anything but a 404
The verifier account misconfigured Every close is refused, and people look for a way around the rule Lab 2, step 3, before go-live
Evidence edited after closure The record no longer proves what it says The evidence hash; Chapter 07 recomputes it

Checklist

Back to top

Footnotes

  1. SailPoint, “SDIM: Best Practices” (status mapping decides when a ticket counts as complete), https://documentation.sailpoint.com/connectors/servicenow/sdim/help/integrating_servicenow_sdim/best_practices.html (read November 1, 2026).↩︎

  2. ServiceNow, “Table API” (Zurich API reference), https://www.servicenow.com/docs/bundle/zurich-api-reference/page/integrate/inbound-rest/concept/c_TableAPI.html (read November 1, 2026).↩︎

  3. ServiceNow, “Attachment API” (Zurich), https://www.servicenow.com/docs/r/MVRCUFVyKWd7vUoUukbW6g/lfpHTMPdNaq_xdajhuTwEg (read November 1, 2026).↩︎

  4. ServiceNow, “Table API” (Zurich API reference), https://www.servicenow.com/docs/bundle/zurich-api-reference/page/integrate/inbound-rest/concept/c_TableAPI.html (read November 1, 2026).↩︎

  5. ServiceNow, “Attachment API” (Zurich), https://www.servicenow.com/docs/r/MVRCUFVyKWd7vUoUukbW6g/lfpHTMPdNaq_xdajhuTwEg (read November 1, 2026).↩︎

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

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

  8. Microsoft Learn, “Get a user,” permissions and response, https://learn.microsoft.com/graph/api/user-get?view=graph-rest-1.0 (read November 1, 2026).↩︎

  9. SailPoint, “SDIM: Best Practices” (status mapping decides when a ticket counts as complete), https://documentation.sailpoint.com/connectors/servicenow/sdim/help/integrating_servicenow_sdim/best_practices.html (read November 1, 2026).↩︎

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