diff --git a/docs/BACKLOG.md b/docs/BACKLOG.md index 55cc19edd..0d5ed44de 100644 --- a/docs/BACKLOG.md +++ b/docs/BACKLOG.md @@ -7723,7 +7723,7 @@ The refusal names the scheme it found. A base URL with no scheme fails here too, ## 1114. research an honest pass for ASVS 2.4.1 -- anti-automation on a data plane whose senders are machines and whose intake has no authentication -> 馃敘 **Re-scored 2026-08-20 -> P2.** Value **7/10** 路 Difficulty **5/10** 路 _quick win_. A default install would still take messages at an unbounded rate on first deployment, and for non-MLLP inbounds there is no opt-in bound at all, so no workaround exists there. Difficulty 5 covers the ruling plus the likely follow-on of taking pacing across the transport registry to at least the raw-TCP inbound, with tests and docs. _(was 8/10 路 7/10.)_ +> 馃敘 **Re-scored 2026-08-20 -> P2.** Value **7/10** 路 Difficulty **5/10** 路 _quick win_. A default install would still take messages at an unbounded rate on first deployment, and ~~for non-MLLP inbounds there is no opt-in bound at all, so no workaround exists there~~ **[FALSIFIED 2026-09-04 at `a2eef0f3` -- struck, not deleted. The pacing keys now reach FOUR of the nine externally-facing inbound factories; five still reach nothing. See "Re-measured 2026-09-04" below for the probe and its controls.]**. Difficulty 5 covers the ruling plus ~~the likely follow-on of taking pacing across the transport registry to at least the raw-TCP inbound, with tests and docs~~ **[BOTH SPENT: the raw-TCP limb was delivered 2026-09-03, and the ruling it prices was written on 2026-08-16, BEFORE this score was set. The numbers are left as the scoring pass recorded them -- re-scoring is that pass's act, not a builder's -- but neither half of the justification now holds.]** _(was 8/10 路 7/10.)_ > Research: done 2026-08-20 > > **Filed 2026-08-08 - not started. RESEARCH item: the goal is an HONEST pass, and "cannot honestly reach pass" is a valid finding.** ASVS **2.4.1** (L2) currently scores **partial**. The pinned verb asks for anti-automation controls against excessive calls leading to exfiltration, garbage data, quota exhaustion, denial of service, or overuse of costly resources. What holds it short is that ~~no message-rate or volume control exists on the data plane in any configuration~~ **[FALSIFIED 2026-08-12 by `e0e979d5`/`b3b278e6` -- struck, not deleted. A pacer EXISTS and ships OFF by default; `transports/mllp.py:109` calls that *"a DELIBERATE DEVIATION from this module's key-absent-means-secure-default convention, ruled 2026-08-11 (ASVS 2.4.1 / 15.2.2)"* -- naming this very cell, so the module knew about this item while this item did not know about the module. THE ITEM STAYS OPEN AND ITS QUESTION HARDENS:** not *does a control exist* (answered, yes) but **can a control that ships OFF be an honest pass.** The `docs/SECURITY.md:1582` citation below also needs re-pointing -- the sentence it quotes was rewritten when the pacer landed, in the same edit that broke ASVS cell 2.1.3's anchor.]** -- the product says so in its own documentation at `docs/SECURITY.md:1582`. @@ -7731,7 +7731,7 @@ The refusal names the scheme it found. A base URL with no scheme fails here too, > Closing-act: scorecard-rescore **Cluster:** Security / ASVS remediation research. **Priority:** P1. **Verdict:** research. -**Severity:** on a first deployment, any sender able to reach the NIC-bound data plane could submit messages at an unbounded RATE, each durably persisted before the ACK -- a cumulative store-growth and denial-of-service exposure. MLLP has neither intake authentication nor rate limiting, so there is no actor to charge a budget against. +**Severity:** on a first deployment, any sender able to reach the NIC-bound data plane could submit messages at an unbounded RATE, each durably persisted before the ACK -- a cumulative store-growth and denial-of-service exposure. MLLP has ~~neither intake authentication nor rate limiting~~ **[NARROWED 2026-09-04: intake authentication in NO configuration, and rate limiting in none that SHIPS -- an opt-in pacer has existed on MLLP since the 2026-08-11 deviation ruling recorded in `transports/mllp.py`, so the flat claim contradicted this item's own body, which is the same defect the 2026-08-13 amendment below was raised to fix]**, so there is no actor to charge a budget against. > 鈿狅笍 **AMENDED 2026-08-13 (dispatcher) -- the Severity line said "unbounded messages", which CONTRADICTED THIS ITEM'S OWN BODY and overstated the exposure. Corrected to "at an unbounded RATE"; the finding is unchanged.** Four bounds ship **on** and were re-verified at `origin/main` `96c9a860`: `DEFAULT_MAX_FRAME_BYTES` 16 MiB (`transports/mllp.py:105`), `DEFAULT_MAX_CONNECTIONS` 256 (`:106`), `DEFAULT_RECEIVE_TIMEOUT` 60.0 s (`:107`), and `max_file_bytes` (`transports/file.py:384`, `remotefile.py:808`). They bound **SIZE and CONCURRENCY, not RATE** -- which is exactly what *"What holds it short today"* already said two paragraphs above, so the Severity line was contradicting its own item rather than describing the engine. **The correction runs in the direction that makes the engine look BETTER**, which is why it is stated explicitly rather than left for a reader to notice: a severity line is the sentence most often quoted onward without its body. @@ -7773,6 +7773,20 @@ The refusal names the scheme it found. A base URL with no scheme fails here too, **One gap measured on the way and left as a separate subject:** `X12` is absent from `_TRANSPORTS` in `messagefoundry/config/connections_file.py` entirely, so NO X12 setting is expressible in `connections.toml` -- the pacing keys are not a special case of that. The code-first surface reaches the pacer on X12 today; the data surface reaches nothing on X12 today. Pinned as a measurement in `tests/test_ingress_message_pacing.py` so the missing row reads as known rather than as an oversight. +**Re-measured 2026-09-04 at `a2eef0f3`. This pass changed no code and nothing outside this row: the item's hardened question is answered by a standing ruling, and the honest act is to record that rather than to build past it.** + +**The pacing keys reach four of the nine externally-facing inbound factories.** Executed against this worktree, confirmed by `messagefoundry.transports.mllp.__file__` resolving inside it: `MLLP`, `Tcp`, `X12` and `Http` accept both `max_messages_per_second` and `message_burst` and carry each into the connection spec's `settings`; `DICOM`, `File`, `Sftp`, `Ftp` and `DatabasePoll` construct normally and then raise `TypeError` on each key. Every row ran a control leg constructing the factory WITHOUT the keys first, so a rejection is a fact about the key and not about a bad signature -- three rows failed exactly that control on the first run, reported themselves uninterpretable rather than absent, and were re-run against the real signatures. `Timer`, `Loopback` and `PassThrough` register sources too and are out of the probe's scope, because none of them takes externally-controlled volume. By symbol, `_MessagePacer` and `_pacing_settings` each appear in `transports/mllp.py`, `tcp.py`, `x12.py` and `http_listener.py` and in no other transport module; two positive controls fire against that zero, `source_ip_allowlist` returning 12 in `dicom.py` -- the very file the pacer scan calls zero -- and `register_source` returning 2 in every inbound transport module including the four the pacer scan calls zero. + +**The half of the 2026-08-20 score that survives is the half that decides the cell.** `DEFAULT_MAX_MESSAGES_PER_SECOND` is still `None`, each of the four factories still defaults both keys to `None`, and `max_messages_per_second` appears nowhere in `messagefoundry/` outside those four transport modules and `config/wiring.py` -- run with the exclusion inverted as its control, which returns 4 and 11 in the two files that do carry it. Nothing reads the key at startup and no gate refuses to start without it. So a default install WOULD still take messages at an unbounded rate on first deployment, on every intake. The asymmetry that makes that interesting is this item's Severity line and is not restated here. + +**The 2026-08-16 owner ruling settles this cell on the shipped default, and no further WIDENING of the pacer's surface moves it.** Walk `docs/ASVS-ASSESSMENT-METHOD.md` section 1 in order. Rule 3 asks whether code implementing the verb exists reachable by ANY configuration: yes, on four intakes, so not `fail`. Rule 4 asks whether the verb is satisfied by a shipped default OR by a gate that refuses to start when the precondition is absent: neither, so not `pass` -- and section 1.1a records the ruling that rule 4 is strict, that an off-by-default control can NEVER be graded `pass`, and that the vacuity argument is pre-rejected by name. Rule 5 therefore selects `partial`, on two independent limbs at once: the control ships off, AND it covers part of the surface. That is why the 2026-09-03 port did not move the verdict, and why finishing the port would not move it either -- widening the surface answers rule 5's third limb while rule 4 goes on asking about the default. `transports/mllp.py` reached this conclusion first and says so in the comment above `DEFAULT_MAX_MESSAGES_PER_SECOND`: *"The cell stays `partial` on the shipped default and the record says why; that is the honest outcome, not a disappointing one."* The module knew before the item did, for the second time in this row's history. + +**What the cell should re-score to, and on what evidence.** `partial` -- the same verdict on materially different evidence, which is the whole of what this pass hands the re-scoring seat. I did NOT open the scorecard; it lives in the vault clone, outside this repo, so `partial` here is what the METHOD selects at `a2eef0f3` and not a reading of what that file currently holds. What changed is the reachability evidence: a cell reasoning that the pacer is MLLP-only is now stale, as is a residual saying a non-MLLP inbound has no opt-in bound. What must still be said is that the shipped default is unbounded on every intake, and that at least `DICOM`, `File`, `Sftp`, `Ftp` and `DatabasePoll` reach no rate control in any configuration. Only two changes could carry this cell to `pass`, and each needs an owner ruling before any build: flipping the default, which the 2026-08-11 ruling set OFF as a deliberate deviation rather than an oversight and which this item already disqualifies on its own terms; and the refuse-to-start gate of rule 4's second limb, which the "Deliberately NOT built" paragraph leaves open as a new gate class. **A builder who reaches this row should not build IN ORDER TO MOVE THIS CELL.** The other subjects on the proposed-work list above stay buildable on their own merits -- the poll-source per-tick ceilings, the DICOM association bound, the egress parameter -- but none of them changes the verdict, so none of them closes this item. The two acts that would are the owner's, and the closing act after that is the re-scoring seat's. + +**The `docs/SECURITY.md:1582` citation is re-pointed, by content.** That line number now lands on account-lockout prose about an assertion leg, so the anchor is dead. The live sentence is the **"Ingest plane"** row of the table under the **"Business-logic limits (ASVS 2.1.3)"** heading; cite it that way rather than by line, because this row has now watched the anchor drift twice. The sentence was rewritten when the pacer landed and it no longer supports the claim this item drew from it. It still states that a default install has NO message-rate bound on the ingest plane and calls that a ruled posture rather than a gap; it now also names the four covered intakes, names the DICOM SCP, the File/RemoteFile/Database poll sources and any per-peer bound as not covered even when set, and cites this item by number. `tests/test_security_doc_rate_limits.py::test_ingest_plane_rate_limit_row_matches_the_code` reads reachability from the factory signatures and requires the row to agree with them, so neither the covered nor the uncovered state is settled by build. + +**One residual measured here and deliberately left unedited.** That same row concludes that "the code-first and the TOML surface both express them" across all four named factories. For `X12` the second half is false, for the reason the separate-subject paragraph above already gives: `_TRANSPORTS` carries no `x12` key, so the shipped sentence generalises past its own premise. The gap is already pinned with its own positive control in `tests/test_ingress_message_pacing.py::test_x12_has_no_toml_surface_at_all_which_is_a_separate_gap`; what is new here is only that the security prose overstates it. Left for its own diff -- this pass touched no document outside this row, and a security sentence deserves a change a reviewer can see on its own. + ## 1115. research an honest pass for ASVS 2.4.2 -- whether human-timing pacing is meaningful for an engine whose only human surface is the console > 馃敘 **Re-scored 2026-08-20 -> P3.** Value **4/10** 路 Difficulty **6/10** 路 _money pit_. The research half is delivered and the code it was written against is unchanged: the /ui surface charges nothing (zero allow_admin_write references in the web console) and the only pacing is a per-request per-actor budget at config/settings.py:2017-2021, never a flow timer. Value 4 because on a first deployment this is a coverage and calibration gap on an admin surface rather than a data-plane exposure; difficulty 6 because a flow timer spanning login, MFA enrolment and approve-then-decide is a new mechanism across auth service, API and console, and the floor has to come from a measurement the record does not have. _(was 3/10 路 5/10.)_