CRUSETRA
Appendix B · Screening Security and data handling ← Back to the findings

Where screening runs, and what stays local.

A matte aluminium padlock; its shackle, closed, is the same deep ruby as the tool's accents.

Screening handles the most sensitive data a bank holds, the names it checks. This page says which part of the tool touches the network and which part never does. It names the test that enforces each rule, because a sentence on its own promises nothing.

One module may reach the network, and a test checks that no other one does

Exactly one module may reach the network, the downloader of the public sanctions lists. The permission is a named allowlist with its reason: the files that may call out, each with the reason it may. A test reads the source of each module for network call sites (fetch, node:http and its relatives, WebSockets). It fails the suite (npm test) if any other module grows one. That test first proves it can see a call planted for it, so its zero means it looked and found none.

The downloader only pulls the list down and sends no data up. When it runs, no name you screen exists yet. The downloader moves public documents only.

Verify the network boundary

Run the suite. The test fails if a network call site appears outside the allowlist.

run it yourself
npm testfails if any module but the downloader touches the network

Where it livessrc/frontiere.test.ts:20 · src/frontiere.test.ts:45

Each list carries a content hash, and a changed file is refused

npm run listes -- --fetch downloads ten public sources: OFAC SDN, the OFAC consolidated (non-SDN) lists, the US Consolidated Screening List (its Commerce and State lists), the UN Security Council list, the EU financial sanctions list, the UK Sanctions List, the EU's designated vessels, Australia's DFAT Consolidated List, the Consolidated Canadian Autonomous Sanctions List and New Zealand's Russia Sanctions Register. It writes a manifest, and the manifest is committed to git. The manifest holds the source, the URL, the date, the sha256 (the content hash of the file), the byte and entry counts. The list data itself stays out of the repository. When the tool later reads a list from disk, it recomputes the content hash. Screening against a list other than the recorded one certifies nothing. So the tool refuses a file that no longer matches the manifest and names both content hashes.

OFAC states its own record count inside the file, and on each fetch the tool checks the entries it read against that count. The UN address is the one the Security Council page publishes. Without the --fetch flag, npm run listes reports what is on disk and touches nothing.

Verify without the network

The no-flag form reads the disk against the manifest and reports that it touched no network.

run it yourself
npm run listesreports date, content hash state and entry counts, with no network touched

Where it livessrc/listes.ts:750 · src/listes.ts:775

The EU's designated vessels come from a dated legal text

The EU financial sanctions list carries asset freezes. It does not carry the vessels the EU bars from its ports and services: those are in Annex XLII of Regulation (EU) No 833/2014, which exists in no machine-readable file. So the tool reads the annex from the consolidated text of the regulation, served by the EU Publications Office, and the manifest records that file like the others, with its date and content hash.

The address names one consolidated version, dated 24 July 2026. A vessel designated by a later sanctions package is not in it until the tool is moved to the next version, so the manifest and every report state the date of the text that was read. The UK Sanctions List is a second source for vessels: it lists ships with their IMO numbers, and the tool keeps every number a ship carries.

Verify the manifest entries

The committed manifest carries one line per source, with its address, date, content hash and entry count.

run it yourself
python3 -c "import json; [print(x['source'], x['disponible'], x.get('entrees'), x.get('telechargeLe','')[:10]) for x in json.load(open('listes-manifest.json'))['listes']]"prints the ten sources, each with its entry count and download date

Where it livessrc/listes.ts:169 · src/listes.ts:280

With the network cut, the whole tool still runs and reports the cut

CRUSETRA_OFFLINE=1, or its former name CASCADE_OFFLINE=1, cuts the network for the whole tool. Under it the suite passes and the measurement of your alert history runs. The one command that needs the network refuses, naming the flag and the way out. It prints no stack trace, the raw dump a crash leaves.

Verify offline

Cut the network and run everything.

run it yourself
CRUSETRA_OFFLINE=1 npm testruns the whole suite with the network cut, and it passesCRUSETRA_OFFLINE=1 npm run listes -- --fetchrefuses, names the flag and what to do next, writes nothing

Where it livessrc/listes.ts:843

No screened name appears in anything the tool writes

When you measure your own alert history, the outputs are written next to your file and nowhere else. They are a report and a public results file with its content hash. That file holds counts, rates, confidence intervals (the range a true rate likely sits in) and the answers keyed by your alert id. No screened name, no list entry you matched and no path with your username leaves the machine. Your identifiers do survive as those keys, so choose ones that carry no meaning. An account number used as an id stays in the outputs.

The screening itself runs on your machine against the lists on disk. There is no callback to us, no telemetry (usage data sent back) and no account. The only thing the evaluation needs from us is the repository, the code itself.

Verify on a decoy

Measure a file with an invented name, then search the outputs for it.

run it yourself
grep -r "the name you invented" your-export-measured.md your-export-measured.jsonfinds nothing, since the outputs hold answers keyed by id and the names stay out

Where it livessrc/your-alerts.ts:24 · src/your-alerts.ts:412