Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,27 @@

[![SAP Transaction Forensics — live demo](demo/preview.svg)](https://chrbailey.github.io/SAP-Transaction-Forensics/)

## The Core Insight

> **Structured data tells you *what happened*. Unstructured text tells you *why*.**

Every enterprise system generates two kinds of data. Structured transactions —
timestamps, amounts, stage changes, user IDs — tell you the official story.
Unstructured text — the emails, Slack threads, tickets, meeting notes, timesheets,
SOWs, and order notes that surround those transactions — tell you what *actually*
happened. **The gap between them is where fraud, waste, and dysfunction hide.**

| Structured data says | Unstructured text reveals |
|---|---|
| "Deal in Negotiation for 6 months" | *"Customer said not ready — Sales moved it forward anyway. No sign-off."* |
| "Purchase Order created 03/15" | *"Requisition wasn't approved yet. Create the PO now, paperwork later."* |
| "Phase 2: On Track, Green" | *"We're 3 weeks behind. Tell the client we're on track while we figure it out."* |

This tool correlates both sides and surfaces the contradictions. It has been used
on real engagements — see **[Field Case Studies](docs/CASE_STUDIES.md)** (3M+ ERP
records, $103K in waste found, ITGC/SOX violations) — and ships a synthetic demo so
you can watch the same detector classes fire without touching a real system.

## The Problem

Existing forensic tools ship with hardcoded rules. "Flag invoices over $X." "Alert on vendor master changes." These rules miss new patterns and fire on irrelevant ones. Every ERP is different. Every client's fraud signature is different. A static rule engine cannot keep up.
Expand Down Expand Up @@ -59,6 +80,21 @@ Then ask Claude: *"Run a conformance check against the o2c-simple reference mode

Full walkthrough: **[QUICKSTART.md](QUICKSTART.md)** · Five-question demo: **[scripts/demo-walkthrough.md](scripts/demo-walkthrough.md)** · Pattern discovery: **[pattern-discovery/README.md](pattern-discovery/README.md)**

## Proven in the Field

Three real consulting engagements motivated this tool. Anonymized, but the figures
are actual. Full write-ups in **[docs/CASE_STUDIES.md](docs/CASE_STUDIES.md)**.

| Engagement | What structured data showed | What the text/forensics revealed | Result |
|---|---|---|---|
| **Healthcare — NetSuite license audit** (289 users) | A clean user list | 8 dormant full-access licenses, ~53 departed employees still licensed, 4 "approve-only" users | **$103,896/yr** savings · 14.4× ROI · 0.8-mo payback |
| **MedTech — ticket forensics during acquisition** (2,525 tickets) | Normal operations | Dummy MRP transactions, mutating item numbers, "URGENT" escalation culture, 257 access-request tickets | Organizational stress invisible in the ERP, surfaced from text |
| **Connected hardware — high-growth ERP + ITGC** (3M+ records) | Orders shipped, invoiced, cleared | Credit holds overridden to ship anyway, **28.6%** account return rate, 7 admin users (one terminated), broken approval chains | SOX-relevant control gaps documented with evidence |

Each maps to a detector class in this repo (segregation-of-duties, policy override,
temporal impossibility, reality-gap). The synthetic demo plants these same patterns
so you can see them fire with no access to a real system.

## What This Is Not

- **Not a governance tool.** For pre-execution approval of AI agent actions, see [PromptSpeak](https://github.com/chrbailey/promptspeak-mcp-server).
Expand Down
75 changes: 74 additions & 1 deletion demo/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,25 @@
.cursor { display: inline-block; width: 8px; background: var(--low); animation: cur .9s steps(1) infinite; }
@keyframes cur { 50% { opacity: 0; } }

/* Case studies */
.case-grid { display: grid; grid-template-columns: repeat(3,1fr); gap: 16px; }
.case { background: var(--surface); border: 1px solid var(--border); border-top: 3px solid var(--accent); border-radius: 14px;
padding: 20px; backdrop-filter: blur(6px); transition: transform .18s, box-shadow .18s; }
.case:hover { transform: translateY(-3px); box-shadow: 0 12px 40px rgba(0,0,0,0.4); }
.case.c-border-critical { border-top-color: var(--critical); }
.case.c-border-high { border-top-color: var(--high); }
.case.c-border-medium { border-top-color: var(--medium); }
.case-top { display: flex; align-items: center; justify-content: space-between; gap: 8px; margin-bottom: 12px; }
.case-tag { font-family: var(--mono); font-size: 11px; color: var(--muted); }
.case-title { font-size: 16px; font-weight: 750; }
.case-headline { color: var(--text); font-size: 13.5px; margin-top: 6px; font-weight: 600; }
.case-stats { display: grid; grid-template-columns: repeat(2,1fr); gap: 8px; margin: 16px 0; }
.case-stats div { background: rgba(255,255,255,0.03); border: 1px solid var(--border); border-radius: 9px; padding: 9px 10px; }
.case-stats b { display: block; font-size: 18px; font-weight: 800; font-variant-numeric: tabular-nums; }
.case-stats span { font-size: 10.5px; color: var(--dim); text-transform: uppercase; letter-spacing: 0.04em; }
.case-body { color: var(--muted); font-size: 12.5px; line-height: 1.7; }
.case-note { text-align: center; color: var(--dim); font-size: 12px; margin-top: 20px; font-style: italic; }

/* CTA */
.cta { text-align: center; padding: 60px 0; }
.cta h3 { font-size: 30px; font-weight: 850; margin-bottom: 12px; }
Expand Down Expand Up @@ -562,6 +581,60 @@ <h2>Find the anomalies your<br><span class="grad">rule engine can't see</span></
</div></div>`;
}

const CASES = [
{
tag: "Healthcare · NetSuite",
title: "License optimization",
headline: "$103,896/yr in waste, invisible in the user list",
roi: "14.4× ROI",
stats: [["289","Users"],["69","Eliminable"],["$103.9K","Saved/yr"],["0.8 mo","Payback"]],
body: "A 289-user NetSuite environment looked clean. Login timestamps cross-referenced with HR termination dates exposed 8 dormant full-access licenses, ~53 departed employees still consuming Center licenses, and 4 users whose entire job was clicking \"Approve.\"",
sev: "medium",
},
{
tag: "MedTech · Acquisition",
title: "Help-desk ticket forensics",
headline: "2,525 tickets revealed stress the ERP couldn't show",
roi: "38% uncategorized",
stats: [["2,525","Tickets"],["3,992","ERP users"],["257","Access reqs"],["38%","Uncategorized"]],
body: "A diagnostics manufacturer acquired by a Fortune 500. Transaction data showed normal operations. The tickets showed dummy transactions fabricated for MRP, item numbers mutating unexplainably, and an \"URGENT\" escalation culture during acquisition chaos.",
sev: "high",
},
{
tag: "Hardware · Public co.",
title: "High-growth ERP + ITGC forensics",
headline: "Credit holds overridden · 28.6% return rate · SOX-risk admin access",
roi: "3M+ records",
stats: [["3M+","CSV rows"],["102K","Sales orders"],["28.6%","RMA rate"],["7","Admin users"]],
body: "Across 3M+ ERP records: sales orders shipped despite \"Customer On Credit Hold,\" a 28.62% account return rate, 7 users with Administrator role (one terminated), and a deduction approval chain so broken a \"reroute\" field exists specifically because the normal chain fails.",
sev: "critical",
},
];

function casesSection() {
const cards = CASES.map(c=>`
<div class="case reveal c-border-${c.sev}">
<div class="case-top">
<span class="case-tag">${esc(c.tag)}</span>
<span class="sev ${c.sev}">${esc(c.roi)}</span>
</div>
<h4 class="case-title">${esc(c.title)}</h4>
<p class="case-headline">${esc(c.headline)}</p>
<div class="case-stats">${c.stats.map(s=>`<div><b>${esc(s[0])}</b><span>${esc(s[1])}</span></div>`).join("")}</div>
<p class="case-body">${esc(c.body)}</p>
</div>`).join("");
return `
<section class="block" id="cases"><div class="wrap">
<div class="sec-head reveal">
<div class="kicker">Proven In The Field</div>
<h3>Three real engagements behind the method</h3>
<p>The synthetic demo above shows the detectors firing on generated data. These are the actual consulting engagements that motivated them — anonymized, but the figures are real. <a href="https://github.com/chrbailey/SAP-Transaction-Forensics/blob/main/docs/CASE_STUDIES.md" target="_blank" rel="noopener">Full write-ups →</a></p>
</div>
<div class="case-grid">${cards}</div>
<p class="case-note reveal">Company, individual, and email identifiers removed. Financial figures, ticket counts, and category distributions are actual. Used with permission for educational purposes.</p>
</div></section>`;
}

function cta() {
return `
<section class="block"><div class="wrap"><div class="cta reveal">
Expand Down Expand Up @@ -615,7 +688,7 @@ <h3>Run this on your own machine in 60 seconds</h3>
const app = document.getElementById("app");
app.innerHTML = hero(d) +
`<section class="block"><div class="wrap">${riskBand(d)}</div></section>` +
anomaliesSection(d) + crossSection(d) + quarterSection(d) + evidenceSection(d) + cta(d);
anomaliesSection(d) + crossSection(d) + quarterSection(d) + evidenceSection(d) + casesSection() + cta(d);
const foot = document.createElement("footer"); foot.className="site";
foot.innerHTML = `<div class="wrap">Generated ${esc((d.generated_at||"").slice(0,10))} · ${d.dataset.opportunities} opportunities · ${d.dataset.event_log_records} event-log records · seed ${d.dataset.seed}. Built by <a href="https://github.com/chrbailey" target="_blank" rel="noopener">Christopher Bailey</a> · MIT · <a href="https://github.com/chrbailey/SAP-Transaction-Forensics" target="_blank" rel="noopener">source</a></div>`;
document.body.appendChild(foot);
Expand Down
135 changes: 135 additions & 0 deletions docs/CASE_STUDIES.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
# Field Case Studies

Three real consulting engagements, anonymized. **Company names, individual names,
and email addresses have been removed. Financial figures, ticket counts, record
counts, and category distributions are actual.** Used with permission for
educational purposes.

These engagements predate this open-source tool — they are the field experience
that motivated it. The patterns each case surfaced (license waste, organizational
stress, control-bypass) are the same classes of finding the detectors in this
repository are built to catch.

> **The thesis:** Structured data tells you *what happened*. Unstructured text
> tells you *why*. Every enterprise system generates both — timestamps, amounts,
> stage changes, user IDs on one side; emails, Slack threads, tickets, notes, SOWs
> on the other. The gap between them is where fraud, waste, and dysfunction hide.

---

## Case 1 — Healthcare Company: NetSuite License Optimization

**Engagement:** ERP user license audit
**Environment:** 289-user NetSuite

Automated license classification found **$103,896 in annual savings** — a **14.4×
ROI** with a **0.8-month payback period**.

| Metric | Value |
|---|---|
| Total users | 289 |
| Eliminable licenses | 69 |
| Annual savings identified | $103,896 |
| Payback period | 0.8 months |

**Savings by category**

| Category | Annual savings |
|---|---|
| Dormant full-access (8 users, no login 90+ days) | $46,464 |
| Departed-employee Center licenses (est. 53) | $31,800 |
| Approval-only users (4, replaceable with SuiteFlow) | $23,232 |
| Deprecated integrations (est. 4 of 8) | $2,400 |

**Structured data vs. what we found**

- **Structured:** the NetSuite user list shows 289 active users with assigned roles. Looks clean.
- **Unstructured signals:** login timestamps reveal 8 full-access users ($5,808/yr each) with no login in 90+ days. Cross-referencing HR termination dates shows ~53 Employee Center licenses still consumed by departed employees. 4 users' entire activity is clicking "Approve" on purchase orders — replaceable by a no-cost email workflow.
- **The gap:** $103,896/year in waste invisible to anyone reading the user list alone.

---

## Case 2 — MedTech Manufacturer: Help-Desk Ticket Forensics During Acquisition

**Engagement:** NetSuite implementation + post-acquisition support
**Context:** a diagnostics manufacturer acquired by a Fortune 500. Structured data
showed normal operations. **2,525 help-desk tickets told a different story.**

| Metric | Value |
|---|---|
| Help-desk tickets | 2,525 |
| Categories | 11 |
| Uncategorized | 38% |
| ERP users | 3,992 |
| Inventory items | 1,423 |

**Ticket category distribution:** Uncategorized 956 · Finance 469 · Access 257 ·
Procurement 215 · Inventory 119 · Manufacturing 107 · Warehouse 103 ·
Cost Accounting 84 · Quality 77 · Order Mgmt 66.

**What the ticket text revealed (real tickets):**

- **Data integrity —** *"How did 20413 turn into 20433?"* The inventory team can't explain an item-number mutation. Both items exist in structured data; the ticket reveals someone doesn't trust the data — and is right to.
- **System workarounds —** *"Explore creating dummy transactions for MRP."* Manufacturing is fabricating transactions to work around MRP limits. Structured data records them as real; auditors would never know.
- **Escalation culture —** repeated *"URGENT"* tickets for routine vendor payments. Payments posted on time, so the stress is invisible in transaction data.
- **Acquisition chaos —** 257 *"Request for NetSuite Access"* tickets (10% of all tickets), many from the acquiring company's domains. IT is drowning in onboarding.

**The contrast:** ERP data showed 3,992 employees, 1,044 active customers, 1,423
items, 307 BOMs, 465 GL accounts, 5,035 bin locations — *status: operational*.
The ticket text showed dummy transactions, mutating item numbers, an overwhelmed
team (38% uncategorized), and an "URGENT" escalation culture.

---

## Case 3 — Connected Hardware Manufacturer: High-Growth ERP Forensics

**Engagement:** ERP migration assessment + ITGC audit + international expansion (multi-year)
**Context:** a high-growth hardware manufacturer scaling rapidly, migrating
legacy → enterprise ERP. **3M+ ERP records** analyzed alongside ITGC audit
findings and process documentation.

| Metric | Value |
|---|---|
| CSV rows analyzed | 3M+ |
| Sales orders | 102K |
| RMA returns | 97K |
| Vendors | 43K |
| Customers | 10K |
| RMA rate | 28.6% |

**Data sources:** master data (10K customers, 43K vendors, 8.7K fixed assets, 5K
contacts); transaction data (102K sales orders, 1M+ EDI lines, 97K RMAs, 164K
credit memos); governance/text (ITGC audit, SOD analysis, 7,610 deductions, call notes).

**Forensic findings:**

- **ITGC violations (external audit):** 7 users with Administrator role; a terminated employee still active. 153 active users across 40 roles, with SOD violations at both role and user level. 4 generic shared accounts, no formalized change-management policy, admin access to both dev and prod, no post-implementation review — critical gaps for a publicly traded company.
- **Credit-hold overrides:** sales-order headers carry both a "Customer On Credit Hold" flag and a "Shipment Hold Released by Finance" field. Cross-referencing reveals orders shipped to customers already flagged for credit risk. The structured status says "shipped"; the override field says it shouldn't have been.
- **Return-rate anomaly:** of 1,090 customer accounts, 312 had at least one RMA event (**28.62%**), with only 67.5% on-time delivery. 97K RMA line items across 6 types (Open Box, Closed Box, Destroyed in Field, Stock Rotation, Warranty, Error Shipment) — reason codes hint at systemic quality/logistics failures the structured data can't explain.
- **Approval-chain complexity:** 7,610 customer deductions routed through "Next Approver" and "Set Rerouted Next Approver" chains. The rerouting field exists *specifically because the normal approval chain fails regularly.*

**The contrast:** ERP data said 102K orders processed, 97K returns authorized, 43K
vendors, orders shipped/invoiced/cleared, international entities operational —
*status: functioning*. The governance + text layer revealed credit holds overridden
to ship anyway, a 28.6% return rate signaling systemic issues, 7 admin users (SOX
risk for a public company), a terminated employee still in the system, and approval
chains so broken a "reroute" field had to exist.

---

## Why this matters for the tool

Each case is a real instance of a detector class shipped in this repo:

| Field finding | Maps to detector |
|---|---|
| Dormant / departed-employee licenses | access & lifecycle anomalies |
| Dummy transactions for MRP | fabricated-transaction / reality-gap detection |
| Credit-hold overrides | `POLICY_OVERRIDE` / `APPROVAL_BYPASS` contradiction types |
| Terminated employee still active; 7 admins | `SOD_VIOLATION`, segregation-of-duties analysis |
| PO created before PR | `TEMPORAL_IMPOSSIBILITY` / retroactive-documentation |
| Reroute-because-chain-fails | conformance deviation vs. reference process model |

The synthetic demo in [`demo/`](../demo/) plants these same pattern classes into
generated data so anyone can see the detectors fire — with zero access to a real
SAP or NetSuite system.
Loading