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.
- Recognised (a known remote machine or collaborator): move that use to its own named user, so admin actions are traceable to a person.
- Not recognised: revoke that token, and preferably all 5 CLI tokens on the account. Then rotate the password and review SetupAuditTrail at the 20 login timestamps.
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)
| # | Action | Why now | User impact | Hrs |
|---|---|---|---|---|
| 1 | Customer 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 |
| 2 | Freeze 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. | None | 0.5 |
| 3 | Remove 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 only | 0.5–1 |
| 4 | Stop 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 |
| 5 | Insurance 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. | None | 1–2 |
| 6 | Remove 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. | None | 1 |
| 7 | Apex exception email recipients: add a shared, monitored mailbox. There are 0 recipients configured today. | Flow and Apex failures only email the last modifier. | None | 0.5 |
Phase 2Weeks 2–3: make failures visible, and make tests trustworthy (≈25–35 h)
| # | Action | Why | Hrs |
|---|---|---|---|
| 8 | Flow 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 |
| 9 | Log 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 |
| 10 | Repair 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)
| # | Action | Why | Hrs |
|---|---|---|---|
| 11 | Re-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 |
| 12 | Move 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 |
| 13 | Integration 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)
| Item | Detail | Hrs |
|---|---|---|
| Nightly Transportation close | Move 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 fix | GenericGetParentChildRecController: 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 sweeper | Add an alert when any series sits in Pending. The sweeper is unscheduled today; 0 are stuck. | 0.5 |
| After-save self-updates | Convert 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 order | Pin 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 addresses | 39 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 triggers | One dispatcher trigger calling the 5 handlers in a fixed order. | 6–10 |
| Connected apps | Admin-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 config | External 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 users | Freeze 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.
| Topic | Measured | Decision |
|---|---|---|
| Critical Student sensitive data | 165,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 MFA | Org-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 Sharing | Case, 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 All | 109 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 profile | 8 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 sync | 13,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 open | About 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 emails | The 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 backlog | 7,573 open noise cases (form notifications and self-mail). | Bulk close with email suppression, using the existing runbook. |
| Med Smaller behaviour changes | Duplicate 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 hygiene | 1,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 Retention | 594,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
- A6: 40 record-triggered Case flows, with TriggerOrder on 2. Status is written by 14 flows, queueId__c by 9, OwnerId by 7.
- A7: after-save self-updates. The Contact integration stamp adds a second save for each of the 55,215 Contacts modified in 7 days (the per-save count is unmeasured). The SBI loop adds about 925 Case saves a month.
- A8:
Toby_Google_case_assignment_Flowemails on every save, with no change guard. - A9: 80 hardcoded IDs in 39 flows (2 inactive users, 1 deleted queue); 28 individual addresses, including departed staff on live paths.
- A10: 11 of 32 scheduled jobs and the ForwardEdge email service run as Joe's user.
- A11: Facility_Request__c has 5 triggers and 11 flows.
Facility_Request_Demo_Emailsis the only sender of the Received, Approved and Declined emails, so do not deactivate it. - A12: 58 of 152 flow definitions are inactive; 586 versions are obsolete; 9 active flows run on objects with no records in 365 days (Focus__c excluded: it got a record in August).
- A13: 7 record-type lookups use Name without SobjectType; 1 can never match.
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
- C4: send results are discarded; 14 operational catch blocks only call
System.debug. - C5: see A10 (reconciled count: 11 jobs).
- C6: One2One makes 3 callouts, 2 queries and 1 DML per case, so it fails above about 33 cases per transaction. It logs the token and stores the client secret in plain CMT. The integration itself succeeds 98.5% of the time (930/944, 60 days).
- C7:
FacilityRecurrenceSweeper, described as "the correctness guarantee", is unscheduled. 0 records are Pending today.
Low C8–C12 · Structure and hygiene
- 5 triggers on Facility_Request__c with no dispatcher.
- The ForwardEdge invocables query once per record.
- About 35 dead components: community boilerplate, GraphicsPack v38, a 2022 batch,
SendCaseEmailWithTemplate. - 9 hardcoded recipients in
CaseIdleNotificationProcessor. - 11 files say "NOT DEPLOYED" but are deployed.
- 12 classes have no sharing keyword, including the 3 guest REST classes. Declare them explicitly.
- A null-pointer bug in
fileUploadController.
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
- D3: Resolved counts as open (see decisions).
- D4: 3,994 of 10,570 staff Contacts have no integration update since before 2026; 462 duplicate-email groups; no staff duplicate rule.
- D5: 12 active flows sit on 5 dead objects (Focus__c is still in use); 6 objects have never held a record.
- D6: archival candidates (see decisions).
- D7: picklist splits.
- D8: Case field history is at 20 of 20, with 2 slots on dead fields.
- D9: 279 of 476 reports haven't run in 12 months; 11 dashboards run as Joe.
- D10: 59% of real cases have no Contact; 11,606 of them carry a matchable email.
Low D11–D15
- 250 empty unmanaged fields; 25 are safe to retire (over 12 months old, no references).
- Unused packages: dlrs, Field Trip, Field Spy.
- 6 Facility Requests have the off-picklist status "Approved".
- Response_Team__c has had no records since 20 Aug 2025 while its forms are active.
- A new required lookup filter on Device_Request__c locks 3 terminal records.
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
- 18 of 19 connected apps self-authorize, and their refresh tokens never expire.
- The trusted range /17 (32,768 addresses) is used by 1 IP. The SysAdmin and ITM Admin profiles allow logins from 0.0.0.0–255.255.255.255.
- The ESW test site is Live with an active guest user and 0 sessions ever.
- Both certificates have expired (latest 28 Oct); no consumer was observed.
- 3 remote sites have protocol security disabled.
- 27 dormant users; 81 inactive group members.
- 29 of 49 profiles are empty, including "Mike Edwards" (View All Data).
- 148 users have View Setup and View Setup Audit Trail.
Corrections made before publishing
An independent second review re-measured the load-bearing numbers. These changed:
| Item | First draft | Corrected |
|---|---|---|
| Series email gate (C1) | 205 requests | 1,896 (205 parents + 1,691 occurrences); actual missed sends unmeasured. Severity lowered to Med (design question). |
| Dormant admin tokens (S2) | 6 | 5 |
| India login IPs (S9) | 8 | 14, 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__c | Focus__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 saves | 55,215 records; the save count is unmeasured |
| Injection fix (C2) | schema allow-list | an 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 |
- Log-and-continue fault paths can change rollback into a partial save.
- Before-save conversion drops email and Apex actions.
- Pinning TriggerOrder can change outcomes.
- Re-owning jobs changes the sender and $User.
- Admin-approving apps locks out token holders unless they are pre-authorized first.
- Deleting expired certificates is unsafe until consumers are known.
- Freezing users needs a per-user dependency check.
- Dashboard running-user changes alter what viewers see.
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
- 0 Process Builder, 0 custom workflow rules or approvals. No active flow has a newer unactivated draft.
- All 32 scheduled jobs are WAITING and firing. All 111 classes and 10 triggers are valid; every trigger is thin and collection-based.
- The new Facilities code (2026) is bulkified and well tested (up to 306 asserts per test class).
- Guest access is minimal: 3 Facility Calendar REST classes with bind variables and no requester PII; guest object access is Knowledge Read only.
- Modify All Data and View All Data are held only via the System Administrator profile (7 users). Nobody can view encrypted data.
- The password policy meets standard. DKIM is active and rotating. SPF and bounce handling are on. The SSO IdP certificate is valid to Jul 2027.
- Limits are healthy: data 17%, file 26%, API 0.8%, scheduled flow runs 21%.
- Student, guardian and Student ID duplicates: 0. No validation rule hardcodes an ID.
- One2One integration success rate is 98.5%. The FormAssembly alias change removed the notification duplicates without losing real form cases.
- The personal-mailbox From problem is fixed: 0 of 420 user-sent case emails since 29 Oct.
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/.