Skip to content

#1134: retire the seven stale corpus-size claims, and record the measurement that settles ASVS 6.2.4 - #810

Merged
wshallwshall merged 1 commit into
mainfrom
backlog-1134-stale-corpus-claims
Sep 4, 2026
Merged

#1134: retire the seven stale corpus-size claims, and record the measurement that settles ASVS 6.2.4#810
wshallwshall merged 1 commit into
mainfrom
backlog-1134-stale-corpus-claims

Conversation

@wshallwshall

Copy link
Copy Markdown
Collaborator

What this is

BACKLOG #1134, the prose-and-measurement half. Per the 2026-09-03 scope split, the corpus tooling lane owns the regeneration script, the corpus file, common_passwords.NOTICE and tests/test_auth_core.py. This PR touches none of those four.

The premise every brief on this item carries is false

Briefs cut from the 2026-08-20 research say the bundled corpus holds 10,000 entries of which 18 reach 15 characters. Re-measured on this tree before touching anything:

Measurement Value
Bundled entries (lines / distinct) 15,256 / 15,045
Clearing PasswordPolicy(check_breached=False, check_username=False) at the shipped floor of 15 5,274
ASVS 6.2.4 bar at least 3,000

The cell is satisfied on the strict reading, with no corpus change. Regenerating the corpus, which the brief asked for, would have been a no-op on a corpus that was already correct. Re-measuring first is what saved the work.

Two instruments, each with a control that fires. awk over raw lines gives 5,274 at 15-plus characters; the shipped loader gives 5,274 clearing the full policy. Controls: the file carries zero comment or blank lines, length >= 1 returns the full 15,256, and the policy count tracks the floor (9,135 at 8, 6,020 at 10, 5,367 at 12, 3,173 at 16, 1,735 at 17, 512 at 20) rather than sitting constant.

The deny-list is not a binding constraint, and the reason matters

The two counts agree exactly. Removing the context clause changes the count by 0, and zero of the 5,274 carry a CONTEXT_WORDS term -- because the build filtered them out, not because the clause is inert. Control: it still rejects a planted probe containing messagefoundry. Adding a term would cut real entries (qwerty removes 98, love 78, 2010 30), so any re-score trigger must name password_check_context beside password_min_length.

Headroom is one step, which nobody had measured: the 3,000 bar clears at a floor of 16 and fails at 17.

Seven stale sites, not the four the briefs named

Verified by symbol and surrounding text, because every line anchor in the older briefs has drifted:

  1. the messagefoundry.auth.policy module docstring
  2. the operator-facing startup warning in _warn_if_corpus_unreadable (auth/service.py)
  3. the AuthSettings.password_breach_corpus_file comment
  4. the password_check_breached row of docs/CONFIGURATION.md
  5. the password_breach_corpus_file row of docs/CONFIGURATION.md
  6. and 7. two paragraphs of docs/SECURITY.md "Password policy"

None is a control, so nothing broke and nothing reported it. Each now describes the list without a size, so counts live only beside the data. The operator reference gains the caveat that made the bundled list short: size a corpus by how many entries clear the policy, never by its line count.

This half is cherry-picked from 1d012f8f9, written for this item on 2026-08-22 and never merged.

Reviewer, please read this: those three gates in 1d012f8f9 that keep this prose from rotting again live in tests/test_auth_core.py, which is the other lane's file. They ship without a gate here. The commit is worth taking from.

One false packaging claim corrected in passing

pyproject.toml named tests/test_password_*.py as the guard proving the corpus ships in the wheel. No such file has ever existed, so the stated control pointed at nothing while a working one -- tests/test_auth_core.py -- sat one name away.

Three findings handed to the tooling lane, in the backlog row

  1. A recorded digest needs a .gitattributes pin or it is true only by luck. git hash-object on the corpus returns 4482f231... under core.autocrlf=true and 7ca7ef4a... under autocrlf=false -- same file, same commit. An unpinned digest accuses a clean checkout of tampering, and a wheel built on Windows ships CRLF matching no recorded checksum. Converting the corpus working tree to LF produces an empty content diff, so the pin costs nothing.
  2. Gate the by-floor counts, not just the headline -- the headroom claim derives from the table.
  3. A digest gate is a source-tree control, not an operator-side one. messagefoundry/integrity.py attests installed files against the wheel's RECORD, but _ATTESTED_SUFFIX is ".py", so shipped data is skipped. Pre-existing, not a regression here, needs its own item.

Traps avoided

password_min_length is untouched. The hashed k-anonymity corpus is still ruled out in principle: auth/policy.py stores 40-hex digests, so an entry's length is unknowable and unfilterable.

Checks

Green: ruff check, ruff format --check, mypy messagefoundry (strict, 267 files), and pytest over test_auth_core.py, test_auth_service.py, test_settings.py, test_security_doc_rate_limits.py, test_backlog_status_check.py, test_backlog_citation_check.py, test_citation_line_check.py, test_claude_section_citations.py -- 301 passed. backlog_status_check.py reports 657 items each declaring one status.

Not run to completion in this session: the full suite. Every edit is a comment, a docstring, a log format string or a Markdown cell, so there is no behaviour change, but the hosted legs still need reading -- in particular windows-service-smoke, which never runs locally.

🤖 Generated with Claude Code

…urement that settles ASVS 6.2.4

The corpus rebuild grew the bundled breach list past ten thousand entries. Seven
shipped sites kept describing it by the old size. None of them is a control, so
nothing broke and nothing reported it -- they asserted a number that had stopped
being true, in the places an operator reads before deciding whether to configure
a larger corpus.

THE MEASUREMENT, because the premise every brief on this item carries is false.
Those briefs say the bundled corpus holds 10,000 entries of which 18 reach 15
characters. Measured on this tree: 15,256 lines, 15,045 distinct entries, and
5,274 clearing PasswordPolicy(check_breached=False, check_username=False) at the
shipped password_min_length of 15, against an ASVS 6.2.4 bar of 3,000. The cell
is satisfied on the strict reading with no corpus change.

Two instruments agree, each with a control. awk over raw lines gives 5,274 at
15-plus characters; the shipped loader gives 5,274 clearing the full policy. The
file carries zero comment or blank lines, length>=1 returns the full 15,256, and
the policy count tracks the floor (9,135 at 8, 3,173 at 16, 1,735 at 17, 512 at
20) rather than sitting constant.

The two counts agree exactly because the deny-list is not a binding constraint:
removing the context clause changes the count by 0, and zero of the 5,274 carry a
CONTEXT_WORDS term -- because the build filtered them out, not because the clause
is inert. It still rejects a planted probe, and adding `qwerty` as a term would
cut 98 entries, so a re-score trigger must name password_check_context beside
password_min_length.

The stale sites, by symbol rather than line number, because every line anchor in
the older briefs has drifted: the messagefoundry.auth.policy module docstring;
the operator-facing startup warning in _warn_if_corpus_unreadable; the
AuthSettings.password_breach_corpus_file comment; the password_check_breached and
password_breach_corpus_file rows of docs/CONFIGURATION.md; and two paragraphs of
docs/SECURITY.md "Password policy". The briefs named four. There were seven.

Counts now live only beside the data, and the operator reference gains the caveat
that made the bundled list short: size a corpus by how many entries clear the
policy, never by its line count. The prose half is cherry-picked from 1d012f8f9,
written for this item on 2026-08-22 and never merged.

pyproject.toml named tests/test_password_*.py as the guard proving the corpus
ships in the wheel. No such file has ever existed; the real guard is
tests/test_auth_core.py, one name away.

Per the 2026-09-03 scope split this touches none of the corpus tooling -- the
regeneration script, the corpus, its NOTICE and tests/test_auth_core.py belong to
a concurrent lane. Three findings measured here are handed to it in the backlog
row, including that a recorded digest needs a .gitattributes pin: git hash-object
returns 4482f23 under core.autocrlf=true and 7ca7ef4a under autocrlf=false, so
an unpinned digest accuses a clean checkout of tampering.

No behaviour changes: every edit is a comment, a docstring, a log format string
or a Markdown cell. ruff check, ruff format, mypy --strict (267 files), and the
auth, settings, doc-drift and citation suites (301 passed) are green.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@wshallwshall wshallwshall added the reviewed A reviewer has read this. Removed automatically when new commits arrive. label Sep 3, 2026
@wshallwshall

Copy link
Copy Markdown
Collaborator Author

For the corpus tooling lane, not for this PR.

Before the 2026-09-03 scope split this branch also carried the provenance and gate layer. Rather than delete it, it is pushed as reference/1134-corpus-provenance-do-not-merge (tip ec723cc33). Do not merge that branch -- it touches tests/test_auth_core.py, .gitattributes, common_passwords.NOTICE and predates the split. Cherry-pick from it instead.

What is in it, all measured and green at the time:

  1. messagefoundry/auth/data/common_passwords.txt -text in .gitattributes, with the reasoning. Without the pin a recorded digest is true only where core.autocrlf matches the recorder's: git hash-object returns 4482f231 under true and 7ca7ef4a under false, same file, same commit. Converting the working tree to LF is an empty content diff, so the pin is free.
  2. A NOTICE Integrity section whose digest (sha256 136e7bcf...) reproduces under a plain sha256sum once pinned, plus an honest note that the upstream blob digests were never recorded and were not reconstructed.
  3. A by-floor table in the NOTICE and a gate checking all eight counts against live measurement, not just the headline -- the headroom claim derives from the table, so gating only the headline leaves the load-bearing half unverified.
  4. A positive control on the corpus-depth gate, and its real reach written into its docstring: built from shipped defaults, it self-breaks when the shipped default moves, never when a deploying site raises its own minimum.
  5. The three prose gates from 1d012f8f9 that keep this PR's seven corrections from rotting again -- a glob scan of messagefoundry/**/*.py plus the two operator docs, its planted-claim positive control, and a pin on the password_breach_corpus_file caveat row.

Two things it deliberately does not do, both wider than #1134: widen integrity.py's attestation past _ATTESTED_SUFFIX = ".py" so an installed-corpus tamper is detected, and add the regeneration script that the pinned counts currently have no tool behind.

@wshallwshall

Copy link
Copy Markdown
Collaborator Author

Correction to the Checks section: a full suite DID complete after I wrote that line.

Result: 1,816 passed, 50 skipped, 1 failed in 35 minutes. The one failure was self-inflicted and is not a defect in either branch.

What happened. I started that run on the pre-split tree, then restructured the branch about 20 minutes in. The branch switch reverted common_passwords.NOTICE to the version on main while test_notice_records_the_digest_of_the_corpus_it_describes was still pending. It read a NOTICE with no Integrity section and failed.

How I know that is the cause rather than a real defect. The traceback shows the digest computed as 136e7bcf..., which is the correct value, and the CRLF guard passed -- so the corpus was intact and only the NOTICE lookup failed. The NOTICE text quoted in the assertion is main's, ending at the Ordering section with no digest in it.

Verified rather than assumed. I replayed both gates against the stored blobs of reference/1134-corpus-provenance-do-not-merge using git cat-file blob, which reads what the branch actually contains rather than whatever a working tree holds:

CRLF guard  : PASS
digest      : 136e7bcfa519a8881da7e6bf872691a8766ccf11a12db38216b3cef5bc808b80
digest gate : PASS
headline    : 5,274
table       : {8: 9135, 10: 6020, 12: 5367, 15: 5274, 16: 3173, 17: 1735, 20: 512}
CONTROL (planted byte must NOT match): fires

The -text pin is present on that branch. It is sound to cherry-pick from.

What this means for THIS PR. It contains none of the files involved in that failure. The 1,816 passing tests did include the seven prose corrections that are in this PR, so the run is positive evidence for it. The hosted legs still need reading, windows-service-smoke in particular.

One instrument trap worth passing on. The background-task notification reported exit code 0 for that run while pytest reported 1 failed. The wrapper's exit status and the suite's verdict are not the same sentence -- read the summary line, not the exit code.

@wshallwshall

Copy link
Copy Markdown
Collaborator Author

Final check status, replacing the "full suite not run to completion" line in the body.

The second full-suite attempt was killed and produced no output, so there is still no completed whole-suite run against this PR's exact tree. Rather than claim one, I enumerated every test file that reads any of the seven files this PR changes and ran all of them here.

grep -rln "CONFIGURATION.md|SECURITY.md|pyproject.toml" tests/ --include=*.py   ->  45 files

Run on this branch, in three batches:

Batch Result
15 doc-gate and ASVS drift files 691 passed
14 packaging, licence, PHI, release files 231 passed, 1 skipped
10 scan, threat-model, version, upload files 232 passed, 89 skipped
auth, service, settings (earlier) 187 passed
backlog, citation, doc-rate-limit gates (earlier) 114 passed

Zero failures. That is every test that reads a file this PR touches, plus the auth and settings suites.

Two warnings appear and neither comes from this change: docs/security/** is withheld from public checkouts, so the threat-model content assertions and two master-test-plan baselines are inert in any local run. They are environmental and pre-existing.

What still needs a hosted runner, unchanged: windows-service-smoke, and the CodeQL and SBOM legs.

@wshallwshall
wshallwshall added this pull request to the merge queue Sep 4, 2026
@wshallwshall
wshallwshall removed this pull request from the merge queue due to a manual request Sep 4, 2026
@wshallwshall
wshallwshall added this pull request to the merge queue Sep 4, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Sep 4, 2026
@github-actions github-actions Bot added the ci-red A required check went red. Attribute it before retrying. label Sep 4, 2026
@wshallwshall
wshallwshall added this pull request to the merge queue Sep 4, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Sep 4, 2026
@wshallwshall
wshallwshall added this pull request to the merge queue Sep 4, 2026
Merged via the queue into main with commit 8360bb7 Sep 4, 2026
44 of 45 checks passed
@wshallwshall
wshallwshall deleted the backlog-1134-stale-corpus-claims branch September 4, 2026 17:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ci-red A required check went red. Attribute it before retrying. reviewed A reviewer has read this. Removed automatically when new commits arrive.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant