# Reproducibility pack v2 (de-identified): Security-Header Deployment on Directory-Listed U.S. Local-Business Websites (2026-09-24)

Cross-sectional measurement of HTTP security response headers on homepages of 7,040 domains
sampled from U.S. locality "Business and Economy" categories in the Curlie web directory.
Business size was not verified; the sampling frame is a directory category, not a business registry.

This v2 pack supersedes the 2026-09-24 v1 pack, which must not be published (it contains raw
header maps and the domain list). This release contains no raw header values, no domains,
no cookies, no CSP text, no nonces and no timestamps.

## Glossary (standard across all artifacts)

1. Sampled rows (n=7,040): directory rows drawn at random, covering 7,022 unique initial
   registrable domains.
2. Registrable domain: the domain name with the last two labels, or three when the
   second-level part is a common public suffix such as co.uk. A heuristic, not a full
   Public Suffix List lookup.
3. Final registrable domain: the registrable domain of the final URL after redirects.
   "Final host" means the full hostname, used only for the HSTS host-scope table.
4. Usable responses (n=5,642 rows, 5,614 unique final registrable domains): rows with a
   usable HTTPS response. Tables label this base "all-usable".
5. HTTP-200 responses (n=4,701 rows): usable rows whose final status was 200.
6. Principal base, also called "dedup-200" (n=4,688): unique final registrable domains
   among the HTTP-200 responses. One row kept per final registrable domain - the LAST row
   in scan-2 check-completion order (the released file's row order; sorting by row_id,
   i.e. scan order, changes the dedup). Every headline figure uses this base. Code and
   column names such as n_dedup200 refer to this base.

## Contents

- `data/scan2-2026-09-24-deidentified.jsonl` - one row per sampled directory row. The file
  covers ALL 7,040 sampled rows (over 7,022 unique initial registrable domains): 5,642 with
  a usable HTTPS response (ok=true) and 1,398 without
  (ok=false; the error class is recorded, per-check class fields are absent). No sampled row
  was dropped or excluded. The analysis bases are filters over this file, not removals from
  it: all-usable 5,642, HTTP-200 4,701, principal base (dedup-200) 4,688. Final-rubric classes per check,
  platform label, dedup group, host kind. See data-dictionary.md.
- `data/state-counts-2026-09-24.tsv` - precomputed per-state counts (the 9x2 omnibus table
  reproduces from the n>=150 rows without a full run).
- `final-tables.md` - every final table, generated by the command below.
- `reanalysis4.py` - the analysis (aggregation only; reads the de-identified dataset).
- `rubric3.py`, `csp3.py` - final grading rubric and CSP3 classifier (v3).
- `test_csp3.py`, `test_golden.py` - parser unit tests + golden corpus (65 tests total).
- `scripts/sample-from-curlie.py` - sampling script (seed 42).
- `scripts/scan.py`, `scripts/scan2.py` - scanner, scan 1 / scan 2 configurations.
- `scripts/build-deidentified-dataset.py` - how the released dataset was built from the
  private raw results (needs the raw data and the team-held dedup key; not runnable from
  this pack alone).
- `requirements.txt` - exact dependency pin (scipy==1.15.3; everything else is standard library).
- `MANIFEST.sha256` - SHA-256 of every file in the pack.

## One-command reproduction (data to tables)

    python3 reanalysis4.py data/ final-tables.md

Verified environment: Python 3.10.12 with the exact pin in requirements.txt (scipy==1.15.3). scipy is used only for the state
omnibus chi-square; everything else is standard library. The script re-derives every
table in final-tables.md from the de-identified dataset and regenerates the state-counts
TSV. Sanity assertions on the standard base sizes (5,642 / 4,701 / 4,688) and headline
counts fail loudly if the dataset is altered. Run the test suites with:

    python3 test_csp3.py && python3 -m unittest test_golden

What this does NOT reproduce: re-scanning the web (needs live network and the Curlie
snapshot; results are time-dependent), the scan1-vs-scan2 drift figures (scan 1 is not
released; precomputed values are labeled in section 10 of final-tables.md), and the
raw-retention facts (section 8, also labeled). No chart code exists; the score-distribution
bar chart draws from the table in section 3.

## De-identification

- `row_id` is the sample row number in scan order (r0001-r7040). Honest caveat: anyone who
  re-runs the sampler with seed 42 against the same Curlie snapshot can re-derive the sample
  and re-identify rows. That is inherent to reproducible sampling and is the decided
  release shape.
- `dedup_group` is a keyed pseudonym (HMAC-SHA256, truncated) of the final registrable
  domain; the key is held by the study team and is not released.
- No site is named anywhere in this pack. The manual audit of the eight qualifying CSP
  policies (final-tables.md section 9) and the real-policy fixtures in test_golden.py refer
  to policies by pseudonym (q1-q8) with no row IDs; fixture strings have nonces redacted and
  site-identifying hosts replaced by neutral placeholders, and classify identically to the
  originals (locked by the golden tests).
- Dataset rows appear in scan-2 check-completion order. The principal base keeps the LAST
  row per dedup_group in file order; sorting the file by row_id changes the dedup.

## Scanner configuration (both scans, from the private pipeline)

- Method: HTTPS GET, one request chain per sampled URL, at most 3 redirects followed. The
  response body was never read: the socket was closed after the header terminator.
- User-Agent: `RACKCRUNCH-header-check/1.0 (+https://rackcrunch.com/security-headers)`;
  Accept: `text/html,*/*`. TLS certificate verification on; only public IPs contacted.
- Timeouts: 3 s DNS, 3 s per hop, 8 s total per domain. Concurrency: 24 workers; no retries.
- Egress: Google Cloud (AS396982), The Dalles, Oregon, US; scan 1 12:26-12:31 EDT,
  scan 2 13:16-13:21 EDT on 2026-09-24.

## Source data and attribution

Sampling frame: Curlie web directory snapshot `curlie-rdf-all.tar.gz`, downloaded 2026-09-24
from https://curlie.org/download (rdf files dated 2026-02-02). Curlie directory data is
licensed under the Creative Commons Attribution 3.0 Unported License
(https://creativecommons.org/licenses/by/3.0/); see https://curlie.org/docs/en/license.html
for the required attribution forms. Government, healthcare, financial, education and
military categories were excluded at the sampling stage.

## Known limitations

One page, one moment, one vantage point; headers vary by page, CDN edge and login state.
GET and HEAD can differ; we used GET. Dedup uses a registrable-domain heuristic (last two
labels + common second-level public suffixes). Platform attribution covers sites exposing
identifying headers only. The composite score is an unvalidated descriptive index.
