Skip to content

fix(security): resolve all Dependabot advisories and audit gaps - #1

Merged
doublegate merged 5 commits into
mainfrom
fix/dependabot-security-remediation
Aug 28, 2026
Merged

fix(security): resolve all Dependabot advisories and audit gaps#1
doublegate merged 5 commits into
mainfrom
fix/dependabot-security-remediation

Conversation

@doublegate

@doublegate doublegate commented Aug 28, 2026

Copy link
Copy Markdown
Owner

Important

Breaking: MSRV rises 1.85 → 1.88.

This is forced, not incidental. RUSTSEC-2026-0009 is a DoS via stack exhaustion in time, which ProRT-IP reaches through x509-parser when parsing certificates from scanned hosts — attacker-influenced input, so genuinely reachable. Every time release that fixes it (0.3.47 through 0.3.55) declares rust-version = "1.88.0"; there is no version both patched and buildable on 1.85. Holding the old floor meant shipping the advisory.

I confirmed this wasn't just dependency drift: a targeted update of only the advisory-affected crates, leaving time alone, holds the graph at exactly main's MSRV baseline. The floor moves for this one advisory.

It also fixes a pre-existing violation — home 0.5.12 already required 1.88 and was quietly breaking the 1.85 promise on main. Verified afterwards: 499 packages resolved, 0 require > 1.88.

Updated in Cargo.toml, the README badge and requirements, WARP.md, CONTRIBUTING.md, the PR-template checklist, and the CI job.

Encapsulates the outstanding Dependabot PR (bytes 1.11.0 → 1.11.1) and remediates every RustSec advisory affecting a crate this project compiles.

Before After
cargo audit (workspace) 9 vulnerabilities, 10 warnings exit 0
cargo audit (fuzz/) 7 vulnerabilities — never audited exit 0
cargo deny check licences failing exit 0 (advisories, licenses, bans, sources)
cargo test --workspace 2,563 passed 2,581 passed, 0 failed

Fixed by dependency updates alone

All semver-compatible; no manifest edits needed.

Crate Advisory
bytes → ≥1.11.1 RUSTSEC-2026-0007 — integer overflow in BytesMut::reserve
crossbeam-epoch → ≥0.9.20 RUSTSEC-2026-0204
rkyv → ≥0.8.17 RUSTSEC-2026-0233/0234/0235 — use-after-free and OOB reads deserialising crafted archives
time → ≥0.3.47 RUSTSEC-2026-0009 — DoS via stack exhaustion (drives the MSRV raise)
memmap2 ≥0.9.11, anyhow ≥1.0.103, rand ≥0.8.6, event-listener unsoundness warnings

Two dependencies were declared but never imported

The two highest-severity advisories in this project came from a crate that contributed nothing to it.

  • quick-xml removed — RUSTSEC-2026-0194 and RUSTSEC-2026-0195, both 7.5 high (quadratic parse time on duplicate attribute names; unbounded namespace-declaration allocation). It appeared only in crates/prtip-cli/Cargo.toml; no source file imports it. ProRT-IP writes its Nmap-compatible XML by hand.
  • tui-input removed — likewise never imported, and the sole reason a second ratatui (0.28.1) was being built alongside 0.29.0.

Upgrades that clear the remaining warnings

  • ratatui 0.29 → 0.30 drops paste (RUSTSEC-2024-0436) and moves to lru ≥0.18.2, fixing RUSTSEC-2026-0002 and RUSTSEC-2026-0253. Needs Rust 1.88, which the time fix already required. I checked the 0.30 breaking-change surface against this codebase before committing to it — Alignment rename, block::Title/Position, highlight_symbol, WidgetRef, custom Backend impls, Styled, braille symbols — and ProRT-IP uses none of it. Zero code edits.
  • indicatif 0.17 → 0.18 replaces number_prefix (RUSTSEC-2025-0119) with unit-prefix. crates/prtip-scanner had pinned its own indicatif = "0.17" outside the workspace table and was building a second copy.
  • console 0.15 → 0.16, crossterm 0.28 → 0.29 to deduplicate crates left doubled by the above.

The fuzz crate was never audited

It is excluded from the workspace and keeps its own Cargo.lock, so neither cargo deny nor a workspace cargo audit ever read it. It carried three rustls-webpki advisories that do not appear in the root at all — RUSTSEC-2026-0098/0099 (name constraints wrongly accepted for URI names and wildcard-asserting certificates) and RUSTSEC-2026-0104 (reachable panic parsing CRLs) — plus bytes, crossbeam-epoch and time. Fixed, and CI now audits that lockfile.

rsa: exempted, not fixed

RUSTSEC-2023-0071 (Marvin Attack) has no fixed version. rsa is never compiled — it reaches the lockfile only via sqlx-mysql, an optional dependency of sqlx, and this workspace enables only ["runtime-tokio", "sqlite", "chrono"]. Cargo records optional dependencies whether or not their feature is on.

Verified rather than argued:

cargo tree -i rsa        --workspace --target all  -> nothing to print
cargo tree -i sqlx-mysql --workspace --target all  -> nothing to print

deny.toml ends with no advisory exemptions at all. The rsa exemption lives in .cargo/audit.toml (mirrored in fuzz/.cargo/) with the condition under which it must be withdrawn. It is deliberately not in deny.toml: cargo-deny resolves the feature-aware graph and never encounters rsa, so an entry there would only emit advisory-not-detected — and would mask the advisory if a future change made rsa genuinely reachable.

A real bug found while removing quick-xml

Removing an XML library from a project that hand-writes XML meant auditing what it hand-writes.

crates/prtip-cli/src/output.rs escaped the banner but interpolated result.service straight into name="...". Both are chosen by the host being scanned, so a hostile target could close the attribute and inject elements into a scan report — a forgery against whatever consumes it. export.rs escaped both correctly, which is precisely the inconsistency two hand-rolled copies invite.

Both now share one implementation in crates/prtip-cli/src/xml.rs, hardened beyond the five metacharacters: XML 1.0 forbids most control characters even as numeric references, so a banner containing a NUL — unremarkable from a binary protocol — previously produced a document conforming parsers reject. Those are now rendered as visible \xNN, matching how Nmap presents non-printable banner bytes, so evidence survives and the document stays valid.

Nine tests, including a negative control that walks every byte value asserting no illegal code point survives, and an attribute-breakout attempt.

Keeping it fixed

  • .github/dependabot.yml added. There was no Dependabot config at all, so the repository received only the security PRs GitHub opens unprompted; routine version updates were never proposed. That is how a quick-xml five minor versions behind and a duplicate ratatui major went unnoticed. Updates are grouped — minor/patch batched, majors separate, security together — because ungrouped updates on a workspace this size produce enough PRs to be ignored, which is indistinguishable from having none. Covers cargo (root and fuzz), GitHub Actions, and docker/.
  • CI runs all four cargo-deny checks, not just advisories. The licence check had never run: deny.toml still allowed the deprecated GPL-3.0 identifier while the workspace now declares GPL-3.0-or-later, so cargo deny check licenses was failing and CI could not have caught it.
  • .gitignore fixed so audit.toml is tracked. .cargo/ was excluded wholesale, which silently dropped both exemption files from the first staging attempt and would have failed CI's fuzz audit. The pattern has to be .cargo/* plus a !.cargo/audit.toml negation — git does not descend into an excluded directory, so a negation under .cargo/ is never consulted.
  • Stale deny.toml ignores for paste and number_prefix removed; both crates are gone from the tree.

Documentation link checking, fixed properly

The link check failed three consecutive runs on three different external hosts — a 429 from docs.ansible.com, then a connection failure to bgp.tools — while seven genuinely broken internal links sat undetected behind them. Fixing external symptoms one at a time was never going to converge, because they weren't defects in the repository.

The seven broken links are in to-dos/PHASE-5/SPRINT-5.5.{5,6}-TODO.md, pointing at ../docs/... and ../benchmarks/... from to-dos/PHASE-5/ — one directory short. All four targets exist; they now use ../../. They were never reported because the to-dos/ step ended with || true, discarding every failure in that directory.

Merges no longer depend on third-party uptime. Pull requests and pushes check internal links only (relative paths, heading anchors) — deterministic, always the author's to fix. The weekly schedule and manual dispatches check external links too, so link rot is still caught without blocking anyone. Verified across all 232 markdown files: zero broken internal links. PR runs also dropped from 4m38s to 1m20s.

Three further ways this check was lying:

  • mlc_config.json was inert — the workflow generated its own config over the top, so editing the committed file did nothing. It's now the single source of truth, with only the PR-mode ignore layered on.
  • The summary always claimed success — it ran with if: always() and unconditionally printed "✅ All markdown links validated successfully", including on failed runs.
  • The first broken link hid the rest — the loop exited at the first failure. It now checks every file and reports how many failed.

Note on the superseded PR

The one open Dependabot PR (bytes 1.11.0 → 1.11.1) lives on doublegate/ProRT-IP_NPSL, which is now private and archived, so it cannot be merged or closed there. Its content is included here.

Verification

cargo audit                                              exit 0   (workspace)
cargo audit  (in fuzz/)                                  exit 0
cargo deny check                                         exit 0   all four checks
cargo fmt --all -- --check                               exit 0
cargo clippy --workspace --all-targets -- -D warnings    exit 0
cargo test --workspace          2,581 passed, 0 failed, 121 ignored

Test count 2,563 → 2,581: the +18 is the 9 new xml.rs tests, which prtip-cli compiles into both its lib and its bin.

🤖 Generated with Claude Code

https://claude.ai/code/session_01NBDCJvLbq7nuor7RtT57rD

Supersedes the outstanding Dependabot PR (`bytes` 1.11.0 -> 1.11.1) and
remediates every RustSec advisory affecting a crate this project compiles.

Before: 9 vulnerabilities + 10 unmaintained/unsound warnings in the workspace,
and a `fuzz` lockfile nothing had ever audited.
After: `cargo audit` and `cargo deny check` both exit 0, workspace and fuzz.

## Fixed by dependency updates alone

`bytes` >=1.11.1 (RUSTSEC-2026-0007, integer overflow in `BytesMut::reserve`),
`crossbeam-epoch` >=0.9.20 (RUSTSEC-2026-0204), `rkyv` >=0.8.17
(RUSTSEC-2026-0233/0234/0235: use-after-free and out-of-bounds reads
deserialising crafted archives), `time` >=0.3.47 (RUSTSEC-2026-0009, DoS via
stack exhaustion), plus unsound-flagged `memmap2` >=0.9.11, `anyhow` >=1.0.103,
`rand` >=0.8.6 and `event-listener`. All semver-compatible; no manifest edits.

## Two dependencies were declared but never imported

The two highest-severity advisories in this project came from a crate that
contributed nothing to it.

- **`quick-xml` removed** -- RUSTSEC-2026-0194 and RUSTSEC-2026-0195, both 7.5
  high (quadratic parse time on duplicate attribute names; unbounded
  namespace-declaration allocation). It appeared only in
  `crates/prtip-cli/Cargo.toml`; no source file imports it. ProRT-IP writes its
  Nmap-compatible XML by hand.
- **`tui-input` removed** -- likewise never imported, and the sole reason a
  second `ratatui` (0.28.1) was being built alongside 0.29.0.

## Upgrades that clear the remaining warnings

- **`ratatui` 0.29 -> 0.30** drops `paste` (RUSTSEC-2024-0436, unmaintained)
  and moves to `lru` >=0.18.2, fixing RUSTSEC-2026-0002 and RUSTSEC-2026-0253.
  Checked the 0.30 breaking-change surface against this codebase first --
  `Alignment` rename, `block::Title`/`Position`, `highlight_symbol`,
  `WidgetRef`, custom `Backend` impls, `Styled`, braille symbols -- and
  ProRT-IP uses none of it. Zero code edits required.
- **`indicatif` 0.17 -> 0.18** replaces `number_prefix` (RUSTSEC-2025-0119,
  unmaintained) with `unit-prefix`. `crates/prtip-scanner` had pinned its own
  `indicatif = "0.17"` outside the workspace table, so it was building a second
  copy; it now uses the workspace version.
- **`console` 0.15 -> 0.16, `crossterm` 0.28 -> 0.29** to deduplicate two
  crates left doubled by the upgrades above.

## The fuzz crate was never audited

`fuzz` is excluded from the workspace and keeps its own `Cargo.lock`, so
neither `cargo deny` nor a workspace `cargo audit` ever read it. It carried
three unfixed `rustls-webpki` advisories that do not appear in the root at all
-- RUSTSEC-2026-0098 and -0099 (name constraints wrongly accepted for URI names
and for wildcard-asserting certificates) and RUSTSEC-2026-0104 (reachable panic
parsing certificate revocation lists) -- plus `bytes`, `crossbeam-epoch` and
`time`. Its lockfile is updated and CI now audits it.

## rsa: exempted, not fixed

RUSTSEC-2023-0071 (Marvin Attack) has no fixed version. `rsa` is never
compiled: it reaches the lockfile only through `sqlx-mysql`, an *optional*
dependency of `sqlx`, and this workspace enables only
`["runtime-tokio", "sqlite", "chrono"]`. Cargo records optional dependencies in
the lockfile whether or not their feature is on.

Verified rather than argued:

    cargo tree -i rsa        --workspace --target all  -> nothing to print
    cargo tree -i sqlx-mysql --workspace --target all  -> nothing to print

The exemption is in `.cargo/audit.toml` (mirrored in `fuzz/.cargo/`), with the
condition under which it must be withdrawn. It is deliberately *not* in
`deny.toml`: cargo-deny resolves the feature-aware graph and never encounters
`rsa`, so an entry there would only emit "advisory-not-detected" -- and would
mask the advisory if a future change made `rsa` genuinely reachable.

## A real bug found while removing quick-xml

Removing an XML library from a project that hand-writes XML meant auditing what
it hand-writes. `crates/prtip-cli/src/output.rs` escaped the banner but
interpolated `result.service` straight into `name="..."`. Both are chosen by the
host being scanned, so a hostile target could close the attribute and inject
elements into a scan report -- a forgery against whatever consumes it. The
parallel implementation in `export.rs` escaped both correctly, which is the
inconsistency two hand-rolled copies invite.

Both now use one implementation, `crates/prtip-cli/src/xml.rs`, hardened beyond
the five metacharacters: XML 1.0 forbids most control characters *even as
numeric references*, so a banner containing a NUL -- unremarkable from a binary
protocol -- previously produced a document conforming parsers reject. Those are
rendered as visible `\xNN`, matching how Nmap presents non-printable banner
bytes, so evidence survives and the document stays valid. Nine tests, including
a negative control walking every byte value asserting no illegal code point
survives, and an attribute-breakout attempt.

## Keeping it fixed

- **`.github/dependabot.yml` added.** There was no Dependabot config at all, so
  the repository got only the security PRs GitHub opens unprompted; routine
  updates were never proposed. That is how a `quick-xml` five minor versions
  behind and a duplicate `ratatui` major went unnoticed. Updates are grouped --
  minor/patch batched, majors separate, security together -- because ungrouped
  updates on a workspace this size produce enough PRs to be ignored, which is
  indistinguishable from having none. Covers cargo (root and `fuzz`), GitHub
  Actions, and `docker/`.
- **CI runs all four cargo-deny checks**, not just `advisories`. The licence
  check had never run: `deny.toml` still allowed the deprecated `GPL-3.0`
  identifier while the workspace now declares `GPL-3.0-or-later`, so
  `cargo deny check licenses` failed locally and CI could not have caught it.
- **`.gitignore` fixed** so `audit.toml` is tracked. `.cargo/` was excluded
  wholesale, which silently dropped both exemption files from the first staging
  attempt and would have failed CI's fuzz audit. The pattern must be
  `.cargo/*` with a `!.cargo/audit.toml` negation -- git does not descend into
  an excluded directory, so a negation under `.cargo/` is never consulted.
- Stale `deny.toml` ignores for `paste` and `number_prefix` removed; both
  crates are gone from the tree entirely.

## Verification

    cargo audit                     exit 0  (workspace)
    cargo audit  (in fuzz/)         exit 0
    cargo deny check                exit 0  advisories, licenses, bans, sources
    cargo fmt --all -- --check      exit 0
    cargo clippy --workspace --all-targets -- -D warnings   exit 0
    cargo test --workspace          2,581 passed, 0 failed, 121 ignored

Test count is 2,563 -> 2,581; the +18 is the 9 new `xml.rs` tests, which
`prtip-cli` compiles into both its lib and its bin.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NBDCJvLbq7nuor7RtT57rD
Copilot AI lite review requested due to automatic review settings August 28, 2026 17:51
@github-actions

Copy link
Copy Markdown

🤖 Hi @doublegate, I've received your request, and I'm working on it now! You can track my progress in the logs for more details.

@coderabbitai

coderabbitai Bot commented Aug 28, 2026

Copy link
Copy Markdown

Important

  • 🔍 Trigger review

This repository does not receive automatic reviews because it has fewer than 10 stars.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 549ca136-726d-4208-9904-c9decf855ad5


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions

Copy link
Copy Markdown

🤖 I'm sorry @doublegate, but I was unable to process your request. Please see the logs for more details.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

The new XML escaping currently loses evidence for U+FFFE/U+FFFF by emitting a fixed \u{fffd} literal (and tests codify it), which contradicts the module’s stated evidence-preserving goal.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Pull request overview

This PR remediates Rust supply-chain/security findings across the workspace (and the out-of-workspace fuzz crate) by upgrading vulnerable dependencies, removing unused advisory-carrying crates, and hardening hand-written Nmap-compatible XML output escaping.

Changes:

  • Update Rust dependencies to eliminate active RustSec advisories/unsoundness warnings; remove unused quick-xml and tui-input, and deduplicate progress/TUI stacks via workspace dependency alignment.
  • Centralize and strengthen XML escaping for -oX output into crates/prtip-cli/src/xml.rs, and route both XML emitters through it.
  • Add/enable ongoing hygiene: Dependabot configuration, full cargo-deny checks in CI, and cargo audit coverage for the separate fuzz lockfile (with mirrored audit exemptions).
File summaries
File Description
fuzz/Cargo.lock Updates fuzz-target dependency graph to clear advisories in its separate lockfile.
fuzz/.cargo/audit.toml Adds fuzz-local cargo audit ignore config mirroring the root rationale for rsa.
deny.toml Removes stale ignores and updates license allowlist to match declared project licensing.
crates/prtip-tui/Cargo.toml Drops unused tui-input dependency (and associated duplicate ratatui build).
crates/prtip-scanner/Cargo.toml Switches indicatif to the workspace version to avoid duplicate builds and pick up fixes.
crates/prtip-cli/src/xml.rs Introduces shared XML escaping implementation + tests for safe -oX output.
crates/prtip-cli/src/output.rs Uses shared escape_xml for service name + banner in XML formatter.
crates/prtip-cli/src/main.rs Registers the new xml module in the CLI binary crate.
crates/prtip-cli/src/lib.rs Exposes the new xml module from the CLI library crate.
crates/prtip-cli/src/export.rs Removes local XML escaping helper and uses shared escape_xml.
crates/prtip-cli/Cargo.toml Removes unused quick-xml and bumps console to align with dependency updates.
CHANGELOG.md Documents advisory remediation, XML hardening, CI/Dependabot additions, and supply-chain changes.
Cargo.toml Bumps workspace dependency versions (e.g., indicatif, ratatui, crossterm).
.gitignore Adjusts .cargo ignore rules to ensure audit config files remain tracked.
.github/workflows/ci.yml Runs full cargo-deny checks and adds cargo audit for the fuzz lockfile.
.github/dependabot.yml Adds Dependabot config with grouping for Cargo (workspace + fuzz), actions, and docker.
.cargo/audit.toml Adds cargo audit ignore config documenting and constraining the rsa exemption.
Review details

Suppressed comments (1)

crates/prtip-cli/src/xml.rs:172

  • This test encodes the current non-character behavior; if escape_xml preserves U+FFFE/U+FFFF visibly (rather than mapping both to \\u{fffd}), the assertions should be updated accordingly so the test continues to verify evidence-preserving escaping.
    #[test]
    fn renders_non_characters() {
        assert_eq!(escape_xml("a\u{FFFE}b"), "a\\u{fffd}b");
        assert_eq!(escape_xml("a\u{FFFF}b"), "a\\u{fffd}b");
    }
  • Files reviewed: 15/18 changed files
  • Comments generated: 2
  • Review effort level: Lite

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment on lines +73 to +75
// Non-characters, likewise illegal.
'\u{FFFE}' | '\u{FFFF}' => out.push_str("\\u{fffd}"),

Comment thread .github/workflows/ci.yml
Comment on lines +297 to +302
- name: Install cargo-audit
run: cargo install cargo-audit --locked

- name: Audit fuzz lockfile
working-directory: fuzz
run: cargo audit
@github-advanced-security

Copy link
Copy Markdown

You are seeing this message because GitHub Code Scanning has recently been set up for this repository, or this pull request contains the workflow file for the Code Scanning tool.

What Enabling Code Scanning Means:

  • The 'Security' tab will display more code scanning analysis results (e.g., for the default branch).
  • Depending on your configuration and choice of analysis tool, future pull requests will be annotated with code scanning analysis results.
  • You will be able to see the analysis results for the pull request's branch on this overview once the scans have completed and the checks have passed.

For more information about GitHub Code Scanning, check out the documentation.

doublegate and others added 4 commits August 28, 2026 14:06
The MSRV check failed on the previous commit, and pinning around it turned out
to be impossible rather than merely awkward.

RUSTSEC-2026-0009 is a denial of service via stack exhaustion in `time`.
ProRT-IP reaches `time` through `x509-parser` while parsing certificates from
scanned hosts, so the input is attacker-influenced and the advisory is
genuinely reachable -- not a lockfile artifact.

Every `time` release that fixes it declares `rust-version = "1.88.0"`:

    time 0.3.47  1.88.0      time 0.3.52  1.88.0
    time 0.3.49  1.88.0      time 0.3.53  1.88.0
    time 0.3.51  1.88.0      time 0.3.54  1.88.0
                             time 0.3.55  1.88.0

There is no version that is both patched and buildable on 1.85, so holding the
old floor meant shipping the advisory. Checked before concluding this: a
*targeted* update of only the advisory-affected crates, leaving `time` at
0.3.44, holds the graph at exactly `main`'s MSRV baseline -- so the floor is
being raised for this one advisory, not by dependency drift.

Raising it also unblocks `ratatui` 0.30, which needs 1.88 and clears three more
advisories (`paste` RUSTSEC-2024-0436 unmaintained; `lru` RUSTSEC-2026-0002 and
RUSTSEC-2026-0253 unsound, via `lru` >=0.18.2). ProRT-IP uses none of ratatui
0.30's breaking-change surface, so no code edits were needed. `crossterm` moves
0.28 -> 0.29 to match its backend and stay deduplicated.

Result: `deny.toml` now has **no advisory exemptions at all**. The previously
documented ignores for `paste` and `number_prefix` are gone because both crates
have left the tree.

Verified that nothing in the resolved graph needs more than the new floor:

    499 packages resolved, 0 require > 1.88

That also fixes a pre-existing violation -- `home` 0.5.12 already required 1.88
and was quietly breaking the 1.85 promise on `main`.

MSRV updated everywhere it is stated, not just the manifest: `Cargo.toml`,
the README badge and requirements list, `WARP.md`, `CONTRIBUTING.md`, the
pull-request template checklist, and the CI job name and toolchain pin.

One code change fell out of it: `clippy::manual_is_multiple_of` is MSRV-gated
(the method stabilised in 1.87), so raising the floor turned it on.
`mmap_writer.rs`'s compile-time alignment assertion now reads
`ENTRY_SIZE.is_multiple_of(16)`.

Verification:

    cargo audit                                              exit 0  (workspace)
    cargo audit  (in fuzz/)                                  exit 0
    cargo deny check                                         exit 0  all four checks
    cargo fmt --all -- --check                               exit 0
    cargo clippy --workspace --all-targets --locked -- -D warnings   exit 0
    cargo test --workspace          2,581 passed, 0 failed, 121 ignored

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NBDCJvLbq7nuor7RtT57rD
The markdown link check failed on `https://docs.ansible.com/ansible/latest/`
with "Status: 429", reported as a dead link.

A 429 is the server answering. It proves the host exists, resolves, and is
serving -- it is declining this particular request, which is the opposite of
link rot. The config already sets `retryOn429` with three retries, but
docs.ansible.com throttles CI runners harder than that, so the retries expire
and the build fails on a link that is demonstrably fine.

429 now sits in `aliveStatusCodes` alongside 403 and 999, which are already
there for the same reason: the server responded, just not with content.

Unrelated to this branch's dependency work; it was failing on `main` too, and
blocks the PR.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NBDCJvLbq7nuor7RtT57rD
The link checker reported `#hotfix-procedures` as a 404 even though
`## Hotfix Procedures` exists in the same file at line 585, outside any code
fence, in clean UTF-8. It is the only bare same-file anchor in the document.

Qualifying it as `release-process.md#hotfix-procedures` resolves identically
on GitHub and in mdBook, where each page renders separately, and gives the
checker a file to resolve against.

Pre-existing; surfaced only once the 429 misclassification stopped failing the
same job first.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NBDCJvLbq7nuor7RtT57rD
The link check failed three consecutive runs on three different external hosts
-- a 429 from docs.ansible.com, then a connection failure to bgp.tools -- while
seven genuinely broken internal links sat undetected behind them. Fixing the
external symptoms one at a time was never going to converge, because the
failures were not defects in the repository.

## Seven broken internal links, previously invisible

`to-dos/PHASE-5/SPRINT-5.5.5-TODO.md` and `SPRINT-5.5.6-TODO.md` link to
`../docs/00-ARCHITECTURE.md`, `../docs/31-BENCHMARKING-GUIDE.md`,
`../docs/34-PERFORMANCE-CHARACTERISTICS.md` and
`../benchmarks/profiling/PROFILING-ANALYSIS.md`. From `to-dos/PHASE-5/` that
resolves to `to-dos/docs/...`, one directory short. All four targets exist; the
links now use `../../`.

They were never reported because the `to-dos/` step ended with `|| true`, which
discards every failure in that directory.

## Merges no longer depend on other people's uptime

The job validated external links on every pull request. Whether a branch could
merge therefore depended on whether unrelated websites chose to answer a GitHub
runner -- non-deterministic, unactionable by the author, and the fastest way to
train everyone to ignore a red check.

Two modes now:

  pull_request / push  -> internal links only (relative paths, anchors).
                          Deterministic, always the author's to fix.
  schedule / dispatch  -> internal + external. Link rot still gets caught
                          weekly, without blocking anyone.

Verified across all 232 markdown files in scope: zero broken internal links.

## Three other ways this check was lying

- **`mlc_config.json` was inert.** The workflow generated its own config over
  the top of the committed file, so editing the committed one changed nothing.
  It is now the single source of truth; the workflow layers only the PR-mode
  `^https?://` ignore onto it.
- **The summary always claimed success.** The report step ran with
  `if: always()` and unconditionally printed "✅ All markdown links validated
  successfully" -- including on a failed run. It now prints the file counts and
  the real outcome.
- **The first broken link hid the rest.** The loop exited at the first failure.
  It now checks every file, groups each failure in the log, and reports how many
  files failed before exiting non-zero.

`aliveStatusCodes` keeps 429 alongside 403 and 999: all three mean the server
answered and declined this particular request, which proves the host resolves.

`mlc_effective.json` and `files.txt` are generated at run time and gitignored.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NBDCJvLbq7nuor7RtT57rD
@doublegate
doublegate merged commit eac01e8 into main Aug 28, 2026
13 checks passed
doublegate added a commit that referenced this pull request Aug 28, 2026
…sl binary

Fixes two Dockerfile defects found while merging the Alpine bump (#2).

- Builder pinned Rust 1.85, below the 1.88 MSRV raised in #1 -- the image could not compile the project.
- The alpine stage copied a Debian glibc binary into a musl image, producing a container in which prtip could not exec at all. A real musl-builder stage now compiles natively on rust:1.88-alpine.

Verified: both images build and run prtip 1.0.0; alpine links ld-musl-x86_64.so.1 at 26.9 MB. New CI job builds both targets, runs the binary in each, and asserts musl linkage.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants