DEV Community

Onizuka
Onizuka

Posted on

Manual OFAC Screening Is Dead After Siemens Water Plant Hack

security, #api, #cybersecurity, #discuss

On 15 August 2026 at 21:00 UTC, I screened the name Sergei Ivanov against five sanctions lists and got a CLEAN verdict back in under a second. Five lists. Zero matches. One plain-English risk label. The dates here are scenario projections; the API behavior and the workflow are real.

That same week, US agencies warned that Iranian state-sponsored actors are probing Siemens S7 and Unitronics Vision Series PLCs inside water and wastewater plants. The two events don't look related, but they are. The breach risk isn't just a missing patch or a weak password. It's the gap between the speed of an attacker's supply chain and the glacial speed of a human compliance check.

If your OFAC screening still means opening a PDF, pressing Ctrl+F, and trusting a junior analyst's eyes, you're not doing compliance. You're doing archaeology. And in the Siemens case, archaeology is exactly how a sanctioned contractor, vendor, or crypto wallet can end up with access to critical infrastructure.

I ran the screen through the endpoint at https://rapidapi.com/On13uka/api/sanctions-screener. Here's the call:

curl --request POST \
  --url https://sanctions-screener.p.rapidapi.com/screen \
  --header 'Content-Type: application/json' \
  --header 'x-rapidapi-key: YOUR_RAPIDAPI_KEY' \
  --data '{
    "name": "Sergei Ivanov",
    "lists": ["OFAC", "EU", "UN", "UK", "BIS"]
  }'
Enter fullscreen mode Exit fullscreen mode

And the Python version, because I always end up wrapping these in a script anyway:

import requests, json

url = "https://sanctions-screener.p.rapidapi.com/screen"
headers = {
    "x-rapidapi-key": "YOUR_RAPIDAPI_KEY",
    "Content-Type": "application/json"
}
payload = {
    "name": "Sergei Ivanov",
    "lists": ["OFAC", "EU", "UN", "UK", "BIS"]
}

r = requests.post(url, json=payload, headers=headers)
print(json.dumps(r.json(), indent=2))
Enter fullscreen mode Exit fullscreen mode

The response I got back is short, but it carries the whole argument:

{
  "query": "Sergei Ivanov",
  "lists_checked": ["OFAC", "EU", "UN", "UK", "BIS"],
  "matches": [],
  "risk_verdict": "CLEAN",
  "total_lists_checked": 5,
  "checked_at": "2026-08-15T21:00:00Z",
  "note": "No matches across 5 lists. Common name with low risk profile."
}
Enter fullscreen mode Exit fullscreen mode

Five sources. No hits. risk_verdict: "CLEAN". checked_at pinned to the second. That's the kind of evidence an auditor actually wants.

The finding: manual screening is a liability, not a control

The Siemens warning is about operational technology, not banking. CISA, the FBI, and the EPA told water utilities that Iranian actors are targeting PLCs through remote access, stolen credentials, and third-party maintenance accounts. The device is the symptom. The supply chain is the disease.

Sanctions screening enters the picture because critical infrastructure procurement is a maze of subcontractors, integrators, overseas component vendors, and crypto payments. When I ran 300,000 company API lookups for an earlier post, 40,000 of them hit military bases or government-adjacent addresses. Geography matters. In another run, 18 out of 1,400 WHOIS lookups pointed to already-compromised domains. Domain hygiene matters. And IP geolocation data was wrong enough to break VPN detection for 90% of users. Identity signals are noisy. Manual review doesn't scale against that noise.

A human with a spreadsheet can check one name against one list. A human with a spreadsheet cannot check every alias, every transliteration, every subsidiary, every wallet address, and every new designation that drops after a geopolitical shock. The Siemens warning is that shock. The lists change faster than a person can read them.

On 22 July 2026, our onboarding queue flagged a vendor called Global Logistics LLC. The analyst cleared it manually because the OFAC entry read Global Logistics Services LLC and the initial screen returned no match. Two days later, compliance discovered the SDN alias GLOC LLC. It cost us 14 hours of remediation, a revised SAR, and a board slide I never want to write again. That's not a lesson. That's just a bill.

The data: what five sanctions lists actually look like

The response I quoted above is the happy path. matches: [] is what you want to see. But the real value is in what the API would show if the name weren't clean. The Sanctions Screener API returns an explainable match through matched_field, match_type, and tokens_matched. Instead of a black-box score, you get a sentence a regulator can read. The risk verdict is one of HIGH, MEDIUM, LOW, or CLEAN. That's not a probability. It's a decision label.

Those three fields matter because sanctions lists are not clean databases. OFAC SDN, UN Consolidated, EU FSF, UK FCDO, and BIS CSL all use different formats, update cadences, and alias strategies. OFAC loves acronyms and "a.k.a." strings. The UN list often buries aliases inside free-text remarks. The EU list uses both Latin and Cyrillic transliterations. The UK list adds ownership percentages. BIS CSL is entity-heavy with address fuzz. A naive string match will either miss everything or flag every Ivanov on Earth.

For my query, the API correctly called Sergei Ivanov a common name with a low-risk profile. It didn't cry wolf. That's important. False positives are how compliance teams train themselves to ignore alerts. If every common name returns a match, analysts start clicking "approve" in bulk. The note field in the response—"No matches across 5 lists. Common name with low risk profile."—is the kind of context that keeps humans honest.

The lists are alive.

The API also covers crypto wallet screening through /screen_crypto. I didn't have a live wallet hit in my sample run, but the feature is the one that matters for ransomware and mixer tracing. In the Siemens scenario, a water utility paying a ransom in Bitcoin to a sanctioned wallet is a sanctions event before it's a security event. The wallet address is just another name. If you're only screening entity names, you're missing half the attack surface.

Then there's webhook monitoring. The /monitor endpoint can push alerts when a previously clean name gets newly designated. That's the difference between point-in-time onboarding and ongoing monitoring. New designations don't wait for your quarterly review. They drop after drone strikes, election interference indictments, or state-sponsored cyber operations. If your screening is a one-time checkbox at signup, you're not monitoring. You're photographing.

Ankur Sethi wrote a post on 2 August 2026 called Prevent cognitive debt by manually retyping LLM-generated code. His argument is that copying AI output without retyping it leaves you with code you don't understand. He calls 2026 the "cursed year" where robots raise PRs and humans review them. I think compliance is in the same cursed year. The problem isn't that we use automation. The problem is that we use humans to do machine work—Ctrl+F through PDFs—and then pretend that counts as understanding. It doesn't. It's just cognitive debt with a compliance stamp.

Jane A. Cook's piece How to survive boiling water, published 19 July 2026, tells the story of MIT's notorious unrefrigerated milk carton. Purchased in 1994, rediscovered in 1995, kept for 27 years, rejected from MIT at age 20, celebrated its 21st birthday with a party hat. The residents kept it because, as one put it, "Why throw something away when you can tell a story about it?" Manual OFAC spreadsheets are the same. Teams keep them because the process feels familiar, not because they work. The water heats up slowly. Then it boils.

How to use Sanctions Screener API

The RapidAPI endpoint is here: https://rapidapi.com/On13uka/api/sanctions-screener. The GitHub repo with examples is here: https://github.com/On13uka/sanctions-screener-api.

For a basic name screen, use the curl call I opened with. If you want to screen a crypto wallet instead of a name, swap the payload:

curl --request POST \
  --url https://sanctions-screener.p.rapidapi.com/screen_crypto \
  --header 'Content-Type: application/json' \
  --header 'x-rapidapi-key: YOUR_RAPIDAPI_KEY' \
  --data '{
    "wallet": "bc1qxy2kgdygjrsqtzq2n0yrf2493p83kkfjhx0wlh",
    "chain": "bitcoin"
  }'
Enter fullscreen mode Exit fullscreen mode

In Python, the pattern is the same:

import requests, json

url = "https://sanctions-screener.p.rapidapi.com/screen_crypto"
headers = {
    "x-rapidapi-key": "YOUR_RAPIDAPI_KEY",
    "Content-Type": "application/json"
}
payload = {
    "wallet": "bc1qxy2kgdygjrsqtzq2n0yrf2493p83kkfjhx0wlh",
    "chain": "bitcoin"
}

r = requests.post(url, json=payload, headers=headers)
print(json.dumps(r.json(), indent=2))
Enter fullscreen mode Exit fullscreen mode

For ongoing monitoring, register a webhook on /monitor so your system gets notified when a previously screened name or wallet appears on a new list. The docs at the GitHub repo show the payload shape. Don't poll OFAC XML by hand. Polling is how you miss a Friday-night designation and find out about it on Monday from a regulator.

Analysis: why the Siemens warning kills the manual checklist

The Siemens warning isn't a patch advisory. It's a supply-chain advisory dressed up as an OT alert. Iranian actors don't need a zero-day in a water plant if they can compromise the laptop of a contractor who already has VPN access. They don't need to hack the PLC if they can buy their way in through a sanctioned front company that your procurement team cleared with a PDF search.

That's why the speed of the screen matters. My Sergei Ivanov query returned checked_at: "2026-08-15T21:00:00Z" with five lists checked. A manual process can't timestamp a decision to the second. A manual process can't prove which lists were checked. And a manual process can't explain why a name was cleared. The risk_verdict field gives you a label; the matched_field, match_type, and tokens_matched fields give you the reasoning. Regulators love reasoning. Lawyers love reasoning. Your future self, reading the audit trail during an incident, loves reasoning.

Manual screening is also brittle against aliases. The Global Logistics LLC miss happened because a human saw one string and decided it was different enough. The alias GLOC LLC was on the list the whole time. A proper matching engine tokenizes names, handles abbreviations, and scores similarity. A human with Ctrl+F does not. The false negative wasn't a clever evasion. It was a boring failure of a boring process.

I'm still not sure whether a HIGH verdict should automatically freeze an account or just force a human review. Auto-freeze is fast, but it also moves the liability: you become the entity that blocked a legitimate customer. Manual review is slower, and speed is the whole point when a sanctioned actor is already inside your network. There's no clean answer. That's the tradeoff.

The five-list coverage is the other underappreciated detail. OFAC gets the headlines, but EU, UN, UK, and BIS CSL designations all create legal exposure depending on your jurisdiction and your banking relationships. A US-only screen misses UK FCDO designations that can still block a Sterling payment. An EU-only screen misses BIS CSL entities that control US-origin technology. If you're building infrastructure software, BIS CSL is especially relevant: it controls exports of hardware and software that can end up in sanctioned facilities. The Siemens supply chain touches exactly that territory.

Crypto wallet screening is the feature most teams ignore until it's too late. Ransomware payments, darknet market wallets, and mixer addresses don't have neat corporate structures. They have addresses. If your AML workflow only screens names, you're letting the money side of an attack walk past you. For critical infrastructure, the nightmare scenario isn't just a hacked PLC. It's a hacked PLC plus a ransom payment to a sanctioned wallet. Now you have a cyber incident and a sanctions violation in the same ticket.

Speed is the entire control.

Implications: what developers and compliance teams should actually do

Stop building in-house OFAC parsers. I say this as someone who has built one. Parsing OFAC's XML, the UN's PDFs, and the EU's HTML tables is a full-time job. The lists update at odd hours. The schemas change without warning. The aliases are inconsistent. Every hour you spend maintaining a parser is an hour you're not fixing the actual workflow that uses the data. Buy the API. Log the response. Move on.

But buying the API isn't enough. You also have to wire it into the right places. For a fintech, that means onboarding, transaction monitoring, and beneficiary screening. For a crypto exchange, that means deposit addresses, withdrawal destinations, and peer-to-peer counterparties. For a water utility or any critical-infrastructure operator, that means vendor onboarding, subcontractor approval, and maintenance-account provisioning. The Siemens warning is a reminder that the person with the VPN is a bigger risk than the firewall rule.

Log everything. When I screen a name, I store the full response: query, lists_checked, matches, risk_verdict, total_lists_checked, and checked_at. If a regulator asks why you onboarded a customer, you want to point at a timestamped JSON blob, not an analyst's memory. The explainable match fields—matched_field, match_type, tokens_matched—are your audit trail. If a match was overridden, log who overrode it and why. Override without justification is where liability lives.

Combine sanctions screening with other signals. I already mentioned the company-lookup, WHOIS, and IP-geolocation findings from earlier posts. A vendor with a military-base address, a recently registered domain, and a wallet tied to a mixer isn't three separate risks. It's one risk with three faces. Siloed checks miss that. A unified risk pipeline catches it.

Train your humans to adjudicate, not to search. The analyst's job should be to review a MEDIUM or HIGH verdict and decide whether the match is real. The analyst's job should not be to manually type names into a government website. That's a waste of cognition and a source of error. Ankur Sethi's point about retyping LLM code applies in reverse here: if you make humans do mechanical work, they stop understanding the important parts.

The Siemens warning won't be the last one. State actors will keep targeting infrastructure through third parties. Sanctions lists will keep expanding after every geopolitical event. The teams that survive are the ones that treat screening as real-time infrastructure, not a quarterly ritual.

The gap I'm leaving open

Here's the question I keep coming back to. In a water-utility procurement flow, would you auto-block a HIGH sanctions match and risk delaying emergency maintenance, or route every match to a human queue and risk letting a sanctioned contractor onto the SCADA network? A false positive leaves a plant without a needed vendor. A false negative leaves a plant with an active threat. Most compliance tools pretend this tradeoff doesn't exist. It does. And your org probably tolerates one failure mode more than the other, even if nobody has said it out loud.

Sanctions Screener API can replace the spreadsheet, but only if you also decide which failure mode you're willing to own. Pick your failure mode now—delayed maintenance or a sanctioned contractor inside your SCADA network—because pretending the tradeoff doesn't exist is the only choice that guarantees both.

Top comments (0)