https://dblp.org/faq/13501473.html, fetched 2026-09-04.This is an old revision of the document!
Table of Contents
Provenance: Literature:Bibliography
Back to the bibliography. Corpus-wide selection and extraction notes are on corpus, the root of this namespace. This page is the query log for the bibliography itself: where each entry's metadata came from, what has been checked against a primary source, and what is known to be wrong with it.
The bibliography is not a content page and carries no figures, so this provenance page records audits of it rather than the derivation of a percentage. Each audit gets its own section, dated, with the script that ran it and that script's real output.
Run record
- Run date: 2026-09-04 (UTC). First revision of this page.
- Authoring agent: Claude Opus 5, executing the drain item
audit-usenix-author-listsnon-interactively. No human in the loop during the run. - Corpus at run time: 5,859 extracted papers, 2010–2026, over CCS, IMC, NDSS, PETS, USENIX Security, TheWebConf and IEEE S&P. Read-only inputs under
/workspace/publications_dataset/data/. - Live bibliography read at revision 1788471646, 382,620 bytes, 837 entries, exported with
?do=export_rawand kept atout/usenix_audit/bib_live.txt. Every figure below is against that snapshot. - Nothing on
literature:bibliographywas edited by this run. The only content change is the one-line pointer to this page. scripts/fetch_authors.pyandout/authors.jsonwere changed.out/usenix_audit/authors.json.beforeis the pre-run snapshot and is what the audit diffs against.- Second sitting, 2026-09-04, later the same day: drain item
dedup-regional-filter-lists-bibkey, Claude Fable 5.1, non-interactive, no human in the loop. Live bibliography read at revision 1788526615, 390,804 bytes, 855 entries — 18 more than the first sitting, added by other items in between — and saved at revision 1788543026 with 850. This sitting did editliterature:bibliography, and 20 other pages; every change is listed under Audit 2026-09-04, second sitting below.
What the audits found, in one table
Every figure below is derived and defended further down; this is here so that someone checking one number does not have to read the history first.
| Question | Population | Answer |
|---|---|---|
| Did the broken USENIX parser corrupt the author cache? | 90 cached USENIX lists | yes, 18 of 90 carried an affiliation as an author |
| Did it drop a real author from the cache? | same 90 | no, 0 |
Did any of that reach literature:bibliography? | 85 cached-and-cited entries (pass 1) | no, 0 |
| Is any live USENIX entry's author list wrong in any way? | all 159 live USENIX Security entries, names and order, against their own landing pages (pass 3) | no, 0 |
| Is any PoPETs DOI on the wrong prefix, or dead? | 84 PoPETs entries with a DOI; the prefix rule covers 83 | 0 wrong prefix, 0 dead |
| Is the same paper in the bibliography twice? | all 837 entries (first sitting) | yes, 5 pairs — latent, no page cited both keys of a pair |
| … and after the second sitting? | all 850 entries on the saved page | 0 pairs, both scans; 23 markers on 13 pages repointed first, then the 5 entries deleted |
Did the umlaut slip (bottger/boettger) recur for any other author? | 855 entries, first-author surname folded so Böttger = Boettger = Bottger, same year | no — 58 candidates printed, all read, none a duplicate |
| Are the checks above capable of failing? | 13 mutations | all 13 change the reported counts |
Audit 2026-09-04: USENIX author lists
Why the audit was run
scripts/fetch_authors.py fills in author lists for PETS and USENIX, whose
records in data/corpus2/.meta carry no authors, because they were scraped
from venue listing pages rather than from OpenAlex. Re-derived against the
current 16,864-record index on 2026-09-04, not carried over from the old
one:
| Venue | Index records | No authors | No DOI |
|---|---|---|---|
| USENIX | 3,012 | 3,012 (100%) | 3,012 (100%) |
| PETS | 1,253 | 1,253 (100%) | 140 (11.2%) |
| every other venue | 12,599 | 1 | 1,618 |
(The per-venue record counts in that table are already published on
corpus and agree with these exactly — CCS 3,381, IEEE S&P 1,837,
IMC 862, NDSS 1,577, PETS 1,253, USENIX 3,012, WWW 4,942. What is new here is
the author and DOI breakdown, which is the reason this audit had to happen at
all.) So the author gap is PETS and USENIX and essentially nothing else — 4,265 of
16,864 records, plus a single stray NDSS record
(NDSS/2018/veil-private-browsing-semantics-without-browser-side-assistance,
authors: null) — while
the DOI gap is narrower than the author gap for PETS: 1,113 of its 1,253 records
do carry one. For USENIX it parsed the prose blob a presentation page prints under the
title, which has the shape
A, Affil1; B, C, and D, Affil2; E, Affil3
Three separate defects have now been found in that parser. The first two were fixed on 2026-08-13 and are what this drain item was raised to clean up after; the third was found by this audit and had survived that fix.
| # | Defect | Effect | Found |
|---|---|---|---|
| 1 | The whole string was split on commas before being split on ; | The first author of each group was glued onto the previous group's affiliation (“University of Maryland; Richard Roberts”) and then dropped by the affiliation filter | 2026-08-13 |
| 2 | A leading “and ” was never stripped, and a \s+and\s+ split cannot see an “and” at position 0 | The last author of every multi-author group was dropped | 2026-08-13 |
| 3 | An affiliation that itself contains a comma splits into an affiliation-shaped half and a name-shaped half | “University of California, Berkeley” loses “University of California” to the filter and keeps “Berkeley” as an author | 2026-09-04, this audit |
Defect 3 is not fixable by any rule over the prose that does not encode a list of institutions, which is why the fix below stops parsing the prose.
Where the corrected lists come from
Every USENIX page publishes the author list unambiguously somewhere other than the prose. The audit reads them in this order and requires the first two to agree:
| Order | Source | Shape | Pages in this population |
|---|---|---|---|
| 1 | citation_author meta tags | one tag per author, emitted next to citation_author_institution, so USENIX has already separated names from affiliations | 155 of 162 |
| 2 | USENIX's own published BibTeX block | author = {A and B and C} | 155 of 162 |
| 3 | italic affiliation spans | <i> on the 2011 technical-sessions pages, <em> on 2025+ pages USENIX has not given Scholar metadata | 7 of 162 |
Source 3 is the weakest of the three, so the entries resting on it are named
rather than counted. All seven, from scripts/usenix_source_agreement.py:
| Year | Citekey | Slug |
|---|---|---|
| 2010 | aggarwal2010_analysis | an-analysis-of-private-browsing-modes-in-modern-browsers |
| 2010 | bandhakavi2010_vex | vex-vetting-browser-extensions-for-security-vulnerabilities |
| 2010 | djeric2010_securing | securing-script-based-extensibility-in-web-browsers |
| 2011 | kanich2011_show | show-me-the-money-characterizing-spam-advertised-revenue |
| 2011 | mulliner2011_death | sms-of-death-from-analyzing-to-attacking-mobile-phones-on-a-large-scale |
| 2011 | stringhini2011_botmagnifier | botmagnifier-locating-spambots-on-the-internet |
| 2025 | [1Tang, Brian; Bui, Duc; Shin, Kang G. (2025): "Navigating Cookie Consent Violations Across the Globe", in: Proceedings of the USENIX Security Symposium. (Link)] | navigating-cookie-consent-violations-across-the-globe |
Six are legacy session pages, from 2010 and 2011, not 2011 alone as an
earlier draft said. The seventh is different in kind and is the only claim on
this page that is a negative about a live third-party page that can change
under it.
usenixsecurity25/presentation/tang was re-fetched on 2026-09-04 and still
carries zero citation_author tags, zero BibTeX block, and the single line
<p>Brian Tang, Duc Bui, and Kang G. Shin, <em>University of Michigan</em></p>.
Unlike everything else on this page that is a negative about a mutable
external page, so it is dated rather than stated flat. USENIX's structure is
otherwise stable at least one cycle forward: a usenixsecurity26 presentation
page checked the same day carries both citation_author tags and a BibTeX
block, and none of the four 2024–2026 pages checked carries a citation_doi.
That forward check is one page (usenixsecurity26/presentation/hu-zhenkai,
7 citation_author tags and a BibTeX block), so it is evidence the structure
has not been dropped, not evidence that it will not be.
Sources 1 and 2 are present together or not at all: no page in this population carries one without the other. The 162 pages are the 90 cached author lists plus the 72 extra fetched for pass 3 below.
Sources 1 and 2 disagreed on a long list of papers at first, and every one of
those disagreements was a bug in the reading, not in the data. The script that
produced that first list no longer exists, so the figure is not stated from
memory: scripts/usenix_source_agreement.py re-derives the comparison from the
same cached HTML under each of the three parser states this audit went through.
| Parser state | Disagree, of the 87 cached pages carrying both | Disagree, of all 155 pages carrying both |
|---|---|---|
| A — non-greedy brace match, no diacritic fold | 11 | 17 |
| B — brace-balanced, no diacritic fold | 2 | 4 |
| C — brace-balanced and diacritic-folded (what ships) | 0 | 0 |
Two reading bugs and one genuine source difference sit behind that table:
- A → B. A non-greedy brace match ends at the first inner brace, so
Adam Doup{\'e}came back asAdam Doup{\'e. Brace-counting fixes it. - B → C. For
iot-market-dynamicsUSENIX's meta saysCarlos H. Gañánand USENIX's own BibTeX saysCarlos H. Ganan. That one is a genuine difference between USENIX's two sources; source 1 is preferred for exactly that reason, and the diacritic fold is what stops it being reported as a defect. - The third, found in review on 2026-09-04. USENIX writes Benoît as
Beno{\^\i}t, using LaTeX's dotless\i. Nothing in the decoder handled\i, so no accent pattern matched and the final brace strip producedBeno^it. It survived every other check because it is oncohn2020_delf, which is not one of the 90 cached lists — only the all-162 comparison could see it.usenix_author_audit.py –selftestnow carries a nine-casede_latextest including that exact string.
So: three decoder bugs, and four pages where USENIX's own BibTeX drops a
diacritic its meta keeps. Calling the second kind “agreement” is a choice —
the fold makes Gañán and Ganan compare equal — and it is the right one
here, because the two sources are naming the same person and source 1 is
preferred anyway. Under that rule USENIX's two structured sources agree on all
155 pages that carry both. Under a byte-exact rule, four would not.
USENIX's own metadata is not infallible
Three papers are overridden against a source outside usenix.org. Each override
is a literal list in ADJUDICATED in the script, so it is visible rather than
buried in a heuristic, and emptying that table makes the run fail (mutation
A6).
| Paper | What USENIX's page says | What was published | Settled by |
|---|---|---|---|
| [2McCoy, Damon; Pitsillidis, Andreas; Jordan, Grant; Weaver, Nicholas; Kreibich, Christian; Krebs, Brian; Voelker, Geoffrey M.; Savage, Stefan; Levchenko, Kirill (2012): "PharmaLeaks: Understanding the Business of Online Pharmaceutical Affiliate Programs", in: Proceedings of the USENIX Security Symposium. (Link)] | meta and BibTeX say Jordan Grant and Geoffrey Voelker | Grant Jordan, Geoffrey M. Voelker | The paper's own title block in data/fulltext/2012/USENIX/pharmaleaks-…/paper.cols.txt and the DBLP record, which agree with each other |
| [3Oest, Adam; Safaei, Yeganeh; Zhang, Penghui; Wardman, Brad; Tyers, Kevin; Shoshitaishvili, Yan; Doupé, Adam; Ahn, Gail-Joon (2020): "PhishTime: Continuous Longitudinal Measurement of the Effectiveness of Anti-phishing Blacklists", in: Proceedings of the USENIX Security Symposium. (Link)] | meta and BibTeX both stop at 7 authors | 8 authors, with Gail-Joon Ahn | The prose blob on the same page, and the paper PDF. DBLP repeats the omission — see below |
| [4Hsieh, Jhih-Yi; Raghunathan, Aditi; Shah, Nihar B. (2025): "Vulnerability of Text-Matching in ML/AI Conference Reviewer Assignments to Collusions", in: Proceedings of the USENIX Security Symposium. (Link)] | meta and the paper's title block both print the nickname, Jhih-Yi (Janet) Hsieh | Jhih-Yi Hsieh | DBLP's normalised form. A judgement call, not a correction — see below |
DBLP is the adjudicating source in two of the three rows. Its search API
(dblp.org/search/publ/api?q=…&format=json) answered correctly on
2026-09-04, but DBLP's own FAQ describes that API as “still in flux …
work-in-progress”, so a later re-audit should re-verify the endpoint rather than
assume it.1)
The PhishTime case is the one that matters for method, and its second half
matters more than its first. Had the audit trusted the structured metadata
alone, it would have deleted a real author while “correcting” the entry —
which is why every paper is additionally cross-checked against the prose blob.
But the natural fallback for a disagreement is DBLP, and DBLP is wrong here
too: record conf/uss/OestSZWTSD20, fetched 2026-09-04, lists seven authors
and stops at Adam Doupé, exactly as USENIX's metadata does. Three independent
machine-readable sources agree on a list the paper itself contradicts:
$ head -5 data/fulltext/2020/USENIX/phishtime-.../paper.cols.txt PhishTime: Continuous Longitudinal Measurement of the Effectiveness of Anti-phishing Blacklists Adam Oest, Yeganeh Safaei, and Penghui Zhang, Arizona State University; Brad Wardman and Kevin Tyers, PayPal; Yan Shoshitaishvili and Adam Doupé, Arizona State University; Gail-Joon Ahn, Arizona State University, Samsung Research
The published entry follows the paper. An earlier draft of this page claimed DBLP corroborated the correction. It does not, and a reviewer caught that on 2026-09-04.
What the audit found
Population: the 90 USENIX keys in out/authors.json as of 2026-09-04.
Not 5,859 papers, not 837 bibliography entries — 90 cached author lists.
| Result | Papers |
|---|---|
| USENIX keys audited | 90 |
| author list already correct | 72 |
| author list corrected | 18 (20.0%) |
| of the corrected, had a non-author in the list | 18 |
| of the corrected, were missing a real author | 0 |
| overridden against the paper or DBLP | 3 |
| unresolved, needing a human | 0 |
Defects 1 and 2 left no trace in out/authors.json. Not one of the 90
cached lists was missing an author. The drain item's premise — that entries
written before 2026-08-13 may be short an author — did not hold for anything
still in the cache. What 18 of the 90 did carry was defect 3: an affiliation
fragment sitting in the list as if it were a person. Berkeley, Santa
Barbara, San Diego, PayPal, Trustwave, AppCensus, ICSI,
UC Davis, UCL, UC3M, Georgia Tech, DeustoTech, Eurecom,
BIT, BUPT, USTC, Engineering, Behavioural Insights Team.
[5Kapravelos, Alexandros; Grier, Chris; Chachra, Neha; Kruegel, Christopher; Vigna, Giovanni; Paxson, Vern (2014): "Hulk: Eliciting Malicious Behavior in Browser Extensions", in: Proceedings of the USENIX Security Symposium. (Link)] was the worst: 11 names cached for a 6-author
paper, alternating author, campus, author, campus.
| Paper | Cached | Real | Fragments that were sitting in the author list |
|---|---|---|---|
| [5Kapravelos, Alexandros; Grier, Chris; Chachra, Neha; Kruegel, Christopher; Vigna, Giovanni; Paxson, Vern (2014): "Hulk: Eliciting Malicious Behavior in Browser Extensions", in: Proceedings of the USENIX Security Symposium. (Link)] | 11 | 6 | Santa Barbara, Berkeley, San Diego, Santa Barbara, Berkeley |
| [6Lyons, Allan; Gamba, Julien; Shawaga, Austin; Reardon, Joel; Tapiador, Juan; Egelman, Serge; Vallina-Rodriguez, Narseo (2023): "Log: It's Big, It's Heavy, It's Filled with Personal Data! Measuring the Logging of Sensitive Information in the Android Ecosystem", in: Proceedings of the USENIX Security Symposium. (Link)] | 13 | 7 | AppCensus ×3, Universidad Carlos III de Madrid, ICSI, UC Berkeley |
| [7Vekaria, Yash; Canino, Aurelio Loris; Levitsky, Jonathan; Ciechonski, Alex; Callejo, Patricia; Mandalari, Anna Maria; Shafiq, Zubair (2025): "Big Help or Big Brother? Auditing Tracking, Profiling, and Personalization in Generative AI Assistants", in: Proceedings of the USENIX Security Symposium. (Link)] | 14 | 7 | UC Davis ×3, UNIRC, UCL ×2, UC3M |
| [8Bielova, Nataliia; Litvine, Laura; Nguyen, Anysia; Chammat, Mariam; Toubiana, Vincent; Hary, Estelle (2024): "The Effect of Design Patterns on (Present and Future) Cookie Consent Decisions", in: Proceedings of the USENIX Security Symposium. (Link)] | 8 | 6 | Behavioural Insights Team, Interministerial Directorate for Public Transformation |
| [9Carlini, Nicholas; Felt, Adrienne Porter; Wagner, David (2012): "An Evaluation of the Google Chrome Extension Security Architecture", in: Proceedings of the USENIX Security Symposium. (Link)] | 4 | 3 | Berkeley |
| [10Trickel, Erik; Starov, Oleksii; Kapravelos, Alexandros; Nikiforakis, Nick; Doupé, Adam (2019): "Everyone is Different: Client-side Diversification for Defending Against Extension Fingerprinting", in: Proceedings of the USENIX Security Symposium. (Link)] | 5 | 5 | none — this one was already right |
The last row is there because it is the common case: 72 of the 90 needed no change at all. The defect fires only when an affiliation contains a comma and its tail happens to look like a surname.
What reached literature:bibliography: nothing
This is the part worth stating carefully, because it is a negative result and a negative result from a check that has not been shown able to fail is worth nothing.
| Pass | What it can see | Population | Defects found |
|---|---|---|---|
| 1 | the author field of every live entry that title-matches a key in out/authors.json | 85 of the 90 keys (5 are cached but never cited) | 0 |
| 2 | an affiliation-shaped name in any live USENIX Security entry, cached or not | 159 entries | 0 |
| 3 | the full author list — names and order — of every live USENIX Security entry against its own landing page | 159 of 159 (157 distinct papers) | 0 |
Pass 3 checks order because an earlier version of it did not. Its comment
claimed it reported “separately on order”; the code compared only Python sets,
and a reviewer demonstrated the gap on 2026-09-04 by swapping the first and last
author of acharya2024_imitation — a pass-3-only entry, so passes 1 and 2 are
blind to it by construction — and getting a clean bill from all three passes. A
first-authorship swap is a worse citation defect than a missing middle author,
and it was the one defect class nothing on this page could see. It is now
mutation B5, and it is the only mutation that PASS3order catches alone.
Pass 1 first reported 3 “wrong” entries. All three were the encoding
difference between out/authors.json (literal UTF-8) and the bibliography
(Doup{\'e}), not an author defect: 43 of the 837 live entries write
accents as a LaTeX accent command and 66 as a literal non-ASCII letter, both are
valid BibTeX, and both were confirmed to render as Doupé against pages that
actually cite an entry using each form — security:phishing for the braced
Doup{\'e} and privacy:browser_extensions for the unbraced Doup\'e.
A first draft named programming:crawler_detection instead, which does render
Doupé but from zhang2021_crawlphish and zhang2022_spartacus, whose
author fields carry a literal é — so it verified nothing. Caught in review.
The comparison now decodes LaTeX accents before diffing, and the house style was
left alone: rewriting 43 author fields on the page every other page depends on,
for a cosmetic gain, is a bad trade.
So the broken parser contaminated the cache and never the page. The
entries were evidently cleaned by hand between bibgen.mjs and the save —
kapravelos2014_hulk is live with exactly its 6 real authors. Pass 3 extends
the guarantee past the 85: all 159 live USENIX Security entries match the author
list their own landing page publishes.
The 159 entries resolve to only 157 distinct papers, and the two-entry gap
is two of the duplicate pairs found below — bouhoula2024automated /
bouhoula2024_automated and lerner2016internet / lerner2016_internet
are both USENIX Security. The three populations reconcile exactly: 157 papers
behind live entries, plus the 5 cached keys nothing cites, is the 162 landing
pages on disk.
Pass 3 is the one to re-run after any bibliography edit. It does not depend on
out/authors.json at all, so it covers entries that were typed by hand — and
those are the ones no cache-based check can see.
Proof that the checks can fail
Thirteen mutations — seven against the audit script, six against the
bibliography — each breaking one thing the scripts rely on. (A2 is the
exception and breaks two, for the reason given below.) A mutation that left the
reported counts unchanged would be a rule asserting nothing. Real output of
scripts/usenix_author_audit_mutations.py:
=== scripts/usenix_author_audit.py — diffed against out/usenix_audit/authors.json.before mutation rc unchanged corrected unresolved (baseline, unmutated) 0 72 18 0 A1 citation_author parser truncated to 1 author 1 3 0 87 A2 prose cross-check disabled *and* adjudication emptied 0 70 20 0 A3 BibTeX brace balancing broken 1 3 0 87 A4 legacy block no longer matched by title 1 69 18 3 A5 diacritic folding removed 1 70 18 2 A6 adjudication table emptied 1 70 18 2 A7 institution list ignored (prose residue no longer filtered) 1 64 10 16 === scripts/usenix_bib_diff.py — mutated BIBLIOGRAPHIES mutation PASS1 PASS2 PASS3set PASS3order (baseline, unmutated) 0 0 0 0 B1 author deleted from a cached entry 1 0 1 0 B2 affiliation added to a cached entry 1 1 1 0 B3 affiliation added to a NON-cached entry 0 1 1 0 B4 given/family names swapped 1 0 1 0 B5 author ORDER swapped, NON-cached entry 0 0 0 1 B6 author DELETED from a NON-cached entry 0 0 1 0 mutations that changed nothing (a rule asserting nothing): 0
B6 is the one that justifies pass 3 existing at all: an author deleted from
an entry out/authors.json never touched is invisible to pass 1 (not cached)
and to pass 2 (nothing affiliation-shaped was added), and only the landing-page
comparison sees it. B5 is the order swap, and only the ordered comparison
sees that.
A2 is the one to read closely. On its own, disabling the prose cross-check
changes nothing, because every paper it fires on is already in
ADJUDICATED and that branch runs first — today the cross-check is a tripwire
for the next refresh, not a live assertion. Mutating both together shows what it
is worth: without it, [2McCoy, Damon; Pitsillidis, Andreas; Jordan, Grant; Weaver, Nicholas; Kreibich, Christian; Krebs, Brian; Voelker, Geoffrey M.; Savage, Stefan; Levchenko, Kirill (2012): "PharmaLeaks: Understanding the Business of Online Pharmaceutical Affiliate Programs", in: Proceedings of the USENIX Security Symposium. (Link)] silently regresses to the
wrong Jordan Grant (corrected goes 18 → 20) instead of stopping the run.
Reporting A2 as a pass on its own would have been the exact
“passing check that asserts nothing” this table exists to rule out.
Folding and residue
Two normalisations, both with their residue accounted for.
- Name comparison folds Unicode diacritics away (NFKD, combining marks dropped), lower-cases, and strips punctuation — but not word count, so
Geoffrey VoelkerandGeoffrey M. Voelkercompare as different and were adjudicated rather than silently merged. Removing the diacritic fold makes 2 papers unresolved (mutationA5), so the fold is doing work. - The prose cross-check needs to tell a person's name from an affiliation tail in the ambiguous blob. The first version used a hand-written stop-list of institution tail words, which is the kind of list that needs a new entry per venue-year and fails silently when it misses one. It was replaced by a rule that needs no maintenance: drop any candidate whose folded form appears in one of the affiliations the page itself declares — its
citation_author_institutionmeta tags, or its <i>/<em> spans.
Residue of that fold: zero, in the direction that matters — everything the
rule drops is an affiliation tail, verified one by one. It says nothing about
the other direction: a real author whose folded name is contained in a declared
affiliation (someone called Max Planck at a Max Planck Institute) would be
dropped from the cross-check silently. The risk is small because
prose_parse feeds only the tripwire and never the published list, but it is
a direction this fold does not cover. Turning the rule off (mutation A7) leaves 16 papers unresolved on 15 distinct affiliation fragments, and every one of them is contained in a declared affiliation on its own page:
barbara berkeley canada d davis diego electronics icsi leuven madrid team tech transformation uk v
d is the tail of NCS Cyber Special Ops-R&D and v the tail of
Datenanfragen.de e.V. — both are what is left of an institution after the
comma split, which is the same defect the whole audit is about, seen from the
other side.
Quotes and primary sources checked by hand
There are no evidence.quote values behind this page — it publishes no
extraction figure. What was checked against a primary source:
The three adjudications above are load-bearing and their sources are in that table; they are not repeated here. What remains:
| Claim | Primary source | Result |
|---|---|---|
Doup{\'e} (braced) renders through bibtex4dw | rendered DOM of security:phishing, which cites [3Oest, Adam; Safaei, Yeganeh; Zhang, Penghui; Wardman, Brad; Tyers, Kevin; Shoshitaishvili, Yan; Doupé, Adam; Ahn, Gail-Joon (2020): "PhishTime: Continuous Longitudinal Measurement of the Effectiveness of Anti-phishing Blacklists", in: Proceedings of the USENIX Security Symposium. (Link)] and oest2020_sunrise — the two entries using the braced form | Renders as Doupé. Not a defect |
Doup\'e (unbraced) renders through bibtex4dw | rendered DOM of privacy:browser_extensions, which cites [10Trickel, Erik; Starov, Oleksii; Kapravelos, Alexandros; Nikiforakis, Nick; Doupé, Adam (2019): "Everyone is Different: Client-side Diversification for Defending Against Extension Fingerprinting", in: Proceedings of the USENIX Security Symposium. (Link)], the one entry using the unbraced form | Renders as Doupé. Not a defect |
| the rewritten fetcher works end to end | live fetch of USENIX/2024/a-formal-analysis-of-sctp-attack-synthesis-and-patch-verification, USENIX/2021/dynamic-proofs-of-retrievability-with-low-server-storage and USENIX/2024/deeptype-refining-indirect-call-targets-with-strong-multi-layer-type-analysis — three keys not in the cache, so all three exercise the whole fetch-and-parse path | Correct lists including Michael Tüxen, Mélanie de Jonghe, Clément Pernet. Deliberately not persisted, so out/authors.json still holds 132 entries; re-run those three keys to reproduce |
The fix to scripts/fetch_authors.py
parse_usenix no longer parses the prose at all. It returns
meta_authors(page) or bibtex_authors(page) or italic_authors(page, title)
from scripts/usenix_author_audit.py, and applies ADJUDICATED. Verified
live on three USENIX keys outside the cache (row 5 above). Note the division of
labour: fetch_authors.py skips anything already cached, so it can only ever
fill gaps — re-deriving a cached entry is what usenix_author_audit.py is
for, and that script cross-checks and diffs before it writes.
- fetch_authors.py
#!/usr/bin/env python3 """Fetch author lists for PETS and USENIX papers, which the corpus index lacks. Every PETS and USENIX record in data/corpus2/.meta was scraped from a venue listing page and carries no authors: 4,265 of the 16,864 records, re-counted 2026-09-04 (USENIX 3,012, PETS 1,253). USENIX additionally has no DOI on any of its 3,012; PETS has one on 1,113 of its 1,253. Their landing pages do carry the authors. WebFetch gets 403 from usenix.org, so this uses curl with a browser User-Agent. python3 scripts/fetch_authors.py PETS/2016/near-optimal-... [...] Writes/updates out/authors.json keyed on "<venue>/<year>/<slug>". Fails loudly per paper: a slug whose authors cannot be parsed is reported, not silently skipped. To RE-derive an entry that is already cached, this script is the wrong tool — it skips anything present in out/authors.json. Use scripts/usenix_author_audit.py, which re-parses from cached HTML, cross-checks against a second source and diffs before writing. """ import glob import html import json import os import re import subprocess import sys sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) from usenix_author_audit import (ADJUDICATED, bibtex_authors, italic_authors, meta_authors) ROOT = "/workspace/publications_dataset/data" CACHE = "out/authors.json" UA = ("Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) " "Chrome/126.0.0.0 Safari/537.36") def load_index(): idx = {} for f in glob.glob(os.path.join(ROOT, "corpus2/.meta/*.json")): for p in json.load(open(f))["papers"]: idx[f"{p['venue']}/{p['year']}/{p['slug']}"] = p return idx def fetch(url): r = subprocess.run(["curl", "-sL", "-A", UA, "--max-time", "40", url], capture_output=True, text=True) if r.returncode != 0: raise RuntimeError(f"curl failed for {url}: {r.returncode}") return r.stdout def strip(s): s = re.sub(r"<[^>]+>", " ", s) return re.sub(r"\s+", " ", html.unescape(s)).strip() def parse_popets(page): # PoPETs landing pages carry "Authors: A (Affil), B (Affil)" in a paragraph. m = re.search(r"Authors?:\s*(.*?)</", page, re.S) if not m: return None raw = strip(m.group(1)) names = [] for part in re.split(r",(?![^(]*\))", raw): n = re.sub(r"\([^)]*\)", "", part).strip().rstrip(",") if n and not n.lower().startswith("and "): names.append(n) elif n.lower().startswith("and "): names.append(n[4:].strip()) return [n for n in names if n] def parse_usenix(page, title=""): """Authors from a USENIX presentation page, from AUTHORITATIVE markup. This used to parse the prose blob A, Affil1; B, C, and D, Affil2; E, Affil3 and that was wrong three separate ways. Two were fixed on 2026-08-13: a comma split applied before the ";" split, which glued the first author of each group onto the previous affiliation and then dropped it; and an unstripped leading "and ", which dropped the LAST author of every multi-author group. The third survived that fix and was found by the audit in scripts/usenix_author_audit.py on 2026-09-04: "University of California, Berkeley" splits into an affiliation-shaped half that gets filtered out and a NAME-shaped half, "Berkeley", that gets kept as an author. 18 of the 90 USENIX entries in out/authors.json carried at least one such fragment ("PayPal", "UC Davis", "Trustwave", "Georgia Tech", "AppCensus"). No rule over the prose can fix that without encoding a list of institutions. So do not parse the prose. Every USENIX page publishes the author list unambiguously somewhere else, in this order of preference: 1. <meta name="citation_author"> — one tag per author, emitted alongside citation_author_institution, so USENIX has already separated them. 2. USENIX's own BibTeX block, author = {A and B and C}. 3. Affiliations wrapped in <i>/<em>; removing those spans leaves the names. Used by the 2011 session pages and by 2025+ pages USENIX has not yet given Scholar metadata. `title` is only needed for (3) on a legacy session page, whose URL addresses a whole session rather than one paper. Neither (1) nor (2) is infallible — see ADJUDICATED in scripts/usenix_author_audit.py for the two papers where USENIX's own metadata disagrees with the published paper — so a full re-derivation should go through that script, which also cross-checks against the prose. """ return meta_authors(page) or bibtex_authors(page) \ or italic_authors(page, title) or None def main(): idx = load_index() cache = json.load(open(CACHE)) if os.path.exists(CACHE) else {} failed = [] for k in sys.argv[1:]: if k in cache: print(f"cached {k}: {'; '.join(cache[k])}") continue p = idx[k] # KeyError here means a bad slug: correct. url = p["landingUrl"] page = fetch(url) names = (parse_popets(page) if "petsymposium" in url else parse_usenix(page, p["title"])) if k in ADJUDICATED: names = ADJUDICATED[k][0] if not names: failed.append((k, url)) print(f"FAILED {k} {url}") continue cache[k] = names print(f"ok {k}: {'; '.join(names)}") json.dump(cache, open(CACHE, "w"), indent=1, sort_keys=True) if failed: print(f"\n{len(failed)} unparsed; fill by hand in {CACHE}:") for k, u in failed: print(f" {k} {u}") return 1 return 0 if __name__ == "__main__": sys.exit(main())
Audit 2026-09-04: PoPETs DOI prefixes
scripts/bibgen.mjs used to hardcode the DOI prefix 10.56553 for all
years, which produces a dead DOI for every PoPETs paper before 2022-0059.
Fixed 2026-08-13. PoPETs has changed prefix twice:
| Range | Prefix | Registrant |
|---|---|---|
| 2015 – 2018 | 10.1515 | De Gruyter Open |
| 2019 – 2022-0058 | 10.2478 | De Gruyter / Sciendo |
| 2022-0059 onward | 10.56553 | PoPETs' own prefix |
The boundaries were tested from both sides on 2026-09-04, not assumed:
10.2478/popets-2022-0058 resolves 200 and 10.56553/popets-2022-0058
404s; 10.1515/popets-2018-0001 and 10.2478/popets-2019-0001 resolve
and 10.1515/popets-2019-0001 404s. Prefix ownership was confirmed
against api.crossref.org/prefixes/: 10.1515 and 10.2478 are both
registered to Walter de Gruyter GmbH (member 374) and 10.56553 to the
Privacy Enhancing Technologies Symposium Advisory Board (member 35752). No
fourth range has opened for 2026 — the five 2026 entries all resolve under
10.56553.
Every PoPETs DOI on the live page was re-checked on both axes independently — a prefix can disagree with the boundary and still resolve, and a correct prefix can still be a dead DOI.
| Result | Entries |
|---|---|
| PoPETs entries with a DOI | 84 |
| prefix disagrees with the boundary rule | 0 |
does not resolve (HTTP through doi.org, redirects followed) | 0 |
| PoPETs entries with no DOI | 2 |
DOI not in popets-YYYY-NNNN form, so not checked against the prefix rule | 1 |
Read the last row carefully: the boundary rule covers 83 of the 84, not all
84. The one it cannot cover is eckersley2010_unique, whose DOI is
10.1007/978-3-642-14527-8_1 — a Springer LNCS chapter from PETS 2010, before
PoPETs existed as a journal, so there is no popets-YYYY-NNNN number for the
rule to read. It resolves 200. The script has always printed that row; an
earlier draft of this page omitted it, which made 84 read as complete coverage.
So the hardcoded prefix never reached the wiki; it was caught by hand each time,
as the drain item suspected. Two entries have no DOI. lukic2026_mv3 was fixed in this run: its url is
a direct PDF for popets-2026-0027, so the DOI is 10.56553/popets-2026-0027,
which was resolved (HTTP 200, petsymposium.org/popets/2026/popets-2026-0027.php)
before being added. calzavara2026_syntactic was not: its recorded url points
at a paper list page rather than the paper, so there is no article number to
derive from and no DOI was found. It is noted under what could not be
established.
Found at 837 entries: five papers were in the bibliography twice
A citekey-collision check passes while the same paper sits in the file under two different keys, so the DOI scan below was run over all 837 entries.
| Duplicate | Keys | Matched on |
|---|---|---|
| PoPETs 2022-0063 | fouad2022my, fouad2022_cookie | same DOI 10.56553/popets-2022-0063 |
| PoPETs 2025-0063 | bottger2025_regional, boettger2025_regional | same DOI 10.56553/popets-2025-0063 |
| PoPETs 2026-0109 | ahmad2026_ipfp, ahmad2026_more | same DOI 10.56553/popets-2026-0109 |
| Bouhoula 2024 | bouhoula2024automated, bouhoula2024_automated | same title, neither has a DOI |
| Lerner 2016 | lerner2016internet, lerner2016_internet | same title, neither has a DOI |
Colliding citekeys: 0. Distinct citekeys: 837 of 837 entries.
No page currently cites both keys of a pair, checked by
scripts/bib_dupe_cocitation.py over the 160 other pages on the wiki (161 in
dw.mjs pages, less literature:bibliography itself). 26 pages cite at
least one key of some pair; bouhoula2024_automated alone is on 11. so no reference list renders the paper twice today. This is latent, not
visible. Consolidating means choosing one key per pair and rewriting {[key]}
markers across the 26 pages that cite one, which is a different piece of work
with its own breakage risk — filed as the drain item
dedupe-bibliography-entries rather than half-done here.
Closed later the same day by the item dedup-regional-filter-lists-bibkey;
the consolidation and its invariants are the section Audit 2026-09-04, second
sitting below. The table and counts in this section describe the 837-entry
file and are left as they were.
Audit 2026-09-04, second sitting: the five duplicates consolidated
Drain item dedup-regional-filter-lists-bibkey, raised on 2026-08-14 while
openwpm was being written and its author noticed
bottger2025_regional next to boettger2025_regional. The first sitting
above found all five pairs and deferred them; this sitting closed them. Every
figure here is against a fresh ?do=export_raw of the bibliography at
revision 1788526615 (855 entries) and of all 161 other pages, taken at 17:21
UTC and re-checked against dw.mjs pages immediately before saving: no
revision had moved.
Which key was kept, and why
Neither key of any pair was minted by bibgen.mjs — PETS and USENIX index
records carry no authors, so all ten were typed by hand. The tie-break is the
file's own majority convention, surname + year + _ + first title word,
diacritics dropped, which is also what bibgen.mjs mints when it has an
author list. scripts/bib_key_convention.py counts how the 25 entries whose
first author's surname carries a diacritic spell it in the key:
| Key spelling of the diacritic | Entries (of 25) | Examples |
|---|---|---|
stripped — Böttger → bottger | 19 | sjosten2020_filter, bosch2016_tales, tornberg2024_bestpractices, gross2021_iuipc |
German digraph — Böttger → boettger | 4 | boettger2025_regional, stoever2023_owners, rueth2018_digging, schoeni2024_cookieblock |
letter dropped — Kührer → khrer | 2 | khrer2015_going, som2017_content: the [^a-z] filter in bibgen.mjs autoKey eating a non-ASCII letter — a bug, not a convention, and not touched here |
| Paper | Kept | Deleted | Content pages citing kept / deleted, before | What the deleted entry had that the kept one lacked |
|---|---|---|---|---|
| Fouad et al., PoPETs 2022 | fouad2022_cookie | fouad2022my | 2 / 4 | a url that repeats the DOI — dropped |
| Böttger et al., PoPETs 2025 | bottger2025_regional | boettger2025_regional | 2 / 1 (+1 provenance page) | nothing; the kept entry additionally carries volume, number and pages |
| Ahmad et al., PoPETs 2026 | ahmad2026_more | ahmad2026_ipfp | 1 / 1 | nothing; the kept title braces {IPv6} |
| Bouhoula et al., USENIX Sec 2024 | bouhoula2024_automated | bouhoula2024automated | 6 (+5 provenance) / 3 (+1 provenance) | pages = {1723–1739} — carried over, after reading USENIX's own BibTeX block and the citation_firstpage/citation_lastpage meta tags on usenixsecurity24/presentation/bouhoula on 2026-09-04, both of which say 1723–1739. isbn, address, publisher, month and an author-homepage url — dropped; no other entry in the file carries any of them |
| Lerner et al., USENIX Sec 2016 | lerner2016_internet | lerner2016internet | 2 / 2 (+1 provenance) | nothing |
The per-page lists behind the fourth column are in the apply output below.
Three of the five deleted keys were the Google-Scholar style (no underscore),
and one of those, fouad2022my, was on more content pages than its
replacement — four against two. Majority-of-pages would have kept it. Convention
won, because the convention is what the next bibgen.mjs run will mint, and a
file with two live key styles is how these five pairs arose.
What was done, in order
- Export every page and the bibliography fresh: 161 + 1 files.
dw.mjs searchwas not used; its index is stale (first sitting). - Run
scripts/bib_dedup_scan.py— the wider scan this item asked for — on the export: 5 definite pairs, 58 candidates. - Run
scripts/bib_dedup_apply.pyoffline. It rewrites the markers and the bibliography intoout/dedup_apply/and asserts the invariants in its docstring. It saves nothing. - Save the 20 pages first, each with
–if-revagainst the revision read at export, then the bibliography. In that order nothing ever renders unresolved: the kept keys existed while the deleted ones were still cited, and the deleted ones were gone only once no page cited them. - Purge the bibtex4dw cache on the bibliography and every repointed page (
?purge=true; see the first sitting for why a rendered check passes on a stale cache without this). - Re-fetch the 13 repointed pages and compare rendered reference-list length and marker count against copies fetched before the edit (
scripts/bib_dedup_render_check.py). - Re-export the bibliography and all 21 saved pages and confirm byte identity with the saved files; re-run both duplicate scans on the live bibliography — 0 pairs each.
The invariants, and the real output
Each figure on this page about the consolidation is a line of this output.
bibliography : out/live_bibliography_20260904_1721.txt
entries : 855 -> 850 (removed 5: fouad2022my, boettger2025_regional, ahmad2026_ipfp, bouhoula2024automated, lerner2016internet)
field added : bouhoula2024_automated [('pages', '1723--1739')]
pages read : 161 (bibliography excluded)
loser-key marker occurrences before, by key:
fouad2022my 7
boettger2025_regional 3
ahmad2026_ipfp 1
bouhoula2024automated 7
lerner2016internet 5
pages citing each key BEFORE (content pages; provenance pages in brackets):
kept fouad2022_cookie 2 [0] practices:legal_enforcement, statistics:hypothesis_testing
deleted fouad2022my 4 [0] privacy:browser_storage, privacy:fingerprinting, programming:crawler:openwpm, programming:stateful_stateless
kept bottger2025_regional 2 [0] programming:crawler:openwpm, programming:filter_lists
deleted boettger2025_regional 1 [1] statistics:pvalue_corrections [provenance:statistics:pvalue_corrections]
kept ahmad2026_more 1 [0] programming:cloudflare_radar
deleted ahmad2026_ipfp 1 [0] design:longitudinal
kept bouhoula2024_automated 6 [5] privacy:darkpatterns, privacy:tcf_consent_strings, programming:crawler:openwpm, programming:deployment, programming:multilingual_support, statistics:pvalue_corrections [provenance:privacy:tcf_consent_strings, provenance:programming:crawler:openwpm, provenance:programming:deployment, provenance:programming:multilingual_support, provenance:statistics:pvalue_corrections]
deleted bouhoula2024automated 3 [1] design:crawling_location, privacy:consent, privacy:requests [provenance:privacy:consent]
kept lerner2016_internet 2 [0] privacy:fingerprinting, statistics:biases
deleted lerner2016internet 2 [1] design:archives, privacy:browser_storage [provenance:design:archives]
^ page ^ markers repointed ^ distinct keys before ^ after ^ pairs ^
| design:archives | 3 | 14 | 14 | lerner2016internet→lerner2016_internet |
| design:crawling_location | 2 | 15 | 15 | bouhoula2024automated→bouhoula2024_automated |
| design:longitudinal | 1 | 32 | 32 | ahmad2026_ipfp→ahmad2026_more |
| privacy:browser_storage | 2 | 29 | 29 | fouad2022my→fouad2022_cookie; lerner2016internet→lerner2016_internet |
| privacy:consent | 3 | 28 | 28 | bouhoula2024automated→bouhoula2024_automated |
| privacy:fingerprinting | 2 | 35 | 35 | fouad2022my→fouad2022_cookie |
| privacy:requests | 1 | 39 | 39 | bouhoula2024automated→bouhoula2024_automated |
| programming:crawler:openwpm | 1 | 26 | 26 | fouad2022my→fouad2022_cookie |
| programming:stateful_stateless | 3 | 28 | 28 | fouad2022my→fouad2022_cookie |
| provenance:design:archives | 1 | 7 | 7 | lerner2016internet→lerner2016_internet |
| provenance:privacy:consent | 1 | 3 | 3 | bouhoula2024automated→bouhoula2024_automated |
| provenance:statistics:pvalue_corrections | 2 | 18 | 18 | boettger2025_regional→bottger2025_regional |
| statistics:pvalue_corrections | 1 | 28 | 28 | boettger2025_regional→bottger2025_regional |
pages changed : 13
markers repointed : 23 (loser occurrences before: 23)
provenance amendments : 10
files written : 21 under out/dedup_apply/
markers naming a key the bibliography does not define, BEFORE: 22 on 4 key(s)
... (1 page(s))
cite (1 page(s))
citekey (2 page(s))
key (18 page(s))
same, AFTER: 22 on 4 key(s)
... (1 page(s))
cite (1 page(s))
citekey (2 page(s))
key (18 page(s))
deleted keys still named in PROSE (not markers), left as historical record: 26 page(s)
provenance:design:archives boettger2025_regional, bouhoula2024automated, fouad2022my, lerner2016internet
provenance:design:crawling_location bouhoula2024automated
provenance:design:dns ahmad2026_ipfp, boettger2025_regional, bouhoula2024automated, fouad2022my, lerner2016internet
provenance:design:longitudinal ahmad2026_ipfp, lerner2016internet
provenance:design:platforms ahmad2026_ipfp, boettger2025_regional, bouhoula2024automated, fouad2022my, lerner2016internet
provenance:design:website_classification ahmad2026_ipfp, boettger2025_regional, bouhoula2024automated, fouad2022my, lerner2016internet
provenance:literature:bibliography ahmad2026_ipfp, boettger2025_regional, bouhoula2024automated, fouad2022my, lerner2016internet
provenance:privacy:browser_extensions lerner2016internet
provenance:privacy:browser_protection ahmad2026_ipfp, boettger2025_regional, fouad2022my
provenance:privacy:browser_storage fouad2022my, lerner2016internet
provenance:privacy:consent bouhoula2024automated
provenance:privacy:cookie_syncing boettger2025_regional, fouad2022my
provenance:privacy:darkpatterns bouhoula2024automated
provenance:privacy:fingerprinting fouad2022my
provenance:privacy:privacy_sandbox ahmad2026_ipfp, boettger2025_regional, bouhoula2024automated, fouad2022my, lerner2016internet
provenance:privacy:requests bouhoula2024automated
provenance:privacy:server_side_tracking fouad2022my
provenance:privacy:tcf_consent_strings boettger2025_regional, bouhoula2024automated, fouad2022my, lerner2016internet
provenance:programming:crawler:openwpm boettger2025_regional, bouhoula2024automated, fouad2022my
provenance:programming:deployment bouhoula2024automated
provenance:programming:filter_lists boettger2025_regional
provenance:programming:stateful_stateless boettger2025_regional, fouad2022my
provenance:statistics:biases boettger2025_regional, bouhoula2024automated, lerner2016internet
provenance:statistics:how_many_sites lerner2016internet
provenance:statistics:pvalue_corrections boettger2025_regional
provenance:statistics:regression boettger2025_regional, bouhoula2024automated, fouad2022my, lerner2016internet
all invariants hold
Reading it: 23 deleted-key occurrences before, 23 repointed; every page's
distinct-key count is unchanged, which is the check that no page cited both
keys of a pair (a page that had would have lost a reference silently); and the
four “unresolved” strings — key, citekey, cite, … — are
documentation examples written as literal markers in prose on 18 pages,
identical before and after. They are a pre-existing residue this run did not
touch and did not create.
Rendered check
^ page ^ references (dt) before → after ^ citekey spans before → after ^ deleted-key strings after ^ verdict ^ | design:archives | 14 → 14 | 84 → 84 | 0 | unchanged | | design:crawling_location | 15 → 15 | 40 → 40 | 0 | unchanged | | design:longitudinal | 32 → 32 | 90 → 90 | 0 | unchanged | | privacy:browser_storage | 28 → 28 | 96 → 96 | 0 | unchanged | | privacy:consent | 28 → 28 | 138 → 138 | 0 | unchanged | | privacy:fingerprinting | 35 → 35 | 118 → 118 | 0 | unchanged | | privacy:requests | 38 → 38 | 156 → 156 | 0 | unchanged | | programming:crawler:openwpm | 26 → 26 | 96 → 96 | 0 | unchanged | | programming:stateful_stateless | 28 → 28 | 150 → 150 | 0 | unchanged | | provenance:design:archives | -1 → -1 | 46 → 46 | 5 | unchanged | | provenance:privacy:consent | 3 → 3 | 6 → 6 | 3 | unchanged | | provenance:statistics:pvalue_corrections | 18 → 18 | 48 → 48 | 1 | unchanged | | statistics:pvalue_corrections | 27 → 27 | 112 → 112 | 0 | unchanged | pages checked: 13 pages whose counts moved: 0
-1 is archives, which cites papers but carries no
<bibtex bibliography> block, so it has no reference list to count. The
“deleted-key strings” on the three provenance pages are their dated amendment
and their historical notes, not citations — the same rows show their marker
counts unchanged.
The wider scan: the umlaut slip did not recur
The item asked whether the transliteration mistake existed for other
German-named authors. bib_dedup_scan.py adds two candidate generators to
the DOI-and-title scan of the first sitting: near-identical squashed titles
(difflib ratio ≥ 0.85, or one title a prefix of the other, for a dropped
subtitle), and same year plus same folded first-author surname, where the
fold strips diacritics and collapses oe/ue/ae/ss to o/u/a/s so that Böttger,
B“ottger, Boettger and Bottger compare equal. On the 855-entry file it
reports the five definite pairs and 58 candidates; on the saved 850-entry
file, 0 definite and the same 58.
All 58 were read by hand and none is a duplicate. Fifty-four are one first
author with two or three different papers in one year (Durumeric 2013–2015,
Li 2017 and 2024, Liu 2025, Agarwal 2025 …); one is a replication and its
original, caught by the title rule (bratton2019_replication /
sumner2014_exaggeration); three are DuckDuckGo artefacts whose “surname” is
the vendor name. No second umlaut pair exists in the file. The list is the
residue of the fold and is printed in full so the judgement can be checked:
bibliography : out/live_bibliography_20260904_1721.txt
entries : 855 distinct citekeys: 855
DEFINITE duplicate pairs (rule A or B): 5
[ABCD] ahmad2026_ipfp / ahmad2026_more
More Space, Less Privacy? Measuring the Effectiveness of IP-based Website Fingerprinting i
More Space, Less Privacy? Measuring the Effectiveness of IP-based Website Fingerprinting i
[ABCD] boettger2025_regional / bottger2025_regional
Understanding Regional Filter Lists: Efficacy and Impact
Understanding Regional Filter Lists: Efficacy and Impact
[BCD] bouhoula2024_automated / bouhoula2024automated
Automated Large-Scale Analysis of Cookie Notice Compliance
Automated Large-Scale Analysis of Cookie Notice Compliance
[ABCD] fouad2022_cookie / fouad2022my
My Cookie is a phoenix: detection, measurement, and lawfulness of cookie respawning with b
My Cookie is a phoenix: Detection, measurement, and lawfulness of cookie respawning with b
[BCD] lerner2016_internet / lerner2016internet
Internet Jones and the Raiders of the Lost Trackers: An Archaeological Study of Web Tracki
Internet Jones and the Raiders of the lost trackers: An archaeological study of web tracki
CANDIDATE pairs (rule C or D only) — judged by hand, see the provenance page: 58
[D] LePochat2019_tranco / LePochat2019_tranco_eval
2019 Tranco: A Research-Oriented Top Sites Ranking Hardened Against Manipulation
2019 Evaluating the Long-term Effects of Parameters on the Characteristics of the {Tranco} Top Sites Rank
[D] agarwal2024_peeking / agarwal2024_poster
2024 Peeking through the window: Fingerprinting Browser Extensions through Page-Visible Execution Traces
2024 Poster: A Comprehensive Categorization of SMS Scams
[D] agarwal2025_dropped / agarwal2025_fishing
2025 'Hey mum, I dropped my phone down the toilet': Investigating Hi Mum and Dad SMS Scams in the United
2025 Fishing for Smishing: Understanding SMS Phishing Infrastructure and Strategies by Mining Public User
[D] agarwal2025_dropped / agarwal2025_mindsets
2025 'Hey mum, I dropped my phone down the toilet': Investigating Hi Mum and Dad SMS Scams in the United
2025 "I have no idea how to make it safer": Studying Security and Privacy Mindsets of Browser Extension D
[D] agarwal2025_fishing / agarwal2025_mindsets
2025 Fishing for Smishing: Understanding SMS Phishing Infrastructure and Strategies by Mining Public User
2025 "I have no idea how to make it safer": Studying Security and Privacy Mindsets of Browser Extension D
[D] alroomi2023_login / alroomi2023_password
2023 A Large-Scale Measurement of Website Login Policies
2023 Measuring Website Password Creation Policies At Scale
[D] bahrami2025_bytedefender / bahrami2025_cookieguard
2025 Byte by Byte: Unmasking Browser Fingerprinting at the Function Level Using V8 Bytecode Transformers
2025 {CookieGuard}: Characterizing and Isolating the First-Party Cookie Jar
[D] bashir2019_adstxt / bashir2019_quantity
2019 A Longitudinal Analysis of the ads.txt Standard
2019 Quantity vs. Quality: Evaluating User Interest Profiles Using Ad Preference Managers
[D] bhuiyan2025_digital / bhuiyan2025_visitors
2025 Digital Disparities: A Comparative Web Measurement Study Across Economic Boundaries
2025 Not All Visitors are Bilingual: A Measurement Study of the Multilingual Web from an Accessibility Pe
[C] bratton2019_replication / sumner2014_exaggeration
2019 The Association Between Exaggeration in Health-Related Science News and Academic Press Releases: A R
2014 The Association Between Exaggeration in Health Related Science News and Academic Press Releases: Ret
[D] chen2021_cookieswap / chen2021_detecting
2021 Cookie Swap Party: Abusing First-Party Cookies for Web Tracking
2021 Detecting Filter List Evasion with Event-Loop-Turn Granularity JavaScript Signatures
[D] chen2025_parents / chen2025_semantics
2025 Empowering Parents to Support Children's Online Security and Privacy: Findings from a Randomized Con
2025 Semantics-Aware Cookie Purpose Compliance
[CD] duckduckgo_tracker_radar_2026 / duckduckgo_tracker_radar_detector_2026
2026 DuckDuckGo Tracker Radar
2026 DuckDuckGo Tracker Radar Detector
[D] duckduckgo_tracker_radar_2026 / duckduckgo_trc_2026
2026 DuckDuckGo Tracker Radar
2026 Tracker Radar Collector
[D] duckduckgo_tracker_radar_detector_2026 / duckduckgo_trc_2026
2026 DuckDuckGo Tracker Radar Detector
2026 Tracker Radar Collector
[D] durumeric2013_https / durumeric2013_zmap
2013 Analysis of the HTTPS certificate ecosystem
2013 {ZMap}: Fast Internet-wide Scanning and Its Security Applications
[D] durumeric2014_heartbleed / durumeric2014_view
2014 The Matter of Heartbleed
2014 An Internet-Wide View of Internet-Wide Scanning
[D] durumeric2015_neither / durumeric2015_search
2015 Neither Snow Nor Rain Nor MITM...: An Empirical Analysis of Email Delivery Security
2015 A Search Engine Backed by Internet-Wide Scanning
[D] edu2022_alexa / edu2022_exploring
2022 Measuring Alexa Skill Privacy Practices across Three Years
2022 Exploring the security and privacy risks of chatbots in messaging services
[D] iqbal2022_khaleesi / iqbal2022_left
2022 Khaleesi: Breaker of Advertising and Tracking Request Chains
2022 Left or Right: A Peek into the Political Biases in Email Spam Filtering Algorithms During US Electio
[D] kancherla2025_johnny / kancherla2025_least
2025 Johnny Can't Revoke Consent Either: Measuring Compliance of Consent Revocation on the Web
2025 Least Privilege Access for Persistent Storage Mechanisms in Web Browsers
[D] kirchner2024_black / kirchner2024_dancer
2024 A Black-Box Privacy Analysis of Messaging Service Providers' Chat Message Processing
2024 Dancer in the Dark: Synthesizing and Evaluating Polyglots for Blind Cross-Site Scripting
[D] lee2023_adtargeting / lee2023_track
2023 When and Why Do People Want Ad Targeting Explanations? Evidence from a Four-Week, Mixed-Methods Fiel
2023 Net-track: Generic Web Tracking Detection Using Packet Metadata
[D] li2016_remedying / li2016_youve
2016 Remedying Web Hijacking: Notification Effectiveness and Webmaster Comprehension
2016 You've Got Vulnerability: Exploring Effective Vulnerability Notifications
[D] li2017_radar / li2017_security
2017 FBS-Radar: Uncovering Fake Base Stations at Scale in the Wild
2017 A Large-Scale Empirical Study of Security Patches
[D] li2017_radar / li2017_static
2017 FBS-Radar: Uncovering Fake Base Stations at Scale in the Wild
2017 Static analysis of Android apps: A systematic literature review
[D] li2017_security / li2017_static
2017 A Large-Scale Empirical Study of Security Patches
2017 Static analysis of Android apps: A systematic literature review
[D] li2024_bounce / li2024_wellinformed
2024 Bounce in the Wild: A Deep Dive into Email Delivery Failures from a Large Email Service Provider
2024 Are We Getting Well-informed? An In-depth Study of Runtime Privacy Notice Practice in Mobile Apps
[D] li2024_bounce / li2024_worldwide
2024 Bounce in the Wild: A Deep Dive into Email Delivery Failures from a Large Email Service Provider
2024 A Worldwide View on the Reachability of Encrypted DNS Services
[D] li2024_wellinformed / li2024_worldwide
2024 Are We Getting Well-informed? An In-depth Study of Runtime Privacy Notice Practice in Mobile Apps
2024 A Worldwide View on the Reachability of Encrypted DNS Services
[D] liao2016_characterizing / liao2016_seeking
2016 Characterizing Long-tail SEO Spam on Cloud Web Hosting Services
2016 Seeking Nonsense, Looking for Trouble: Efficient Promotional-Infection Detection through Semantic In
[D] lin2021_longitudinal / lin2021_phishpedia
2021 A Longitudinal Study of Removed Apps in {iOS} App Store
2021 Phishpedia: A Hybrid Deep Learning Based Approach to Visually Identify Phishing Webpages
[D] lin2022_investigating / lin2022_sheep
2022 Investigating Advertisers' Domain-changing Behaviors and Their Impacts on Ad-blocker Filter Lists
2022 Phish in Sheep's Clothing: Exploring the Authentication Pitfalls of Browser Fingerprinting
[D] liu2024_opted / liu2024_promises
2024 Opted Out, Yet Tracked: Are Regulations Enough to Protect Your Privacy?
2024 From Promises to Practice: Evaluating the Private Browsing Modes of Android Browser Apps
[D] liu2025_domino / liu2025_fingerprinting
2025 The DOMino Effect: Detecting and Exploiting DOM Clobbering Gadgets via Concolic Execution with Symbo
2025 The First Early Evidence of the Use of Browser Fingerprinting for Online Tracking
[D] liu2025_domino / liu2025_somesite
2025 The DOMino Effect: Detecting and Exploiting DOM Clobbering Gadgets via Concolic Execution with Symbo
2025 Somesite I Used To Crawl: Awareness, Agency and Efficacy in Protecting Content Creators From AI Craw
[D] liu2025_fingerprinting / liu2025_somesite
2025 The First Early Evidence of the Use of Browser Fingerprinting for Online Tracking
2025 Somesite I Used To Crawl: Awareness, Agency and Efficacy in Protecting Content Creators From AI Craw
[D] nguyen2025_breaking / nguyen2025_please
2025 Breaking the Shield: Analyzing and Attacking Canvas Fingerprinting Defenses in the Wild
2025 "Please don't send that bot anything": A Mixed-methods Study of Personal Impersonation Attacks Targe
[D] nisenoff2023_awareness / nisenoff2023_defining
2023 User Awareness and Behaviors Concerning Encrypted {DNS} Settings in Web Browsers
2023 Defining "Broken": User Experiences and Remediation Tactics When Ad-Blocking or Tracking-Protection
[D] oest2020_phishtime / oest2020_sunrise
2020 PhishTime: Continuous Longitudinal Measurement of the Effectiveness of Anti-phishing Blacklists
2020 Sunrise to Sunset: Analyzing the End-to-end Life Cycle and Effectiveness of Phishing Attacks at Scal
[D] papadogiannakis2025_before / papadogiannakis2025_darkside
2025 Before \& After: The Effect of EU's 2022 Code of Practice on Disinformation
2025 Welcome to the Dark Side: Analyzing the Revenue Flows of Fraud in the Online Ad Ecosystem
[D] ruth2022_toppling / ruth2022_world
2022 Toppling Top Lists: Evaluating the Accuracy of Popular Website Lists
2022 A World Wide View of Browsing the World Wide Web
[D] scheitle2018_long / scheitle2018_rise
2018 A Long Way to the Top: Significance, Structure, and Stability of Internet Top Lists
2018 The Rise of Certificate Transparency and Its Implications on the Internet Ecosystem
[D] starov2017_extended / starov2017_xhound
2017 Extended Tracking Powers: Measuring the Privacy Diffusion Enabled by Browser Extensions
2017 XHOUND: Quantifying the Fingerprintability of Browser Extensions
[D] tang2025_misuse / tang2025_navigating
2025 Misuse, Misreporting, Misinterpretation of Statistical Methods in Usable Privacy and Security Papers
2025 Navigating Cookie Consent Violations Across the Globe
[D] utz2023_comparing / utz2023_rarely
2023 Comparing Large-Scale Privacy and Security Notifications
2023 Privacy Rarely Considered: Exploring Considerations in the Adoption of Third-Party Services by Websi
[D] vastel2018_scanner / vastel2018_stalker
2018 Fp-Scanner: The Privacy Implications of Browser Fingerprint Inconsistencies
2018 FP-STALKER: Tracking Browser Fingerprint Evolutions
[D] vekaria2025_bighelp / vekaria2025_soktracking
2025 Big Help or Big Brother? Auditing Tracking, Profiling, and Personalization in Generative AI Assistan
2025 SoK: Advances and Open Problems in Web Tracking
[D] venkatadri2019_auditing / venkatadri2019_investigating
2019 Auditing Offline Data Brokers via Facebook's Advertising Platform
2019 Investigating sources of PII used in Facebook’s targeted advertising
[D] wang2026_masks / wang2026_sipconfusion
2026 The Masks We (Think We) Wear: Privacy Threats of Browser-Extension Wallets in the Web3 Ecosystem
2026 SIPConfusion: Exploiting SIP Semantic Ambiguities for Caller ID and SMS Spoofing
[D] wu2025_appprivacyreport / wu2025_revealing
2025 Transparency or Information Overload? Evaluating Users’ Comprehension and Perceptions of the iOS App
2025 Revealing the Black Box of Device Search Engine: Scanning Assets, Strategies, and Ethical Considerat
[D] wu2026_email / wu2026_tracking
2026 One Email, Many Faces: A Deep Dive into Identity Confusion in Email Aliases
2026 Tracking the Stray Sheep: Understanding DNS Response Manipulation in the Wild
[D] xie2024_arcanum / xie2024_crawling
2024 Arcanum: Detecting and Evaluating the Privacy Risks of Browser Extensions on Web Pages and Web Conte
2024 Crawling to the Top: An Empirical Evaluation of Top List Use
[D] yang2022_extensive / yang2022_wtagraph
2022 An Extensive Study of Residential Proxies in China
2022 WTAGRAPH: Web Tracking and Advertising Detection using Graph Neural Networks
[D] zhang2022_harpo / zhang2022_spartacus
2022 HARPO: Learning to Subvert Online Behavioral Advertising
2022 I'm SPARTACUS, No, I'm SPARTACUS: Proactively Protecting Users from Phishing by Intentionally Trigge
[D] zhang2024_inbox / zhang2024_quic
2024 Inbox Invasion: Exploiting MIME Ambiguities to Evade Email Attachment Detectors
2024 QUIC is not Quick Enough over Fast Internet
[D] zhang2025_abusability / zhang2025_qrcode
2025 Abusability of Automation Apps in Intimate Partner Violence
2025 Demystifying the (In)Security of {QR} Code-based Login in Real-world Deployments
[D] zhu2020_label / zhu2020_vtset
2020 Measuring and Modeling the Label Dynamics of Online Anti-Malware Engines
2020 Demo: Benchmarking Label Dynamics of VirusTotal Engines
Prose that still names the deleted keys
26 provenance pages name a deleted key in a run record or review log — “found
and left alone”, “this page cites bouhoula2024automated”. Those are
statements about the bibliography as it was when they were written and were
not rewritten; the list is the last block of the apply output above. The
ten provenance pages whose content page was repointed each got a dated
Amendment, 2026-09-04: citekey consolidation section instead, so a reader
who follows one of those statements finds the correction on the same page.
What could not be established, second sitting
- Whether anything outside the wiki cites a deleted key — a BibTeX file someone exported from the site, a draft that copied
fouad2022my. Nothing on the site does; nothing off it can be checked. - Whether
khrer2015_goingandsom2017_contentshould be re-keyed. They are the[^a-z]bug inbibgen.mjs, not duplicates, and each is cited; re-keying them is the same kind of wiki-wide rewrite as this item and was not done inside it.
Judgement calls, second sitting
| Call | Alternative a reasonable person would pick | Why this one |
|---|---|---|
| Keep the stripped-diacritic, underscore key even where the other key was older and on more pages | Keep whichever key more pages used | The convention is what bibgen.mjs mints; keeping a minority style alive is how the pairs arose. 19 of 25 diacritic surnames in the file are already spelled this way |
Carry pages over to bouhoula2024_automated; drop isbn, address, publisher, month | Merge every field of the deleted entry | pages was verified against USENIX's page; the other four appear on no other entry, and adding them to one would make that entry the odd one |
| Leave the 26 provenance pages' historical prose alone; amend only the 10 whose content page was repointed | Rewrite every mention of a deleted key | A review log records what was true when written. Rewriting it is the mistake the first sitting's own review found — a debunked claim surviving in the log — turned inside out |
Pages first, bibliography last, one –if-rev per save | Bibliography first, or one combined pass | The other order leaves 23 markers unresolved between two saves; –if-rev is what makes a 21-save run safe against an edit landing in the middle |
| Publish the 58 candidates in full | Publish the count and the verdict | A hand judgement over a list nobody can read is not checkable |
Extend this page rather than create provenance:literature:bibliography:dedup | A page per item | The provenance page mirrors the content page's id; the bibliography has one id. Two sittings, two dated audits, one page |
What could not be established
- Whether defects 1 and 2 ever reached the wiki. Zero missing authors survive in
out/authors.jsonand zero in the live bibliography, but the cache carries no history, so it cannot be shown whether a dropped author was ever cached and later corrected or was never cached at all. The live page is clean either way, which is the question that matters. - The 42 PETS entries in
out/authors.json. Out of scope for this item. They come fromparse_popets, a different function against a different page shape (parenthesised affiliations, so the comma ambiguity does not arise).scripts/bib_dupe_cocitation.pyscans all 42 for an affiliation-shaped or single-token name and finds none — but that is a smoke test, not the three-source audit the USENIX keys got. - The 86 live PoPETs/PETS entries' author lists. Their DOIs were checked; their authors were not.
parse_popetsreads a different page shape, and PETS records do carry DOIs (1,113 of 1,253), so the venue has an independent metadata path the USENIX records lack — but that is an argument for expecting fewer defects, not evidence of none. - The other 592 live entries. Neither USENIX Security nor PoPETs; not examined at all. Their authors come from OpenAlex/Crossref via the index, never from
fetch_authors.py. calzavara2026_syntactichas no DOI and its recorded url is a listing page. Whether the paper has a DOI yet was not established.- Author order for the 5 keys nothing cites. Pass 3 covers every live entry, and pass 1 covers the 85 cached keys that are cited. The 5 cached-but-uncited keys have no live entry to compare against, so their order is unverified — harmless while nothing cites them, and pass 3 will cover them the moment something does.
- Whether
bibgen.mjsoutput was hand-cleaned, or the affected entries were written from another source. The live entries are correct and the cache was not; the intermediate step left no record. The conclusion “the parser contaminated the cache and never the page” is a statement about the two endpoints, not about what happened between them.
Judgement calls
| Call | Alternative a reasonable person would pick | Why this one |
|---|---|---|
| Stop parsing the prose entirely rather than patch defect 3 | Add an institution stop-list to the prose parser | A stop-list needs a new entry per venue-year and fails silently when it misses one. Every page already publishes the list unambiguously; there was no reason to keep guessing |
Publish Jhih-Yi Hsieh, against both the paper and USENIX's own page | Keep the paper's Jhih-Yi (Janet) Hsieh | A parenthetical is not a name particle; BibTeX parses it into the given name. DBLP normalises it. Recorded in ADJUDICATED so the next run sees the decision rather than re-making it |
| Leave the LaTeX accent escapes alone | Normalise all 43 to literal UTF-8 for consistency with the other 66 | Both render correctly (checked on the rendered DOM). Rewriting 43 author fields on a page every other page depends on, for a cosmetic gain, is a bad trade |
| Fix pass 3's order-blindness rather than disclose it as a known gap | Publish the gap and file it | It was a four-line fix and a mutation, and leaving it would mean publishing “0 defects” over a check that could not see a first-authorship swap. A gap you can close in an hour is not a caveat |
| Publish the three decoder bugs this audit had, by name and with their mutations | Publish only the result | The result is a row of zeros. A row of zeros is worth nothing without the record of what had to be fixed before it came out that way, and two of the three were found only because someone objected to an unreproducible number |
Correct fetch_authors.py and bibgen.mjs as part of this item | Report the parser bug and stop | The item asked for the cache and the bibliography to be corrected. Leaving the producer broken would mean the next fetch re-introduces exactly the fragments this run removed |
| Extend the audit to all 159 live USENIX entries (pass 3), beyond the 90 the item named | Stop at the 90 keys | Pass 2 can only see an affiliation that got in, never an author that fell out, and 74 live entries are invisible to a cache-based check. 72 extra page fetches closed the gap |
| Defer the 5 duplicate entries | Consolidate them in the same sitting | ~30 pages would need {[key]} rewrites; nothing renders wrong today. Done as its own item later the same day: 13 pages and 23 markers, not ~30 — see the second-sitting audit |
Put this page at provenance:literature:bibliography | Fold it into corpus | corpus states the rule this follows: a provenance page mirrors its content page's id exactly. It is also corpus-level — selection, the funnel, extraction reliability — while this is bibliography-level: where one file's author lists and DOIs came from |
Keep <bibtex bibliography> on a provenance page | Follow corpus, which says provenance pages carry no bibliography | That convention was settled on a page that cites no papers. This one cites nine, and without the block the markers render as bare numbers with no reference list. The neighbouring provenance pages that cite papers all carry it. No ~~DISCUSSION~~ block, per the same convention |
Scripts
Committed under scripts/. Run from /workspace/artifacts/wiki. Step 1
fetches and caches HTML; every later step re-parses from disk and never
re-hammers usenix.org.
Step 1 — fetch the landing pages
- usenix_author_audit_fetch.py
#!/usr/bin/env python3 """Step 1 of the USENIX author-list audit: fetch every USENIX landing page named in out/authors.json and cache the raw HTML under out/usenix_audit/. Separate from parsing on purpose: the parser was wrong twice already, so the HTML is kept on disk and every later pass re-parses it offline instead of re-hammering usenix.org. """ import glob, json, os, subprocess, sys, time ROOT = "/workspace/publications_dataset/data" OUTDIR = "out/usenix_audit/html" UA = ("Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) " "Chrome/126.0.0.0 Safari/537.36") idx = {} for f in glob.glob(os.path.join(ROOT, "corpus2/.meta/*.json")): for p in json.load(open(f))["papers"]: idx[f"{p['venue']}/{p['year']}/{p['slug']}"] = p keys = [k for k in json.load(open("out/authors.json")) if k.startswith("USENIX/")] os.makedirs(OUTDIR, exist_ok=True) print(f"{len(keys)} USENIX keys in out/authors.json") urls = {} for i, k in enumerate(sorted(keys)): dest = os.path.join(OUTDIR, k.replace("/", "__") + ".html") url = idx[k]["landingUrl"] # KeyError = bad slug, correct to crash urls[k] = url if os.path.exists(dest) and os.path.getsize(dest) > 2000: print(f"have {k}") continue r = subprocess.run(["curl", "-sL", "-A", UA, "--max-time", "40", "-w", "%{http_code}", "-o", dest, url], capture_output=True, text=True) code = r.stdout.strip() size = os.path.getsize(dest) if os.path.exists(dest) else 0 print(f"{code} {size:>7} {k}") if code != "200": print(f" !! non-200 for {url}") time.sleep(1.0) json.dump(urls, open("out/usenix_audit/urls.json", "w"), indent=1, sort_keys=True)
Step 1b — extend the fetch to every live USENIX entry
- usenix_bib_fetch_all.py
#!/usr/bin/env python3 """Extend the USENIX author audit to EVERY live USENIX Security entry. out/authors.json holds 90 keys, of which 85 are cited; literature:bibliography carries 159 entries in a USENIX Security series. The other 74 were filled from somewhere else, so the prose-parser bugs cannot explain a defect in them — but a dropped author can reach a bibliography by more than one route, and pass 2 of scripts/usenix_bib_diff.py can only see an affiliation that got IN, never an author that fell OUT. This fetches the landing page for the rest so pass 3 can check the whole population. Resolves each entry to a landing URL by its own url= field where it has one, and otherwise by title-matching into data/corpus2/.meta. """ import glob import json import os import re import subprocess import sys import time sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) from usenix_bib_diff import field, squash ROOT = "/workspace/publications_dataset/data" OUTDIR = "out/usenix_audit/html" UA = ("Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) " "Chrome/126.0.0.0 Safari/537.36") idx = {} for f in glob.glob(os.path.join(ROOT, "corpus2/.meta/*.json")): for p in json.load(open(f))["papers"]: if p["venue"] == "USENIX": idx[squash(p["title"])] = p text = open("out/usenix_audit/bib_live.txt", encoding="utf-8").read() entries = re.findall(r"@\w+\{[^@]*?\n\}", text, re.S) targets, unresolved = {}, [] for e in entries: series = (field(e, "series") or "") + " " + (field(e, "booktitle") or "") if not re.search(r"USENIX Security", series, re.I): continue ckey = re.match(r"@\w+\{([^,]+),", e).group(1) p = idx.get(squash(field(e, "title"))) url = field(e, "url") if p: targets[ckey] = (f"{p['venue']}/{p['year']}/{p['slug']}", p["landingUrl"], p["title"]) elif url and "usenix.org" in url: targets[ckey] = (f"url:{ckey}", url, field(e, "title")) else: unresolved.append(ckey) json.dump({k: list(v) for k, v in targets.items()}, open("out/usenix_audit/bib_targets.json", "w"), indent=1, sort_keys=True) print(f"live USENIX Security entries resolved to a landing page: {len(targets)}") print(f"unresolved (no index match, no usenix.org url) : {len(unresolved)}" + (f" {unresolved}" if unresolved else "")) os.makedirs(OUTDIR, exist_ok=True) n_new = 0 for ckey, (key, url, _t) in sorted(targets.items()): dest = os.path.join(OUTDIR, key.replace("/", "__").replace(":", "__") + ".html") if os.path.exists(dest) and os.path.getsize(dest) > 2000: continue r = subprocess.run(["curl", "-sL", "-A", UA, "--max-time", "40", "-w", "%{http_code}", "-o", dest, url], capture_output=True, text=True) n_new += 1 print(f"{r.stdout.strip()} {os.path.getsize(dest):>7} {key}") time.sleep(1.0) print(f"newly fetched: {n_new}")
Step 2 — re-derive and diff the cached author lists
- usenix_author_audit.py
#!/usr/bin/env python3 """Step 2 of the USENIX author-list audit: re-derive every USENIX author list from AUTHORITATIVE page markup and diff against out/authors.json. Why not just re-run the fixed prose parser (scripts/fetch_authors.py). The prose blob — "A and B, University of California, Santa Barbara; C, MIT" — is genuinely ambiguous: an affiliation tail is not distinguishable from a person's name by any rule that does not encode a list of institutions. It cost two parser bugs already (a comma split that crossed ";" boundaries, and an unstripped leading "and ") and, even after those were fixed on 2026-08-13, it still leaked affiliation FRAGMENTS into the author list, because "University of California, Berkeley" splits into an affiliation-looking half that gets dropped and a name-looking half ("Berkeley") that gets kept. Every USENIX page carries at least one unambiguous author list instead: modern (2012-) presentation pages A. <meta name="citation_author" content="..."> — one tag per author, the Google Scholar metadata, emitted alongside citation_author_institution so names and affiliations are already separated by USENIX. B. USENIX's own published BibTeX block, author = {A and B and C}. Both are read; they must agree, or the paper is reported, not guessed at. pages with no structured metadata at all C. Affiliations are wrapped in an italic tag: <i> on the legacy (2011) technical-sessions pages, <em> inside the people-text div on the modern pages USENIX has not yet populated with Scholar metadata (one such page in this population, usenixsecurity25/presentation/tang). Deleting the italic spans leaves exactly the names. On a legacy page the paper's own block is first located BY TITLE, because those URLs address a whole session and taking the first block returns another talk's authors. USENIX's own structured metadata is NOT unconditionally trustworthy, which is why every paper is additionally cross-checked against the prose blob: * PhishTime (2020) — meta AND BibTeX both omit the 8th author, Gail-Joon Ahn, whom the prose blob and the paper both list. * PharmaLeaks (2012) — meta AND BibTeX say "Jordan Grant" and drop a middle initial; the paper PDF and DBLP both say "Grant Jordan" and "Geoffrey M. Voelker". So a structured list is accepted only when the prose blob agrees on the set of surnames. Every mismatch is printed for a human and adjudicated in ADJUDICATED below, with the source that settled it. python3 scripts/usenix_author_audit.py # report only python3 scripts/usenix_author_audit.py --write # rewrite out/authors.json Reads the HTML cached by scripts/usenix_author_audit_fetch.py; never refetches. """ import glob import html import json import os import re import sys import unicodedata HTMLDIR = "out/usenix_audit/html" CACHE = "out/authors.json" ROOT = "/workspace/publications_dataset/data" def unesc(s): return re.sub(r"\s+", " ", html.unescape(re.sub(r"<[^>]+>", " ", s))).strip() # ---------------------------------------------------------------- source A def meta_authors(page): return [unesc(m.group(1)) for m in re.finditer( r'<meta\s+name="citation_author"\s+content="([^"]*)"', page) if unesc(m.group(1))] # ---------------------------------------------------------------- source B LATEX = { r"\'": "́", r'\"': "̈", r"\`": "̀", r"\^": "̂", r"\~": "̃", r"\=": "̄", r"\.": "̇", r"\c": "̧", r"\v": "̌", r"\u": "̆", r"\H": "̋", r"\r": "̊", } LETTERS = {r"\i": "i", r"\j": "j", r"\ss": "ß", r"\o": "ø", r"\O": "Ø", r"\aa": "å", r"\AA": "Å", r"\l": "ł", r"\L": "Ł", r"\ae": "æ", r"\AE": "Æ", r"\oe": "œ", r"\OE": "Œ"} def de_latex(s): """Doup{\\'e} -> Doupé. Needed because the block is real BibTeX. The dotless-i pass has to run FIRST. USENIX writes Benoît as Beno{\\^\\i}t, and with \\i still in place none of the accent patterns below match (the character after the accent command is a backslash, not a letter); the final brace/backslash strip then produced "Beno^it". Found 2026-09-04 by scripts/usenix_source_agreement.py, which is the only check that compares the two sources across all 162 pages rather than the 90. """ def one(m): return unicodedata.normalize("NFC", m.group(2) + LATEX[m.group(1)]) for cmd, ch in sorted(LETTERS.items(), key=lambda kv: -len(kv[0])): s = re.sub(re.escape(cmd) + r"(?![A-Za-z])", ch, s) # {\'e} / {\"u} / \'{e} / \'e s = re.sub(r"\{(\\[\'\"`^~=.cvuHr])\s*\{?([^\W\d_])\}?\}", one, s) s = re.sub(r"(\\[\'\"`^~=.cvuHr])\s*\{([^\W\d_])\}", one, s) s = re.sub(r"(\\[\'\"`^~=.cvuHr])([^\W\d_])", one, s) return re.sub(r"[{}\\]", "", s) DE_LATEX_TESTS = [ (r"Beno{\^\i}t", "Benoît"), # dotless i — the 2026-09-04 regression (r"Adam Doup{\'e}", "Adam Doupé"), (r"Adam Doup\'e", "Adam Doupé"), (r"Dominik Sch{\"u}rmann", "Dominik Schürmann"), (r"Mathias L{\'e}cuyer", "Mathias Lécuyer"), # \c is cedilla, so \c{s} decodes to U+015F (s WITH CEDILLA), not the # U+0219 comma-below Romanian actually uses. That is the literally correct # reading of the BibTeX and it does not matter here: fold() strips both. (r"Dumitra\c{s}", "Dumitra\u015f"), (r"{Luki\'c}", "Lukić"), (r"Gro\ss{}", "Groß"), (r"Plain Name", "Plain Name"), ] def selftest(): bad = [(a, de_latex(a), b) for a, b in DE_LATEX_TESTS if de_latex(a) != b] for a, got, want in bad: print(f" FAIL {a!r} -> {got!r}, want {want!r}") print(f"de_latex self-test: {len(DE_LATEX_TESTS) - len(bad)}/" f"{len(DE_LATEX_TESTS)} pass") return 1 if bad else 0 def brace_group(s, start): """Return the contents of the {...} beginning at s[start], brace-balanced. A non-greedy \\{(.*?)\\} truncates every LaTeX accent — "Adam Doup{\\'e}" ends the match at the inner brace and yields "Adam Doup{\\'e", which is how an earlier version of THIS script reported 13 spurious meta/BibTeX disagreements. Count braces instead. """ depth, i = 0, start while i < len(s): if s[i] == "{": depth += 1 elif s[i] == "}": depth -= 1 if depth == 0: return s[start + 1:i] i += 1 raise ValueError("unbalanced braces in BibTeX block") def bibtex_authors(page): m = re.search(r'bibtex-text-entry[^>]*>(.*?)</div>', page, re.S) if not m: return [] block = html.unescape(re.sub(r"<br\s*/?>", "\n", m.group(1))) a = re.search(r"author\s*=\s*\{", block) if not a: return [] raw = re.sub(r"\s+", " ", brace_group(block, a.end() - 1)).strip() return [de_latex(n).strip() for n in re.split(r"\s+and\s+", raw) if n.strip()] # ---------------------------------------------------------------- source C def _names_minus_italics(fragment): """Names are whatever is left once the <i>/<em> affiliation spans go.""" raw = unesc(re.sub(r"<(i|em)\b.*?</\1>", " ; ", fragment, flags=re.S)) out = [] for part in re.split(r"[;,]", raw): part = re.sub(r"^\s*and\s+", "", part.strip(), flags=re.I) for n in re.split(r"\s+and\s+", part): n = n.strip(" .,") if n: out.append(n) return out def italic_authors(page, title): """Author list from a page carrying no citation_author and no BibTeX.""" def squash(t): return re.sub(r"[^a-z0-9]", "", html.unescape(t).lower()) # Modern page, Scholar metadata not yet populated: the people-text div with # <em>affiliation</em>. Only ONE page in this population is like this # (usenixsecurity25/presentation/tang) — but it is also the only entry the # audit would otherwise have to report as unverifiable. m = re.search(r'field-name-field-paper-people-text.*?<p>(.*?)</p>', page, re.S) if m and re.search(r"<(i|em)\b", m.group(1)): return _names_minus_italics(m.group(1)) # Legacy 2011 session page: find this paper's own block by title. for m in re.finditer(r'<p class="fullpaper\d*">(.*?)</p>', page, re.S): b = m.group(1) t = re.search(r"<b>(.*?)</b>", b, re.S) if t and squash(unesc(t.group(1))) == squash(title): return _names_minus_italics(b[t.end():]) return [] # ---------------------------------------------------------------- compare def fold(names): """Compare ignoring diacritics, punctuation and case, but not word count.""" def f(s): s = unicodedata.normalize("NFKD", s) s = "".join(c for c in s if not unicodedata.combining(c)) return re.sub(r"\s+", " ", re.sub(r"[^a-z ]", " ", s.lower())).strip() return [f(x) for x in names] # The affiliation filter for the over-generating prose splitter below. Kept here # rather than imported from scripts/fetch_authors.py: that module now imports # the parsers below, and the dependency has to run one way. AFFIL_RE = re.compile( r"universit|institut|college|inc\.|labs?\b|corporat|technolog|research|" r"school|academy|center|centre|gmbh|ltd|company|cispa|kaist|inria|inc\b|" r"microsoft|google|meta|apple|amazon|ibm|nec|eth |epfl|tu |csiro|" r"laborator|foundation|agency|department|academia sinica|polytechnic", re.I, ) def page_institutions(page): """Every affiliation the page itself declares. Modern pages emit one <meta name="citation_author_institution"> per author; legacy 2011 pages wrap each affiliation in <i>. Using the page's own list beats a hand-maintained stop-list of institution tail words: it needs no maintenance, and it takes the unfiltered residue (15 fragments over 16 papers, mutation A7) to zero. """ inst = [unesc(m.group(1)) for m in re.finditer( r'<meta\s+name="citation_author_institution"\s+content="([^"]*)"', page)] inst += [unesc(m.group(1)) for m in re.finditer(r"<i>(.*?)</i>", page, re.S)] return " | ".join(fold(inst)) def prose_parse(blob, inst=""): """Candidate personal names in the prose blob. Over-generates on purpose. Anything whose folded form is contained in one of the page's own declared affiliations is an affiliation tail, not an author, and is dropped. """ out = [] for group in blob.split(";"): parts = [p.strip() for p in group.split(",") if p.strip()] if len(parts) > 1 and AFFIL_RE.search(parts[-1]): parts = parts[:-1] for part in parts: part = re.sub(r"^and\s+", "", part.strip(), flags=re.I) for n in re.split(r"\s+and\s+", part): n = re.sub(r"\s*\([^)]*\)\s*", " ", n).strip(" .,") n = re.sub(r"\s+", " ", n) if not n or AFFIL_RE.search(n) or len(n.split()) < 2: continue if len(n.split()) > 5 or not re.match(r"^[A-ZÀ-ɏ]", n): continue f = fold([n])[0] if not f or (inst and f in inst): continue out.append(n) return out # ------------------------------------------------- cross-check against prose # The prose blob is the ambiguous source this audit exists to stop trusting, # but it is an INDEPENDENT one, so it is still good enough to answer the much # easier question "does a surname appear at all?". Any surname on one side and # not the other is printed and adjudicated by hand. def prose_blob(page, title): for pat in (r'field-name-field-paper-people-text.*?<p>(.*?)</p>', r'field-name-field-paper-people-text[^>]*>(.*?)<div'): m = re.search(pat, page, re.S) if m: return unesc(m.group(1)) # Legacy 2011 pages address a whole SESSION: locate this paper's own block # by title. Taking the first <p class="fullpaper"> on the page instead made # all three 2011 papers report the authors of an unrelated talk. def squash(t): return re.sub(r"[^a-z0-9]", "", html.unescape(t).lower()) for m in re.finditer(r'<p class="fullpaper\d*">(.*?)</p>', page, re.S): b = m.group(1) t = re.search(r"<b>(.*?)</b>", b, re.S) if t and squash(unesc(t.group(1))) == squash(title): return unesc(b[t.end():]) return "" def surnames(names): return {fold([n])[0].split()[-1] for n in names if fold([n])[0]} # Papers where the structured metadata and the prose blob disagree, each # settled against a source outside USENIX's landing page. Anything not listed # here is reported as unresolved and blocks --write. ADJUDICATED = { "USENIX/2012/pharmaleaks-understanding-the-business-of-online-pharmaceutical-affiliate-progra": ( ["Damon McCoy", "Andreas Pitsillidis", "Grant Jordan", "Nicholas Weaver", "Christian Kreibich", "Brian Krebs", "Geoffrey M. Voelker", "Stefan Savage", "Kirill Levchenko"], "USENIX meta/BibTeX say 'Jordan Grant' and 'Geoffrey Voelker'. The paper " "PDF title block (data/fulltext/2012/USENIX/pharmaleaks-.../paper.cols.txt) " "and DBLP both say 'Grant Jordan' and 'Geoffrey M. Voelker'. Paper wins."), "USENIX/2020/phishtime-continuous-longitudinal-measurement-of-the-effectiveness-of-anti-phish": ( ["Adam Oest", "Yeganeh Safaei", "Penghui Zhang", "Brad Wardman", "Kevin Tyers", "Yan Shoshitaishvili", "Adam Doup\u00e9", "Gail-Joon Ahn"], "USENIX meta AND BibTeX both stop at 7 authors. The prose blob on the " "same page and the paper's own title block carry Gail-Joon Ahn as the " "8th. DBLP does NOT: record conf/uss/OestSZWTSD20, checked 2026-09-04, " "also lists seven and stops at Adam Doupe. Three machine-readable " "sources agree on a list the paper contradicts. Paper wins."), "USENIX/2025/vulnerability-of-text-matching-in-ml-ai-conference-reviewer-assignments-to-collu": ( ["Jhih-Yi Hsieh", "Aditi Raghunathan", "Nihar B. Shah"], "The paper's title block and USENIX's meta both print the nickname, " "'Jhih-Yi (Janet) Hsieh'. Normalised to DBLP's form: a parenthetical is " "not a name particle and BibTeX would parse it as part of the given " "name. Judgement call, not a correction of the source."), } def load_titles(): t = {} for f in glob.glob(os.path.join(ROOT, "corpus2/.meta/*.json")): for p in json.load(open(f))["papers"]: t[f"{p['venue']}/{p['year']}/{p['slug']}"] = p["title"] return t def main(): write = "--write" in sys.argv # --cache <path> diffs against a snapshot instead of the live file, so the # committed audit trail can be regenerated after --write has already run. # Without it, re-running after a write reports "90 unchanged" and the # committed -output.txt becomes a record of nothing. src_cache = sys.argv[sys.argv.index("--cache") + 1] \ if "--cache" in sys.argv else CACHE cache = json.load(open(src_cache)) titles = load_titles() rows, unchanged, problems, accepted, adjudged = [], [], [], {}, [] # Iterate the CACHE keys, not the HTML directory: scripts/usenix_bib_fetch_all.py # drops 72 more pages in there for pass 3 of usenix_bib_diff.py, and those # papers are not in out/authors.json at all. files = sorted(k for k in cache if k.startswith("USENIX/")) for key in files: f = os.path.join(HTMLDIR, key.replace("/", "__") + ".html") page = open(f, encoding="utf-8", errors="replace").read() old = cache[key] meta, bib = meta_authors(page), bibtex_authors(page) if meta or bib: if meta and bib and fold(meta) != fold(bib): problems.append((key, "META/BIBTEX DISAGREE", old, meta, bib)) continue # Prefer meta: it keeps diacritics USENIX's BibTeX sometimes drops # (Gan~an) and needs no LaTeX decoding. new, src = (meta, "meta+bibtex") if (meta and bib) else \ ((meta, "meta") if meta else (bib, "bibtex")) else: new = italic_authors(page, titles[key]) src = "italic" if not new: problems.append((key, "NO AUTHORS IN ANY SOURCE", old, [], [])) continue # Cross-check the structured list against the independent prose blob. blob = prose_blob(page, titles[key]) fblob = fold([blob])[0].split() missing_from_prose = [n for n in new if fold([n])[0].split()[-1] not in fblob] prose_only = sorted(surnames(prose_parse(blob, page_institutions(page))) - surnames(new)) # An adjudicated paper overrides the page UNCONDITIONALLY. Applying it # only when the prose cross-check fires made the Hsieh entry dead code: # the prose agrees with the metadata there, and it is the metadata # itself that is being overridden. if key in ADJUDICATED: new, why = ADJUDICATED[key] src += " +adjudicated" adjudged.append((key, why)) elif missing_from_prose or prose_only: problems.append((key, "STRUCTURED/PROSE DISAGREE: " f"not in prose={missing_from_prose} " f"prose-only surnames={prose_only}", old, meta, bib)) continue if fold(old) != fold(new): rows.append((key, src, old, new, [a for a in new if fold([a])[0] not in fold(old)], [a for a in old if fold([a])[0] not in fold(new)])) else: unchanged.append(key) accepted[key] = new print(f"USENIX keys audited : {len(files)}") print(f" author list unchanged : {len(unchanged)}") print(f" author list CORRECTED : {len(rows)}") print(f" overruled by the paper/DBLP : {len(adjudged)}") print(f" unresolved, needs a human : {len(problems)}") print(f" of the corrected, missing an author : " f"{sum(1 for r in rows if r[4])}") print(f" of the corrected, carried a non-author : " f"{sum(1 for r in rows if r[5])}") print("=" * 78) for key, src, old, new, dropped, spurious in rows: print(f"\n{key} [{src}]") print(f" was : {'; '.join(old)}") print(f" now : {'; '.join(new)}") if dropped: print(f" + AUTHOR(S) RESTORED : {'; '.join(dropped)}") if spurious: print(f" - NON-AUTHOR(S) REMOVED : {'; '.join(spurious)}") if adjudged: print("\n" + "=" * 78 + "\nADJUDICATED against a source outside usenix.org:") for key, why in adjudged: print(f"\n {key}\n {why}") if problems: print("\n" + "=" * 78 + "\nUNRESOLVED — needs a human:") for key, why, old, meta, bib in problems: print(f"\n {key}\n {why}") print(f" cache : {'; '.join(old)}") print(f" meta : {'; '.join(meta)}") print(f" bibtex: {'; '.join(bib)}") if write: if problems: print("\nREFUSING to write: unresolved papers above.") return 1 full = json.load(open(CACHE)) full.update(accepted) json.dump(full, open(CACHE, "w"), indent=1, sort_keys=True) print(f"\nwrote {len(accepted)} USENIX entries to {CACHE}") return 1 if problems else 0 if __name__ == "__main__": sys.exit(selftest() if "--selftest" in sys.argv else main())
Real output, diffed against the pre-run snapshot
out/usenix_audit/authors.json.before:
USENIX keys audited : 90
author list unchanged : 72
author list CORRECTED : 18
overruled by the paper/DBLP : 3
unresolved, needs a human : 0
of the corrected, missing an author : 0
of the corrected, carried a non-author : 18
==============================================================================
USENIX/2012/an-evaluation-of-the-google-chrome-extension-security-architecture [meta+bibtex]
was : Nicholas Carlini; Adrienne Porter Felt; David Wagner; Berkeley
now : Nicholas Carlini; Adrienne Porter Felt; David Wagner
- NON-AUTHOR(S) REMOVED : Berkeley
USENIX/2012/privilege-separation-in-html5-applications [meta+bibtex]
was : Devdatta Akhawe; Prateek Saxena; Dawn Song; Berkeley
now : Devdatta Akhawe; Prateek Saxena; Dawn Song
- NON-AUTHOR(S) REMOVED : Berkeley
USENIX/2014/hulk-eliciting-malicious-behavior-in-browser-extensions [meta+bibtex]
was : Alexandros Kapravelos; Santa Barbara; Chris Grier; Berkeley; Neha Chachra; San Diego; Christopher Kruegel; Giovanni Vigna; Santa Barbara; Vern Paxson; Berkeley
now : Alexandros Kapravelos; Chris Grier; Neha Chachra; Christopher Kruegel; Giovanni Vigna; Vern Paxson
- NON-AUTHOR(S) REMOVED : Santa Barbara; Berkeley; San Diego; Santa Barbara; Berkeley
USENIX/2017/extension-breakdown-security-analysis-of-browsers-extension-resources-control-po [meta+bibtex]
was : Iskander Sanchez-Rola; Igor Santos; DeustoTech; Davide Balzarotti; Eurecom
now : Iskander Sanchez-Rola; Igor Santos; Davide Balzarotti
- NON-AUTHOR(S) REMOVED : DeustoTech; Eurecom
USENIX/2019/50-ways-to-leak-your-data-an-exploration-of-apps-circumvention-of-the-android-pe [meta+bibtex]
was : Joel Reardon; Álvaro Feal; Primal Wijesekera; U.C. Berkeley / ICSI; Amit Elazari Bar On; U.C. Berkeley; Narseo Vallina-Rodriguez; Serge Egelman
now : Joel Reardon; Álvaro Feal; Primal Wijesekera; Amit Elazari Bar On; Narseo Vallina-Rodriguez; Serge Egelman
- NON-AUTHOR(S) REMOVED : U.C. Berkeley / ICSI; U.C. Berkeley
USENIX/2020/measuring-and-modeling-the-label-dynamics-of-online-anti-malware-engines [meta+bibtex]
was : Shuofei Zhu; Jianjun Shi; BIT; Limin Yang; Boqin Qin; BUPT; Ziyi Zhang; USTC; Linhai Song; Gang Wang
now : Shuofei Zhu; Jianjun Shi; Limin Yang; Boqin Qin; Ziyi Zhang; Linhai Song; Gang Wang
- NON-AUTHOR(S) REMOVED : BIT; BUPT; USTC
USENIX/2020/phishtime-continuous-longitudinal-measurement-of-the-effectiveness-of-anti-phish [meta+bibtex +adjudicated]
was : Adam Oest; Yeganeh Safaei; Penghui Zhang; Brad Wardman; Kevin Tyers; PayPal; Yan Shoshitaishvili; Adam Doupé; Gail-Joon Ahn
now : Adam Oest; Yeganeh Safaei; Penghui Zhang; Brad Wardman; Kevin Tyers; Yan Shoshitaishvili; Adam Doupé; Gail-Joon Ahn
- NON-AUTHOR(S) REMOVED : PayPal
USENIX/2020/sunrise-to-sunset-analyzing-the-end-to-end-life-cycle-and-effectiveness-of-phish [meta+bibtex]
was : Adam Oest; Penghui Zhang; Brad Wardman; Eric Nunes; Jakub Burgis; PayPal; Ali Zand; Kurt Thomas; Adam Doupé; Gail-Joon Ahn
now : Adam Oest; Penghui Zhang; Brad Wardman; Eric Nunes; Jakub Burgis; Ali Zand; Kurt Thomas; Adam Doupé; Gail-Joon Ahn
- NON-AUTHOR(S) REMOVED : PayPal
USENIX/2021/phishpedia-a-hybrid-deep-learning-based-approach-to-visually-identify-phishing-w [meta+bibtex]
was : Yun Lin; Ruofan Liu; Dinil Mon Divakaran; Trustwave; Jun Yang Ng; Qing Zhou Chan; Yiwen Lu; Yuxuan Si; Fan Zhang; Jin Song Dong
now : Yun Lin; Ruofan Liu; Dinil Mon Divakaran; Jun Yang Ng; Qing Zhou Chan; Yiwen Lu; Yuxuan Si; Fan Zhang; Jin Song Dong
- NON-AUTHOR(S) REMOVED : Trustwave
USENIX/2022/inferring-phishing-intention-via-webpage-appearance-and-dynamics-a-deep-vision-b [meta+bibtex]
was : Ruofan Liu; Yun Lin; Xianglin Yang; Siang Hwee Ng; Dinil Mon Divakaran; Trustwave; Jin Song Dong
now : Ruofan Liu; Yun Lin; Xianglin Yang; Siang Hwee Ng; Dinil Mon Divakaran; Jin Song Dong
- NON-AUTHOR(S) REMOVED : Trustwave
USENIX/2023/glowing-in-the-dark-uncovering-ipv6-address-discovery-and-scanning-strategies-in [meta+bibtex]
was : Hammas Bin Tanveer; Rachee Singh; Paul Pearce; Georgia Tech; Rishab Nithyanand
now : Hammas Bin Tanveer; Rachee Singh; Paul Pearce; Rishab Nithyanand
- NON-AUTHOR(S) REMOVED : Georgia Tech
USENIX/2023/log-it-s-big-it-s-heavy-it-s-filled-with-personal-data-measuring-the-logging-of [meta+bibtex]
was : Allan Lyons; Julien Gamba; Austin Shawaga; Joel Reardon; AppCensus; Juan Tapiador; Universidad Carlos III de Madrid; Serge Egelman; ICSI; UC Berkeley; AppCensus; Narseo Vallina-Rodriguez; AppCensus
now : Allan Lyons; Julien Gamba; Austin Shawaga; Joel Reardon; Juan Tapiador; Serge Egelman; Narseo Vallina-Rodriguez
- NON-AUTHOR(S) REMOVED : AppCensus; Universidad Carlos III de Madrid; ICSI; UC Berkeley; AppCensus; AppCensus
USENIX/2023/rods-with-laser-beams-understanding-browser-fingerprinting-on-phishing-pages [meta+bibtex]
was : Iskander Sanchez-Rola; Leyla Bilge; Davide Balzarotti; EURECOM; Armin Buescher; Petros Efstathopoulos
now : Iskander Sanchez-Rola; Leyla Bilge; Davide Balzarotti; Armin Buescher; Petros Efstathopoulos
- NON-AUTHOR(S) REMOVED : EURECOM
USENIX/2023/the-ok-is-not-enough-a-large-scale-study-of-consent-dialogs-in-smartphone-applic [meta+bibtex]
was : Simon Koch; Benjamin Altpeter; Datenanfragen.de e.V.; Martin Johns
now : Simon Koch; Benjamin Altpeter; Martin Johns
- NON-AUTHOR(S) REMOVED : Datenanfragen.de e.V.
USENIX/2024/knowphish-large-language-models-meet-multimodal-knowledge-graphs-for-enhancing-r [meta+bibtex]
was : Yuexin Li; Chengyu Huang; Shumin Deng; Mei Lin Lock; NCS Cyber Special Ops-R&D; Tri Cao; Nay Oo; Hoon Wei Lim; NCS Cyber Special Ops-R&D; Bryan Hooi
now : Yuexin Li; Chengyu Huang; Shumin Deng; Mei Lin Lock; Tri Cao; Nay Oo; Hoon Wei Lim; Bryan Hooi
- NON-AUTHOR(S) REMOVED : NCS Cyber Special Ops-R&D; NCS Cyber Special Ops-R&D
USENIX/2024/the-effect-of-design-patterns-on-present-and-future-cookie-consent-decisions [meta+bibtex]
was : Nataliia Bielova; Laura Litvine; Anysia Nguyen; Behavioural Insights Team; Mariam Chammat; Interministerial Directorate for Public Transformation; Vincent Toubiana; Estelle Hary
now : Nataliia Bielova; Laura Litvine; Anysia Nguyen; Mariam Chammat; Vincent Toubiana; Estelle Hary
- NON-AUTHOR(S) REMOVED : Behavioural Insights Team; Interministerial Directorate for Public Transformation
USENIX/2025/big-help-or-big-brother-auditing-tracking-profiling-and-personalization-in-gener [meta+bibtex]
was : Yash Vekaria; UC Davis; Aurelio Loris Canino; UNIRC; Jonathan Levitsky; UC Davis; Alex Ciechonski; UCL; Patricia Callejo; UC3M; Anna Maria Mandalari; UCL; Zubair Shafiq; UC Davis
now : Yash Vekaria; Aurelio Loris Canino; Jonathan Levitsky; Alex Ciechonski; Patricia Callejo; Anna Maria Mandalari; Zubair Shafiq
- NON-AUTHOR(S) REMOVED : UC Davis; UNIRC; UC Davis; UCL; UC3M; UCL; UC Davis
USENIX/2025/privacy-law-enforcement-under-centralized-governance-a-qualitative-analysis-of-f [meta+bibtex]
was : Tao Jing; Engineering; Yao Li; Jingzhou Ye; Jie Wang; Engineering; Xueqiang Wang
now : Tao Jing; Yao Li; Jingzhou Ye; Jie Wang; Xueqiang Wang
- NON-AUTHOR(S) REMOVED : Engineering; Engineering
==============================================================================
ADJUDICATED against a source outside usenix.org:
USENIX/2012/pharmaleaks-understanding-the-business-of-online-pharmaceutical-affiliate-progra
USENIX meta/BibTeX say 'Jordan Grant' and 'Geoffrey Voelker'. The paper PDF title block (data/fulltext/2012/USENIX/pharmaleaks-.../paper.cols.txt) and DBLP both say 'Grant Jordan' and 'Geoffrey M. Voelker'. Paper wins.
USENIX/2020/phishtime-continuous-longitudinal-measurement-of-the-effectiveness-of-anti-phish
USENIX meta AND BibTeX both stop at 7 authors. The prose blob on the same page and the paper's own title block carry Gail-Joon Ahn as the 8th. DBLP does NOT: record conf/uss/OestSZWTSD20, checked 2026-09-04, also lists seven and stops at Adam Doupe. Three machine-readable sources agree on a list the paper contradicts. Paper wins.
USENIX/2025/vulnerability-of-text-matching-in-ml-ai-conference-reviewer-assignments-to-collu
The paper's title block and USENIX's meta both print the nickname, 'Jhih-Yi (Janet) Hsieh'. Normalised to DBLP's form: a parenthetical is not a name particle and BibTeX would parse it as part of the given name. Judgement call, not a correction of the source.
Step 3 — diff the live bibliography
- usenix_bib_diff.py
#!/usr/bin/env python3 """Step 3 of the USENIX author-list audit: diff literature:bibliography against the corrected out/authors.json, and flag any USENIX entry the audit cannot see. Two passes, because they catch different things: PASS 1 title-match every USENIX key in out/authors.json to a live BibTeX entry and diff the author field. This finds entries the broken prose parser wrote. PASS 2 scan EVERY live entry in a USENIX Security series for an author name that is affiliation-shaped, whether or not it is in out/authors.json. Entries added by hand or from another source are invisible to pass 1, and the same class of defect can reach them. PASS 3 compare EVERY live USENIX Security entry against the author list its own landing page publishes, as an ORDERED list. Pass 2 can only see an affiliation that got IN; only this pass can see an author that fell OUT of, or was reordered within, an entry out/authors.json never touched. Needs the pages fetched by scripts/usenix_bib_fetch_all.py. python3 scripts/usenix_bib_diff.py [--bib out/usenix_audit/bib_live.txt] Prints a corrected author line per affected entry; it does not edit the wiki. """ import glob import json import os import re import sys import unicodedata sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) from usenix_author_audit import (de_latex, meta_authors, bibtex_authors, italic_authors, prose_blob, prose_parse, page_institutions, surnames, ADJUDICATED) from usenix_author_audit import fold as afold CACHE = "out/authors.json" ROOT = "/workspace/publications_dataset/data" DEFAULT_BIB = "out/usenix_audit/bib_live.txt" def fold(s): s = unicodedata.normalize("NFKD", s) s = "".join(c for c in s if not unicodedata.combining(c)) return re.sub(r"\s+", " ", re.sub(r"[^a-z0-9 ]", " ", s.lower())).strip() def squash(s): return re.sub(r"[^a-z0-9]", "", fold(s)) def field(entry, name): m = re.search(r"\b" + name + r"\s*=\s*\{", entry) if not m: return None depth, i = 0, m.end() - 1 while i < len(entry): if entry[i] == "{": depth += 1 elif entry[i] == "}": depth -= 1 if depth == 0: return re.sub(r"\s+", " ", entry[m.end():i]).strip() i += 1 raise ValueError("unbalanced braces in " + entry[:60]) def split_bib_authors(a): """'Last, First and Last, First' -> ['First Last', ...]""" out = [] for n in re.split(r"\s+and\s+", a): n = n.strip() if not n: continue if "," in n: last, first = n.split(",", 1) n = f"{first.strip()} {last.strip()}".strip() # 43 of the 837 live entries spell an accent with a LaTeX accent # command and 66 with a literal non-ASCII letter (see # scripts/bib_accent_encoding.py). Both render correctly through # bibtex4dw: checked on the rendered DOM of security:phishing, which # cites the braced "Doup{\'e}" entries, and privacy:browser_extensions, # which cites the one unbraced "Doup\'e" entry. Decode before diffing, # or those three entries drown a real defect in encoding noise. out.append(re.sub(r"\s+", " ", de_latex(n))) return out def fmt_bib(names): out = [] for a in names: parts = a.strip().split() last = parts.pop() out.append(f"{last}, {' '.join(parts)}" if parts else last) return " and ".join(out) # An author name that is really an affiliation. Deliberately wide: every hit is # printed in full for a human, nothing is auto-removed on the strength of it. AFFIL_SHAPED = re.compile( r"universit|universidad|institut|college|\binc\b|\blabs?\b|corporat|" r"technolog|\bresearch\b|school|academy|center|centre|gmbh|\bltd\b|" r"company|cispa|kaist|inria|microsoft|\bgoogle\b|\bapple\b|\bamazon\b|" r"\bibm\b|\bepfl\b|\beth\b|laborator|foundation|agency|department|" r"polytechnic|\bpaypal\b|trustwave|appcensus|\bicsi\b|\bteam\b|\be\.?\s?v\.?$", re.I) SINGLE_TOKEN_OK = {"anonymous"} def main(): bibpath = sys.argv[sys.argv.index("--bib") + 1] if "--bib" in sys.argv \ else DEFAULT_BIB text = open(bibpath, encoding="utf-8").read() entries = re.findall(r"@\w+\{[^@]*?\n\}", text, re.S) print(f"bibliography : {bibpath}") print(f"entries parsed : {len(entries)}") by_title = {} for e in entries: t = field(e, "title") if t: by_title.setdefault(squash(t), []).append(e) titles = {} for f in glob.glob(os.path.join(ROOT, "corpus2/.meta/*.json")): for p in json.load(open(f))["papers"]: titles[f"{p['venue']}/{p['year']}/{p['slug']}"] = p["title"] cache = json.load(open(CACHE)) keys = sorted(k for k in cache if k.startswith("USENIX/")) print(f"USENIX keys in {CACHE}: {len(keys)}\n") # ---------------------------------------------------------------- PASS 1 matched, wrong, absent = 0, [], [] for k in keys: cands = by_title.get(squash(titles[k]), []) if not cands: absent.append(k) continue matched += 1 for e in cands: key = re.match(r"@\w+\{([^,]+),", e).group(1) have = split_bib_authors(field(e, "author")) want = cache[k] if [fold(x) for x in have] != [fold(x) for x in want]: wrong.append((key, k, have, want)) print("=" * 78) print(f"PASS 1 title-matched to the bibliography : {matched}/{len(keys)}") print(f" author field WRONG : {len(wrong)}") print(f" key not cited on the wiki at all : {len(absent)}") for key, k, have, want in wrong: print(f"\n {{[{key}]}} ({k})") print(f" live : {'; '.join(have)}") print(f" corrected : {'; '.join(want)}") print(f" author = {{{fmt_bib(want)}}},") # ---------------------------------------------------------------- PASS 2 print("\n" + "=" * 78) flagged = [] us = 0 for e in entries: series = (field(e, "series") or "") + " " + (field(e, "booktitle") or "") if not re.search(r"USENIX Security", series, re.I): continue us += 1 names = split_bib_authors(field(e, "author") or "") bad = [n for n in names if AFFIL_SHAPED.search(n) or (len(n.split()) < 2 and fold(n) not in SINGLE_TOKEN_OK)] if bad: key = re.match(r"@\w+\{([^,]+),", e).group(1) flagged.append((key, names, bad)) print(f"PASS 2 live entries in a USENIX Security series : {us}") print(f" entries with an affiliation-shaped author: {len(flagged)}") for key, names, bad in flagged: print(f"\n {{[{key}]}}") print(f" authors : {'; '.join(names)}") print(f" FLAGGED : {'; '.join(bad)}") # ---------------------------------------------------------------- PASS 3 print("\n" + "=" * 78) tpath = "out/usenix_audit/bib_targets.json" if not os.path.exists(tpath): print("PASS 3 SKIPPED: run scripts/usenix_bib_fetch_all.py first") else: targets = json.load(open(tpath)) by_ckey = {re.match(r"@\w+\{([^,]+),", e).group(1): e for e in entries} checked, p3_wrong, p3_skip = 0, [], [] for ckey, (k, _url, title) in sorted(targets.items()): h = os.path.join("out/usenix_audit/html", k.replace("/", "__").replace(":", "__") + ".html") if not os.path.exists(h): p3_skip.append((ckey, "page not fetched")) continue page = open(h, encoding="utf-8", errors="replace").read() m, b = meta_authors(page), bibtex_authors(page) page_authors = m or b or italic_authors(page, title) if not page_authors: p3_skip.append((ckey, "no author list on the page")) continue if k in ADJUDICATED: page_authors = ADJUDICATED[k][0] checked += 1 have = split_bib_authors(field(by_ckey[ckey], "author")) hs, ps = {fold(x) for x in have}, {fold(x) for x in page_authors} missing = sorted(ps - hs) extra = sorted(hs - ps) # Set difference alone is NOT enough. An earlier version compared # only the sets, with a comment claiming it also checked order — # it did not, and a review on 2026-09-04 demonstrated the gap by # swapping first and last author on a pass-3-only entry: all three # passes reported clean. Author ORDER is first authorship; a swap # is a worse citation defect than a missing middle author. reordered = (not missing and not extra and [fold(x) for x in have] != [fold(x) for x in page_authors]) if missing or extra or reordered: p3_wrong.append((ckey, k, have, page_authors, missing, extra, reordered)) print(f"PASS 3 live entries checked against their landing page : " f"{checked}/{len(targets)}") print(f" author SET differs from the page : " f"{sum(1 for r in p3_wrong if r[4] or r[5])}") print(f" same set, WRONG ORDER : " f"{sum(1 for r in p3_wrong if r[6])}") print(f" skipped : " f"{len(p3_skip)}") for ckey, k, have, want, missing, extra, reordered in p3_wrong: print(f"\n {{[{ckey}]}} ({k})") print(f" live : {'; '.join(have)}") print(f" page : {'; '.join(want)}") if missing: print(f" ON THE PAGE, NOT IN THE ENTRY : {'; '.join(missing)}") if extra: print(f" IN THE ENTRY, NOT ON THE PAGE : {'; '.join(extra)}") if reordered: print(f" SAME NAMES, DIFFERENT ORDER — check first authorship") for ckey, why in p3_skip: print(f" skipped {ckey}: {why}") if absent: print("\n" + "=" * 78) print("Keys in out/authors.json that no live entry cites " "(fetched for the audit, never published):") for k in absent: print(f" {k}") return 0 if __name__ == "__main__": sys.exit(main())
bibliography : out/usenix_audit/bib_live.txt
entries parsed : 837
USENIX keys in out/authors.json: 90
==============================================================================
PASS 1 title-matched to the bibliography : 85/90
author field WRONG : 0
key not cited on the wiki at all : 5
==============================================================================
PASS 2 live entries in a USENIX Security series : 159
entries with an affiliation-shaped author: 0
==============================================================================
PASS 3 live entries checked against their landing page : 159/159
author SET differs from the page : 0
same set, WRONG ORDER : 0
skipped : 0
==============================================================================
Keys in out/authors.json that no live entry cites (fetched for the audit, never published):
USENIX/2022/inferring-phishing-intention-via-webpage-appearance-and-dynamics-a-deep-vision-b
USENIX/2022/ovrseen-auditing-network-traffic-and-privacy-policies-in-oculus-vr
USENIX/2023/glowing-in-the-dark-uncovering-ipv6-address-discovery-and-scanning-strategies-in
USENIX/2023/knowledge-expansion-and-counterfactual-interaction-for-reference-based-phishing
USENIX/2024/knowphish-large-language-models-meet-multimodal-knowledge-graphs-for-enhancing-r
Mutation harness
The mutated bibliographies it runs against are generated, not hand-made:
- make_mutated_bibs.py
#!/usr/bin/env python3 """Build the mutated bibliographies scripts/usenix_author_audit_mutations.py runs against, so the mutation set is reproducible instead of a set of files someone once made by hand. Each writes out/usenix_audit/mut/<id>.bib, a copy of the live snapshot with one defect injected: b1 an author DELETED from an entry that is in out/authors.json b2 an affiliation ADDED as an author, same entry b3 an affiliation ADDED to an entry that is NOT in out/authors.json b4 one author's given and family names SWAPPED b5 the author ORDER reversed at both ends of a NON-cached entry — the defect that passed every check until 2026-09-04 b6 an author DELETED from a NON-cached entry. b1 does this to a CACHED entry, where pass 1 catches it; b6 is the case pass 3 exists for, and nothing else on this page can see it python3 scripts/make_mutated_bibs.py """ import json import os import re import sys sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) from usenix_bib_diff import field, split_bib_authors SRC = "out/usenix_audit/bib_live.txt" DEST = "out/usenix_audit/mut" CACHED = "author = {Carlini, Nicholas and Felt, Adrienne Porter and Wagner, David}," NONCACHED = ("author={Xie, Qinge and Tang, Shujun and Zheng, Xiaofeng and " "Lin, Qingran and Liu, Baojun and Duan, Haixin and Li, Frank}") SWAP_FROM = "author = {McCoy, Damon and Pitsillidis, Andreas and Jordan, Grant and" B5_KEY = "acharya2024_imitation" MUTS = { "b1": (CACHED, "author = {Carlini, Nicholas and Felt, Adrienne Porter},"), "b2": (CACHED, "author = {Carlini, Nicholas and Felt, Adrienne Porter and " "Wagner, David and Berkeley},"), "b3": (NONCACHED, NONCACHED[:-1] + " and Georgia Institute of Technology}"), "b4": (SWAP_FROM, "author = {McCoy, Damon and Pitsillidis, Andreas and " "Grant, Jordan and"), } def main(): bib = open(SRC, encoding="utf-8").read() os.makedirs(DEST, exist_ok=True) for name, (a, b) in MUTS.items(): if a not in bib: raise SystemExit(f"{name}: anchor not in {SRC}; the snapshot moved") open(os.path.join(DEST, name + ".bib"), "w").write(bib.replace(a, b, 1)) print(f"wrote {name}.bib") # b5 is derived rather than hardcoded, so it stays valid if the entry moves. ents = {re.match(r"@\w+\{([^,]+),", e).group(1): e for e in re.findall(r"@\w+\{[^@]*?\n\}", bib, re.S)} orig = field(ents[B5_KEY], "author") parts = re.split(r"\s+and\s+", orig) if len(parts) < 3: raise SystemExit(f"{B5_KEY} has too few authors to reorder") swapped = " and ".join([parts[-1]] + parts[1:-1] + [parts[0]]) if "{" + orig + "}" not in bib: raise SystemExit(f"b5: could not locate {B5_KEY}'s author field") open(os.path.join(DEST, "b5.bib"), "w").write( bib.replace("{" + orig + "}", "{" + swapped + "}", 1)) print(f"wrote b5.bib ({B5_KEY}: first and last author swapped)") print(f" from: {orig}") print(f" to : {swapped}") dropped = " and ".join(parts[:-1]) open(os.path.join(DEST, "b6.bib"), "w").write( bib.replace("{" + orig + "}", "{" + dropped + "}", 1)) print(f"wrote b6.bib ({B5_KEY}: last author deleted — {parts[-1]})") if __name__ == "__main__": sys.exit(main())
- usenix_author_audit_mutations.py
#!/usr/bin/env python3 """Mutation-test scripts/usenix_author_audit.py and scripts/usenix_bib_diff.py. Both scripts report "0 unresolved" and "0 defects", and a check that passes is worth nothing until it has been shown it can fail. Each mutation below breaks exactly one load-bearing rule; a mutation that does NOT change the reported counts is a rule that is asserting nothing. The audit mutations diff against the PRE-FIX snapshot (out/usenix_audit/authors.json.before), because against the already-corrected out/authors.json every run reports 90 unchanged and no mutation can move it. python3 scripts/usenix_author_audit_mutations.py """ import os import re import subprocess import sys BEFORE = "out/usenix_audit/authors.json.before" LIVEBIB = "out/usenix_audit/bib_live.txt" MUTBIB = "out/usenix_audit/mut" TMP = "scripts/_mut_tmp.py" AUDIT = "scripts/usenix_author_audit.py" AUDIT_MUTS = [ ("A1 citation_author parser truncated to 1 author", 'return [unesc(m.group(1)) for m in re.finditer(\n' ' r\'<meta\\s+name="citation_author"\\s+content="([^"]*)"\', page)\n' ' if unesc(m.group(1))]', 'return [unesc(m.group(1)) for m in list(re.finditer(\n' ' r\'<meta\\s+name="citation_author"\\s+content="([^"]*)"\', page))[:1]\n' ' if unesc(m.group(1))]'), # A2 must ALSO empty the adjudication table. On its own it changes nothing, # because every paper the cross-check fires on is already adjudicated and # the adjudication branch runs first — the check is a tripwire for the next # corpus refresh, not a live assertion today. Emptying both shows what it # is worth: without it, PharmaLeaks silently regresses to the wrong # "Jordan Grant" (corrected goes 18 -> 20) instead of stopping the run. ("A2 prose cross-check disabled *and* adjudication emptied", [(" elif missing_from_prose or prose_only:", " elif False and (missing_from_prose or prose_only):"), ('ADJUDICATED = {\n "USENIX/2012/pharmaleaks', 'ADJUDICATED = {}\n_UNUSED = {\n "USENIX/2012/pharmaleaks')], None), ("A3 BibTeX brace balancing broken", " return s[start + 1:i]", " return s[start + 1:start + 8]"), ("A4 legacy block no longer matched by title", " if t and squash(unesc(t.group(1))) == squash(title):", " if t:"), ("A5 diacritic folding removed", ' s = "".join(c for c in s if not unicodedata.combining(c))', " pass"), ("A6 adjudication table emptied", 'ADJUDICATED = {\n "USENIX/2012/pharmaleaks', 'ADJUDICATED = {}\n_UNUSED = {\n "USENIX/2012/pharmaleaks'), ("A7 institution list ignored (prose residue no longer filtered)", " if not f or (inst and f in inst):", " if not f:"), ] # The mutated bibliographies under out/usenix_audit/mut/ are built by # scripts/make_mutated_bibs.py, so the mutation set is reproducible rather than # a set of files someone once made by hand. def field(out, pat): m = re.search(pat, out) return m.group(1) if m else "?" def run_audit(path, extra=()): r = subprocess.run([sys.executable, path, "--cache", BEFORE, *extra], capture_output=True, text=True) return (r.returncode, field(r.stdout, r"unchanged +: (\d+)"), field(r.stdout, r"CORRECTED +: (\d+)"), field(r.stdout, r"unresolved, needs a human +: (\d+)")) def run_diff(bib): r = subprocess.run([sys.executable, "scripts/usenix_bib_diff.py", "--bib", bib], capture_output=True, text=True) return (field(r.stdout, r"author field WRONG +: (\d+)"), field(r.stdout, r"affiliation-shaped author: (\d+)"), field(r.stdout, r"author SET differs from the page +: (\d+)"), field(r.stdout, r"same set, WRONG ORDER +: (\d+)")) def main(): src = open(AUDIT).read() print(f"=== {AUDIT} — diffed against {BEFORE}") print("%-58s %-4s %-10s %-10s %s" % ("mutation", "rc", "unchanged", "corrected", "unresolved")) rc, u, c, p = run_audit(AUDIT) print("%-58s %-4s %-10s %-10s %s" % ("(baseline, unmutated)", rc, u, c, p)) bad = [] for name, a, b in AUDIT_MUTS: steps = a if isinstance(a, list) else [(a, b)] mutated, stale = src, False for fro, to in steps: if fro not in mutated: stale = True break mutated = mutated.replace(fro, to, 1) if stale: print(f"{name}: PATTERN NOT FOUND — mutation is stale") bad.append(name) continue open(TMP, "w").write(mutated) rc2, u2, c2, p2 = run_audit(TMP) print("%-58s %-4s %-10s %-10s %s" % (name, rc2, u2, c2, p2)) if (u2, c2, p2) == (u, c, p): bad.append(name) if os.path.exists(TMP): os.remove(TMP) print(f"\n=== scripts/usenix_bib_diff.py — mutated BIBLIOGRAPHIES") print("%-58s %-7s %-7s %-7s %s" % ("mutation", "PASS1", "PASS2", "PASS3set", "PASS3order")) base = run_diff(LIVEBIB) print("%-58s %-7s %-7s %-7s %s" % ("(baseline, unmutated)", *base)) for name, f in [("B1 author deleted from a cached entry", "b1"), ("B2 affiliation added to a cached entry", "b2"), ("B3 affiliation added to a NON-cached entry", "b3"), ("B4 given/family names swapped", "b4"), ("B5 author ORDER swapped, NON-cached entry", "b5"), ("B6 author DELETED from a NON-cached entry", "b6")]: path = os.path.join(MUTBIB, f + ".bib") if not os.path.exists(path): print(f"{name}: {path} missing") bad.append(name) continue got = run_diff(path) print("%-58s %-7s %-7s %-7s %s" % (name, *got)) if got == base: bad.append(name) print(f"\nmutations that changed nothing (a rule asserting nothing): " f"{len(bad)}" + (f" {bad}" if bad else "")) return 1 if bad else 0 if __name__ == "__main__": sys.exit(main())
Source-agreement reconstruction
- usenix_source_agreement.py
#!/usr/bin/env python3 """Measure how often USENIX's two structured author sources disagree, under the three parser states this audit went through. The provenance page claimed "13 of the first 90 disagreed" from a run whose script no longer exists, which is not a reproducible figure. This reconstructs the comparison from the same cached HTML under each state, so the number on the page is one anybody can re-derive: A non-greedy brace match, no diacritic fold — the audit's first, buggy state B brace-balanced, no diacritic fold — after the brace fix only C brace-balanced, diacritic-folded — what ships python3 scripts/usenix_source_agreement.py """ import glob import html import json import os import re import sys sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) from usenix_author_audit import (bibtex_authors, de_latex, fold, meta_authors) HTMLDIR = "out/usenix_audit/html" def bibtex_authors_nongreedy(page): """The original bug: \\{(.*?)\\} ends at the first inner brace.""" m = re.search(r"bibtex-text-entry[^>]*>(.*?)</div>", page, re.S) if not m: return [] block = html.unescape(re.sub(r"<br\s*/?>", "\n", m.group(1))) a = re.search(r"author\s*=\s*\{(.*?)\}", block, re.S) if not a: return [] raw = re.sub(r"\s+", " ", a.group(1)).strip() return [de_latex(n).strip() for n in re.split(r"\s+and\s+", raw) if n.strip()] def main(): cached = sorted(k for k in json.load(open("out/authors.json")) if k.startswith("USENIX/")) allpages = sorted(glob.glob(os.path.join(HTMLDIR, "*.html"))) pops = [("the 90 cached author lists", [os.path.join(HTMLDIR, k.replace("/", "__") + ".html") for k in cached]), ("all 162 fetched landing pages", allpages)] for label, files in pops: rows = {"A non-greedy brace, no fold": 0, "B brace-balanced, no fold": 0, "C brace-balanced + diacritic fold (ships)": 0} both = 0 cases = {k: [] for k in rows} for f in files: page = open(f, encoding="utf-8", errors="replace").read() m = meta_authors(page) if not m: continue bg, bn = bibtex_authors(page), bibtex_authors_nongreedy(page) if not bg: continue both += 1 key = os.path.basename(f)[:-5].replace("__", "/") if [x.strip() for x in m] != [x.strip() for x in bn]: rows["A non-greedy brace, no fold"] += 1 cases["A non-greedy brace, no fold"].append(key) if [x.strip() for x in m] != [x.strip() for x in bg]: rows["B brace-balanced, no fold"] += 1 cases["B brace-balanced, no fold"].append(key) if fold(m) != fold(bg): rows["C brace-balanced + diacritic fold (ships)"] += 1 cases["C brace-balanced + diacritic fold (ships)"].append(key) print(f"=== {label}: {both} carry BOTH citation_author and a BibTeX block") for k, v in rows.items(): print(f" {k:<46} disagree on {v}") rem = cases["C brace-balanced + diacritic fold (ships)"] print(f" still disagreeing under C: {rem if rem else 'none'}") onlyB = [x for x in cases["B brace-balanced, no fold"] if x not in rem] print(f" fixed by the diacritic fold alone: " f"{[x.split('/')[-1][:40] for x in onlyB] or 'none'}") print() return 0 if __name__ == "__main__": sys.exit(main())
=== the 90 cached author lists: 87 carry BOTH citation_author and a BibTeX block
A non-greedy brace, no fold disagree on 11
B brace-balanced, no fold disagree on 2
C brace-balanced + diacritic fold (ships) disagree on 0
still disagreeing under C: none
fixed by the diacritic fold alone: ['iot-market-dynamics-an-analysis-of-devic', 'addressing-the-address-books-interdepend']
=== all 162 fetched landing pages: 155 carry BOTH citation_author and a BibTeX block
A non-greedy brace, no fold disagree on 17
B brace-balanced, no fold disagree on 4
C brace-balanced + diacritic fold (ships) disagree on 0
still disagreeing under C: none
fixed by the diacritic fold alone: ['vulnerability-disclosure-in-the-age-of-s', 'dissecting-privacy-perspectives-of-websi', 'iot-market-dynamics-an-analysis-of-devic', 'addressing-the-address-books-interdepend']
Accent-encoding count
- bib_accent_encoding.py
#!/usr/bin/env python3 """Count how literature:bibliography spells accented author names. The audit needs this because the author-field diff has to decode LaTeX accents before comparing, and the reason it has to is that the file uses BOTH conventions. That was asserted in prose on the provenance page with no script behind it; three independent counters then produced three different splits (44/67, 43/66, 45/66), because "has a LaTeX escape" and "has a non-ASCII letter" are not complements and the edge cases sit in neither or both. This defines the buckets explicitly and prints the edge cases instead of hiding them. Only the AUTHOR field is counted: an accent in a title has no bearing on the author diff. python3 scripts/bib_accent_encoding.py [--bib out/usenix_audit/bib_live.txt] """ import os import re import sys sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) from usenix_bib_diff import field # A LaTeX ACCENT command: \'e, \"u, \c{s}, \v{c}, \~a, \^i, \=o, \.z, \u{a}, # \H{o}, \r{a}, braced or not. Deliberately NOT \ss, \o, \aa, \O, \AA — those # are ligature/letter commands, not accents, and they are reported separately # because they are exactly what made the three earlier counts disagree. ACCENT = re.compile(r"\\[\'\"`^~=.]|\\[cvuHr]\s*\{") LETTER_CMD = re.compile(r"\\(ss|o|O|aa|AA|l|L|ae|AE|oe|OE)\b|\\\{?(ss|o|O|aa|AA)\}") NONASCII = re.compile(r"[^\x00-\x7f]") def main(): bib = sys.argv[sys.argv.index("--bib") + 1] if "--bib" in sys.argv \ else "out/usenix_audit/bib_live.txt" entries = re.findall(r"@\w+\{[^@]*?\n\}", open(bib, encoding="utf-8").read(), re.S) n = len(entries) acc, uni, cmd, both, acc_cmd, neither = [], [], [], [], [], 0 for e in entries: key = re.match(r"@\w+\{([^,]+),", e).group(1) a = field(e, "author") or "" A, U, C = bool(ACCENT.search(a)), bool(NONASCII.search(a)), \ bool(LETTER_CMD.search(a)) if A: acc.append(key) if U: uni.append(key) if C: cmd.append(key) if A and U: both.append(key) if A and C: acc_cmd.append(key) if not (A or U or C): neither += 1 print(f"bibliography : {bib}") print(f"entries : {n}") print(f"AUTHOR field has a LaTeX accent command : {len(acc)}") print(f"AUTHOR field has a literal non-ASCII letter : {len(uni)}") print(f"AUTHOR field has a LaTeX LETTER command : {len(cmd)} " f"(\\ss, \\o, \\aa — not accents)") print(f"AUTHOR field has BOTH accent and non-ASCII : {len(both)}") print(f"AUTHOR field has BOTH accent and letter command: {len(acc_cmd)}") print(f"AUTHOR field is plain ASCII, no commands : {neither}") print(f"AUTHOR field is plain ASCII, no commands : {neither}" if False else "", end="") print(f"\nThe buckets OVERLAP — do not add the rows up. Distinct entries " f"touched by any of the three: {len(set(acc) | set(uni) | set(cmd))}") print(f"Distinct entries using an accent command or a non-ASCII letter: " f"{len(set(acc) | set(uni))}") if both: print("\nMIXED — these entries spell one name each way:") for k in both: print(f" {k}") if cmd: print("\nLaTeX LETTER commands (the reason three earlier hand counts " "disagreed — they are neither accents nor non-ASCII):") for k in cmd: e = next(x for x in entries if x.startswith("@") and re.match(r"@\w+\{([^,]+),", x).group(1) == k) m = LETTER_CMD.search(field(e, "author")) print(f" {k:<32} {m.group(0)!r}") return 0 if __name__ == "__main__": sys.exit(main())
bibliography : out/usenix_audit/bib_live.txt entries : 837 AUTHOR field has a LaTeX accent command : 43 AUTHOR field has a literal non-ASCII letter : 66 AUTHOR field has a LaTeX LETTER command : 3 (\ss, \o, \aa — not accents) AUTHOR field has BOTH accent and non-ASCII : 0 AUTHOR field has BOTH accent and letter command: 1 AUTHOR field is plain ASCII, no commands : 726 The buckets OVERLAP — do not add the rows up. Distinct entries touched by any of the three: 111 Distinct entries using an accent command or a non-ASCII letter: 109 LaTeX LETTER commands (the reason three earlier hand counts disagreed — they are neither accents nor non-ASCII): demir2023_similarity '\\ss' mackinnon2023_clusterrobust '\\O' coopamootoo2017_forgotten '\\ss'
PoPETs DOI check
- popets_doi_check.py
#!/usr/bin/env python3 """Check every PoPETs DOI in literature:bibliography against the correct prefix boundary, and resolve each one. scripts/bibgen.mjs used to hardcode 10.56553 for all years. PoPETs has changed DOI prefix twice: 2015 - 2018 10.1515 (De Gruyter Open) 2019 - 2022-0058 10.2478 (De Gruyter / Sciendo) 2022-0059 onward 10.56553 (PoPETs' own prefix) so the hardcoded prefix produced a dead DOI for every PoPETs paper before 2022-0059. Fixed 2026-08-13. This checks what actually reached the wiki: a prefix that disagrees with the boundary is reported whether or not it resolves, and a DOI that does not resolve is reported whether or not the prefix is right — the two failures are independent. python3 scripts/popets_doi_check.py [--bib out/usenix_audit/bib_live.txt] [--no-network] """ import os import re import subprocess import sys import time sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) from usenix_bib_diff import field DEFAULT_BIB = "out/usenix_audit/bib_live.txt" UA = ("Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) " "Chrome/126.0.0.0 Safari/537.36") def expected_prefix(year, article): y, n = int(year), int(article) if y <= 2018: return "10.1515" if y < 2022 or (y == 2022 and n <= 58): return "10.2478" return "10.56553" def resolve(doi): """HTTP status of https://doi.org/<doi>, following redirects.""" r = subprocess.run( ["curl", "-sIL", "-A", UA, "--max-time", "30", "-o", "/dev/null", "-w", "%{http_code} %{url_effective}", f"https://doi.org/{doi}"], capture_output=True, text=True) code, _, url = r.stdout.strip().partition(" ") return code, url def main(): bib = sys.argv[sys.argv.index("--bib") + 1] if "--bib" in sys.argv \ else DEFAULT_BIB network = "--no-network" not in sys.argv entries = re.findall(r"@\w+\{[^@]*?\n\}", open(bib, encoding="utf-8").read(), re.S) rows, no_doi = [], [] for e in entries: series = (field(e, "series") or "") + " " + (field(e, "booktitle") or "") if not re.search(r"PoPETs|Privacy Enhancing Technolog", series, re.I): continue key = re.match(r"@\w+\{([^,]+),", e).group(1) doi = field(e, "doi") if not doi: no_doi.append((key, field(e, "url") or "no url either")) continue doi = re.sub(r"^https?://(dx\.)?doi\.org/", "", doi) m = re.search(r"popets-(\d{4})-(\d{4})", doi) exp = expected_prefix(*m.groups()) if m else None rows.append([key, doi, exp, doi.split("/")[0], "", ""]) print(f"bibliography : {bib}") print(f"PoPETs entries with a DOI : {len(rows)}") print(f"PoPETs entries without : {len(no_doi)}") if network: for r in rows: r[4], r[5] = resolve(r[1]) time.sleep(0.3) bad_prefix = [r for r in rows if r[2] and r[2] != r[3]] unparsed = [r for r in rows if not r[2]] dead = [r for r in rows if network and not r[4].startswith(("2", "3"))] print(f" prefix disagrees with the boundary : {len(bad_prefix)}") print(f" DOI not in popets-YYYY-NNNN form : {len(unparsed)}") print(f" does not resolve : " f"{len(dead) if network else 'not checked (--no-network)'}") print() print("%-34s %-26s %-9s %-5s %s" % ("citekey", "doi", "expected", "http", "resolves to")) for key, doi, exp, got, code, url in sorted(rows): flag = " <-- PREFIX" if exp and exp != got else "" print("%-34s %-26s %-9s %-5s %s%s" % (key, doi, exp or "n/a", code or "-", url[:60], flag)) for key, url in no_doi: print(f"{key:<34} NO DOI - - {url}") return 1 if (bad_prefix or dead) else 0 if __name__ == "__main__": sys.exit(main())
Real output, abridged here to its counters — the full 86-row table is in
scripts/popets_doi_check-output.txt:
bibliography : out/usenix_audit/bib_live.txt PoPETs entries with a DOI : 84 PoPETs entries without : 2 prefix disagrees with the boundary : 0 DOI not in popets-YYYY-NNNN form : 1 does not resolve : 0 citekey doi expected http resolves to
Duplicate-entry scan
- bib_doi_duplicates.py
#!/usr/bin/env python3 """Scan literature:bibliography for two entries describing the same paper. A citekey-collision check passes while the same paper sits in the file twice under different keys, and the bibtex4dw plugin will happily render both in the reference list. Match on DOI first (exact), then on squashed title for the entries that have no DOI. python3 scripts/bib_doi_duplicates.py [--bib out/usenix_audit/bib_live.txt] """ import os import re import sys from collections import defaultdict sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) from usenix_bib_diff import field, squash bib = sys.argv[sys.argv.index("--bib") + 1] if "--bib" in sys.argv \ else "out/usenix_audit/bib_live.txt" entries = re.findall(r"@\w+\{[^@]*?\n\}", open(bib, encoding="utf-8").read(), re.S) by_doi, by_title, keys = defaultdict(list), defaultdict(list), [] for e in entries: k = re.match(r"@\w+\{([^,]+),", e).group(1) keys.append(k) doi = field(e, "doi") if doi: by_doi[re.sub(r"^https?://(dx\.)?doi\.org/", "", doi).lower()].append(k) t = field(e, "title") if t: by_title[squash(t)].append(k) print(f"bibliography : {bib}") print(f"entries : {len(entries)}") print(f"distinct citekeys: {len(set(keys))}") dupkeys = sorted({k for k in keys if keys.count(k) > 1}) print(f"COLLIDING citekeys : {len(dupkeys)}" + (f" {dupkeys}" if dupkeys else "")) d_doi = {k: v for k, v in by_doi.items() if len(v) > 1} d_tit = {k: v for k, v in by_title.items() if len(v) > 1 and not any(set(v) <= set(x) for x in d_doi.values())} print(f"same DOI under different citekeys : {len(d_doi)}") for doi, ks in sorted(d_doi.items()): print(f" {doi}\n {', '.join(ks)}") print(f"same TITLE, no shared DOI : {len(d_tit)}") for t, ks in sorted(d_tit.items()): print(f" {t[:70]}\n {', '.join(ks)}") sys.exit(1 if (dupkeys or d_doi or d_tit) else 0)
bibliography : out/usenix_audit/bib_live.txt
entries : 837
distinct citekeys: 837
COLLIDING citekeys : 0
same DOI under different citekeys : 3
10.56553/popets-2022-0063
fouad2022my, fouad2022_cookie
10.56553/popets-2025-0063
bottger2025_regional, boettger2025_regional
10.56553/popets-2026-0109
ahmad2026_ipfp, ahmad2026_more
same TITLE, no shared DOI : 2
automatedlargescaleanalysisofcookienoticecompliance
bouhoula2024automated, bouhoula2024_automated
internetjonesandtheraidersofthelosttrackersanarchaeologicalstudyofwebt
lerner2016internet, lerner2016_internet
Duplicate co-citation, and the PETS smoke test
- bib_dupe_cocitation.py
#!/usr/bin/env python3 """Two small checks the provenance page was asserting without a script. 1. For each duplicate pair from scripts/bib_doi_duplicates.py, does any wiki page cite BOTH keys? If one does, its rendered reference list shows the same paper twice — that is the difference between a latent duplicate and a visible one, and the page's "nothing renders wrong today" rests on it. 2. The 42 PETS entries in out/authors.json come from a different parser (parse_popets) against a different page shape, so they are out of scope for the USENIX audit. This is the smoke test that says so honestly: an affiliation-shaped or single-token name in any of them. It is NOT the three-source audit the USENIX keys got, and it is labelled that way. Reads the page exports under out/usenix_audit/pages/, made with `node scripts/dw.mjs pages` + `?do=export_raw` per page. python3 scripts/bib_dupe_cocitation.py """ import glob import json import os import re import sys PAGES = "out/usenix_audit/pages" BIBPAGE = "literature_bibliography" PAIRS = [("fouad2022my", "fouad2022_cookie"), ("bottger2025_regional", "boettger2025_regional"), ("ahmad2026_ipfp", "ahmad2026_more"), ("bouhoula2024automated", "bouhoula2024_automated"), ("lerner2016internet", "lerner2016_internet")] AFFIL = re.compile( r"universi|institut|college|inc\.|labs?\b|corporat|technolog|research|" r"school|center|centre|gmbh|ltd|cispa|kaist|inria|microsoft|google|meta|" r"apple|amazon|ibm|eth |epfl|tu |laborator|foundation|agency|department|" r"polytechnic|\bteam\b|e\.?\s?v\.?$", re.I) def main(): files = [f for f in sorted(glob.glob(os.path.join(PAGES, "*.txt"))) if BIBPAGE not in os.path.basename(f)] pages = {os.path.basename(f)[:-4]: open(f, encoding="utf-8", errors="replace").read() for f in files} print(f"page exports read (literature:bibliography excluded) : {len(pages)}") worst, touched = 0, set() print("\n^ duplicate pair ^ pages citing key A ^ citing key B ^ citing BOTH ^") for a, b in PAIRS: ca = {p for p, t in pages.items() if "{[" + a + "]}" in t} cb = {p for p, t in pages.items() if "{[" + b + "]}" in t} both = sorted(ca & cb) worst = max(worst, len(both)) touched |= ca | cb print(f"| {a} / {b} | {len(ca)} | {len(cb)} | " f"{'**' + ', '.join(both) + '**' if both else '0'} |") print(f"\npages citing at least one key of any pair : {len(touched)}") print(f"pages citing BOTH keys of any pair : {worst}") print("A page citing both would render the same paper twice in its " "reference list.") cache = json.load(open("out/authors.json")) pets = {k: v for k, v in cache.items() if k.startswith("PETS/")} flagged = {k: [n for n in v if AFFIL.search(n) or len(n.split()) < 2] for k, v in pets.items()} flagged = {k: v for k, v in flagged.items() if v} print(f"\nPETS entries in out/authors.json : {len(pets)}") print(f" with an affiliation-shaped or single-token name: {len(flagged)}") for k, v in sorted(flagged.items()): print(f" {k}: {v}") print(" This is a smoke test over parse_popets output, not the " "three-source audit the USENIX keys got.") return 1 if (worst or flagged) else 0 if __name__ == "__main__": sys.exit(main())
page exports read (literature:bibliography excluded) : 160 ^ duplicate pair ^ pages citing key A ^ citing key B ^ citing BOTH ^ | fouad2022my / fouad2022_cookie | 4 | 2 | 0 | | bottger2025_regional / boettger2025_regional | 2 | 2 | 0 | | ahmad2026_ipfp / ahmad2026_more | 1 | 1 | 0 | | bouhoula2024automated / bouhoula2024_automated | 4 | 11 | 0 | | lerner2016internet / lerner2016_internet | 3 | 2 | 0 | pages citing at least one key of any pair : 26 pages citing BOTH keys of any pair : 0 A page citing both would render the same paper twice in its reference list. PETS entries in out/authors.json : 42 with an affiliation-shaped or single-token name: 0 This is a smoke test over parse_popets output, not the three-source audit the USENIX keys got.
Second sitting — the wider duplicate scan
Run on the 855-entry export before the change (output above, under the audit) and on the saved 850-entry page:
- bib_dedup_scan.py
#!/usr/bin/env python3 """Find every pair of literature:bibliography entries that may describe one paper. scripts/bib_doi_duplicates.py matches on exact DOI and exact squashed title. That found five pairs on 2026-09-04, and one of them was an umlaut transliteration in the citekey (bottger / boettger). The same slip can also land in the TITLE (a subtitle dropped, "{IPv6}" braced) or leave no shared DOI, so this scan casts wider and prints its candidates for a human to judge: A same DOI (definite) B same squashed title (definite) C near-identical title, difflib ratio >= 0.85 on the squashed form, or one squashed title a prefix of the other (subtitle dropped) D same year AND same folded first-author surname — the fold strips diacritics and collapses the German digraphs oe/ue/ae/ss to o/u/a/s, so Böttger, B\"ottger, Boettger and Bottger all compare equal C and D are candidate generators, not verdicts. Every C/D pair that is not already in A/B is printed with both titles so the residue is visible; the verdicts recorded on provenance:literature:bibliography are hand judgements over that printed list, not the script's. python3 scripts/bib_dedup_scan.py --bib out/live_bibliography_YYYYMMDD.txt """ import difflib import os import re import sys import unicodedata from collections import defaultdict sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) from usenix_bib_diff import field, squash LATEX = {r'\"o': 'oe', r'\"u': 'ue', r'\"a': 'ae', r'\"O': 'Oe', r'\"U': 'Ue', r'\"A': 'Ae', r'\ss': 'ss'} def fold_surname(author_field): """First author's surname, lower-cased, diacritics stripped, German digraphs collapsed. Handles 'Last, First' and 'First Last'.""" first = re.split(r"\s+and\s+", author_field)[0] s = first for k, v in LATEX.items(): s = s.replace(k, v) s = re.sub(r"\\[\'`^~vcH=.]\{?(\w)\}?", r"\1", s) # \'e, \v{c}, {\'e} s = re.sub(r"[{}]", "", s) s = unicodedata.normalize("NFKD", s) s = "".join(c for c in s if not unicodedata.combining(c)) last = s.split(",")[0].strip() if "," in s else s.strip().split()[-1] last = last.lower() last = re.sub(r"[^a-z]", "", last) for dg, one in (("oe", "o"), ("ue", "u"), ("ae", "a"), ("ss", "s")): last = last.replace(dg, one) return last def main(): bib = sys.argv[sys.argv.index("--bib") + 1] text = open(bib, encoding="utf-8").read() entries = re.findall(r"@\w+\{[^@]*?\n\}", text, re.S) rec = {} for e in entries: k = re.match(r"@\w+\{([^,]+),", e).group(1) if k in rec: raise SystemExit(f"colliding citekey {k}") doi = field(e, "doi") or "" doi = re.sub(r"^https?://(dx\.)?doi\.org/", "", doi).lower() title = field(e, "title") or "" author = field(e, "author") or "" year = field(e, "year") or "" rec[k] = dict(doi=doi, title=title, sq=squash(title), year=year, surname=fold_surname(author) if author else "") print(f"bibliography : {bib}") print(f"entries : {len(entries)} distinct citekeys: {len(rec)}") pairs = {} # frozenset(k1,k2) -> set of rules def add(a, b, rule): pairs.setdefault(frozenset((a, b)), set()).add(rule) by = defaultdict(list) for k, r in rec.items(): if r["doi"]: by[("A", r["doi"])].append(k) if r["sq"]: by[("B", r["sq"])].append(k) if r["year"] and r["surname"]: by[("D", r["year"], r["surname"])].append(k) for key, ks in by.items(): for i in range(len(ks)): for j in range(i + 1, len(ks)): add(ks[i], ks[j], key[0]) keys = sorted(rec) for i in range(len(keys)): a = rec[keys[i]]["sq"] if len(a) < 20: continue for j in range(i + 1, len(keys)): b = rec[keys[j]]["sq"] if len(b) < 20: continue if a.startswith(b) or b.startswith(a) or \ difflib.SequenceMatcher(None, a, b).ratio() >= 0.85: add(keys[i], keys[j], "C") definite = {p: r for p, r in pairs.items() if r & {"A", "B"}} candidates = {p: r for p, r in pairs.items() if not r & {"A", "B"}} print(f"\nDEFINITE duplicate pairs (rule A or B): {len(definite)}") for p, rules in sorted(definite.items(), key=lambda x: sorted(x[0])): a, b = sorted(p) print(f" [{''.join(sorted(rules))}] {a} / {b}") print(f" {rec[a]['title'][:90]}") print(f" {rec[b]['title'][:90]}") print(f"\nCANDIDATE pairs (rule C or D only) — judged by hand, see the " f"provenance page: {len(candidates)}") for p, rules in sorted(candidates.items(), key=lambda x: sorted(x[0])): a, b = sorted(p) print(f" [{''.join(sorted(rules))}] {a} / {b}") print(f" {rec[a]['year']} {rec[a]['title'][:100]}") print(f" {rec[b]['year']} {rec[b]['title'][:100]}") return 1 if definite else 0 if __name__ == "__main__": sys.exit(main())
bibliography : out/live_bibliography_20260904_after.txt
entries : 850 distinct citekeys: 850
DEFINITE duplicate pairs (rule A or B): 0
CANDIDATE pairs (rule C or D only) — judged by hand, see the provenance page: 58
[D] LePochat2019_tranco / LePochat2019_tranco_eval
2019 Tranco: A Research-Oriented Top Sites Ranking Hardened Against Manipulation
2019 Evaluating the Long-term Effects of Parameters on the Characteristics of the {Tranco} Top Sites Rank
[D] agarwal2024_peeking / agarwal2024_poster
2024 Peeking through the window: Fingerprinting Browser Extensions through Page-Visible Execution Traces
2024 Poster: A Comprehensive Categorization of SMS Scams
[D] agarwal2025_dropped / agarwal2025_fishing
2025 'Hey mum, I dropped my phone down the toilet': Investigating Hi Mum and Dad SMS Scams in the United
2025 Fishing for Smishing: Understanding SMS Phishing Infrastructure and Strategies by Mining Public User
[D] agarwal2025_dropped / agarwal2025_mindsets
2025 'Hey mum, I dropped my phone down the toilet': Investigating Hi Mum and Dad SMS Scams in the United
2025 "I have no idea how to make it safer": Studying Security and Privacy Mindsets of Browser Extension D
[D] agarwal2025_fishing / agarwal2025_mindsets
2025 Fishing for Smishing: Understanding SMS Phishing Infrastructure and Strategies by Mining Public User
2025 "I have no idea how to make it safer": Studying Security and Privacy Mindsets of Browser Extension D
[D] alroomi2023_login / alroomi2023_password
2023 A Large-Scale Measurement of Website Login Policies
2023 Measuring Website Password Creation Policies At Scale
[D] bahrami2025_bytedefender / bahrami2025_cookieguard
2025 Byte by Byte: Unmasking Browser Fingerprinting at the Function Level Using V8 Bytecode Transformers
2025 {CookieGuard}: Characterizing and Isolating the First-Party Cookie Jar
[D] bashir2019_adstxt / bashir2019_quantity
2019 A Longitudinal Analysis of the ads.txt Standard
2019 Quantity vs. Quality: Evaluating User Interest Profiles Using Ad Preference Managers
[D] bhuiyan2025_digital / bhuiyan2025_visitors
2025 Digital Disparities: A Comparative Web Measurement Study Across Economic Boundaries
2025 Not All Visitors are Bilingual: A Measurement Study of the Multilingual Web from an Accessibility Pe
[C] bratton2019_replication / sumner2014_exaggeration
2019 The Association Between Exaggeration in Health-Related Science News and Academic Press Releases: A R
2014 The Association Between Exaggeration in Health Related Science News and Academic Press Releases: Ret
[D] chen2021_cookieswap / chen2021_detecting
2021 Cookie Swap Party: Abusing First-Party Cookies for Web Tracking
2021 Detecting Filter List Evasion with Event-Loop-Turn Granularity JavaScript Signatures
[D] chen2025_parents / chen2025_semantics
2025 Empowering Parents to Support Children's Online Security and Privacy: Findings from a Randomized Con
2025 Semantics-Aware Cookie Purpose Compliance
[CD] duckduckgo_tracker_radar_2026 / duckduckgo_tracker_radar_detector_2026
2026 DuckDuckGo Tracker Radar
2026 DuckDuckGo Tracker Radar Detector
[D] duckduckgo_tracker_radar_2026 / duckduckgo_trc_2026
2026 DuckDuckGo Tracker Radar
2026 Tracker Radar Collector
[D] duckduckgo_tracker_radar_detector_2026 / duckduckgo_trc_2026
2026 DuckDuckGo Tracker Radar Detector
2026 Tracker Radar Collector
[D] durumeric2013_https / durumeric2013_zmap
2013 Analysis of the HTTPS certificate ecosystem
2013 {ZMap}: Fast Internet-wide Scanning and Its Security Applications
[D] durumeric2014_heartbleed / durumeric2014_view
2014 The Matter of Heartbleed
2014 An Internet-Wide View of Internet-Wide Scanning
[D] durumeric2015_neither / durumeric2015_search
2015 Neither Snow Nor Rain Nor MITM...: An Empirical Analysis of Email Delivery Security
2015 A Search Engine Backed by Internet-Wide Scanning
[D] edu2022_alexa / edu2022_exploring
2022 Measuring Alexa Skill Privacy Practices across Three Years
2022 Exploring the security and privacy risks of chatbots in messaging services
[D] iqbal2022_khaleesi / iqbal2022_left
2022 Khaleesi: Breaker of Advertising and Tracking Request Chains
2022 Left or Right: A Peek into the Political Biases in Email Spam Filtering Algorithms During US Electio
[D] kancherla2025_johnny / kancherla2025_least
2025 Johnny Can't Revoke Consent Either: Measuring Compliance of Consent Revocation on the Web
2025 Least Privilege Access for Persistent Storage Mechanisms in Web Browsers
[D] kirchner2024_black / kirchner2024_dancer
2024 A Black-Box Privacy Analysis of Messaging Service Providers' Chat Message Processing
2024 Dancer in the Dark: Synthesizing and Evaluating Polyglots for Blind Cross-Site Scripting
[D] lee2023_adtargeting / lee2023_track
2023 When and Why Do People Want Ad Targeting Explanations? Evidence from a Four-Week, Mixed-Methods Fiel
2023 Net-track: Generic Web Tracking Detection Using Packet Metadata
[D] li2016_remedying / li2016_youve
2016 Remedying Web Hijacking: Notification Effectiveness and Webmaster Comprehension
2016 You've Got Vulnerability: Exploring Effective Vulnerability Notifications
[D] li2017_radar / li2017_security
2017 FBS-Radar: Uncovering Fake Base Stations at Scale in the Wild
2017 A Large-Scale Empirical Study of Security Patches
[D] li2017_radar / li2017_static
2017 FBS-Radar: Uncovering Fake Base Stations at Scale in the Wild
2017 Static analysis of Android apps: A systematic literature review
[D] li2017_security / li2017_static
2017 A Large-Scale Empirical Study of Security Patches
2017 Static analysis of Android apps: A systematic literature review
[D] li2024_bounce / li2024_wellinformed
2024 Bounce in the Wild: A Deep Dive into Email Delivery Failures from a Large Email Service Provider
2024 Are We Getting Well-informed? An In-depth Study of Runtime Privacy Notice Practice in Mobile Apps
[D] li2024_bounce / li2024_worldwide
2024 Bounce in the Wild: A Deep Dive into Email Delivery Failures from a Large Email Service Provider
2024 A Worldwide View on the Reachability of Encrypted DNS Services
[D] li2024_wellinformed / li2024_worldwide
2024 Are We Getting Well-informed? An In-depth Study of Runtime Privacy Notice Practice in Mobile Apps
2024 A Worldwide View on the Reachability of Encrypted DNS Services
[D] liao2016_characterizing / liao2016_seeking
2016 Characterizing Long-tail SEO Spam on Cloud Web Hosting Services
2016 Seeking Nonsense, Looking for Trouble: Efficient Promotional-Infection Detection through Semantic In
[D] lin2021_longitudinal / lin2021_phishpedia
2021 A Longitudinal Study of Removed Apps in {iOS} App Store
2021 Phishpedia: A Hybrid Deep Learning Based Approach to Visually Identify Phishing Webpages
[D] lin2022_investigating / lin2022_sheep
2022 Investigating Advertisers' Domain-changing Behaviors and Their Impacts on Ad-blocker Filter Lists
2022 Phish in Sheep's Clothing: Exploring the Authentication Pitfalls of Browser Fingerprinting
[D] liu2024_opted / liu2024_promises
2024 Opted Out, Yet Tracked: Are Regulations Enough to Protect Your Privacy?
2024 From Promises to Practice: Evaluating the Private Browsing Modes of Android Browser Apps
[D] liu2025_domino / liu2025_fingerprinting
2025 The DOMino Effect: Detecting and Exploiting DOM Clobbering Gadgets via Concolic Execution with Symbo
2025 The First Early Evidence of the Use of Browser Fingerprinting for Online Tracking
[D] liu2025_domino / liu2025_somesite
2025 The DOMino Effect: Detecting and Exploiting DOM Clobbering Gadgets via Concolic Execution with Symbo
2025 Somesite I Used To Crawl: Awareness, Agency and Efficacy in Protecting Content Creators From AI Craw
[D] liu2025_fingerprinting / liu2025_somesite
2025 The First Early Evidence of the Use of Browser Fingerprinting for Online Tracking
2025 Somesite I Used To Crawl: Awareness, Agency and Efficacy in Protecting Content Creators From AI Craw
[D] nguyen2025_breaking / nguyen2025_please
2025 Breaking the Shield: Analyzing and Attacking Canvas Fingerprinting Defenses in the Wild
2025 "Please don't send that bot anything": A Mixed-methods Study of Personal Impersonation Attacks Targe
[D] nisenoff2023_awareness / nisenoff2023_defining
2023 User Awareness and Behaviors Concerning Encrypted {DNS} Settings in Web Browsers
2023 Defining "Broken": User Experiences and Remediation Tactics When Ad-Blocking or Tracking-Protection
[D] oest2020_phishtime / oest2020_sunrise
2020 PhishTime: Continuous Longitudinal Measurement of the Effectiveness of Anti-phishing Blacklists
2020 Sunrise to Sunset: Analyzing the End-to-end Life Cycle and Effectiveness of Phishing Attacks at Scal
[D] papadogiannakis2025_before / papadogiannakis2025_darkside
2025 Before \& After: The Effect of EU's 2022 Code of Practice on Disinformation
2025 Welcome to the Dark Side: Analyzing the Revenue Flows of Fraud in the Online Ad Ecosystem
[D] ruth2022_toppling / ruth2022_world
2022 Toppling Top Lists: Evaluating the Accuracy of Popular Website Lists
2022 A World Wide View of Browsing the World Wide Web
[D] scheitle2018_long / scheitle2018_rise
2018 A Long Way to the Top: Significance, Structure, and Stability of Internet Top Lists
2018 The Rise of Certificate Transparency and Its Implications on the Internet Ecosystem
[D] starov2017_extended / starov2017_xhound
2017 Extended Tracking Powers: Measuring the Privacy Diffusion Enabled by Browser Extensions
2017 XHOUND: Quantifying the Fingerprintability of Browser Extensions
[D] tang2025_misuse / tang2025_navigating
2025 Misuse, Misreporting, Misinterpretation of Statistical Methods in Usable Privacy and Security Papers
2025 Navigating Cookie Consent Violations Across the Globe
[D] utz2023_comparing / utz2023_rarely
2023 Comparing Large-Scale Privacy and Security Notifications
2023 Privacy Rarely Considered: Exploring Considerations in the Adoption of Third-Party Services by Websi
[D] vastel2018_scanner / vastel2018_stalker
2018 Fp-Scanner: The Privacy Implications of Browser Fingerprint Inconsistencies
2018 FP-STALKER: Tracking Browser Fingerprint Evolutions
[D] vekaria2025_bighelp / vekaria2025_soktracking
2025 Big Help or Big Brother? Auditing Tracking, Profiling, and Personalization in Generative AI Assistan
2025 SoK: Advances and Open Problems in Web Tracking
[D] venkatadri2019_auditing / venkatadri2019_investigating
2019 Auditing Offline Data Brokers via Facebook's Advertising Platform
2019 Investigating sources of PII used in Facebook’s targeted advertising
[D] wang2026_masks / wang2026_sipconfusion
2026 The Masks We (Think We) Wear: Privacy Threats of Browser-Extension Wallets in the Web3 Ecosystem
2026 SIPConfusion: Exploiting SIP Semantic Ambiguities for Caller ID and SMS Spoofing
[D] wu2025_appprivacyreport / wu2025_revealing
2025 Transparency or Information Overload? Evaluating Users’ Comprehension and Perceptions of the iOS App
2025 Revealing the Black Box of Device Search Engine: Scanning Assets, Strategies, and Ethical Considerat
[D] wu2026_email / wu2026_tracking
2026 One Email, Many Faces: A Deep Dive into Identity Confusion in Email Aliases
2026 Tracking the Stray Sheep: Understanding DNS Response Manipulation in the Wild
[D] xie2024_arcanum / xie2024_crawling
2024 Arcanum: Detecting and Evaluating the Privacy Risks of Browser Extensions on Web Pages and Web Conte
2024 Crawling to the Top: An Empirical Evaluation of Top List Use
[D] yang2022_extensive / yang2022_wtagraph
2022 An Extensive Study of Residential Proxies in China
2022 WTAGRAPH: Web Tracking and Advertising Detection using Graph Neural Networks
[D] zhang2022_harpo / zhang2022_spartacus
2022 HARPO: Learning to Subvert Online Behavioral Advertising
2022 I'm SPARTACUS, No, I'm SPARTACUS: Proactively Protecting Users from Phishing by Intentionally Trigge
[D] zhang2024_inbox / zhang2024_quic
2024 Inbox Invasion: Exploiting MIME Ambiguities to Evade Email Attachment Detectors
2024 QUIC is not Quick Enough over Fast Internet
[D] zhang2025_abusability / zhang2025_qrcode
2025 Abusability of Automation Apps in Intimate Partner Violence
2025 Demystifying the (In)Security of {QR} Code-based Login in Real-world Deployments
[D] zhu2020_label / zhu2020_vtset
2020 Measuring and Modeling the Label Dynamics of Online Anti-Malware Engines
2020 Demo: Benchmarking Label Dynamics of VirusTotal Engines
Second sitting — citekey convention census
- bib_key_convention.py
#!/usr/bin/env python3 """How does literature:bibliography spell a first author's diacritic in the citekey? Needed to pick between bottger2025_regional and boettger2025_regional on evidence rather than taste. For every entry whose FIRST AUTHOR'S SURNAME carries a non-ASCII letter or a LaTeX accent command, classify how the key's surname part renders it: stripped diacritic removed, base letter kept (Böttger -> bottger, Sjösten -> sjosten) digraph German transliteration (Böttger -> boettger, Rüth -> rueth) dropped the accented letter vanished entirely (Kührer -> khrer — the bibgen.mjs [^a-z] filter, not a convention) other none of the above (printed; judge by hand) python3 scripts/bib_key_convention.py --bib out/live_bibliography_X.txt """ import os import re import sys import unicodedata from collections import Counter sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) from usenix_bib_diff import field LATEX_ACCENT = re.compile(r"\\[\"'`^~vcH=.]\s*\{?\\?(\w)\}?|\{\\[\"'`^~vcH=.]\s*(\w)\}") LETTER_CMD = {r"\ss": "ss", r"\o": "o", r"\O": "O", r"\l": "l", r"\L": "L", r"\ae": "ae", r"\i": "i"} def first_surname(author): first = re.split(r"\s+and\s+", author)[0] return first.split(",")[0].strip() if "," in first else first.strip().split()[-1] def de_latex(s): s = LATEX_ACCENT.sub(lambda m: m.group(1) or m.group(2), s) for k, v in LETTER_CMD.items(): s = s.replace(k, v) return re.sub(r"[{}]", "", s) def has_diacritic(s): return bool(re.search(r"[^\x00-\x7f]", s) or LATEX_ACCENT.search(s) or any(k in s for k in LETTER_CMD)) def stripped(s): s = unicodedata.normalize("NFKD", de_latex(s)) s = "".join(c for c in s if not unicodedata.combining(c)) return re.sub(r"[^a-z]", "", s.lower().replace("ß", "ss").replace("ø", "o")) def digraph(s): # LaTeX umlauts first (\"o, {\"o}, \"{o}), then literal ones, then de_latex # for whatever accents remain. s = re.sub(r'\{?\\"\{?([aouAOU])\}?\}?', lambda m: m.group(1) + "e", s) for a, b in (("ö", "oe"), ("ü", "ue"), ("ä", "ae"), ("Ö", "Oe"), ("Ü", "Ue"), ("Ä", "Ae"), ("ß", "ss")): s = s.replace(a, b) s = de_latex(s) s = unicodedata.normalize("NFKD", s) s = "".join(c for c in s if not unicodedata.combining(c)) return re.sub(r"[^a-z]", "", s.lower()) def dropped(s): s = de_latex(s) return re.sub(r"[^a-z]", "", "".join(c for c in s if ord(c) < 128).lower()) def main(): bib = sys.argv[sys.argv.index("--bib") + 1] text = open(bib, encoding="utf-8").read() rows, kinds = [], Counter() for e in re.findall(r"@\w+\{[^@]*?\n\}", text, re.S): key = re.match(r"@\w+\{([^,]+),", e).group(1) author = field(e, "author") if not author: continue sur = first_surname(author) if not has_diacritic(sur): continue keysur = re.match(r"[A-Za-z]+", key).group(0).lower() if keysur == stripped(sur): kind = "stripped" elif keysur == digraph(sur) and digraph(sur) != stripped(sur): kind = "digraph" elif keysur == dropped(sur): kind = "dropped" else: kind = "other" kinds[kind] += 1 rows.append((kind, key, sur)) print(f"bibliography : {bib}") print(f"entries whose first author's SURNAME carries a diacritic: {len(rows)}") for k in ("stripped", "digraph", "dropped", "other"): print(f" {k:9s} {kinds[k]}") print() for kind, key, sur in sorted(rows): print(f" {kind:9s} {key:30s} {sur}") if __name__ == "__main__": main()
bibliography : out/live_bibliography_20260904_1721.txt
entries whose first author's SURNAME carries a diacritic: 25
stripped 19
digraph 4
dropped 2
other 0
digraph boettger2025_regional Böttger
digraph rueth2018_digging R{\"u}th
digraph schoeni2024_cookieblock Sch\"oni
digraph stoever2023_owners Stöver
dropped khrer2015_going Kührer
dropped som2017_content Somé
stripped bosch2016_tales B\"osch
stripped bottger2025_regional B\"ottger
stripped gomezboix2018_hiding Gómez-Boix
stripped gross2021_iuipc Groß
stripped gulyas2016_near Gulyás
stripped korczynski2016_zone Korczy{\'n}ski
stripped kubicek2022_emails Kub{\'i}{\v{c}}ek
stripped lecuyer2014_xray Lécuyer
stripped lecuyer2015_sunlight Lécuyer
stripped lukic2026_mv3 Luki\'c
stripped nenadic2026_swiss Nenadi{\'c}
stripped sanchezrola2021_journey Sánchez-Rola
stripped sanchezrola2023_rods Sánchez-Rola
stripped sjosten2019_latex Sj\"osten
stripped sjosten2020_filter Sj\"osten
stripped some2019_empoweb Somé
stripped sorensen2019_beforeafter Sørensen
stripped tornberg2024_bestpractices T\"{o}rnberg
stripped tramer2019_adversarial Tram\`er
Second sitting — the consolidation itself
- bib_dedup_apply.py
#!/usr/bin/env python3 """Consolidate the five duplicate entries in literature:bibliography. Does NOT touch the wiki. It reads a fresh ?do=export_raw of every page and of the bibliography, writes the rewritten files under --out, and prints a report whose invariants must all hold before anything is saved: * each loser key is present in the bibliography exactly once and is removed; each winner is present exactly once and is kept; * entry count drops by exactly len(MERGES); * every {[...]} marker naming a loser is rewritten to the winner; the number of keys replaced equals the number of loser occurrences counted beforehand; * no page cites both keys of a pair (else the rewrite would silently collapse two markers into one reference and the page's distinct-key count would drop) — the distinct-key count of every page is asserted unchanged; * after the rewrite, no marker anywhere names a key the new bibliography does not define, other than keys that were ALREADY unresolved before (printed as residue, never one of the losers). Winners follow the file's own majority convention — surname + year + '_' + first title word, diacritics dropped — which is also what scripts/bibgen.mjs mints. Which key of each pair is the winner is a decision recorded on provenance:literature:bibliography, not something this script infers. python3 scripts/bib_dedup_apply.py --bib out/live_bibliography_X.txt \ --pages out/dedup_pages --out out/dedup_apply """ import glob import os import re import sys from collections import Counter # loser -> winner MERGES = { "fouad2022my": "fouad2022_cookie", "boettger2025_regional": "bottger2025_regional", "ahmad2026_ipfp": "ahmad2026_more", "bouhoula2024automated": "bouhoula2024_automated", "lerner2016internet": "lerner2016_internet", } # A field the loser carried that the winner lacks and that was verified against # the venue page on 2026-09-04 (USENIX's own BibTeX block and citation_firstpage / # citation_lastpage meta tags on usenixsecurity24/presentation/bouhoula). EXTRA_FIELDS = { "bouhoula2024_automated": [("pages", "1723--1739")], } BIBPAGE = "literature__bibliography" MARKER = re.compile(r"\{\[([A-Za-z0-9_:.\-, ]+)\]\}") TEMPLATE_KEYS = {"CitationKey"} # the i_template's example marker # Provenance pages that get a dated amendment because their content page's # markers were repointed. content page id -> provenance page id. PROVENANCE_OF = { "privacy:browser_storage": "provenance:privacy:browser_storage", "privacy:fingerprinting": "provenance:privacy:fingerprinting", "programming:crawler:openwpm": "provenance:programming:crawler:openwpm", "programming:stateful_stateless": "provenance:programming:stateful_stateless", "statistics:pvalue_corrections": "provenance:statistics:pvalue_corrections", "design:longitudinal": "provenance:design:longitudinal", "design:crawling_location": "provenance:design:crawling_location", "privacy:consent": "provenance:privacy:consent", "privacy:requests": "provenance:privacy:requests", "design:archives": "provenance:design:archives", } def pid(fname): return os.path.basename(fname)[:-4].replace("__", ":") def entries_of(text): return re.findall(r"@\w+\{[^@]*?\n\}", text, re.S) def key_of(entry): return re.match(r"@\w+\{([^,]+),", entry).group(1) def rewrite_bib(text): ents = entries_of(text) keys = Counter(key_of(e) for e in ents) for lo, wi in MERGES.items(): assert keys[lo] == 1, f"loser {lo} appears {keys[lo]} times" assert keys[wi] == 1, f"winner {wi} appears {keys[wi]} times" out = text for e in ents: k = key_of(e) if k in MERGES: # remove the entry and one preceding blank line assert out.count(e) == 1 out = out.replace("\n" + e, "", 1) if ("\n" + e) in out else out.replace(e, "", 1) elif k in EXTRA_FIELDS: new = e for fname, val in EXTRA_FIELDS[k]: assert not re.search(r"\b" + fname + r"\s*=", e), f"{k} already has {fname}" new = new[:-1].rstrip("\n") + f"\n {fname:<13} = {{{val}}},\n}}" assert out.count(e) == 1 out = out.replace(e, new, 1) after = entries_of(out) assert len(after) == len(ents) - len(MERGES), (len(ents), len(after)) for lo in MERGES: assert not re.search(r"\b" + re.escape(lo) + r"\b", out), f"{lo} still in bib" return out, len(ents), len(after), {key_of(e) for e in after} def rewrite_markers(text): """Return (new_text, replaced_count, distinct_before, distinct_after).""" replaced = 0 before, after = set(), set() def sub(m): nonlocal replaced keys = [k.strip() for k in m.group(1).split(",")] before.update(keys) new = [] for k in keys: if k in MERGES: replaced += 1 k = MERGES[k] if k not in new: new.append(k) after.update(new) return "{[" + ",".join(new) + "]}" return MARKER.sub(sub, text), replaced, before, after def amendment(content_id, pairs, n_content, n_prov): lines = ["", "===== Amendment, 2026-09-04: citekey consolidation =====", ""] for lo, wi in pairs: lines.append( f" * ''{lo}'' was one of two keys for the same paper in " f"[[:literature:bibliography]]. The wiki-wide consolidation of " f"2026-09-04 (drain item ''dedup-regional-filter-lists-bibkey'') kept " f"''{wi}'' and deleted the other entry.") def n_markers(n): return f"{n} citation marker" + ("s" if n != 1 else "") where = f"{n_markers(n_content)} on [[:{content_id}]]" if n_prov: where += f" and {n_markers(n_prov)} on this page" verb = "were" if n_content + n_prov != 1 else "was" lines.append( f" * {where} {verb} repointed to the kept key. No prose on either page " f"changed, and no figure moved. Statements above that name the deleted " f"key describe the state when they were written. Full query log and the " f"invariants checked before saving: " f"[[:provenance:literature:bibliography]].") lines.append("") return "\n".join(lines) def insert_amendment(text, note): i = text.find("====== References ======") if i >= 0: return text[:i].rstrip("\n") + "\n" + note + "\n" + text[i:] body = text.rstrip("\n").split("\n") if body[-1].startswith("[["): # trailing back-link line return "\n".join(body[:-1]).rstrip("\n") + "\n" + note + "\n" + body[-1] + "\n" return text.rstrip("\n") + "\n" + note def main(): a = sys.argv bib_path = a[a.index("--bib") + 1] pages_dir = a[a.index("--pages") + 1] out_dir = a[a.index("--out") + 1] os.makedirs(out_dir, exist_ok=True) bib_text = open(bib_path, encoding="utf-8").read() new_bib, n_before, n_after, new_keys = rewrite_bib(bib_text) open(os.path.join(out_dir, BIBPAGE + ".txt"), "w", encoding="utf-8").write(new_bib) old_keys = {key_of(e) for e in entries_of(bib_text)} print(f"bibliography : {bib_path}") print(f"entries : {n_before} -> {n_after} (removed {n_before - n_after}: " f"{', '.join(MERGES)})") for k, fs in EXTRA_FIELDS.items(): print(f"field added : {k} {fs}") files = sorted(f for f in glob.glob(os.path.join(pages_dir, "*.txt")) if pid(f) != BIBPAGE.replace("__", ":")) print(f"pages read : {len(files)} (bibliography excluded)") # occurrences of loser keys inside markers, before loser_occ = Counter() for f in files: for m in MARKER.finditer(open(f, encoding="utf-8").read()): for k in (x.strip() for x in m.group(1).split(",")): if k in MERGES: loser_occ[k] += 1 print("\nloser-key marker occurrences before, by key:") for lo in MERGES: print(f" {lo:26s} {loser_occ[lo]}") # Which pages cited each key of a pair before the rewrite — content pages and # provenance pages separately, so the "which key was on more pages" question # on the provenance page has a printed answer. def citing(key): out = [] for f in files: keys = {k.strip() for m in MARKER.finditer(open(f, encoding="utf-8").read()) for k in m.group(1).split(",")} if key in keys: out.append(pid(f)) return out print("\npages citing each key BEFORE (content pages; provenance pages in brackets):") for lo, wi in MERGES.items(): for k in (wi, lo): ps = citing(k) c = [p_ for p_ in ps if not p_.startswith("provenance:")] pr = [p_ for p_ in ps if p_.startswith("provenance:")] tag = "kept " if k == wi else "deleted" print(f" {tag} {k:26s} {len(c)} [{len(pr)}] {', '.join(c)}" + (f" [{', '.join(pr)}]" if pr else "")) changed = {} total_replaced = 0 unresolved_before, unresolved_after = Counter(), Counter() all_after_keys = set() print("\n^ page ^ markers repointed ^ distinct keys before ^ after ^ pairs ^") for f in files: text = open(f, encoding="utf-8").read() new, n, kb, ka = rewrite_markers(text) kb -= TEMPLATE_KEYS ka -= TEMPLATE_KEYS for k in kb - old_keys: unresolved_before[k] += 1 for k in ka - new_keys: unresolved_after[k] += 1 all_after_keys |= ka if n: assert len(kb) == len(ka), f"{pid(f)}: distinct-key count changed {len(kb)}->{len(ka)}" pairs = sorted((lo, MERGES[lo]) for lo in kb if lo in MERGES) changed[pid(f)] = (new, n, pairs) total_replaced += n print(f"| {pid(f)} | {n} | {len(kb)} | {len(ka)} | " f"{'; '.join(f'{lo}→{wi}' for lo, wi in pairs)} |") print(f"\npages changed : {len(changed)}") print(f"markers repointed : {total_replaced} (loser occurrences before: " f"{sum(loser_occ.values())})") assert total_replaced == sum(loser_occ.values()) for lo in MERGES: assert lo not in all_after_keys, f"{lo} survives in a marker" # provenance amendments prov_texts = {pid(f): open(f, encoding="utf-8").read() for f in files} n_prov_notes = 0 for cid, prov in PROVENANCE_OF.items(): assert cid in changed, f"{cid} listed in PROVENANCE_OF but unchanged" _, n_content, pairs = changed[cid] n_prov = changed[prov][1] if prov in changed else 0 base = changed[prov][0] if prov in changed else prov_texts[prov] assert "citekey consolidation" not in base, f"{prov} already amended" changed[prov] = (insert_amendment(base, amendment(cid, pairs, n_content, n_prov)), n_prov, pairs) n_prov_notes += 1 for cid in changed: if not cid.startswith("provenance:"): assert cid in PROVENANCE_OF, f"{cid} changed but has no provenance page listed" print(f"provenance amendments : {n_prov_notes}") for id_, (text, _, _) in changed.items(): open(os.path.join(out_dir, id_.replace(":", "__") + ".txt"), "w", encoding="utf-8").write(text) print(f"files written : {len(changed) + 1} under {out_dir}/") print(f"\nmarkers naming a key the bibliography does not define, BEFORE: " f"{sum(unresolved_before.values())} on {len(unresolved_before)} key(s)") for k, n in sorted(unresolved_before.items()): print(f" {k} ({n} page(s))") print(f"same, AFTER: {sum(unresolved_after.values())} on {len(unresolved_after)} key(s)") for k, n in sorted(unresolved_after.items()): print(f" {k} ({n} page(s))") assert set(unresolved_after) == set(unresolved_before), "the rewrite created an unresolved key" # Deleted keys that survive as PROSE on other pages (inside ''...'' or %%...%% # in review logs and run records). Those are historical statements about the # bibliography as it was, not citations, and are left as written; they are # printed so the residue is visible rather than silently ignored. prose = {} for f in files: text = changed[pid(f)][0] if pid(f) in changed else open(f, encoding="utf-8").read() stripped = MARKER.sub("", text) hits = sorted(lo for lo in MERGES if re.search(r"\b" + re.escape(lo) + r"\b", stripped)) if hits: prose[pid(f)] = hits print(f"\ndeleted keys still named in PROSE (not markers), left as historical record: " f"{len(prose)} page(s)") for p_, hits in sorted(prose.items()): print(f" {p_:48s} {', '.join(hits)}") print("\nall invariants hold") if __name__ == "__main__": main()
Second sitting — rendered before/after
- bib_dedup_render_check.py
#!/usr/bin/env python3 """Rendered-DOM check for the 2026-09-04 citekey consolidation. For each page whose markers were repointed, compare the rendered page fetched BEFORE the edit with the one fetched AFTER the bibliography was saved and the bibtex4dw cache purged. Both counts must be unchanged: a repointed marker is still one marker (citekey spans), and because no page cited both keys of a pair the reference list keeps its length (<dt> inside dl.bibtex_references). -1 means the page has no reference list at all (provenance pages that carry no <bibtex bibliography> block). Counts are scoped to the wikipage start/stop comments. "deleted-key strings" counts the deleted keys as TEXT anywhere in the body — on provenance pages that is the dated amendment and the historical notes, not a citation, so it is printed rather than asserted. python3 scripts/bib_dedup_render_check.py out/dedup_render_before out/dedup_render_after """ import os import re import sys DELETED = ["fouad2022my", "boettger2025_regional", "ahmad2026_ipfp", "bouhoula2024automated", "lerner2016internet"] def stats(path): h = open(path, encoding="utf-8", errors="replace").read() s, e = h.find("<!-- wikipage start -->"), h.find("<!-- wikipage stop -->") body = h[s:e] if 0 <= s < e else h dl = re.search(r'<dl class="bibtex_references">(.*?)</dl>', body, re.S) dts = len(re.findall(r"<dt", dl.group(1))) if dl else -1 spans = len(re.findall(r"bibtex_citekey", body)) strings = sum(body.count(k) for k in DELETED) return dts, spans, strings def main(): before, after = sys.argv[1], sys.argv[2] bad = 0 print("^ page ^ references (dt) before → after ^ citekey spans before → after ^ " "deleted-key strings after ^ verdict ^") for f in sorted(os.listdir(after)): pid = f[:-5].replace("__", ":") b, a = stats(os.path.join(before, f)), stats(os.path.join(after, f)) ok = (a[0], a[1]) == (b[0], b[1]) bad += not ok print(f"| {pid} | {b[0]} → {a[0]} | {b[1]} → {a[1]} | {a[2]} | " f"{'unchanged' if ok else 'CHANGED'} |") print(f"\npages checked: {len(os.listdir(after))} pages whose counts moved: {bad}") return 1 if bad else 0 if __name__ == "__main__": sys.exit(main())
Review log
Four reviewers, all told explicitly that the authoring context may not be exhaustive, and all handed the page text, every script and its real output. Findings and dispositions are recorded below, including the two that were rejected or downgraded.
Sonnet — figures vs script
Handed the frozen draft, every script and every committed output, and told to re-run everything rather than read it.
| Finding | Disposition |
|---|---|
The draft was generated from a stale copy of the generator; three figures were already fixed in scripts/ and never regenerated | Accepted, and it is the reviewer's most useful finding — not because the figures were wrong (they had been fixed) but because it proves the freeze/publish gap is real. The draft handed to reviewers is now regenerated after every generator edit |
| “2,974 of 12,843 records… 100% of both venues carry no authors and no DOI” is wrong: the index is 16,864, and 1,113 of 1,253 PETS records do carry a DOI | Accepted. Replaced with the per-venue table, re-derived on 2026-09-04. This is the “do not carry a number over from an earlier page” trap: the figure came from a docstring written against the previous corpus. fetch_authors.py and bibgen.mjs carried the same stale number in comments and were corrected too |
| Source-count table said 158/158/4 of 162; the real split is 155/155/7 | Accepted. Independently re-derived; the reviewer's number is right |
Pass 3 compares author SETS, not ordered lists, while its comment claims otherwise. Demonstrated by swapping first and last author on acharya2024_imitation — all three passes reported clean | Accepted; this is the finding of the review. A first-authorship swap was invisible to every check on the page. Pass 3 now compares ordered lists and reports order-only differences as their own category; the swap is mutation B5, reproduced from the reviewer's own example |
| “44 LaTeX-escape / 67 literal-non-ASCII” has no script behind it, and the reviewer got 43/66 | Accepted. A third count (from the citations reviewer) gave 45/66, which is the tell: the three counters disagreed because \ss, \o and \O are neither accents nor non-ASCII. Wrote bib_accent_encoding.py, which defines the buckets and prints the three edge cases. Published answer: 43 / 66 / 3 |
| “13 papers” meta-vs-BibTeX disagreement describes a script state that no longer exists; the reviewer's reconstruction gave 10 | Accepted. Dropped the unreproducible 13 for usenix_source_agreement.py, which re-derives all three parser states from the cached HTML: 11 / 2 / 0 over the 87, and 17 / 4 / 0 over the 155. Writing that script found a third decoder bug — see below |
| The PoPETs table reports 84 with a DOI and 0 bad prefixes, but 1 of the 84 is never checked against the boundary rule and the page does not say so | Accepted. The row is on the page and eckersley2010_unique is named: a Springer LNCS DOI from PETS 2010, before PoPETs was a journal. Coverage is 83 of 84 |
| The “live fetch of three USENIX keys” claim names no keys, so it cannot be re-checked | Accepted. All three keys are now named |
| “160 exported pages” is imprecise | Accepted. Now “the 160 other pages (161 in dw.mjs pages, less the bibliography itself)” |
“5,859 extracted papers” — the reviewer counted 5,869 paper.cols.txt files | Accepted as a clarification, rejected as an error. 5,859 is the line count of extractions.jsonl (5,857 distinct slugs); 5,869 is the full-text count. Two different populations. The run record now gives both and says neither is used on this page |
The Review log holds a literal unsubstituted REVIEWLOG token | Accepted. This section. Expected at freeze time — the reviewers are what fills it |
Second pass, after the fixes above. The same reviewer re-ran everything and
confirmed all seven fixes, including that the byte-identity guard fires when the
generator ordering is reverted, and that de_latex decodes every
LaTeX-escaped author field in the bibliography and in all 162 cached BibTeX
blocks with no residual artefact. It found three more:
| Finding | Disposition |
|---|---|
| The frozen draft handed to the reviewers was again older than the generator — the freeze/publish gap recurring | Accepted. The page is regenerated immediately before saving, and the saved bytes are diffed against a fresh run |
The code comment in usenix_bib_diff.py still said “44 … 67 … verified on the rendered DOM of privacy:browser_extensions and programming:crawler_detection” — the exact claim the prose had already corrected. Because the script is embedded byte for byte, the wrong claim ships on every regeneration | Accepted, and the best of this pass. The byte-identity guard proves the code on the page is the code on disk; it cannot prove the code on disk is right. Comment corrected to 43/66 and security:phishing. Same failure mode as a stale number surviving in a provenance log: fix the page, and the log keeps repeating it |
The buckets in bib_accent_encoding.py are not disjoint — demir2023_similarity has both an accent command and a \ss, so 43+66+3+726 = 838 > 837 | Accepted. The script now prints the accent-and-letter-command overlap alongside the accent-and-non-ASCII one, and says in its own output that the buckets overlap and must not be added up |
Sonnet — citations and quotes
| Finding | Disposition |
|---|---|
DBLP does NOT carry Gail-Joon Ahn. The page claimed “the prose blob, the paper PDF and DBLP all carry him as the 8th”; DBLP record conf/uss/OestSZWTSD20 lists seven and stops at Adam Doupé | Accepted; the most serious factual error found. Re-checked against DBLP directly: seven authors. The correction stands — the paper carries him — but the claim that DBLP corroborated it was false. Rewritten, and the section now makes the stronger point that three machine-readable sources agree on a list the paper contradicts |
The Doupé render claim names programming:crawler_detection, which cites no entry using a LaTeX escape | Accepted. Verified: that page renders Doupé from zhang2021_crawlphish / zhang2022_spartacus, whose author fields carry a literal é — so it verified nothing. Replaced with security:phishing (braced Doup{'e}) and privacy:browser_extensions (unbraced Doup'e), both re-fetched and confirmed to render Doupé |
The published <file> block for usenix_bib_diff.py is not the committed script: the generator's citekey collapse rewrote its f-string [{key}] to [{key}] | Accepted; a serious generator defect. The page publishes code as its audit trail, so publishing code that is not the code that ran defeats the point. The collapse now runs on the prose template before the script bodies are pasted in, and a guard asserts every embedded script appears byte for byte. The guard was mutation-tested: reinstating the old ordering makes the generator exit 1 with usenix_bib_diff.py is NOT byte-identical on the page |
The residue list published 12 fragments; disabling the filter yields 15, and the published list omits berkeley, d and v | Accepted. The 12 were reconstructed from memory rather than measured — exactly what this page is supposed to stop. Re-derived by running the A7 mutation and collecting its output: 16 papers, 15 distinct fragments, all published |
eckersley2010_unique is counted but never named | Accepted, as above |
| The 9 real prose citekeys all resolve; no collisions | Verified independently, no change |
The PharmaLeaks adjudication, the Hsieh adjudication, the kapravelos2014_hulk 11→6 claim and every row of the worst-offenders table check out exactly | Verified independently, no change |
| The PoPETs prefix boundaries check out on both sides of both boundaries | Verified independently, no change |
| The “161 files vs 160 pages” discrepancy: the extra file is a stray empty-id export | Accepted, cosmetic. The stray file was deleted and the sentence now states 161 and 160 explicitly |
Sonnet — external currency
Everything on the page's external surface was re-fetched. Nothing was found stale, which for a page whose external claims are mostly about DOI prefixes and one vendor's HTML is the expected result.
| Finding | Disposition |
|---|---|
PoPETs prefix boundaries confirmed exactly: 10.56553/popets-2022-0058 404s and 10.1515/popets-2019-0001 404s, so the boundaries are hard, not fuzzy. Registrants confirmed via api.crossref.org/prefixes/: 10.1515 and 10.2478 → Walter de Gruyter GmbH, 10.56553 → PETS Advisory Board. No fourth range for 2026 | Accepted as an addition. The boundary tests and the Crossref registrant lookup are now on the page; the table previously asserted the ranges without showing they had been probed from both sides |
| All 84 PoPETs DOIs still resolve; script output byte-identical on re-run | Verified, no change |
USENIX still emits citation_author and a BibTeX block, including on a usenixsecurity26 page; no citation_doi on any of four 2024–2026 pages | Accepted as an addition. The page now says the structure holds one cycle forward and that USENIX still mints no DOI |
The tang page claim is a negative about a live, mutable third-party page | Accepted. That row is now explicitly dated and quotes the exact line re-fetched on 2026-09-04, unlike the historical claims around it |
| DBLP's search API works but DBLP's own FAQ calls it “still in flux… work-in-progress” | Accepted. Footnoted, so a later re-audit re-verifies the endpoint rather than assuming it |
One bug no reviewer found
Writing usenix_source_agreement.py — which exists only because a reviewer
objected to the unreproducible “13” — surfaced a third decoder bug neither
the reviewers nor the original audit had seen. USENIX writes Benoît as
Beno{\^\i}t with LaTeX's dotless \i, which no accent pattern matched,
so the final brace strip produced Beno^it. It sat on cohn2020_delf, one
of the 72 pages that is not among the 90 cached lists, so only a comparison
across all 162 could reach it. The live entry was always correct and no
published figure moved; de_latex now has a nine-case self-test
(usenix_author_audit.py –selftest) whose first case is that string.
Of the four reviewer findings that led to new code rather than new prose, two were requests to make an already-correct-looking figure reproducible.
Fable — generic
Ran with no checklist after the focused findings were applied.
| Finding | Disposition |
|---|---|
The debunked DBLP claim survived in three more places: the quote-check table, the ADJUDICATED reason string in usenix_author_audit.py, and that string echoed in the committed output file | Accepted; the best finding of this pass. Correcting the prose left the log repeating the error, which is exactly the failure this page exists to prevent. The reason string now records that DBLP repeats the omission, the output was regenerated, and the quote-table rows were cut as redundant with the adjudication table |
| “678 of the 837 live entries were not examined” mislabels its own denominator: 837 − 159 USENIX still contains the 86 PoPETs entries. Non-USENIX non-PETS is 592 | Accepted. Split into two bullets, 86 and 592, and the PoPETs bullet now says plainly that their DOIs were checked and their authors were not |
| “every one of those disagreements was a bug in the reading, not in the data” is contradicted two lines later: four pages are genuine differences where USENIX BibTeX drops a diacritic its meta keeps | Accepted. Rewritten as “three decoder bugs, and four pages where USENIX own BibTeX drops a diacritic”, with the note that treating those as agreement is a choice the fold makes |
“Eleven single-rule mutations, each breaking exactly one thing” — there were twelve rows, now thirteen, and A2 deliberately breaks two | Accepted. Counted and qualified |
No mutation tests the thing pass 3 is justified by: an author deleted from a non-cached entry. B1 deletes from a cached one, where pass 1 catches it | Accepted. Added as B6. It reports PASS1 0, PASS2 0, PASS3set 1 — invisible to everything except pass 3, which is the argument the page had been making without evidence |
| Source 3 seven pages are described as “the 2011 technical-sessions pages”; three of the six legacy pages are from 2010 | Accepted. All seven are now named, with citekey and year |
Three claims with no script behind them: the duplicate co-citation check, the PETS smoke scan, the de_latex self-test | Accepted. scripts/bib_dupe_cocitation.py now does the first two and is published with its output; the self-test result is stated |
| “Read the third row carefully” points at the wrong row | Accepted. It is the last row |
lukic2026_mv3 has a derivable DOI and was filed under a drain item about duplicates, which is not what it is | Accepted, and fixed rather than filed. 10.56553/popets-2026-0027 was resolved before being added to the entry |
| “the author gap is exactly PETS and USENIX” — one NDSS record also has no authors | Accepted. Named |
The usenixsecurity26 forward-compatibility claim rests on one unnamed page | Accepted. Named, and labelled as n=1 |
| “Residue: zero” covers only one direction | Accepted. The uncovered direction is now stated |
| Two different accounts of the 161-versus-160 page count | Accepted. One account now, in the co-citation script own output |
| A LaTeX backslash was lost in two places, from a non-raw template string | Accepted. Both fixed |
| Stale statements inside committed scripts: “the other 69”, a reference to a constant that does not exist, an unused import | Accepted. All three corrected, since they are published byte for byte |
| The answer is buried: someone checking “did the bug reach the bibliography?” reads 160 lines of parser history first | Accepted. A seven-row headline table now sits above the first audit |
| Cut the moralising sentences; the bug narratives read as honest, the commentary on them reads as performance | Accepted. Five removed, and the heading “What the reviewers did not find, and this run did” became “One bug no reviewer found” |
| Redundancy: the three adjudications appear four times, the Benoît bug twice | Partly accepted. The quote-table repeat was cut. The Benoît account stays in both places: one is the narrative, the other is a decoder comment that has to stand on its own |
| The corpus-wide 16,864-record table is corpus material | Accepted as a pointer, rejected as a removal. Checking this finding turned up something neither the reviewer nor this run had noticed: corpus already exists (75 KB, rev 1786550805) and this page had been calling it “not yet written” in its own first paragraph. Its published per-venue record counts agree with this page's exactly, which is an independent confirmation of the denominator; the author and DOI breakdown is new here and stays |
Two findings from the earlier passes were not accepted as stated:
- ”
programming:crawler_detectioncites no Doupé-authored paper at all.“ It cites two,zhang2021_crawlphishandzhang2022_spartacus. The reviewer conclusion was right for a different reason — both spell the name with a literal é, so that page verified nothing about LaTeX escapes — and the fix was made on that basis, not the one offered. - “5,859 extracted papers is wrong; there are 5,869.” Two different populations: 5,859 records in
extractions.jsonl, 5,869 papers with full text on disk. Recorded as a clarification, not a correction; the run record now gives both and says neither is used here.
Second sitting, 2026-09-04: citekey consolidation
Three focused review passes (Sonnet: figures vs script; Sonnet: citations and
claims; Sonnet: external currency) were launched against this frozen draft on
2026-09-04 with the scripts, their outputs and the 21 saved files. The run had
to close before their findings arrived, so this section was published before
any reviewer finding was incorporated and no Fable generic pass has run.
Incorporating the findings, running the generic pass and recording each
disposition here is the follow-up drain item dedup-bibkey-review-followup.
Until then, every figure in this second-sitting audit is script output that
has been re-derived once by the author and not yet independently re-run.
References
- [1]
- Tang, Brian; Bui, Duc; Shin, Kang G. (2025): "Navigating Cookie Consent Violations Across the Globe", in: Proceedings of the USENIX Security Symposium. (Link)
- [2]
- McCoy, Damon; Pitsillidis, Andreas; Jordan, Grant; Weaver, Nicholas; Kreibich, Christian; Krebs, Brian; Voelker, Geoffrey M.; Savage, Stefan; Levchenko, Kirill (2012): "PharmaLeaks: Understanding the Business of Online Pharmaceutical Affiliate Programs", in: Proceedings of the USENIX Security Symposium. (Link)
- [3]
- Oest, Adam; Safaei, Yeganeh; Zhang, Penghui; Wardman, Brad; Tyers, Kevin; Shoshitaishvili, Yan; Doupé, Adam; Ahn, Gail-Joon (2020): "PhishTime: Continuous Longitudinal Measurement of the Effectiveness of Anti-phishing Blacklists", in: Proceedings of the USENIX Security Symposium. (Link)
- [4]
- Hsieh, Jhih-Yi; Raghunathan, Aditi; Shah, Nihar B. (2025): "Vulnerability of Text-Matching in ML/AI Conference Reviewer Assignments to Collusions", in: Proceedings of the USENIX Security Symposium. (Link)
- [5]
- Kapravelos, Alexandros; Grier, Chris; Chachra, Neha; Kruegel, Christopher; Vigna, Giovanni; Paxson, Vern (2014): "Hulk: Eliciting Malicious Behavior in Browser Extensions", in: Proceedings of the USENIX Security Symposium. (Link)
- [6]
- Lyons, Allan; Gamba, Julien; Shawaga, Austin; Reardon, Joel; Tapiador, Juan; Egelman, Serge; Vallina-Rodriguez, Narseo (2023): "Log: It's Big, It's Heavy, It's Filled with Personal Data! Measuring the Logging of Sensitive Information in the Android Ecosystem", in: Proceedings of the USENIX Security Symposium. (Link)
- [7]
- Vekaria, Yash; Canino, Aurelio Loris; Levitsky, Jonathan; Ciechonski, Alex; Callejo, Patricia; Mandalari, Anna Maria; Shafiq, Zubair (2025): "Big Help or Big Brother? Auditing Tracking, Profiling, and Personalization in Generative AI Assistants", in: Proceedings of the USENIX Security Symposium. (Link)
- [8]
- Bielova, Nataliia; Litvine, Laura; Nguyen, Anysia; Chammat, Mariam; Toubiana, Vincent; Hary, Estelle (2024): "The Effect of Design Patterns on (Present and Future) Cookie Consent Decisions", in: Proceedings of the USENIX Security Symposium. (Link)
- [9]
- Carlini, Nicholas; Felt, Adrienne Porter; Wagner, David (2012): "An Evaluation of the Google Chrome Extension Security Architecture", in: Proceedings of the USENIX Security Symposium. (Link)
- [10]
- Trickel, Erik; Starov, Oleksii; Kapravelos, Alexandros; Nikiforakis, Nick; Doupé, Adam (2019): "Everyone is Different: Client-side Diversification for Defending Against Extension Fingerprinting", in: Proceedings of the USENIX Security Symposium. (Link)
