INTERNAL — NOT FOR DISTRIBUTION AS-IS

CPS Salesforce Production Audit

Org CPS-Prod (00D5Y0000024aG1UAI) · measured 6 November 2026 · read-only · findings independently re-checked before publishing

57findings across code, automation, data, security
1live save failure users hit today (Customer Care Inquiry)
252 / 258flow DML / email / Apex steps with no fault handling
772self-addressed noise cases in 30 days, from 2 flows
146users who can read student disability data (165,352 rows)
83Salesforce Health Check score (7 high-risk settings)

Every item below was measured against production with read-only queries; nothing in the org was changed. The plan favours work users will not notice: logging, guards, bulkification, test fixes, dead-code and token cleanup, and automation efficiency. There are no page layout or UI changes. Anything that would change what a user sees or receives is listed separately as a district decision.

Answer today

1. CLI logins from India on Joe's admin account Confirm

LoginHistory has 20 successful Salesforce CLI logins from 14 IP addresses geolocated to Hyderabad, India, from 25 Jun 2026 to 4 Nov 2026 14:56:50Z. Every other login on the account comes from 47.205.158.9 (US).

Lead: the CLI refresh token created 15 Mar 2024 (291 uses) was last used at exactly 4 Nov 2026 14:56:50Z, the time of the latest India login.

The cause is not inferred. This is a fact pattern to confirm.

2. Salesforce-employee permission change on 28 Oct Confirm

SetupAuditTrail, 28 Oct 16:54Z: "A Salesforce employee ran Manage Project Guardian Org Permissions in Update mode for case 500EE000022QKoy" (3 rows). If that support case is not ours, ask Salesforce Support what was changed.

Prioritised game plan

Order is by risk removed per hour, with dependencies respected. Two dependencies drive the order: observability comes before refactors, so regressions are visible, and the jobs owned by Joe's user are re-owned before anything touches that account. Every item is a production write, so each needs approval and goes in a change window. Hours are estimates of human time.

Phase 1Week 1: fix what is broken, close exposures (≈10–14 h)

#ActionWhy nowUser impactHrs
1Customer Care Inquiry save failure. Remove the hardcoded inactive user (Steven Spohn, deactivated 8 Jun) from Queue_Field_Auto_Populate_on_Case_V1A_New and route to the Customer Care queue. Add a fault path on Update_Case_Owner.Saves fail outright. Paige Matthews failed 3 times on 4 Nov, then filed the case as Technology Incident.Fixes a failure. The new owner (queue or named person) is a quick decision.1–2
2Freeze the dormant consultant admin "Yurgosky Consulting" and revoke its 5 OAuth tokens, including a CLI token with 74 uses.A System Administrator with Modify All Data, dormant 361 days, holds non-expiring tokens. Live checks found it owns no scheduled job, dashboard or email service.None0.5
3Remove personal Gmail recipients from the OMC "website updates" email step (2 addresses, 11 cases since 30 Jul) and the cc on the Urgent_SF_Request_Created alert. Replace them with a district mailbox. The separate Admin Queue route is unchanged.District case content goes to consumer mailboxes.Recipient change only0.5–1
4Stop the Student Behavior email loop. Remove $User.Email from the 3 recipient assignments in CPS_Student_Behaviour_Incident_On_Create_Update_Cases. In flows, $User is the form-connector identity, so these notices land in an individual mailbox (617 of 679 in 90 days). Add an Email-to-Case or mail rule scoped to exactly the 2 measured subjects: "Transportation Behavioral Incident Requires Your Input" and "Case has been closed".All 772 of 772 self-addressed Email-to-Case cases in 30 days come from this flow pair, and every one carries student incident details into the Admin Queue.One mailbox stops receiving copies, and the Admin Queue loses its noise.3–5
5Insurance auto-read heap failure. Check ContentSize before loading VersionData in FacilityDocumentController.readPdf.2 failures on 5 Nov ("heap size too large: 26,973,324"). They were contained and routed to manual review.None1–2
6Remove the bearer-token debug line in One2OneDeviceIncidentHandler, and filter the record type in the trigger rather than inside the future.The token is written to debug logs. 198 of 256 async jobs in 7 days did nothing.None1
7Apex exception email recipients: add a shared, monitored mailbox. There are 0 recipients configured today.Flow and Apex failures only email the last modifier.None0.5

Phase 2Weeks 2–3: make failures visible, and make tests trustworthy (≈25–35 h)

#ActionWhyHrs
8Flow fault handling. Continue CPS-175 (top 10), then batch 2: Update_Case_Owner, CPS_Assign_Status_On_Create, Case_Create_Student_Behavior_Incidents, the nightly Transportation close, Due_Date_Update_Flow_Case and the Contact integration stamp. Re-wire the 6 existing fault paths that swallow errors into a variable. For each element, choose log-and-continue or log-and-fail. "Continue" can turn a rollback into a partial save.78 of 83 action flows are unprotected, and nothing is logged.20–25
9Log email send results. Capture the Messaging.sendEmail results in SendTemplatedEmail, and point the 14 operational catch blocks that only call System.debug at the existing FlowRunLogger.Facility and HR email failures leave no trace.4–8
10Repair the test suite. 6 methods are expected to fail from causes found by reading the code (an inactive "Report Broken Device" RT name, and a seed that misses a required-fields rule). 5 tests have no asserts, including the "known passing" deploy target. Then a full RunLocalTests in a quiet window, which also gives the first real coverage number.Every deploy currently has to use RunSpecifiedTests, and coverage cannot be proven.3–5

Phase 3Weeks 3–4: remove single-person dependencies (≈15–25 h)

#ActionWhyHrs
11Re-own 11 scheduled jobs and the ForwardEdge email-service RunAs from Joe's user to a service identity (svc-* pattern, no role), one job at a time, with a smoke check after each. Also route Salesforce Request cases to a queue instead of a hardcoded user ID.Deactivating one account would stop 4 scheduled flows, the facility digests, idle-case alerts and report schedules. It would also break Salesforce Request creation the same way Customer Care breaks today. The sender identity and $User may change, so tell recipients.3–5
12Move the FormAssembly connector off a named human admin (Kerrie Peters) to the CPS Ticketing System or a svc-formassembly user. This needs a builder session, because an API save wipes connectors.Form intake breaks if that person leaves.2–4
13Integration users to least privilege. Learn21, YC Integration and CPS Ticketing run as full System Administrators. Move them to the Integration licence (5 unused) with permission sets built from their measured footprint, after testing in a sandbox.A compromised vendor credential today grants Modify All Data over 500K student and guardian records.12–20

Phase 4Month 2: efficiency and hardening, invisible but regression-tested (≈35–55 h)

ItemDetailHrs
Nightly Transportation closeMove the 5-record-type and status criteria into the start filter, which cuts about 50,000 interviews a night to about 250. Keep the current branch logic. The trailing-tab bug and the 19 archived-route cases are separate decisions.2–3
Injection fixGenericGetParentChildRecController: an explicit list of the supported objects and fields, rather than "any object in the schema". FLS enforcement is a later phase, after testing the 5 pages.2–3
Recurrence sweeperAdd an alert when any series sits in Pending. The sweeper is unscheduled today; 0 are stuck.0.5
After-save self-updatesConvert the Contact integration stamp, Due Date and SBI numbering to before-save, but only flows with no email or Apex actions (before-save cannot run those). Profile Case save CPU/DML first.6–10
Case flow orderPin TriggerOrder on the 14 flows that write Status, OwnerId or queueId__c, then run a full regression. Today's effective order is not observable, so this is a test project and not "no change".4–6
Hardcoded IDs and addresses39 flows hold 80 IDs and 28 personal addresses (some belong to departed staff). Move them to a routing CMT and queue email lists, in stages.12–20
Facility_Request__c triggersOne dispatcher trigger calling the 5 handlers in a fixed order.6–10
Connected appsAdmin-approve Data Loader, Workbench, Ant, the IDE and similar apps, pre-authorizing the current holders first. Expire idle refresh tokens and revoke stale ones.1–2
Small configExternal OWD to Private (0 external users; recalculation runs off-hours). Narrow the 47.205.128.0/17 trusted range. Renew the expired certificates (delete only after consumers are found). Re-enable protocol security on 3 remote sites, then retest One2One. Retire the ESW test site and its guest user. Delete never-activated flow drafts and obsolete versions.4–6
Dormant usersFreeze Caroline Murphy (ITM Admin, 311 days) and the users over 180 days, after checking each user for schedules, dashboards, alerts and RunAs. Remove 81 inactive group members.2–3

Needs a district (or Joe) decision

These change what someone sees, receives or can do, so they are kept out of the no-impact work.

TopicMeasuredDecision
Critical Student sensitive data165,352 Contacts carry disability details (19,884 with a real classification); 146 users can read them. Birthdate and Race are readable by 155. 11 contractor accounts log in by password only. ExportReport is held by 155 users, and there is no view or export audit trail.Which roles need disability, 504, race and DOB? Then a "Student Sensitive Data" permission set. Fields leave the layout for users who lose access.
High MFAOrg-wide MFA opt-out since 11 Aug. 21 users log in by password only. 49 of 104 SSO users never present an Entra MFA claim.Ask Support for the end date. Require MFA on direct logins, admins first, and enforce MFA in Entra.
High SharingCase, Contact and Account are Public Read/Write internally, including 12,237 HR and 6,661 Behavior cases. 1 role, 0 restriction rules.A sharing design first, then tighten. 24–40 h.
High Case Modify All109 users, mostly via the "Testing Support" (75) and "Phishing/Virus" (46) permission sets. 44 deletes in 90 days.Remove Modify All and View All from those 2 sets. Grant Delete only where a team owns a deletion process.
High ITM Administrator profile8 users can manage users and permission sets, which is a path to full admin. 0 such actions in 59 days.Trim to a narrow password-reset permission set.
High Guardian sync13,052 of 13,077 active students created since 2025 have no guardian relationship. 0 guardians created in 2026.Ask the integration owner whether this was deliberate or broke.
High "Resolved" counts as openAbout 33,700 Resolved cases sit in the open total (about 49,900); 24,532 are untouched 90+ days.Auto-close Resolved after N days, with survey and close-email suppression.
Med Series facility emailsThe Quote, Confirmed-Renter and Approved emails are skipped for every request in a series until pricing is approved. 1,896 requests are affected; 0 are approved, and no flow can approve. Actual missed sends are unmeasured.Ask Facilities whether series quotes go out another way.
Med Email noise backlog7,573 open noise cases (form notifications and self-mail).Bulk close with email suppression, using the existing runbook.
Med Smaller behaviour changesDuplicate Behavior notice (flow B). Google Admin case emails resent on every edit (58 in 30 days). The trailing tab blocks auto-close of 86 "Request New Software" cases. 19 Resolved cases have archived routes.Each is a small, visible change for a few recipients.
Med Data hygiene1,842 cases with the double-space value "Change of Bus Stop" (31% undercount). 98 off-list language values on 125,852 Contacts. 462 duplicate staff-email groups. 59% of real cases have no Contact. 11 dashboards run as Joe.Normalise or dedupe in report-only mode first; repoint dashboards with HR.
Low Retention594,775 legacy course rows (≈1.2 GB est.). 156,035 cases closed more than 2 years ago. 8.6 GB of legacy Case attachments.FERPA retention policy, then archive.

Findings — Automation

Live failure A2 · Customer Care Inquiry cases cannot be saved

The Update_Customer_Care_Inquiry step hardcodes CPS_AddUser__c = 0055Y00000HQbXO (inactive since 8 Jun). Update_Case_Owner then fails with INACTIVE_OWNER_OR_USER, and the whole save rolls back. FlowInterview errors 00098050/51/52 were logged on 4 Nov.

Correction from the second review: the last successful Customer Care Inquiry case was 23 Feb, months before the deactivation, so the deactivation does not explain the whole gap in volume. The failure itself is proven.

High A1 · Student Behavior flow pair creates every self-addressed Email-to-Case case

Two active flows email studentbusincidents@ on each create, and both copies re-enter Email-to-Case. The close path loops the same way. 30 days: 563 + 209 = 772 noise cases. $User.Email adds an individual mailbox to 617 of 679 notices (90 days). An earlier removal was reverted, and v2 is active.

High A3 · Personal Gmail recipients on district case email

2 addresses on the OMC website-update path; a cc on the urgent Salesforce Request alert; 8 unreferenced alerts still carry a test Gmail cc.

High A4 · No failure is ever recorded

252 of 258 action elements have no fault connector. The 6 fault paths that exist only assign a variable. No flow writes to Flow_Run_Log__c. 0 Apex exception recipients. 2 of the CPS-175 top-10 flows run on dead objects (Case__c, CaseSubTask__c); deactivating them beats fault-pathing them.

High A5 · Nightly Transportation close

Its only start filter is Status ≠ Closed, so it runs about 50,000 interviews for about 250 possible matches. It fails on the same 19 archived-route cases every night (63 of 66 retained errors). A trailing tab in a record-type name makes one branch unreachable (86 cases).

Med A6–A13 · Order, self-updates, duplicates, hardcodes, dead flows

Findings — Apex, triggers, VF, LWC

Scope: 111 classes, 10 triggers, 30 VF pages, 6 VF components, 1 Aura bundle and 38 LWC bundles. Every body was pulled and read.

High C2 · SOQL injection and FLS bypass in GenericGetParentChildRecController

objectName is concatenated into the query unescaped, in system mode, with no CRUD/FLS checks. The class is granted to 19 profiles (154 of 168 active users) and is used on 5 Lightning pages.

Med C1 · Series pricing gate silently skips renter money emails

SendTemplatedEmail lines 90–96. It applies to 1,896 series requests (205 parents and 1,691 occurrences), all unapproved, and no active flow calls the approve action. A skip writes only a debug line, and sends are not saved as activity, so delivery cannot be proven either way.

Med C3 · Test suite

6 methods are expected to fail (static causes). 5 tests have 0 asserts. 10 boilerplate tests use SeeAllData. ApexTestRunResult has 0 rows and the coverage aggregate is stale, so a full run has never been recorded.

Med C4–C7 · Silent catches, single-owner schedules, One2One, sweeper
Low C8–C12 · Structure and hygiene

Findings — Data model and quality

Working D2 · The FormAssembly alias change worked

Cases from report@formassembly.com: 43 on 4 Nov, 0 on 5 Nov. NEW HR Form duplicates: 66–90 a day, now 0. Real form cases held steady (362–375 a day against 367–390). What remains is the self-mail loop (A1), about 54 a day.

High D1 · Guardian relationship sync stopped creating records

Relationships created: 2024 14,668 · 2025 118 · 2026 2. Guardians created in 2026: 0. Existing relationships are still updated (119K in 2026). 28% of active students have no relationship.

Med D3–D10
Low D11–D15

Findings — Security and access

High S2–S4 · Dormant admin, integrations as admins, ITM escalation path

Covered in Phase 1 #2, Phase 3 #13 and Decisions. The YC and Learn21 admin accounts also have vendor email addresses, so their password resets leave the district.

Med S8, S10–S16 · Configuration hygiene

Corrections made before publishing

An independent second review re-measured the load-bearing numbers. These changed:

ItemFirst draftCorrected
Series email gate (C1)205 requests1,896 (205 parents + 1,691 occurrences); actual missed sends unmeasured. Severity lowered to Med (design question).
Dormant admin tokens (S2)65
India login IPs (S9)814, tied to one 2024 CLI token
Jobs owned by Joe's user (C5/A10)8 and 11 (conflicting)11
Customer Care (A2)"none since deactivation"Failure proven; the volume gap predates the deactivation
Dead objects (D5)includes Focus__cFocus__c excluded (record created Aug 2026)
Test suite (C3)"cannot pass""6 methods expected to fail", pending a real run
Contact double-save (A7)55,215 saves55,215 records; the save count is unmeasured
Injection fix (C2)schema allow-listan explicit object/field list (a schema check still allows everything)
Open cases (D3)50,071, "+150/weekday"≈49,900 and fluctuating; no growth rate claimed
"No user impact" traps the review flagged, now reflected in the plan:

Not assessed — recommended next

  • Backup and restore: no evidence of a backup product, export cadence or restore test. Establish this before any purge, merge or uninstall.
  • Sandbox drift: known flow-version drift between CPS-sb and prod. Build every fix from a fresh prod pull.
  • Release process: change calendar, approval gate, rollback drill.
  • Sensitive reports: which of the 476 reports expose disability, DOB or race, and who can export them.
  • FormAssembly failure handling: Connector Error Reports are now the only failure signal. Confirm someone watches them.
  • Case save headroom: CPU and DML per save (40 flows, a trigger and HEDA). Profile this before reordering.
  • Knowledge, guest endpoint rate limits, sandbox outbound email, file sharing.
  • Measurement limits: sharing rules, CORS and profile session policies could not be read (the Metadata API is blocked for this login at v67). Flow errors are retained only from 24 Oct and async jobs about 7 days.

Healthy — checked, no action

Method

All queries were read-only (SOQL, Tooling SOQL, describe, /limits) against CPS-Prod on 6 Nov 2026. No DML, deploys, anonymous Apex or test runs. Every flow's active-version metadata and every Apex, trigger, VF and LWC source was pulled and analysed. Findings were then independently re-measured and challenged, and the corrections above were applied. Numbers are point-in-time; case counts move during the day. Anything not measured is labelled as such, and no estimate is presented as a measurement. Working files: Documents/Work/CPS/Audits/2026-11-06/.