Recorded copy of the sample legacy app · synthetic data

Back to the demo

The app we started with

The invoice desk Bob assessed, before anything moved

A small Java 8 web app for a made-up distributor, Ingram Trials. It was built to be imperfect on purpose, with the kinds of problems inherited code has. Bob read this code and wrote the four documents from it; each of the seven migrations moved the same piece of it, the invoice preview.

  1. 1 The code Four layers, and where each part of the desk lives
  2. 2 Start it Build, start and check it with the app's own scripts
  3. 3 Use it Its screen, answering as the real app did

Every answer and figure on this page came from the real app, run on our machine on 22 October 2026. Nothing on it reaches a server.

Step 1

The code

Four layers, the way many business apps of its age were built. The screen asks the server for everything; one Java class holds nearly all the rules and talks to the database.

  1. ScreenOne page and its script, in the browser.index.html desk.js
  2. RoutesTurns each address the screen calls into a call on the rules.DeskController.java lines 20–63
  3. RulesPrices, invoices, credit notes, jobs and reports, in one class.Desk.java
  4. DataAn H2 database in a file. The rules create its eight tables when the app starts, and fill in the sample data the first time.Desk.java lines 37–60

Each part of the desk, and the code it runs

On the screenAddress it callsRulesWhat Bob found there
1 Find customerGET /api/customers
DeskController.java lines 33–34
Desk.java lines 92–95F-001 critical SQL injection via string-concatenated LIKE clauses in customers() and invoices()
2 Preview invoicePOST /api/invoices/preview
DeskController.java lines 41–42
Desk.java lines 146–176, using Desk.java lines 107–113F-004 high Preview/save bulk-discount off-by-one: qty=100 discounted in preview but not in save
3 Save invoicePOST /api/invoices
DeskController.java lines 43–45
Desk.java lines 177–187, using Desk.java lines 114–120F-013 medium Invoice and credit-request IDs generated via COALESCE MAX+1, no database sequence
4 Search invoicesGET /api/invoices
DeskController.java lines 37–38
Desk.java lines 129–133F-001 again: the same pattern, in a second place.
5 Create approved requestPOST /api/credit-requests
DeskController.java lines 48–50
Desk.java lines 194–207F-008 medium Credit requests auto-approved with no workflow or authorization check
Run credit notesPOST /api/jobs/credit-notes
DeskController.java lines 57–58
Desk.java lines 220–225, then Desk.java lines 242–255F-015 medium Reflective job dispatch uses deprecated Class.newInstance(); no compile-time binding
6 Run delivered-order invoicesPOST /api/jobs/invoices
DeskController.java lines 55–56
Desk.java lines 220–225, then Desk.java lines 226–241F-003 high Batch job trigger endpoints unauthenticated in inspected source
7 Monthly salesGET /api/reports/monthly
DeskController.java lines 59–60
Desk.java lines 256–279F-016 high H2-specific FORMATDATETIME SQL function used in reporting queries
Annual GSTGET /api/reports/gst
DeskController.java lines 61–62
Desk.java lines 280–291F-016 again.
Not on the screen: the health checkGET /api/health
DeskController.java lines 26–32
None: the routes answer it themselves.F-002 high Internal database connection string exposed in /api/health response body

The numbers match the blue marks on the screen in Step 3. Each finding ID links to the Assess stage of the demo.

Also in the folder

  • Jobs. jobs.properties names the two job classes, and the rules load whichever one is asked for by its name. A third job class is in no list, and nothing in the app uses the legacy invoice printer class.
  • Schedules. ScheduledJobs.java would run both jobs every night, but a setting keeps them switched off.
  • One test. DeskMoneyTest.java checks how money is rounded, and nothing else.
  • Scripts and documents that came with it. The app's own scripts to build, start and stop it (Step 2) and to start a job by hand. Its scripts and docs folders hold an export script and a customer-accounts document; some of that is out of date on purpose.

The whole app folder The app's README

Step 2

Start it

On Windows with Java 8 and Maven installed, the app's own scripts build it, start it and stop it. This is what they printed when we recorded it.

.\app\Build.ps1

⋯ 4 lines not shown
[INFO] Building base-desk 1.0.0
⋯ 33 lines not shown
[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.043 s - in com.ingramtrials.base.DeskMoneyTest
⋯ 3 lines not shown
[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0
⋯ 8 lines not shown
[INFO] BUILD SUCCESS
⋯ 1 line not shown
[INFO] Total time:  47.042 s
⋯ 2 lines not shown

.\app\Start.ps1 -NoBuild

App ready at http://127.0.0.1:18080/

GET http://127.0.0.1:18080/api/health

HTTP 200

{
  "status": "UP",
  "case": "BASE",
  "javaVersion": "1.8.0_504",
  "businessDate": "2026-10-13",
  ⋯ "database" not shown
}

.\app\Stop.ps1

App server stopped on port 18080
  1. Build. Maven compiles the app with Java 8 and runs its one test. Where lines are left out, a marked line says how many; no line shown is changed.
  2. Start. Starts the built app on this machine only, so nothing outside it can reach it, and waits until it answers.
  3. Check it is up. One field of the answer is not shown here: database, which gives the database file's path on our machine. That the app hands this out at all is one of Bob's findings: F-002 high Internal database connection string exposed in /api/health response body
  4. Stop. Stops the server the start script started.

Recorded with Java 1.8.0_504 and Apache Maven 3.10.0 on Windows. To run it yourself, follow the app's README.

Step 3

Use it

The app's own screen, unchanged, answering from what the real app answered when we recorded it. The blue numbers on the screen are ours; the notes beside it say what each part does, which code runs and what Bob found there.

Try this order. Fills in the screen with its own controls; then press the button the note names.

Type an order in yourself

This copy answers only what the real app was asked when we recorded it, so type an order exactly as below. Press Start again first, so the desk begins from the sample data.

Aster Office Supplies

  1. Customer Aster Office Supplies · Tier A.
  2. Product A4 paper carton · PAPER-A4, which fills in the unit price 10.00; quantity 100; press Add line.
  3. Press Preview invoice: total 1014.79. Or Save invoice: 1035.50, the gap in note 3.

Merlion Retail

  1. Customer Merlion Retail · Tier B.
  2. Product Black toner cartridge · TONER-BK, which fills in the unit price 50.00; quantity 10; press Add line.
  3. Press Preview invoice: total 528.65.

Straits Workshop

  1. Customer Straits Workshop · Tier C.
  2. Product USB cable pack · CABLE-USB, which fills in the unit price 3.3350; quantity 3; press Add line.
  3. Then product A4 paper carton · PAPER-A4, which fills in the unit price 10.00; quantity 101; press Add line.
  4. Press Preview invoice: total 1089.79.

The credit request

  1. Under Credit requests and notes, type Invoice ID 1001, Net amount 48.50 and Reason Two cartons damaged in transit.
  2. Press Create approved request, then Run credit notes under Operations: CN-2026-00001, total 52.87.
  • Leave each unit price as the product fills it in, and add the lines in the order given.
  • The message “lines must contain 1 to 50 entries” means no line was added yet: press Add line before Preview invoice.
  • A line typed wrong: press Remove beside it and add it again. After a save, press Start again before the next order.
  1. Find customer. Searches the customers as you type. Desk.java lines 92–95 puts the typed text straight into the database query: F-001 critical SQL injection via string-concatenated LIKE clauses in customers() and invoices()
  2. Preview invoice. Prices the lines with the customer's discount and GST, and saves nothing. Aster Office Supplies, 100 cartons of A4 paper at 10.00: 931.00 plus GST 83.79, total 1014.79. The bulk discount starts at 100 here: Desk.java lines 107–113.
  3. Save invoice. Prices the order again with a second copy of the rule, which gives the bulk discount only above 100 (Desk.java lines 114–120). The same order saves at 1035.50, not the 1014.79 its preview showed. That gap is F-004 high Preview/save bulk-discount off-by-one: qty=100 discounted in preview but not in save
  4. Search invoices. The same pattern as note 1, in Desk.java lines 129–133: F-001 again.
  5. Create approved request. Any amount up to the invoice's subtotal is saved as approved at once, with no one approving it (Desk.java line 205). Run credit notes then issues it: CN-2026-00001, 48.50 plus GST 4.37, total 52.87. F-008 medium Credit requests auto-approved with no workflow or authorization check
  6. Run delivered-order invoices. Invoices every order delivered the day before the app's fixed date. Order 7001, 101 cartons for Aster, becomes INV-2026-00002 at 1024.94. Anyone who can reach the app can start it: F-003 high Batch job trigger endpoints unauthenticated in inspected source
  7. Monthly sales. Totals a month's invoices and credit notes. On the sample data, October 2026 holds 1 invoice, INV-2026-00001 for Merlion Retail, at 528.65. Its query uses an H2 date function that PostgreSQL and most other production databases don't have: F-016 high H2-specific FORMATDATETIME SQL function used in reporting queries

What this copy can show

  • Lists, an invoice's details, searches for any part of a customer's name or an invoice number, and both reports for 2026 answer as the real app did on the sample data.
  • Preview, Save, credit requests and the two jobs answer for the recorded orders: use Try this order, or type one in exactly as its steps say. After a save, request or job, the lists and reports show its result.
  • Anything else, such as another quantity, gets a note that this copy cannot show it: answering would mean rewriting the app's rules in the browser. For that, start the real app (Step 2).
How this copy works

The screen is the app's own three files. Its page differs from the app's in two places only: its links to them are relative, and two scripts load before the app's own: the recording, and a small script that answers the app's calls from it in place of a server.

The recording holds every answer the real app gave, in each state the recorded orders lead to: the sample data, after each order is saved, after the invoice job, and after a credit request, its credit note and then the invoice job. Saving or starting a job moves the copy to the state the real app was in after doing the same.