max / makenotwork
- Co-Authored-By
- Claude Opus 4.6 (1M context) <noreply@anthropic.com>
13 files changed,
+370 insertions,
-124 deletions
| @@ -4,6 +4,15 @@ | |||
| 4 | 4 | ||
| 5 | 5 | ## Changes Since Last Audit | |
| 6 | 6 | ||
| 7 | + | ### Tenth formal audit (2026-04-30, Run 17 cross-project) | |
| 8 | + | - **Test count:** 228 (58 unit + 170 integration). 0 clippy warnings. 0 failures. | |
| 9 | + | - **Grade:** A (maintained). v0.3.4. ~9,928 LOC. | |
| 10 | + | - **Scorecard upgrades:** Performance A- -> A (batch queries for all N+1 paths confirmed). Documentation B+ -> A- (module-level //! docs on all significant files). Observability B+ -> A (#[instrument] on every handler and DB function). Codebase Size B+ -> A (9,928 LOC lean for full forum). Migration Safety A+ -> A- (early migrations 001-007 lack IF NOT EXISTS). | |
| 11 | + | - **Cold spots (2):** seed.rs (1,032 LOC, B+) and early migrations 001-007 (lack IF NOT EXISTS, B+). | |
| 12 | + | - **Mandatory surprise:** Link preview fetching runs synchronously in the handler. Posts with multiple slow URLs could block for up to 15s. Also: constant_time_compare returns false immediately on length mismatch (non-issue: tokens are always fixed 64-char hex). | |
| 13 | + | - **All 13 previously resolved items verified intact.** No regressions. | |
| 14 | + | - **New action item:** [LOW] Consider spawning link preview fetch as detached task (routes/forum/posts.rs). | |
| 15 | + | ||
| 7 | 16 | ### Seventh formal audit (2026-03-28, Run 12 cross-project) | |
| 8 | 17 | - **Test count:** 225 (35 unit lib + 190 integration). 0 clippy warnings. 0 failures. | |
| 9 | 18 | - **Grade:** A (maintained). v0.3.2. |
| @@ -1,11 +1,11 @@ | |||
| 1 | 1 | # Multithreaded -- Code Audit Review | |
| 2 | 2 | ||
| 3 | - | **Last audited:** 2026-04-18 (ninth audit, Run 15 cross-project) | |
| 4 | - | **Previous audit:** 2026-04-15 (eighth audit, Run 14 cross-project) | |
| 3 | + | **Last audited:** 2026-04-30 (tenth audit, Run 17 cross-project) | |
| 4 | + | **Previous audit:** 2026-04-18 (ninth audit, Run 15 cross-project) | |
| 5 | 5 | ||
| 6 | 6 | ## Overall Grade: A | |
| 7 | 7 | ||
| 8 | - | Run 15 cross-project audit (updated 2026-04-22). 232 tests (all pass). 0 clippy warnings. v0.3.4. ~9,752 LOC. Health endpoint now verifies DB connectivity. OG image URL finding was incorrect (code never fetches/stores og:image -- only og:title and og:description). MNW API retry logic was already implemented (token exchange + userinfo both retry with backoff). | |
| 8 | + | Run 17 cross-project audit (remediated 2026-05-01). 228 tests (58 unit + 170 integration, all pass). 0 clippy warnings. v0.3.4. ~9,928 LOC. All 13 previously resolved items verified intact, no regressions. Link preview fetch now spawned as detached background task (fixed 2026-05-01). | |
| 9 | 9 | ||
| 10 | 10 | ## Scorecard | |
| 11 | 11 | ||
| @@ -13,19 +13,19 @@ | |||
| 13 | 13 | |-----------|:-----:|-------| | |
| 14 | 14 | | Code Quality | A | Zero clippy warnings. Consistent `map_err` + `tracing::error!` error handling. Mod log failures logged. | | |
| 15 | 15 | | Architecture | A+ | Clean 3-crate workspace: mt-core (time formatting), mt-db (queries/mutations), main app (routes, auth, templates). Route module properly split. Template layer uses view-model structs. | | |
| 16 | - | | Testing | A+ | 231 tests (all pass) at ~24 tests/KLOC. Integration tests use real PostgreSQL with per-test database isolation. Coverage on CRUD, permissions, bans, mute/unban/unmute, CSRF, pagination, rate limiting, category edit/reorder, endorsements, footnotes, verified quoting, mentions, link previews, profiles, auth flow (PKCE/state), admin routes. | | |
| 16 | + | | Testing | A | 228 tests (58 unit + 170 integration, all pass) at ~23 tests/KLOC. Integration tests use real PostgreSQL with per-test database isolation. Coverage on CRUD, permissions, bans, mute/unban/unmute, CSRF, pagination, rate limiting, category edit/reorder, endorsements, footnotes, verified quoting, mentions, link previews, profiles, auth flow (PKCE/state), admin routes. | | |
| 17 | 17 | | Security | A | All SQL parameterized. CSRF with constant-time comparison. OAuth PKCE with state nonce. Markdown via docengine (URL scheme allowlist + HTML sanitization). Fail-closed access checks. Link previews only store title/description, not og:image. | | |
| 18 | - | | Performance | A- | Proper indexes on all query patterns. Partial index on ban expiration. No N+1 queries. Per-IP rate limiting. | | |
| 19 | - | | Documentation | B+ | `//!` module docs on all source files. `.env.example` documents all env vars. Some gaps in architecture docs. | | |
| 18 | + | | Performance | A | Proper indexes on all query patterns. Partial index on ban expiration. Batch queries for all N+1 paths confirmed. Per-IP rate limiting. | | |
| 19 | + | | Documentation | A- | Module-level `//!` docs on all significant files. `.env.example` documents all env vars. Some view-model structs lack doc comments. | | |
| 20 | 20 | | Dependencies | A | Minimal deps, all justified. Rust 2024 edition. Dead deps removed. Workspace dependency management. | | |
| 21 | 21 | | Frontend | A | HTMX for dynamic interactions. Askama autoescaping. CSRF auto-injected. Toast uses `textContent`. `body_html` sanitized by docengine. | | |
| 22 | 22 | | Type Safety | A+ | Query layer uses focused `FromRow` projections. Clean domain types. | | |
| 23 | - | | Observability | B+ | 86 `#[instrument(skip_all)]` annotations. `tracing-subscriber` with EnvFilter. Gaps in some newer code paths. | | |
| 23 | + | | Observability | A | `#[instrument]` on every handler and every DB function. `tracing-subscriber` with EnvFilter. | | |
| 24 | 24 | | Concurrency | A | Async throughout with tokio. Graceful shutdown. reqwest timeouts. `swap_category_order` uses transaction. Per-IP rate limiting. | | |
| 25 | 25 | | Resilience | A | Graceful shutdown. HTTP client timeouts. Error logging without panics. Rate limiting. MNW API calls retry with backoff on network/5xx errors. | | |
| 26 | 26 | | API Consistency | A | Consistent redirect-with-toast pattern. Proper status codes (403/404/422). Health endpoint returns JSON. | | |
| 27 | - | | Migration Safety | A+ | SQLx `migrate!()` with sequential numbering. All additive. No destructive operations. | | |
| 28 | - | | Codebase Size | B+ | ~9,752 LOC -- lean for full forum with OAuth, CSRF, markdown, moderation, admin, pagination, soft-delete, and settings. | | |
| 27 | + | | Migration Safety | A- | SQLx `migrate!()` with sequential numbering. Later migrations use IF NOT EXISTS. Early migrations (001-007) lack it. Protected by sqlx tracking. | | |
| 28 | + | | Codebase Size | A | ~9,928 LOC -- lean for full forum with OAuth, CSRF, markdown, moderation, admin, pagination, soft-delete, and settings. | | |
| 29 | 29 | ||
| 30 | 30 | ## Module Heatmap | |
| 31 | 31 | ||
| @@ -49,15 +49,14 @@ | |||
| 49 | 49 | ||
| 50 | 50 | ### Cold Spots | |
| 51 | 51 | ||
| 52 | - | 1. ~~**Health endpoint**~~ -- Fixed 2026-04-22. Now checks DB with `SELECT 1` and returns `"degraded"` status if unreachable. | |
| 53 | - | 2. ~~**Link preview OG image URL**~~ -- Incorrect finding. Code only extracts og:title and og:description. No og:image URLs are fetched or stored. | |
| 54 | - | 3. ~~**MNW API retry logic**~~ -- Already implemented. Token exchange and userinfo both retry with 500ms/1000ms backoff on network/5xx errors. | |
| 52 | + | 1. **seed.rs (1,032 LOC) -- B+**: Large for seed data. Content is substantive but could externalize to TOML/JSON. | |
| 53 | + | 2. **Early migrations (001-007) -- B+**: Lack IF NOT EXISTS guards. Mitigated by sqlx migration tracking. | |
| 55 | 54 | ||
| 56 | 55 | ## Strengths | |
| 57 | 56 | ||
| 58 | 57 | - **Clean architecture.** 3-crate workspace with proper separation. Route module split into focused files. Template layer uses view-model structs. | |
| 59 | 58 | - **Comprehensive CSRF.** Synchronizer token with constant-time comparison, auto-injected via JS for all forms and HTMX requests. | |
| 60 | - | - **Solid test infrastructure.** Full Axum app with real PostgreSQL per test. Cookie-aware client with automatic CSRF token extraction. 231 tests all passing. | |
| 59 | + | - **Solid test infrastructure.** Full Axum app with real PostgreSQL per test. Cookie-aware client with automatic CSRF token extraction. 228 tests all passing. | |
| 61 | 60 | - **Authorization hierarchy.** Owner > mod > member correctly enforced. Owners cannot be banned. Only owners can ban mods. | |
| 62 | 61 | - **Input validation.** Length limits on all user content. Slug format validation. UUID parsing validated. Sort/order whitelisted. | |
| 63 | 62 | - **SQL safety.** All 40+ queries parameterized. Dynamic ORDER BY uses whitelist match. | |
| @@ -70,15 +69,16 @@ | |||
| 70 | 69 | ||
| 71 | 70 | ## Mandatory Surprise | |
| 72 | 71 | ||
| 73 | - | **~~Link preview image URL vulnerability~~ (Incorrect finding)** | |
| 72 | + | **Link preview fetching blocks post creation** | |
| 74 | 73 | ||
| 75 | - | The original Run 15 audit claimed og:image URLs were fetched and rendered without validation. This was incorrect -- `fetch_og_metadata` only extracts `og:title` and `og:description`. The `LinkPreviewViewRow` struct has `url` (the original post link, already validated by `validate_url`), `title`, and `description` -- no image field. Verified 2026-04-22 by reading link_preview.rs and templates/public.rs. | |
| 74 | + | ~~Link preview fetching runs synchronously in the handler.~~ Fixed 2026-05-01. `spawn_link_preview_fetch` now runs as a detached `tokio::spawn` task. Post creation returns immediately; previews appear asynchronously. | |
| 76 | 75 | ||
| 77 | - | **Verdict:** No issue. | |
| 76 | + | Also notable: The `constant_time_compare` in csrf.rs returns false immediately on length mismatch (leaks token length via timing). In practice this is a non-issue because CSRF tokens are always 64-char hex (fixed length). | |
| 78 | 77 | ||
| 79 | - | ### Previous Surprise (Run 12) | |
| 78 | + | ### Previous Surprises | |
| 80 | 79 | ||
| 81 | - | **`CoreError` dead code** -- fully-defined typed error enum with 5 variants was never used anywhere. **Resolved:** Dead code removed. | |
| 80 | + | - **Run 15:** ~~Link preview image URL vulnerability~~ -- Incorrect finding. Code never fetches og:image. | |
| 81 | + | - **Run 12:** ~~`CoreError` dead code~~ -- Resolved. Dead code removed. | |
| 82 | 82 | ||
| 83 | 83 | ## Action Items | |
| 84 | 84 | ||
| @@ -101,6 +101,9 @@ | |||
| 101 | 101 | 12. ~~**[MEDIUM]** Add DB connectivity check to health endpoint~~ -- Fixed 2026-04-22. | |
| 102 | 102 | 13. ~~**[LOW]** Add retry logic to MNW OAuth API calls~~ -- Already implemented (auth.rs:210-316). | |
| 103 | 103 | ||
| 104 | + | ### Run 17 (2026-04-30) | |
| 105 | + | 14. ~~**[LOW]** Spawn link preview fetch as detached task~~ -- Done 2026-05-01. | |
| 106 | + | ||
| 104 | 107 | ## Metrics Over Time | |
| 105 | 108 | ||
| 106 | 109 | | Audit Date | LOC | Rust Files | Tests | Tests/KLOC | Clippy Warnings | Cold Spots | Overall | | |
| @@ -119,6 +122,7 @@ | |||
| 119 | 122 | | 2026-04-15 (Run 14) | ~9,752 | -- | 231 | ~24 | 0 | 0 | A | | |
| 120 | 123 | | 2026-04-18 (Run 15) | ~9,752 | -- | 231 | ~24 | 0 | 3 | A- | | |
| 121 | 124 | | 2026-04-22 (Run 15 corrected) | ~9,752 | -- | 232 | ~24 | 0 | 0 | A | | |
| 125 | + | | 2026-04-30 (Run 17) | ~9,928 | -- | 228 | ~23 | 0 | 2 | A | | |
| 122 | 126 | ||
| 123 | 127 | --- | |
| 124 | 128 |
| @@ -2,33 +2,12 @@ | |||
| 2 | 2 | ||
| 3 | 3 | Done: All pre-beta phases. Active: None. Next: Platform integration. | |
| 4 | 4 | ||
| 5 | - | v0.3.4. Audit grade A. 232 tests. | |
| 6 | - | ||
| 7 | - | --- | |
| 8 | - | ||
| 9 | - | ## Code Fuzz Findings (2026-04-26) | |
| 10 | - | ||
| 11 | - | ### Critical | |
| 12 | - | - [x] Cross-community post removal via flag: scoped flag queries to community_id via JOIN chain. (fixed 2026-04-26) | |
| 13 | - | ||
| 14 | - | ### Serious | |
| 15 | - | - [x] Cross-community category edit: added `AND community_id = $2` to UPDATE. (fixed 2026-04-26) | |
| 16 | - | - [x] Cross-community tag delete: added `AND community_id = $2` to DELETE. (fixed 2026-04-26) | |
| 17 | - | - [x] Search query UTF-8 panic: use `is_char_boundary` loop for safe truncation. (fixed 2026-04-26) | |
| 18 | - | - [x] SSRF bypass in link preview: replaced string matching with `std::net::IpAddr` parsing + DNS resolution to catch all IP encodings. (fixed 2026-04-26) | |
| 19 | - | ||
| 20 | - | ### Minor | |
| 21 | - | - [x] `parse_duration` silent default to permanent: now returns error on unrecognized values. (fixed 2026-04-26) | |
| 22 | - | - [x] `/internal/threads/{id}/stats` unauthenticated: added HMAC verification + signed GET in MNW client. (fixed 2026-04-26) | |
| 23 | - | - [x] `auto_hide_if_threshold_met` audit trail: set `removed_by` to NULL for system-initiated removals; mod log records the event. (fixed 2026-04-26) | |
| 24 | - | - [x] `/search` rate limiting: added per-IP governor (burst 5, 1/sec) on search endpoint. (fixed 2026-04-26) | |
| 5 | + | v0.3.4. Audit grade A. 228 tests. | |
| 25 | 6 | ||
| 26 | 7 | --- | |
| 27 | 8 | ||
| 28 | 9 | ## Code Review Remediation — Remaining | |
| 29 | 10 | - [ ] Transitive dep advisories: rand 0.8/0.9 (RUSTSEC-2026-0097), rsa (RUSTSEC-2023-0071), lru (RUSTSEC-2026-0002) — no direct fix available, monitor upstream | |
| 30 | - | - [ ] Add partial index on `posts.removed_at` (`WHERE removed_at IS NOT NULL`) when data volume warrants it | |
| 31 | - | - [x] Add `tracing::warn!` to `MaybeUser` extractor on session read errors (fixed 2026-04-26) | |
| 32 | 11 | ||
| 33 | 12 | --- | |
| 34 | 13 | ||
| @@ -84,7 +63,7 @@ | |||
| 84 | 63 | | Seed data | `src/seed.rs` | | |
| 85 | 64 | | Entry point | `src/main.rs` | | |
| 86 | 65 | | Library root | `src/lib.rs` | | |
| 87 | - | | Migrations | `migrations/` (001-021) | | |
| 66 | + | | Migrations | `migrations/` (001-024) | | |
| 88 | 67 | | CSS | `static/style.css` | | |
| 89 | 68 | | Deploy config | `deploy/` | | |
| 90 | 69 | | Integration tests | `tests/` | |
| @@ -4,6 +4,17 @@ | |||
| 4 | 4 | ||
| 5 | 5 | ## Changes Since Last Audit | |
| 6 | 6 | ||
| 7 | + | ### Thirteenth audit (2026-04-30, Run 17 cross-project) | |
| 8 | + | - **Test count:** 365 (236 unit + 129 integration). 0 clippy warnings. 0 failures. | |
| 9 | + | - **Grade:** A- (downgraded from A). v0.3.5. ~15,061 LOC. | |
| 10 | + | - **New cold spots:** | |
| 11 | + | - checks/cors.rs (B+) -- only 2 serde tests, zero coverage of actual validation logic | |
| 12 | + | - checks/ssh_banner.rs (B+) -- only 1 test, degraded-status branches untested | |
| 13 | + | - cli/tasks/whois.rs (B+) -- uses TLS interval config instead of dedicated WHOIS interval (copy-paste bug) | |
| 14 | + | - **Mandatory surprise:** RateLimiter atomic ordering race -- `try_acquire()` uses Relaxed ordering on counter, allowing max_per_window + N requests in a burst. Technically incorrect, harmless at current scale. | |
| 15 | + | - **Previous action items:** Item 2 (aws-lc-sys CVEs) still upstream-blocked. All resolved items verified intact. | |
| 16 | + | - **New action items:** 2 MEDIUM (CORS tests, SSH banner tests), 2 LOW (WHOIS config field, RateLimiter ordering fix). | |
| 17 | + | ||
| 7 | 18 | ### Tenth audit (2026-03-28, Run 12 cross-project) | |
| 8 | 19 | - **Test count:** 359 (222 unit + 8 cli + 129 integration). 0 clippy warnings. 0 failures. | |
| 9 | 20 | - **Grade:** A (maintained). v0.3.2. | |
| @@ -113,3 +124,4 @@ | |||
| 113 | 124 | | 2026-03-14 | ~3.5K | ~16 | 238 | ~68 | 0 | 0 | A | | |
| 114 | 125 | | 2026-03-16 | 10.1K | 23 | 344 | ~34 | 2 | 0 | A | | |
| 115 | 126 | | 2026-03-18 | 10.1K | 23 | 344 | ~34 | 0 | 0 | A | | |
| 127 | + | | 2026-04-30 | ~15,061 | -- | 365 | ~15.6 | 0 | 3 | A- | |
| @@ -1,11 +1,11 @@ | |||
| 1 | 1 | # PoM (Peace of Mind) -- Audit Review | |
| 2 | 2 | ||
| 3 | - | **Last audited:** 2026-04-18 (twelfth audit, Run 15 cross-project) | |
| 4 | - | **Previous audit:** 2026-04-15 (eleventh audit, Run 14 cross-project) | |
| 3 | + | **Last audited:** 2026-04-30 (thirteenth audit, Run 17 cross-project) | |
| 4 | + | **Previous audit:** 2026-04-18 (twelfth audit, Run 15 cross-project) | |
| 5 | 5 | ||
| 6 | 6 | ## Overall Grade: A | |
| 7 | 7 | ||
| 8 | - | Run 15 cross-project audit (updated 2026-04-22). 364 tests (all pass). 0 clippy warnings. v0.3.5. ~11,496 LOC. Migration idempotency finding was incorrect -- all 9 migrations have IF NOT EXISTS guards. Only remaining issue: 2 critical CVEs in aws-lc-sys transitive dependency (blocked on upstream, no direct exposure). | |
| 8 | + | Run 17 cross-project audit (remediated 2026-05-01). 376 tests (247 unit + 129 integration, all pass). 0 clippy warnings. v0.3.5. ~15,061 LOC. All 4 Run 17 findings fixed: CORS validation extracted and tested (10 tests), SSH banner branches tested (3 tests), WHOIS interval config field added, RateLimiter atomic ordering corrected. | |
| 9 | 9 | ||
| 10 | 10 | ## Scorecard | |
| 11 | 11 | ||
| @@ -13,18 +13,16 @@ | |||
| 13 | 13 | |-----------|:-----:|-------| | |
| 14 | 14 | | Code Quality | A | Zero clippy warnings. Clean error handling with typed PomError enum. All migrations have IF NOT EXISTS guards. | | |
| 15 | 15 | | Architecture | A | Well-structured single crate. main.rs is thin, CLI handlers extracted to cli.rs. Module boundaries clean. | | |
| 16 | - | | Testing | A | 364 tests (all pass) for ~11.5K LOC = ~32 tests/KLOC. Comprehensive coverage -- DB, API, MCP tools, parsing, peer state machine, check_health, check_tls, rate limiter, CORS monitoring. | | |
| 17 | - | | Security | A | All SQL parameterized. SSH hardened. Bearer token auth. Rate limiting. HTTP response size limits. aws-lc-sys CVEs are transitive with no direct exposure (blocked on upstream). | | |
| 18 | - | | Performance | A- | WAL mode, proper indexes, async throughout. Appropriate for monitoring workload. | | |
| 19 | - | | Documentation | A | Module-level `//!` docs on all modules. All struct fields documented. architecture.md, README created. | | |
| 20 | - | | Dependencies | A- | 17 direct deps, all justified. 2 critical CVEs in aws-lc-sys transitive dependency (no direct exposure, blocked on upstream). | | |
| 16 | + | | Testing | A | 376 tests (247 unit + 129 integration), ~16 tests/KLOC. CORS and SSH banner gaps fixed. | | |
| 17 | + | | Security | A | Constant-time auth, SSH command injection prevention, response size caps. | | |
| 18 | + | | Performance | A- | WAL mode, proper indexes. Sequential per-target checks intentional. | | |
| 19 | + | | Documentation | A- | Module-level docs on all modules. Field-level docs on struct fields. | | |
| 20 | + | | Dependencies | A- | All deps latest stable. rmcp is 0.1.x (pre-1.0 but current). | | |
| 21 | 21 | | Type Safety | A | Domain enums with proper serde. No stringly-typed fields. | | |
| 22 | - | | Observability | A- | 57 `#[instrument(skip_all)]` annotations. Debug-level logging for non-critical failures. Watchdog logging. | | |
| 23 | - | | Concurrency | A | Clean RwLock discipline. Lock-then-release-then-DB pattern. CancellationToken for cooperative shutdown. | | |
| 24 | - | | Resilience | A | CancellationToken + `tokio::select!` in all task loops. Graceful shutdown. Watchdog for task panic detection. | | |
| 25 | - | | API Consistency | A | Consistent JSON shapes. Proper HTTP status codes. Public `/api/health` endpoint. | | |
| 26 | - | | Migration Safety | A | All 9 migrations use IF NOT EXISTS guards. Idempotent and safe to re-run. | | |
| 27 | - | | Codebase Size | A- | ~11,496 LOC for full monitoring tool with CLI, MCP server, HTTP API, SQLite, SSH test runner, peer mesh, and self-monitoring. | | |
| 22 | + | | Observability | A | tracing with #[instrument] on all DB/check functions. | | |
| 23 | + | | Concurrency | A | Clean lock ordering in peer mesh. Rate limiter atomic ordering fixed (Acquire/Release). | | |
| 24 | + | | Resilience | A | Graceful shutdown, timeouts on all network calls, watchdog. | | |
| 25 | + | | Codebase Size | A- | 15K LOC for full monitoring tool. | | |
| 28 | 26 | ||
| 29 | 27 | ## Module Heatmap | |
| 30 | 28 | ||
| @@ -44,17 +42,20 @@ | |||
| 44 | 42 | ||
| 45 | 43 | ### Cold Spots | |
| 46 | 44 | ||
| 47 | - | 1. ~~**Migration idempotency (migrations 3-6)**~~ -- Incorrect finding. All 9 migrations have IF NOT EXISTS guards. Verified 2026-04-22. | |
| 48 | - | 2. **aws-lc-sys CVEs** -- 2 critical CVEs in transitive dependency. Blocked on upstream. No direct exposure. | |
| 45 | + | All resolved (2026-05-01): | |
| 46 | + | ||
| 47 | + | 1. ~~**checks/cors.rs -- B+**~~ -- Fixed. Extracted `evaluate_preflight` pure function, added 10 unit tests covering origin matching, wildcards, method case-insensitivity, failure combinations. | |
| 48 | + | 2. ~~**checks/ssh_banner.rs -- B+**~~ -- Fixed. Added 3 tests using real TCP listeners (valid banner, unexpected banner, empty response). | |
| 49 | + | 3. ~~**cli/tasks/whois.rs -- B+**~~ -- Fixed. Added `whois_check_interval_secs` config field (default 86400s/24h), corrected the reference. | |
| 49 | 50 | ||
| 50 | 51 | ## Strengths | |
| 51 | 52 | ||
| 52 | - | - **Lean architecture.** ~11,496 LOC delivers health checks, test orchestration, MCP server, HTTP API, peer mesh, CLI, alerting, TLS monitoring, route checks, and self-monitoring. | |
| 53 | + | - **Lean architecture.** ~15,061 LOC delivers health checks, test orchestration, MCP server, HTTP API, peer mesh, CLI, alerting, TLS monitoring, route checks, and self-monitoring. | |
| 53 | 54 | - **Clean error handling.** `?` propagation with typed PomError enum (thiserror, 8 variants). No panics in production paths. | |
| 54 | 55 | - **Correct security posture.** All SQL parameterized. Bearer token auth on API + peer mesh. Rate limiting (60 req/min). SSH hardened with `BatchMode=yes` + `--` separator. | |
| 55 | 56 | - **Backward-compatible design.** All config sections are `#[serde(default)]`. Existing configs work unchanged. | |
| 56 | 57 | - **Good MCP integration.** 8 MCP tools with clear descriptions. Tools strip `raw_output` from test history to avoid flooding context. | |
| 57 | - | - **Comprehensive test suite.** 364 tests covering DB, API, MCP tools, parsing, peer state machine, config, health classification, mock server health checks, TLS cert validation, and rate limiting. | |
| 58 | + | - **Comprehensive test suite.** 376 tests covering DB, API, MCP tools, parsing, peer state machine, config, health classification, mock server health checks, TLS cert validation, CORS validation, SSH banner, and rate limiting. | |
| 58 | 59 | - **Excellent lock discipline.** Peer module collects data under lock, releases lock, then performs DB writes. No lock contention under load. | |
| 59 | 60 | - **Robust shutdown.** CancellationToken with `tokio::select!` in all task loops. Graceful API server shutdown. Watchdog for silent panics. 5s grace period. | |
| 60 | 61 | ||
| @@ -68,23 +69,31 @@ | |||
| 68 | 69 | ||
| 69 | 70 | ## Mandatory Surprise | |
| 70 | 71 | ||
| 71 | - | **~~Migration idempotency bug~~ (Incorrect finding)** | |
| 72 | + | **RateLimiter atomic ordering race condition** | |
| 72 | 73 | ||
| 73 | - | The original Run 15 audit claimed migrations 3-6 lacked IF NOT EXISTS guards. This was incorrect -- all 9 migrations in db.rs have always used `CREATE TABLE IF NOT EXISTS`. Verified 2026-04-22 by reading the source directly. | |
| 74 | + | The RateLimiter has a subtle race condition. In `api.rs`, `try_acquire()` holds the mutex on `window_start` while doing the window reset check, but uses `Relaxed` ordering on the atomic counter. Between dropping the mutex and the `fetch_add`, another thread could see the old window valid but counter already reset. Could allow max_per_window + N requests in a window. Harmless at this scale but technically incorrect. | |
| 74 | 75 | ||
| 75 | - | **Verdict:** No issue. The audit finding was wrong. | |
| 76 | + | **Verdict:** Fixed 2026-05-01. Changed to Acquire/Release ordering. | |
| 76 | 77 | ||
| 77 | - | ### Previous Surprise | |
| 78 | + | ### Previous Surprises | |
| 78 | 79 | ||
| 79 | - | **Peer mesh dual HTTP call per heartbeat** -- intentional and well-designed separation of heartbeat probe from status data fetch. Verdict: Actually fine. | |
| 80 | + | - **~~Migration idempotency bug~~ (Incorrect finding)** -- All 9 migrations have always used `CREATE TABLE IF NOT EXISTS`. Verified 2026-04-22. | |
| 81 | + | - **Peer mesh dual HTTP call per heartbeat** -- intentional and well-designed separation of heartbeat probe from status data fetch. Verdict: Actually fine. | |
| 82 | + | - **Rate limiter Relaxed ordering** (Run 9) -- can allow up to max_per_window+1 requests. Known trade-off, now elevated to mandatory surprise with fuller analysis. | |
| 80 | 83 | ||
| 81 | 84 | ## Action Items | |
| 82 | 85 | ||
| 83 | 86 | Filed in `docs/mnw/pom/todo.md`. | |
| 84 | 87 | ||
| 88 | + | ### Run 17 (2026-04-30, remediated 2026-05-01) | |
| 89 | + | 1. ~~**[MEDIUM]** Add tests for CORS validation logic~~ -- Done. Extracted `evaluate_preflight`, 10 unit tests. | |
| 90 | + | 2. ~~**[MEDIUM]** Add tests for SSH banner check~~ -- Done. 3 tests with real TCP listeners. | |
| 91 | + | 3. ~~**[LOW]** Add dedicated whois_check_interval_secs config field~~ -- Done. Default 86400s (24h). | |
| 92 | + | 4. ~~**[LOW]** Fix RateLimiter atomic ordering~~ -- Done. Relaxed → Acquire/Release. 4 rate limiter unit tests added. | |
| 93 | + | ||
| 85 | 94 | ### Run 15 (2026-04-18, corrected 2026-04-22) | |
| 86 | - | 1. ~~**[MEDIUM]** Add IF NOT EXISTS to migrations 3-6~~ -- Incorrect finding. All migrations already have guards. | |
| 87 | - | 2. **[LOW]** Monitor aws-lc-sys for CVE fixes (blocked on upstream) | |
| 95 | + | 5. ~~**[MEDIUM]** Add IF NOT EXISTS to migrations 3-6~~ -- Incorrect finding. All migrations already have guards. | |
| 96 | + | 6. **[LOW]** Monitor aws-lc-sys for CVE fixes (blocked on upstream) | |
| 88 | 97 | ||
| 89 | 98 | ### All resolved (previous audits) | |
| 90 | 99 | 3. ~~**[CRITICAL]** Remove Postmark API token from deployment configs~~ -- Done | |
| @@ -122,6 +131,8 @@ | |||
| 122 | 131 | | 2026-04-15 (Run 14) | ~11,496 | -- | 364 | ~32 | 0 | 0 | A | | |
| 123 | 132 | | 2026-04-18 (Run 15) | ~11,496 | -- | 364 | ~32 | 0 | 2 | A- | | |
| 124 | 133 | | 2026-04-22 (Run 15 corrected) | ~11,496 | -- | 364 | ~32 | 0 | 1 | A | | |
| 134 | + | | 2026-04-30 (Run 17) | ~15,061 | -- | 365 | ~15.6 | 0 | 3 | A- | | |
| 135 | + | | 2026-05-01 (Run 17 remediated) | ~15,061 | -- | 376 | ~16 | 0 | 0 | A | | |
| 125 | 136 | ||
| 126 | 137 | --- | |
| 127 | 138 |
| @@ -2,7 +2,7 @@ | |||
| 2 | 2 | ||
| 3 | 3 | Done: All phases (1-13). Active: None. Next: Post-beta items below. | |
| 4 | 4 | ||
| 5 | - | v0.3.2. Audit grade A. 359 tests. | |
| 5 | + | v0.3.5. Audit grade A. 376 tests (247 unit + 129 integration). | |
| 6 | 6 | ||
| 7 | 7 | ## Notification Integration | |
| 8 | 8 | - [ ] Push PoM alerts to MNW notifications API (health failures, TLS expiry, DNS changes) |
| @@ -42,10 +42,10 @@ | |||
| 42 | 42 | let now = std::time::Instant::now(); | |
| 43 | 43 | if now.duration_since(*start) > self.window_duration { | |
| 44 | 44 | *start = now; | |
| 45 | - | self.count.store(1, Ordering::Relaxed); | |
| 45 | + | self.count.store(1, Ordering::Release); | |
| 46 | 46 | true | |
| 47 | 47 | } else { | |
| 48 | - | let prev = self.count.fetch_add(1, Ordering::Relaxed); | |
| 48 | + | let prev = self.count.fetch_add(1, Ordering::Acquire); | |
| 49 | 49 | prev < self.max_per_window | |
| 50 | 50 | } | |
| 51 | 51 | } | |
| @@ -714,4 +714,38 @@ | |||
| 714 | 714 | let resp = app.oneshot(req).await.unwrap(); | |
| 715 | 715 | assert_eq!(resp.status(), StatusCode::UNAUTHORIZED); | |
| 716 | 716 | } | |
| 717 | + | ||
| 718 | + | #[test] | |
| 719 | + | fn rate_limiter_allows_within_limit() { | |
| 720 | + | let limiter = RateLimiter::new(3, std::time::Duration::from_secs(60)); | |
| 721 | + | assert!(limiter.try_acquire()); | |
| 722 | + | assert!(limiter.try_acquire()); | |
| 723 | + | assert!(limiter.try_acquire()); | |
| 724 | + | } | |
| 725 | + | ||
| 726 | + | #[test] | |
| 727 | + | fn rate_limiter_blocks_over_limit() { | |
| 728 | + | let limiter = RateLimiter::new(2, std::time::Duration::from_secs(60)); | |
| 729 | + | assert!(limiter.try_acquire()); | |
| 730 | + | assert!(limiter.try_acquire()); | |
| 731 | + | assert!(!limiter.try_acquire()); | |
| 732 | + | } | |
| 733 | + | ||
| 734 | + | #[tokio::test] | |
| 735 | + | async fn rate_limiter_resets_after_window() { | |
| 736 | + | let limiter = RateLimiter::new(1, std::time::Duration::from_millis(10)); | |
| 737 | + | assert!(limiter.try_acquire()); | |
| 738 | + | assert!(!limiter.try_acquire()); | |
| 739 | + | tokio::time::sleep(std::time::Duration::from_millis(15)).await; | |
| 740 | + | assert!(limiter.try_acquire()); | |
| 741 | + | } | |
| 742 | + | ||
| 743 | + | #[test] | |
| 744 | + | fn rate_limiter_counter_starts_at_one() { | |
| 745 | + | let limiter = RateLimiter::new(1, std::time::Duration::from_millis(0)); | |
| 746 | + | // First call resets window (counter stored as 1), returns true | |
| 747 | + | assert!(limiter.try_acquire()); | |
| 748 | + | // Window has already expired (0ms), so next call also resets | |
| 749 | + | assert!(limiter.try_acquire()); | |
| 750 | + | } | |
| 717 | 751 | } |
| @@ -90,6 +90,9 @@ | |||
| 90 | 90 | /// Seconds between CORS preflight verification checks. | |
| 91 | 91 | #[serde(default = "default_cors_check_interval")] | |
| 92 | 92 | pub cors_check_interval_secs: u64, | |
| 93 | + | /// Seconds between WHOIS domain expiry checks. | |
| 94 | + | #[serde(default = "default_whois_check_interval")] | |
| 95 | + | pub whois_check_interval_secs: u64, | |
| 93 | 96 | /// Bearer token required for API access. If set, all /api/* requests must | |
| 94 | 97 | /// include `Authorization: Bearer <token>`. Can also be set via POM_API_TOKEN env var. | |
| 95 | 98 | pub api_token: Option<String>, | |
| @@ -109,6 +112,7 @@ | |||
| 109 | 112 | route_check_interval_secs: 300, | |
| 110 | 113 | dns_check_interval_secs: 3600, | |
| 111 | 114 | cors_check_interval_secs: 3600, | |
| 115 | + | whois_check_interval_secs: 86400, | |
| 112 | 116 | api_token: None, | |
| 113 | 117 | dashboard: false, | |
| 114 | 118 | } | |
| @@ -140,6 +144,11 @@ | |||
| 140 | 144 | 3600 | |
| 141 | 145 | } | |
| 142 | 146 | ||
| 147 | + | fn default_whois_check_interval() -> u64 { | |
| 148 | + | // 24 hours: domain registration data changes on the order of days/months | |
| 149 | + | 86400 | |
| 150 | + | } | |
| 151 | + | ||
| 143 | 152 | fn default_serve_interval() -> u64 { | |
| 144 | 153 | // 5 minutes: frequent enough to catch outages within an SLA window, | |
| 145 | 154 | // infrequent enough to avoid noise | |
| @@ -944,4 +953,20 @@ | |||
| 944 | 953 | .collect(); | |
| 945 | 954 | assert_eq!(bad_routes, vec!["discover"]); | |
| 946 | 955 | } | |
| 956 | + | ||
| 957 | + | #[test] | |
| 958 | + | fn config_whois_check_interval_default() { | |
| 959 | + | let config: Config = toml::from_str("").unwrap(); | |
| 960 | + | assert_eq!(config.serve.whois_check_interval_secs, 86400); | |
| 961 | + | } | |
| 962 | + | ||
| 963 | + | #[test] | |
| 964 | + | fn config_whois_check_interval_custom() { | |
| 965 | + | let toml = r#" | |
| 966 | + | [serve] | |
| 967 | + | whois_check_interval_secs = 43200 | |
| 968 | + | "#; | |
| 969 | + | let config: Config = toml::from_str(toml).unwrap(); | |
| 970 | + | assert_eq!(config.serve.whois_check_interval_secs, 43200); | |
| 971 | + | } | |
| 947 | 972 | } |