← Red Threads

Independent technical review: public ThreatExchange update client

Reviewed 9 September 2026. Static inspection only: no captured module was imported or executed, no API or credentials were used, and no new source was acquired. All six retained Python source hashes match captures.json at pinned commit 73a742c45c35d711e94c8bf92254df1a2d9c9f01. This review concerns available software paths, not Lantern's deployment or the live server.

The proposed narrow conclusion is supported: the public client contains paths for processing deletion/update records and representing negative opinions. ThreatUpdateJSON.should_delete calls a record a tombstone and treats either should_delete or the absence of descriptors as a removal condition. ThreatUpdateFileStore._apply_updates_impl removes the key from its local state for such records, otherwise replaces/adds the record, and serializes the result. The wrapper's apply_updates invokes that implementation for a nonempty delta. This is a concrete local-state deletion path in the source, not a record of any deletion actually delivered to a Lantern participant. See [threat_updates.py lines45-49](http[local research file] lines316-333 and396-413.

The exchange adapter separately maps a tombstone to None, NON_MALICIOUS to a negative category, and a DISAGREE_WITH_TAGS reaction to an implicit negative category under its precedence rules. It retains the resulting opinions in the indicator record. An explicit opinion from the same owner takes precedence over the implicit reaction; these routines do not promise to retain every contradictory statement unchanged. A negative category is metadata, not proof of a false accusation, a successful appeal or account restoration. The adapter fetches threat_updates and emits a delta keyed by threat type and indicator. See [fb_threatexchange_api.py lines105-180](http[local research file] and292-325; fetch_state.py lines79-117 define opinion categories.

The 90-day statement is documentation of deletion-history retention in client comments/docstrings. It is not a verified observation of current backend retention, and especially not Lantern's signal-retention policy. Both checkpoint variants use a strict older-than-85-days threshold, providing a five-day margin against the documented loss of deletion records. The relevant age is the stored last_fetch_time, not each signal's creation date. See threat_updates.py lines218-253 and [fb_threatexchange_api.py lines55-73](http[local research file]

The CLI call chain is present to the storage interface: execute_for_collab obtains a checkpoint through _verify_store_and_checkpoint; if stale, the latter calls store.clear(collab) and returns None. execute_for_collab then passes that None into the exchange adapter's fetch_iter, which requests updates without a starting checkpoint, and merges returned deltas into the store. Thus the code requests clearing and begins a fresh fetch path; do not claim that this review observed a complete successful refetch. The command can encounter failures or limits. See [fetch_cmd.py lines184-257](http[local research file] fb_threatexchange_api.py lines292-325 and api.py lines271-308.

A concrete call-path limit remains: the captured fetch_state.py defines abstract merge, flush and clear methods, and the concrete configured CLI storage implementation is not among these six files. The earlier ThreatUpdateFileStore deletion routine and the exchange-adapter/CLI path must not be spliced into one claimed execution trace. This does not erase the demonstrated source-level deletion capability; it limits any statement that the precise deployed CLI persistence path has been independently traced end to end. No broader bug hunt or execution is needed for this inquiry's conclusion.

The missing causal links remain deployment and downstream obligations: whether a particular Lantern participant runs this client/version, ingests the updates promptly, propagates changes to its own exported copies and enforcement systems, or reopens a prior decision. Public client capability cannot establish those facts. The supported refinement is that a public technical mechanism for updates, deletion markers and negative opinions exists; its availability does not demonstrate Lantern-wide correction or remedy performance.

Inspected scope: threat_updates.py selected lines30-62,195-265,270-413; fb_threatexchange_api.py40-217,265-368; fetch_cmd.py95-295; fetch_state.py1-135,185-280; api.py271-308; descriptor.py scoped status/deletion matches only. Source paths and byte hashes remain in captures.json. No package, case or acquisition record was changed.