Federal Modernization Workbook
Chapter 07 — Evidence Records for Book 13 and the ISSO
The ISSO doesn’t need the tracker. The ISSO needs one record per finding, in one shape, that nobody could have edited afterwards without it showing.
The loop’s fifth clause, “emits verified, secured evidence automatically,” for identity findings. evidence_export.py reads closed and cancelled findings from ServiceNow, writes each as a record in Book 13’s evidence shape, checks what an assessor would check, and packages the period with a SHA-256 per file and a hash chain across records and periods, with a verifier the ISSO can run. It builds the ServiceNow half of the kit’s planned Pass 2 evidence export; the SailPoint events half needs a tenant and isn’t built yet. Lab 1 runs on a laptop; Lab 2 exports from a developer instance.
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 produce the period’s evidence package for identity findings, say what each record proves and what it doesn’t yet, and verify a package you were handed.
Audience. Developers who build the evidence pipeline, and the ISSO and assessors who read its output. You should read Python and JSON. You don’t need an instance until Lab 2.
The code: examples/servicenow-sailpoint
What exists
Chapter 05’s re-read client writes the evidence onto each record it closes: u_evidence (the query, the check time, the intended state, a SHA-256 of the response, the differences, the checker and its identity, as canonical JSON), u_evidence_hash (the SHA-256 of that JSON), u_verified_at and a close note, plus a JSON attachment. The business rule allows a close only from the verifier with fresh evidence, or a cancellation with a note under the declared-exception lane. The implementation example maps the record’s fields to Book 13’s in its evidence-record table, and lists the evidence export among the items planned for Pass 2. Nothing read the records back out.
What this chapter adds
clients/python/evidence_export.py:
- One record per finding, schema
book13-evidence-record/identity-finding/v1, with Book 13’s seven fields. Approval, executing identity and what ran are written asnullwith the reason: Pass 1 doesn’t actuate, and the change or catalog record that would hold them is a later pass. The record says so rather than leaving the fields out or filling them. - Four checks per record, each reported by name: the evidence still hashes to
u_evidence_hash(an edit after closure, or a value cut short by the 4,000-character field, fails); a closed record carries evidence at all (a close that bypassed the business rule fails); the evidence says the intended state matched; and the record finished inside its response window, verified at or before its due date. - The package:
records.jsonl, each line with the SHA-256 of the line before it, so a removed or reordered record breaks the chain;summary.json, with window adherence, the count of declared exceptions and the records with problems; andmanifest.json, with a SHA-256 per file and the chain head.--previous-manifestcontinues last period’s chain.--generated-atfixes the timestamp, so the same records give the same bytes. - A verifier.
--verify-dirrecomputes every file’s hash and walks the chain. It is the check an ISSO runs on a package they receive. - A live read through the Table API (
stateclosed or cancelled, updated since--since), declared in Chapter 02’s call list under its own read-only role,x_example_idkit.evidence_reader.
Five fictional records in tests/fixtures/evidence/closed_records.json cover the cases: one closed in its window, one closed late, one whose evidence was edited after it closed, one cancelled as a declared exception, and one closed with no evidence at all.
Window adherence is measured as Book 13 and Book 12, Chapter 12 define it: closures inside the window over closures judged, not a ratio of open findings that improves as new ones arrive. Book 13 also counts findings still open past their window, and Book 12 counts overdue items; an export of closed findings can’t see those, so it gives the closure half and the overdue count comes from a query of the open findings.
The tests
tests/python/test_evidence_export.py, 8 tests: the seven fields and the null later-pass fields, the late, tampered and unevidenced closures, a truncated u_evidence, the package and its verification, tampering detected, the chain continued across periods, reproducible bytes, and evidence written by the re-read client verifying.
The commands below export fictional records. They show the record shape, the checks and the chain, not that a ServiceNow instance returns these fields in this format. The package is tamper-evident, not signed: anyone who can rewrite every file and the manifest can rewrite the chain. Signing, and where the package is stored, are the program’s to decide (see dangers).
Lab 1: fixtures, no tenant
Work in examples/servicenow-sailpoint, with Python 3.10 or later and pytest.
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_evidence_export.py -q
# Step 2: Export October's closed findings
python clients/python/evidence_export.py --records-json tests/fixtures/evidence/closed_records.json --out-dir out/evidence --period 2026-10 --generated-at 2026-11-01T12:00:00Z
# Step 2: Export October's closed findings
python -c "import json; r=json.loads(open('out/evidence/records.jsonl', encoding='utf-8').readline()); [print(k, json.dumps(r[k])) for k in ('approval', 'executing_identity', 'what_ran')]"
# Step 3: Verify the package as the ISSO would
python clients/python/evidence_export.py --verify-dir out/evidence
# Step 4: Start November's chain from October's head
python clients/python/evidence_export.py --records-json tests/fixtures/evidence/closed_records.json --out-dir out/evidence-2026-11 --period 2026-11 --previous-manifest out/evidence/manifest.json --generated-at 2026-12-01T12:00:00Z
# Step 5: Remove one record from October's package, and verify again
python -c "p='out/evidence/records.jsonl'; l=open(p).readlines(); del l[1]; open(p,'w').writelines(l)"
python clients/python/evidence_export.py --verify-dir out/evidence
# Step 6: Delete the out folder
rm -rf outRun this chapter’s tests:
python -m pytest tests/python/test_evidence_export.py -qWhat the terminal shows, run on the kit’s fixtures (the same output as text) 8 passed.Export October’s closed findings:
python clients/python/evidence_export.py --records-json tests/fixtures/evidence/closed_records.json --out-dir out/evidence --period 2026-10 --generated-at 2026-11-01T12:00:00Z5 evidence records: 4 closed, 1 declared exception Window adherence: 2 of 3 closed inside the window IDF0000902: closed after its response window IDF0000903: evidence doesn't hash to u_evidence_hash: edited after closure, or cut short IDF0000905: closed without re-read evidence Chain head 144f5e8494281f83598d2fae5eacac12b9b271c90372c0e503aafc2bd4cf0623; written to out/evidenceWhat the terminal shows, run on the kit’s fixtures (the same output as text) The command exits 1, because three records have problems: the run says so instead of producing a clean-looking package. IDF0000905 was closed with a note that says “done,” the closure by assertion Book 13 calls prose, not evidence. Print three fields of the first line of
out/evidence/records.jsonl:python -c "import json; r=json.loads(open('out/evidence/records.jsonl', encoding='utf-8').readline()); [print(k, json.dumps(r[k])) for k in ('approval', 'executing_identity', 'what_ran')]"What the terminal shows, run on the kit’s fixtures (the same output as text) approval,executing_identityandwhat_ranarenull, each with its reason.Verify the package as the ISSO would:
python clients/python/evidence_export.py --verify-dir out/evidenceWhat the terminal shows, run on the kit’s fixtures (the same output as text) Package holds: every file matches the manifest and the chain is unbroken.Start November’s chain from October’s head:
python clients/python/evidence_export.py --records-json tests/fixtures/evidence/closed_records.json --out-dir out/evidence-2026-11 --period 2026-11 --previous-manifest out/evidence/manifest.json --generated-at 2026-12-01T12:00:00ZWhat the terminal shows, run on the kit’s fixtures (the same output as text) The last line reports chain head
afa59e0f3038613ba36eb4a748da38bb3e76e8427d04038162d08fbdcc167bd1, and November’smanifest.jsonhaschain_previousset to October’s head,144f5e84.... (The lab reuses October’s records; a real November has its own.)Remove one record from October’s package, and verify again:
python -c "p='out/evidence/records.jsonl'; l=open(p).readlines(); del l[1]; open(p,'w').writelines(l)" python clients/python/evidence_export.py --verify-dir out/evidencerecords.jsonl: SHA-256 doesn't match the manifest line 2 (IDF0000903): prev_sha256 doesn't match the line before Package does not hold: 2 problem(s).What the terminal shows, run on the kit’s fixtures (the same output as text) Delete the
outfolder when you’re done:rm -rf outWhat the terminal shows, run on the kit’s fixtures (the same output as text)
Lab 2: a ServiceNow developer instance
Not run by the authors. Use the developer instance from the earlier chapters, with at least one finding closed by Chapter 05’s Lab 2. Work in examples/servicenow-sailpoint. You’ll replace: <instance> (your developer instance’s name), <reader-client-id> and <reader-client-secret> (the evidence reader’s OAuth integration), and <closure-date> (the date of your test closure, as YYYY-MM-DD).
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: Create the evidence reader
python clients/python/kit_calls.py --task evidence --system servicenow --granted "x_example_idkit.evidence_reader"
# Step 2: Export live
export SN_INSTANCE="<instance>"
export SN_ACCESS_TOKEN=$(curl -s -X POST "https://<instance>.service-now.com/oauth_token.do" -d grant_type=client_credentials -d client_id="<reader-client-id>" -d client_secret="<reader-client-secret>" | python -c "import json,sys; print(json.load(sys.stdin)['access_token'])")
python clients/python/evidence_export.py --since "<closure-date>" --out-dir out/evidence-live --period lab
# Step 3: Check the date format
python -c "import json; [print(r['record'], 'due', r['owner_and_clock']['due'], 'finished', r['owner_and_clock']['finished']) for r in map(json.loads, open('out/evidence-live/records.jsonl'))]"
# Step 4: Verify the package
python clients/python/evidence_export.py --verify-dir out/evidence-live
# Step 5: Back it out
rm -rf out/evidence-live
unset SN_INSTANCE SN_ACCESS_TOKENCreate the evidence reader: the role
x_example_idkit.evidence_readerwith read only onu_identity_finding, an integration user and an OAuth client for it, as in Chapter 02. Check it:python clients/python/kit_calls.py --task evidence --system servicenow --granted "x_example_idkit.evidence_reader"Export live, with the reader’s token in
SN_ACCESS_TOKENand the instance inSN_INSTANCE:1export SN_INSTANCE="<instance>" export SN_ACCESS_TOKEN=$(curl -s -X POST "https://<instance>.service-now.com/oauth_token.do" -d grant_type=client_credentials -d client_id="<reader-client-id>" -d client_secret="<reader-client-secret>" | python -c "import json,sys; print(json.load(sys.stdin)['access_token'])") python clients/python/evidence_export.py --since "<closure-date>" --out-dir out/evidence-live --period labExpect each closed test finding with
evidence_intacttrue and no problems.Check the date format. The Table API returns date and time fields in the instance’s internal format; confirm
dueandfinishedin the records are the times you expect, in UTC (research gaps, row 61). This prints them for each record:python -c "import json; [print(r['record'], 'due', r['owner_and_clock']['due'], 'finished', r['owner_and_clock']['finished']) for r in map(json.loads, open('out/evidence-live/records.jsonl'))]"Verify the package:
python clients/python/evidence_export.py --verify-dir out/evidence-liveBack it out. Delete
out/evidence-live, and the evidence reader’s OAuth client, user and role:rm -rf out/evidence-live unset SN_INSTANCE SN_ACCESS_TOKEN
The verified facts
| Item | Value | Source |
|---|---|---|
| Table API read | GET /api/now/table/{tableName} with sysparm_query (encoded, ^OR for alternatives, ORDERBY), sysparm_fields, sysparm_limit, sysparm_offset; the caller needs roles that allow access to the table |
2 |
| ServiceNow Government Community Cloud | FedRAMP Certified, High, JAB | 3 |
SailPoint Search events index |
Exists in the v3 index list, for the SailPoint half of the export when it is built | 4 |
The record shape, the window and the lanes are Book 13’s (program design); the hash chain and the package are design guidance, the same pattern the other developer workbooks use for their evidence runs.
What couldn’t be verified
- The format of date and time values the Table API returns for
due_date,u_verified_atandclosed_aton a given instance, and their time zone. The export parses bothYYYY-MM-DD HH:MM:SS(as UTC) and ISO 8601 (research gaps, row 61). - What SailPoint keeps in the
eventsindex, and for how long. The example guide lists event retention as unverified, which is why the SailPoint half of the export waits (research gaps, row 62).
Federal requirements and evidence
This chapter’s output is the evidence. What each part proves, and for whom:
| What the output proves | Requirement it supports | KSI | Who keeps it |
|---|---|---|---|
| Each identity finding has one record: its origin, owner, clock, intended state, re-read and closure | NIST SP 800-53 CM-3 (retained records of changes) and CA-7 at Moderate;5 Book 13’s evidence record | KSI-CMT-LMC; KSI-MLA-EVC | The ISSO, as part of the continuous-monitoring evidence for the period |
| Records weren’t changed after closure, and none was removed from the period | AU-9 (protection of audit information) at Moderate;6 the integrity half of Book 13’s “verified, secured evidence” | KSI-MLA-OSM | The manifest and the chain, verified with --verify-dir |
| Closures happened inside the program’s windows, and late ones are named | Book 13’s window adherence; CA-7’s monitoring results | KSI-MLA-EVC | summary.json, reviewed by the governance board |
| Exceptions are declared, with a note | Book 13’s declared-exception lane; the POA&M item it needs | None | The cancelled records, and the exception register |
| What isn’t evidenced yet is stated | Honest scope for the assessor: no approval, executing identity or run log until actuation is built | None | The null fields and their reason |
| Lab 1, step 1: the export, its checks and the chain hold, tested before a change merges | SA-11 (developer testing) at Moderate7 | KSI-SCR-MIT | The pytest tests/python/test_evidence_export.py run, in the pull request’s checks |
| Lab 1, step 2: one record per closed or cancelled finding, each carrying the hash of the one before | CM-3; AU-9 | KSI-CMT-LMC; KSI-MLA-OSM | out/evidence/records.jsonl |
| Lab 1, step 2: counts, window adherence, and the records with problems, named; the run exits 1 | CA-7 | KSI-MLA-EVC | out/evidence/summary.json and the run’s output |
| Lab 1, step 2: a SHA-256 per file and the chain head | AU-9 | KSI-MLA-OSM | out/evidence/manifest.json |
| Lab 1, step 3: the package holds as written | AU-9 | KSI-MLA-OSM | The evidence_export.py --verify-dir output, kept with the package |
| Lab 1, step 4: November’s chain starts from October’s head, so a gap between periods shows | AU-9 | KSI-MLA-OSM | chain_previous in out/evidence-2026-11/manifest.json |
| Lab 1, step 5: a removed record breaks the package | AU-9 | KSI-MLA-OSM | The --verify-dir output, Package does not hold |
| Lab 1, step 6: fixture output isn’t kept | None | None | Nobody: rm -rf out deletes fictional data |
| Lab 2, step 1: the evidence reader can read findings and nothing else | AC-6 (least privilege) and AC-5 at Moderate8 | KSI-IAM-ELP; KSI-IAM-JIT | The kit_calls.py --task evidence --granted output, kept with the access approval |
| Lab 2, step 2: a live package for the period, read by the reader’s token | CM-3; CA-7; AU-9 | KSI-CMT-LMC; KSI-MLA-EVC; KSI-MLA-OSM | out/evidence-live/records.jsonl, summary.json and manifest.json, given to the ISSO |
Lab 2, step 3: due and finished are the UTC times expected, so window adherence is right |
CA-7 | KSI-MLA-EVC | The printed dates, recorded with the first live package |
| Lab 2, step 4: the live package holds | AU-9 | KSI-MLA-OSM | The --verify-dir output, kept with the package |
| Lab 2, step 5: no test package, client, user or role is left | AC-2 (account management) at Moderate9 | KSI-IAM-SNU | The instance’s deletion history; rm -rf out/evidence-live |
The KSI column names the FedRAMP 20x Key Security Indicators whose related SP 800-53 controls include the row’s control.10 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.
AU-10 (non-repudiation) and CM-5(1), which Book 13 maps to the loop, aren’t in the Moderate baseline;11 at Moderate the chain supports AU-9’s integrity, and signing the package is a further step. The control mapping is Book 13’s Appendix A, and the federal sources are in the federal framework crosswalk.
Dangers
| Danger | What happens | The guard |
|---|---|---|
| A record edited after closure | The evidence says something the re-read didn’t | The evidence hash is recomputed; a mismatch is reported |
u_evidence cut short by the field length |
The evidence can’t be parsed, or hashes wrong | Reported as invalid JSON; the attachment holds the full copy |
| A close that bypassed the business rule | A record closed with no evidence passes as closed | “Closed without re-read evidence” |
| A record deleted from the period | The package looks complete | The chain breaks; --verify-dir reports it |
| A package rewritten end to end | The chain is recomputed and holds | Keep each period’s manifest where the exporter can’t write, and sign it if the program requires (design guidance) |
| Fields filled to look complete | Approval or execution claimed that never happened | Later-pass fields are null with the reason, and tests pin it |
| The export runs with the verifier’s role | It could close findings | Its own read-only role; kit_calls.py --granted |
Checklist
Footnotes
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).↩︎
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).↩︎
FedRAMP Marketplace data, https://www.fedramp.gov/marketplace/products.json (last change November 1, 2026): ServiceNow Government Community Cloud, F1305072116 (read November 1, 2026).↩︎
SailPoint, Identity Security Cloud OpenAPI specification, v3, search
Indexenumeration, https://github.com/sailpoint-oss/api-specs/blob/main/idn/v3/schemas/search/Index.yaml (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-5, AC-6, AU-9, CA-7, CM-3 and SA-11; doesn’t include AU-10 or CM-5(1)), 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-5, AC-6, AU-9, CA-7, CM-3 and SA-11; doesn’t include AU-10 or CM-5(1)), 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-5, AC-6, AU-9, CA-7, CM-3 and SA-11; doesn’t include AU-10 or CM-5(1)), 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-5, AC-6, AU-9, CA-7, CM-3 and SA-11; doesn’t include AU-10 or CM-5(1)), 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-5, AC-6, AU-9, CA-7, CM-3 and SA-11; doesn’t include AU-10 or CM-5(1)), 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).↩︎
National Institute of Standards and Technology, SP 800-53B Moderate baseline, release 5.2.0 OSCAL profile (includes AC-2, AC-5, AC-6, AU-9, CA-7, CM-3 and SA-11; doesn’t include AU-10 or CM-5(1)), 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).↩︎





![Terminal window showing python -c 'p='out/evidence/records.jsonl'; l=open(p).readlines(); del l[1]; o… and what it printed](captures/07-6.png)
