Trade Compliance

Does obfuscation break denied party screening?

Restricted-party screening is a list, a similarity score and a threshold, and the threshold is the part that decides whether a near match is a hit. This pass protected a file holding all three and renamed the property names a group at a time. The result that changed how I read this whole series is that two thresholds written in opposite directions failed in the same direction, and the reason was not the one the previous pass would have predicted.

The list, the score and the two thresholds

The sample screens an order before it crosses a border. Two parties are on the denied list, held as strings. A similarity score is computed against each, and a match threshold of 0.6 decides what counts as a hit -- lower than the library's default of 0.95, because near misses are the point of screening. Four destination countries are permitted. A minimum consignee-name length of three characters refuses names too short to screen meaningfully. Screening is switched on, and the compliance team's own screener checks end use against the government feed.

Unprotected, an order to 'Volkov Trading Limited' scores 0.67 against the listed 'Volkov Trading Ltd' and is refused. An exact match to the second listed party scores 1.0 and is refused. An embargoed destination, a licensable end use and a two-character consignee name are each refused by their own rule. An ordinary German laboratory order ships.

Two of those rules are numbers, and both are phrased as questions about sufficiency. The match threshold asks whether a score is high enough to be a hit. The name-length minimum asks whether a name is long enough to screen. One is used directly as the refusal condition and the other is used negated. On the previous pass in this series, that difference decided which way a lost threshold failed.

Two thresholds, opposite phrasings, the same direction

Renaming matchThreshold reverts it to the library default of 0.95. 'Volkov Trading Limited' at 0.67 is no longer a hit and the order ships: MAY-SHIP=true cleared(so-2 -> Volkov Trading Limited / DE). Renaming minNameLength reverts it to the library default of zero, and the two-character consignee name ships too.

Both failed open. Not because of how the comparisons were written -- one is negated and one is not -- but because neither number ever became unreadable. These are option keys, and the library has a default for each. A renamed option key does not produce an undefined value in the comparison; it produces the vendor's value, and the direction of the failure is decided by where that value points. Both of these vendor defaults are permissive, which is a reasonable choice by a library that cannot know your compliance posture.

This is the correction the pass bought. Floor-versus-ceiling reasoning applies to a number read out of a record, where losing the name genuinely leaves undefined in the arithmetic. It does not reach an option key at all. Before predicting which way a lost threshold fails, establish first whether the value can go missing or will simply be replaced -- and if it will be replaced, read the vendor's default rather than the comparison.

The arm that makes a spot-check pass

The threshold arm has a property that deserves separating out. The exact match is still caught. 'Meridian Parts Export' scores 1.0, which clears 0.95 as comfortably as it cleared 0.6, and the refusal is unchanged apart from the number printed in it: denied-party-match(Meridian Parts Export score=1 >= 0.95).

So screening still visibly works. An engineer verifying the deployment by typing a known bad name into the order form gets a refusal. A compliance officer asked to confirm the control is live gets a screenshot of a block. Everything that anybody would check by hand still behaves. What has silently gone is the entire band between 0.6 and 0.95, which is where deliberate near-misses live, because a party trying to ship to a listed entity does not use the listed spelling.

Renaming deniedParties removes the list itself, and both parties ship, exact match included. That arm is more severe and much easier to catch, since any spot-check with a known name fails immediately. The threshold arm is the one that survives review.

The list as text, and the field it matches against

The denied parties are strings, and renaming rewrites names in code, never the text inside them. So the list survives every rename intact. What can be lost is the value it is compared to. Renaming partyName refuses everything: the length check sees an empty string and reports name-too-short-to-screen(0 < 3) on the clean German order as readily as on the listed one.

That is loud and it is safe, and it is the same asymmetry visible in this pass's dangerous-goods measurements. Losing the option that carries a rule tends to admit; losing the data the rule is applied to tends to refuse. Neither is a design you chose, and only one of them tells you.

Renaming country behaves the same way -- destination-not-permitted(undefined) on every order -- while renaming allowedCountries, the option holding the permitted list, is quieter: the embargoed destination is still refused, but by the screener's own embargo rule rather than the allowlist, so only the reason moved.

The switch, the screener, and a refusal that reports no hit

Renaming screeningEnabled switches screening off entirely: both listed parties and the licensable nuclear end use all ship. Renaming screener replaces the compliance team's function with the library's built-in, which clears everyone, so the end-use order ships while the function itself sits in the file, correct and uncalled. Renaming both is byte-identical to renaming the flag alone, because the flag is checked first.

Renaming cleared, the field the screener returns its verdict in, fails the opposite way: every order is refused with screener-refused(no-hit-on-the-government-feed). The reason states there was no hit and the order is blocked anyway, which is the same inversion this pass measured in dangerous goods and previous passes measured in payments and authentication. It is a rename signature, not a logic error.

Renaming endUse is the mirror: the licensable end use is no longer visible to the screener, so the order ships with a clean report. The screener ran, found nothing, and was right about what it could see.

What protection alone did, and what to do about it

Nothing. The file was protected on five profiles -- the ES5 default, the modern target, both emit-gate configurations and the string-encoding profile -- and produced byte-identical output on all five. Every screening decision matched. Renaming identifiers and encoding strings does not disturb a screening engine.

Every result above required member renaming, which is off by default and needs a regular expression naming the members it may rewrite. Keep that pattern anchored to names your own code owns on both sides. A screening configuration is the opposite of that: your file writes the option keys, the order fields and the screener's return shape, and installed code reads all three.

Then assert the near miss, not the exact match. Take the party name that scores in the band your threshold was lowered to catch -- 0.67 against a threshold of 0.6 -- and assert that it is refused and that the threshold printed in the refusal is 0.6. Asserting that a known listed name is blocked passes in the arm that quietly reverted your threshold to the vendor's.

Frequently asked questions

Does obfuscating an order bundle turn off sanctions screening?

Not by itself. The sample was protected on five profiles including the ES5 default, the modern target and string encoding, and produced identical screening decisions on all five. Every result in this article required member renaming, which is off unless you enable it and supply a pattern.

What happened when the fuzzy-match threshold was renamed?

It reverted to the library default of 0.95 from the configured 0.6. A near name match scoring 0.67 against a listed party shipped, while the exact match at 1.0 was still refused. Anything verified by typing a known bad name still appears to work.

Do renamed numeric thresholds always fail open?

No, and the distinction matters. A threshold carried by an option key never becomes undefined -- the library substitutes its own default, and the direction is decided by where that default points. Only a number read from a data record actually goes missing, and that is where floor-versus-ceiling reasoning applies.

Can a denied-party list itself be renamed away?

The strings cannot, because renaming rewrites names in code and not the text inside them. The option that carries the list can be, and when it was, both listed parties shipped. That arm is severe but easy to catch with any known-name spot-check.

Why did renaming the consignee-name field refuse every order?

Because the rule was applied to a value that was no longer there. The length check saw an empty string and reported name-too-short-to-screen(0 < 3) on legitimate orders too. Losing the data a rule reads tends to refuse; losing the option that carries the rule tends to admit.

What does screener-refused(no-hit-on-the-government-feed) indicate?

A rename, not a logic bug. The screener ran, decided correctly, and the library read its verdict out of a field whose name had moved, so the refusal quotes a reason describing a clean result. Treat a refusal whose reason asserts success as a rename signature.

What single test would have caught the quietest of these?

Assert the near miss and the threshold together: the party scoring 0.67 must be refused, and the refusal must quote 0.6. Testing only that an exactly listed name is blocked passes in the arm that reverted the threshold to the vendor default.

Related reading