Skip to content

Security posture manifest v2: provenance envelope + CSRF/headers dimensions (Part of #1627)#1879

Merged
madmax983 merged 9 commits into
trunk-devfrom
feat/1627-security-posture-manifest
Jul 14, 2026
Merged

Security posture manifest v2: provenance envelope + CSRF/headers dimensions (Part of #1627)#1879
madmax983 merged 9 commits into
trunk-devfrom
feat/1627-security-posture-manifest

Conversation

@madmax983

Copy link
Copy Markdown
Owner

Part of #1627. Extends the merged #1850 route-audit manifest into a provenance-tagged, multi-dimension security posture manifest (first slice).

Before / After — what the manifest proves

Before (schema v1): autumn routes audit emitted a single routes dimension — a flat list of routes tagged provenance: "provable". That was the only security fact the manifest carried, and there was no vocabulary for facts that come from configuration rather than from code, nor any record of what was not yet covered.

// BEFORE (v1)
{
  "schema_version": 1,
  "dimensions": {
    "routes": [ { "path": "/widgets/{id}", "method": "DELETE", "classification": "gated", "provenance": "provable", ... } ]
  }
}

After (schema v2): every dimension is wrapped in a provenance envelope ({ provenance, source, entries }), two new declared dimensions describe CSRF and security-header posture read from config, and a top-level excluded list names every dimension deferred to a later slice with the provenance class it will eventually carry — so a reader can tell "proven absent" from "not yet measured".

// AFTER (v2)
{
  "schema_version": 2,
  "dimensions": {
    "routes":           { "provenance": "provable", "source": "macro:#[secured]/#[authorize]/#[public]",
      "entries": [ { "path": "/widgets/{id}", "method": "DELETE", "classification": "gated", "provenance": "provable", ... } ] },
    "csrf":             { "provenance": "declared", "source": "config:security.csrf", "exempt_paths": ["/api/"],
      "entries": [ { "path": "/widgets", "method": "POST", "csrf_enforced": true, "exempt": false } ] },
    "security_headers": { "provenance": "declared", "source": "config:security.headers",
      "entries": [ { "header": "x_frame_options", "value": "DENY", "emitted": true } ] }
  },
  "excluded": [
    { "dimension": "policy_registration", "eventual_provenance": "runtime-only", "reason": "boot-time fact; excluded from a build-time manifest by design" },
    ...
  ]
}

The routes entries are byte-for-byte identical to v1 — only their container changed.

How

  • Envelope schema v2. Provenance is a small closed enum (provable / declared / runtime-only). Dimensions has a fixed field order (routes, csrf, security_headers); every list is deterministically sorted. excluded is a fixed, stable-ordered list.
  • Config-dump plumbing (macro-free). The AUTUMN_DUMP_ROUTES dump now also emits the resolved CsrfConfig + HeadersConfig as one compact JSON object on a stderr marker line ([autumn:security-config] , a new SecurityDump type in autumn_web::route_listing), gated on a new AUTUMN_DUMP_SECURITY=1 env var that only autumn routes audit sets. autumn routes is unaffected and the routes-only stdout JSON parse path stays byte-compatible. Emitted lists are sorted for byte-determinism; the audit CLI hides the marker line from forwarded stderr.
  • CSRF predicate mirror. One csrf entry per mutating route (method not in the configured safe_methods). csrf_enforced = enabled && !is_safe(method) && !is_exempt(path), with is_exempt mirroring the runtime CsrfLayer prefix predicate (autumn/src/security/csrf.rs) exactly. Configured exemptions are recorded once at the dimension level.
  • Headers resolution. One security_headers entry per header key, value = the effective declared string (the CSP is the resolved default_content_security_policy() template the runtime uses), emitted = !value.is_empty(), in sorted key order.

Falsifiability tests (AC-6)

Genuine red→green fixtures driving the real build_manifest path (inline in routes_audit.rs, since autumn-cli is binary-only):

  • manifest_v2_envelope_shape_and_provenance_labels — schema v2, three dimensions with correct provenance labels, full excluded list.
  • csrf_dimension_only_covers_mutating_routes — safe (GET) routes never appear in csrf.entries.
  • disabling_csrf_flips_only_csrf_entries — toggling enabled flips exactly the csrf entries; routes/security_headers byte-identical.
  • adding_exempt_path_flips_only_affected_csrf_entries — an /api/ prefix flips only the routes under it; other dimensions byte-identical.
  • weakening_a_header_changes_only_that_entry — DENY→SAMEORIGIN and emptying the CSP each change exactly that header entry; routes/csrf byte-identical.
  • manifest_rebuild_is_byte_identical — two builds of identical source+config are byte-for-byte equal.
  • parse_security_config_reads_the_marker — the stderr marker round-trips.

Plus a consolidated autumn-cli/tests/integration/manifest_posture.rs module covering the autumn-web dump contract (SecurityDump::from_config sorting/determinism and CSRF/header falsifiability at the wire boundary).

Gates run green: cargo fmt --all -- --check, cargo clippy -p autumn-web -p autumn-cli --all-targets -- -D warnings, cargo test -p autumn-cli routes_audit (22 passed), cargo test -p autumn-cli --test cli_tests manifest_posture (4 passed), cargo test -p autumn-web --lib route_listing (44 passed).

Docs: new docs/guide/security-posture-manifest.md defining the three provenance classes with one example each (provable: a #[secured] route; declared: the resolved CSP string; runtime-only: policy registration at boot in excluded), cross-linked from routes-cli.md.

Deferred to later slices

Listed in the manifest's excluded block, not implemented here:

  • authorization_policies — full #[authorize] action→policy-type binding (needs new macro metadata; boolean policy presence already ships in routes.entries[].policy)
  • sessions
  • secrets
  • encryption (#[encrypted] fields provable; key config declared)
  • rate_limit
  • submit_token
  • outbound_http
  • policy_registration (runtime-only boot fact)

Notes

  • CHANGELOG intentionally not edited and no workspace version bump, per repo guidelines (that is a separate, deliberate release step).
  • No changes to autumn-macros — this slice is entirely macro-free.

🤖 Generated with Claude Code

https://claude.ai/code/session_01SjQWjvP71LMRPsQexcYPvi


Generated by Claude Code

…lope (Part of #1627)

Extend the merged #1850 route-audit manifest into a provenance-tagged,
multi-dimension security posture manifest (first #1627 slice).

Schema v2:
- Each dimension is wrapped in a `{ provenance, source, entries }` envelope,
  and a top-level `excluded` list names dimensions deferred to later slices
  with the provenance class they will eventually carry.
- `routes` (provable) keeps its v1 entry shape, only wrapped in the envelope.
- New `csrf` dimension (declared): one entry per mutating route with
  `csrf_enforced`/`exempt`, mirroring the runtime CsrfLayer predicate; the
  configured exemptions are recorded once at the dimension level.
- New `security_headers` dimension (declared): one entry per header key with
  the effective declared value (including the resolved CSP template) and an
  `emitted` flag.

Plumbing (macro-free): the `AUTUMN_DUMP_ROUTES` dump additionally emits the
resolved CSRF + headers config on a stderr marker line
(`[autumn:security-config] `), gated on `AUTUMN_DUMP_SECURITY=1` so only
`autumn routes audit` sees it and the routes-only JSON parse path stays
byte-compatible. Emitted lists are sorted for byte-determinism.

Tests: red->green falsifiability fixtures for each dimension driving the real
`build_manifest` path (disabling CSRF / adding an exempt prefix flips exactly
the affected csrf entries; weakening x_frame_options / emptying the CSP changes
exactly that header entry; byte-identical rebuild), plus a consolidated
`manifest_posture` integration module covering the autumn-web dump contract.

Docs: new guide page defining the provable / declared / runtime-only
provenance classes with one example each, cross-linked from routes-cli.

CHANGELOG intentionally not edited.

@gemini-code-assist gemini-code-assist Bot 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.

Code Review

This pull request upgrades the security posture manifest to schema version 2, wrapping dimensions in a provenance envelope (provable, declared, or runtime-only), adding csrf and security_headers dimensions, and introducing a top-level excluded list. It also updates the CLI and runtime to dump and parse the resolved security configuration, supported by extensive integration tests and updated documentation. The review feedback highlights two opportunities to improve data cleanliness: first, using the sorted and deduplicated exempt_paths variable in build_csrf_dimension rather than the raw config; second, deduplicating safe_methods and exempt_paths in SecurityDump::from_config to ensure a clean serialized payload.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment thread autumn-cli/src/routes_audit.rs Outdated
.iter()
.filter(|r| !method_is_safe(&r.method, &csrf.safe_methods))
.map(|r| {
let exempt = path_is_exempt(&r.path, &csrf.exempt_paths);

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.

medium

In build_csrf_dimension, the path_is_exempt check is performed using the original csrf.exempt_paths instead of the sorted and deduplicated exempt_paths local variable. This defeats the purpose of deduplicating the paths beforehand and can lead to redundant checks.

Suggested change
let exempt = path_is_exempt(&r.path, &csrf.exempt_paths);
let exempt = path_is_exempt(&r.path, &exempt_paths);

Comment on lines +105 to +108
let mut safe_methods = csrf.safe_methods.clone();
safe_methods.sort();
let mut exempt_paths = csrf.exempt_paths.clone();
exempt_paths.sort();

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.

medium

In SecurityDump::from_config, the safe_methods and exempt_paths lists are cloned and sorted, but they are not deduplicated. Deduplicating them here ensures that the serialized SecurityDump on the wire is completely clean and free of duplicates, matching the deduplication logic in build_csrf_dimension.

        let mut safe_methods = csrf.safe_methods.clone();
        safe_methods.sort();
        safe_methods.dedup();
        let mut exempt_paths = csrf.exempt_paths.clone();
        exempt_paths.sort();
        exempt_paths.dedup();

- routes_audit: per-route path_is_exempt now uses the sorted+deduped
  exempt_paths local, matching the dimension-level exempt_paths record.
- route_listing: dedup safe_methods and exempt_paths after sorting so the
  serialized wire payload is duplicate-free.
@madmax983 madmax983 marked this pull request as ready for review July 13, 2026 19:14
@gemini-code-assist

Copy link
Copy Markdown
Contributor

Warning

You have reached your daily quota limit. Please wait up to 24 hours and I will start processing your requests again!

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: bbefdb0874

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

let mut safe_methods = csrf.safe_methods.clone();
safe_methods.sort();
safe_methods.dedup();
let mut exempt_paths = csrf.exempt_paths.clone();

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Include runtime webhook CSRF exemptions

When security.webhooks.endpoints is configured, runtime CSRF exempts each endpoint path via csrf_layer.with_exempt_path(&endpoint.path) in apply_csrf_middleware, but this dump only carries security.csrf.exempt_paths. For a configured POST webhook route that is not duplicated in csrf.exempt_paths, the v2 manifest reports csrf_enforced: true and omits the exemption even though runtime skips CSRF, so the declared CSRF dimension is incorrect.

Useful? React with 👍 / 👎.


let mut entries: Vec<CsrfEntry> = routes
.iter()
.filter(|r| !method_is_safe(&r.method, &csrf.safe_methods))

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Treat WebSocket route listings as CSRF-safe

For #[ws] routes the route listing uses the synthetic method WS, but the router mounts them as GET and CsrfLayer checks the real request method against safe_methods, so the default GET exemption applies. This filter treats WS as mutating because it is not in the configured safe-method list, causing apps with CSRF enabled and WebSocket routes to get manifest entries claiming CSRF is enforced where runtime never validates a token.

Useful? React with 👍 / 👎.

Comment thread autumn/src/route_listing.rs Outdated
x_frame_options: headers.x_frame_options.clone(),
x_content_type_options: headers.x_content_type_options,
xss_protection: headers.xss_protection,
content_security_policy: headers.content_security_policy.clone(),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Report the nonce-aware CSP value

When security.headers.csp_nonce.enabled = true and the CSP is the default, runtime does not emit this config string; ComputedHeaders::from_config switches to the nonce-aware CSP template and substitutes a nonce per request. Copying the raw content_security_policy here makes the new security_headers dimension show a CSP value that responses do not actually send for nonce-enabled apps.

Useful? React with 👍 / 👎.

Comment thread autumn-cli/src/routes_audit.rs Outdated
fn header_entry(header: &str, value: String) -> HeaderEntry {
HeaderEntry {
header: header.to_owned(),
emitted: !value.is_empty(),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Validate emitted headers the same way runtime does

With a non-empty but invalid header value from config or env, such as a CSP or referrer policy containing a newline, this reports emitted: true even though the runtime security headers layer only inserts headers when HeaderValue::from_str succeeds and otherwise skips them. That makes the manifest overstate security headers exactly for a misconfiguration it should expose, so emitted should mirror the runtime parse check.

Useful? React with 👍 / 👎.

…untime

Four honesty fixes so the security-posture manifest never claims more than
the runtime actually enforces:

- CSP nonce (A): dump the nonce-aware CSP template with the stable
  AUTUMN_CSP_NONCE placeholder when nonce injection is active + default CSP,
  mirroring ComputedHeaders::from_config. Extracted a shared
  resolved_content_security_policy() as the single source of truth.
- Webhook CSRF exemptions (B): fold every configured webhook endpoint path
  into csrf.exempt_paths (sorted + deduped), matching apply_csrf_middleware's
  with_exempt_path calls, so webhook POST routes report exempt/unenforced.
- WebSocket routes (C): treat WS-listed routes as CSRF-safe. #[ws] routes are
  mounted as GET, so CsrfLayer sees GET and never validates a token.
- HeaderValue parse (D): emitted = non-empty AND HeaderValue::from_str ok,
  mirroring the runtime layer that skips invalid header values.

Adds a red->green fixture per finding.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 2a98b4f978

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread autumn-cli/src/routes_audit.rs Outdated

let mut entries = vec![
header_entry("content_security_policy", h.content_security_policy.clone()),
header_entry("csp_nonce", bool_header(h.csp_nonce, "enabled")),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Don't report CSP nonce as an emitted header

When security.headers.csp_nonce.enabled is true, this adds a security_headers entry with header: "csp_nonce" and emitted: true, but runtime only stores CspNonce in request extensions and optionally substitutes it into the Content-Security-Policy; it never sends a csp_nonce response header. For nonce-enabled apps the manifest therefore overstates the actual response headers, so this should either be modeled outside the header list or not marked as emitted.

Useful? React with 👍 / 👎.

csp_nonce is a config toggle that shapes the CSP value, not a response
header. Runtime stores the nonce in request extensions and substitutes
it into Content-Security-Policy; it never emits a csp_nonce header.
Reporting it in security_headers overstated the actual response headers.
The nonce toggle's effect stays visible via the nonce-aware CSP value.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: eac7c10f78

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +115 to +117
for endpoint in &config.security.webhooks.endpoints {
exempt_paths.push(endpoint.path.clone());
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Include unsubscribe CSRF exemptions in the dump

When the mail feature is enabled and the default one-click unsubscribe endpoint is mounted (mail.mount_unsubscribe_endpoint/.mount_unsubscribe_endpoint() with a base URL), runtime adds crate::mail::UNSUBSCRIBE_PATH to the CSRF layer in apply_csrf_middleware (autumn/src/router.rs:2188-2189), but this dump only folds webhook endpoints into csrf.exempt_paths. The v2 manifest therefore under-reports a runtime CSRF exemption for that app configuration, so consumers see declared CSRF posture that does not match what the server actually enforces.

Useful? React with 👍 / 👎.

Runtime's apply_csrf_middleware exempts the framework-owned RFC 8058
one-click unsubscribe endpoint from CSRF under the mail feature when
should_mount_unsubscribe_endpoint() holds, but SecurityDump::from_config
only folded webhook endpoints, under-reporting that exemption in the v2
manifest. Fold UNSUBSCRIBE_PATH in under the identical cfg+predicate.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 94fad98121

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread autumn-cli/src/routes_audit.rs Outdated
Comment on lines +207 to +208
if method == "WS" {
return true;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Honor configured GET safety for WS routes

When CSRF is enabled and an app customizes security.csrf.safe_methods to remove GET, runtime no longer treats WebSocket upgrades as safe: #[ws] mounts as a real GET request and CsrfService only skips methods present in the configured safe-method list. Returning true for every synthetic WS route omits those routes from csrf.entries, so the manifest says no CSRF enforcement while runtime will validate/block the GET upgrade; this should map WS to GET and then apply the configured safe/exempt predicate.

Useful? React with 👍 / 👎.

WS routes are listed with the synthetic method WS but mount via
axum::routing::get, so runtime CsrfService checks the real GET against
the configured safe_methods. The audit previously treated WS as
unconditionally safe, so an app that removes GET from safe_methods would
have its WebSocket upgrade CSRF-validated at runtime while the manifest
omitted it entirely. Normalize WS to GET before the safe-method check and
drop the unconditional WS branch, so WS routes are excluded under default
config (GET safe) but surface in csrf.entries with the exempt-path
predicate applied when GET is not a safe method. The entry keeps its
listed WS method for display, consistent with the routes dimension.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 0c8607e8a8

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +274 to +276
let mut entries: Vec<CsrfEntry> = routes
.iter()
.filter(|r| !method_is_safe(&r.method, &csrf.safe_methods))

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Include mutating framework routes in CSRF entries

When actuator.sensitive = true and CSRF is enabled, runtime mounts mutating framework routes such as PUT {prefix}/loggers/{name} and, with http-client, POST {prefix}/webhooks/replay, but the route listing only contributes GET actuator paths (and omits the PUT path). Because this new CSRF dimension is built solely by filtering the listed routes, those runtime-mutating actuator endpoints never appear in csrf.entries, so the manifest under-reports CSRF enforcement for sensitive actuator deployments.

Useful? React with 👍 / 👎.

… sees them

The route listing derived framework actuator routes from
actuator_endpoint_paths, a GET-only, paths-only list, and wrapped every
entry as a framework GET. Two consequences fed the csrf posture dimension
a wrong picture of the mutating attack surface:

- PUT {prefix}/loggers/{name} (mounted when actuator.sensitive) was
  absent from the listing entirely, so csrf never saw it.
- POST {prefix}/webhooks/replay (mounted under http-client) was emitted
  as a phantom GET, since the listing forced GET on every actuator path
  even though runtime mounts only POST there.

Add actuator_mutating_routes(prefix, sensitive) returning (method, path)
pairs gated to match the mounts, a method-aware RouteInfo::framework_route
constructor, and push those routes after the GET loop in
append_framework_routes. Drop /webhooks/replay from the GET path list so
it is emitted once, as POST. /webhooks/dlq stays GET (mounted as GET).

Adds listing-level and csrf-dimension fixtures asserting the PUT loggers
route surfaces with csrf enforced and the phantom GET replay is gone.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 7dd75f2d36

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +123 to +124
if config.mail.should_mount_unsubscribe_endpoint() {
exempt_paths.push(crate::mail::UNSUBSCRIBE_PATH.to_owned());

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Add the mounted unsubscribe POST to the route listing

When the mail feature is enabled and an app opts into the default one-click unsubscribe endpoint, runtime merges unsubscribe_router() with both GET and POST at UNSUBSCRIBE_PATH and exempts that path from CSRF. This branch records the exemption, but no corresponding framework RouteInfo is appended, so build_csrf_dimension (which only iterates listed routes) still emits no entry for the runtime POST /_autumn/unsubscribe; the v2 manifest under-reports CSRF posture for those opted-in apps.

Useful? React with 👍 / 👎.

…posture sees the exempt POST

When the mail feature is enabled and the default RFC 8058 one-click
unsubscribe endpoint is mounted, runtime merges unsubscribe_router()
(GET + POST at UNSUBSCRIBE_PATH) and exempts that path from CSRF.
SecurityDump::from_config already folds UNSUBSCRIBE_PATH into
csrf.exempt_paths, but no framework RouteInfo was appended for it, so
build_csrf_dimension emitted no entry for the runtime POST and the
manifest under-reported CSRF posture.

append_framework_routes now lists GET + POST framework routes at
UNSUBSCRIBE_PATH under the same should_mount_unsubscribe_endpoint()
predicate the exempt fold uses, so the POST surfaces as
{ csrf_enforced: false, exempt: true } matching runtime.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: ae1e8c1a5f

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread autumn/src/actuator.rs Outdated
Comment on lines 3524 to 3526
// Only the GET DLQ listing is enumerated here; `/webhooks/replay`
// is mounted as `POST` (see `actuator_mutating_routes`), not `GET`.
paths.push(actuator_route_path(prefix, "/webhooks/dlq"));

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Keep replay in actuator runtime path sets

When the http-client feature is enabled with actuator.sensitive = true, this removes /webhooks/replay from actuator_endpoint_paths, but runtime code still uses that helper as the actuator allow-list: StartupBarrierState::from_config seeds actuator_paths from it and only adds /loggers as a separate subtree. During startup, POST {prefix}/webhooks/replay is still mounted by actuator_router_with_prefix but no longer bypasses the startup barrier, unlike the other actuator endpoints and unlike before this change; split the route-listing GET-only view from the runtime path set or add the mutating actuator paths to the barrier allow-list.

Useful? React with 👍 / 👎.

…tus route; disclose serve-path routers

Change 1 (regression fix): a prior commit dropped `/webhooks/replay` from
`actuator_endpoint_paths` to kill a phantom GET in the route listing, but that
helper is dual-use — `StartupBarrierState::from_config` seeds the startup
barrier's actuator allow-list from it. Removing the path meant the mounted
`POST {prefix}/webhooks/replay` no longer bypassed the startup barrier. Restore
the path in `actuator_endpoint_paths` (runtime allow-list complete again) and
instead exclude any path also produced by `actuator_mutating_routes` from the
GET-wrap in `append_framework_routes`, so the listing carries only the real
POST and no phantom GET.

Change 2: enumerate the tracked-job status route `GET /_autumn/jobs/{token}`
in `append_framework_routes`, gated on `jobs.tracking.route_enabled` (default
true), matching the router mount.

Change 3: add one honest `excluded` manifest entry (`serve_path_routers`)
disclosing that opt-in serve-path HTTP surfaces (MCP, inbound-mail, storage,
SEO) are injected after the route-dump early-exit and so are not enumerated at
build time, with their CSRF-exempt/self-protected mutating endpoints noted;
enumeration deferred to a dump-plumbing follow-up.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SjQWjvP71LMRPsQexcYPvi
@madmax983 madmax983 merged commit 31a0d7b into trunk-dev Jul 14, 2026
31 of 33 checks passed
@madmax983 madmax983 deleted the feat/1627-security-posture-manifest branch July 14, 2026 00:23
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.

2 participants