Skip to content

Add the Trusted Server CLI#669

Open
ChristianPavilonis wants to merge 2 commits intofeature/ts-config-in-config-storefrom
feature/ts-cli
Open

Add the Trusted Server CLI#669
ChristianPavilonis wants to merge 2 commits intofeature/ts-config-in-config-storefrom
feature/ts-cli

Conversation

@ChristianPavilonis
Copy link
Copy Markdown
Collaborator

@ChristianPavilonis ChristianPavilonis commented Apr 29, 2026

Summary

  • Add the new ts host CLI as a dedicated Rust crate for Trusted Server config, dev, auth, audit, and Fastly provisioning workflows.
  • Reuse the existing runtime-config/core abstractions while splitting host-target CLI verification from the repo’s existing wasm-target verification lanes.
  • Upgrade ts audit from a static HTML fetcher to a Chromium-backed browser audit while preserving the existing audit artifact and draft-config outputs.

Changes

File Change
Cargo.toml Added the new trusted-server-cli workspace crate and host-side CLI/browser dependencies.
Cargo.lock Updated the lockfile for the new CLI, Fastly, and Chromiumoxide dependencies.
crates/trusted-server-cli/Cargo.toml Defined the ts binary crate and its dependencies.
crates/trusted-server-cli/src/lib.rs Added the clap command surface and command dispatch for config, audit, dev, auth, and Fastly provisioning flows.
crates/trusted-server-cli/src/main.rs Added the ts binary entrypoint.
crates/trusted-server-cli/src/error.rs Added CLI-specific error types.
crates/trusted-server-cli/src/output.rs Added shared human/JSON output helpers.
crates/trusted-server-cli/src/config.rs Added config path resolution, starter template writing, and config validation using trusted-server-core runtime config loading.
crates/trusted-server-cli/src/dev.rs Added ts dev and Rust-native local Fastly manifest rendering/launch behavior.
crates/trusted-server-cli/src/fastly/auth.rs Added Fastly credential resolution, secure storage login/logout/status, and precedence handling for FASTLY_API_KEY.
crates/trusted-server-cli/src/fastly/api.rs Added a host-side Fastly API client for provisioning resources and bindings.
crates/trusted-server-cli/src/fastly/provision.rs Added Fastly plan/apply provisioning logic for config stores, secret stores, KV stores, and service bindings.
crates/trusted-server-cli/src/fastly/mod.rs Wired Fastly auth/API/provision modules together.
crates/trusted-server-cli/src/audit.rs Added the stable audit façade that preserves the current audit output contract.
crates/trusted-server-cli/src/audit/collector.rs Added normalized collected-page models shared by the audit pipeline.
crates/trusted-server-cli/src/audit/http_collector.rs Preserved the original static HTTP collector behind an internal seam.
crates/trusted-server-cli/src/audit/browser_collector.rs Added Chromiumoxide-backed browser collection for rendered HTML, title, final URL, script tags, and browser resource entries.
crates/trusted-server-cli/src/audit/analyzer.rs Added analyzer logic that merges DOM and browser-observed script evidence into the existing AuditArtifact schema.
.github/workflows/test.yml Split Rust testing into wasm-target runtime lanes and a host-target CLI lane.
.github/workflows/format.yml Split Rust clippy verification into wasm-target runtime lanes and a host-target CLI lane.
.github/pull_request_template.md Updated the PR test checklist for the new host-target CLI verification commands.
CLAUDE.md Documented the new split runtime/CLI verification workflow.
README.md Added ts CLI usage examples and documented browser prerequisites for ts audit.
docs/guide/getting-started.md Added ts config/dev/audit workflow examples and browser audit prerequisites.
docs/guide/configuration.md Added ts config init / ts config validate guidance.
docs/guide/fastly.md Added ts auth fastly ... and ts provision fastly ... workflow documentation.

Closes

Closes #

Test plan

  • cargo test --workspace --exclude trusted-server-cli
  • cargo test --package trusted-server-cli --target "$(rustc -vV | sed -n 's/^host: //p')"
  • cargo clippy --workspace --exclude trusted-server-cli --all-targets --all-features -- -D warnings
  • cargo clippy --package trusted-server-cli --target "$(rustc -vV | sed -n 's/^host: //p')" --all-targets -- -D warnings
  • cargo fmt --all -- --check
  • JS tests: cd crates/js/lib && npx vitest run
  • JS format: cd crates/js/lib && npm run format
  • Docs format: cd docs && npm run format
  • WASM build: cargo build --package trusted-server-adapter-fastly --release --target wasm32-wasip1
  • Manual testing via fastly compute serve
  • Other: cargo run -p trusted-server-cli --target "$(rustc -vV | sed -n 's/^host: //p')" -- config validate --json --config trusted-server.example.toml
  • Other: manual ts audit https://example.com ... verification of the missing-Chrome/Chromium error path

Checklist

  • Changes follow CLAUDE.md conventions
  • No unwrap() in production code — use expect("should ...")
  • Uses log macros (not println!)
  • New code has tests
  • No secrets or credentials committed

@ChristianPavilonis ChristianPavilonis linked an issue Apr 29, 2026 that may be closed by this pull request
@ChristianPavilonis ChristianPavilonis changed the base branch from main to feature/ts-config-in-config-store April 29, 2026 19:27
@ChristianPavilonis ChristianPavilonis changed the title Add Chromium-backed ts audit collection Add the Trusted Server CLI Apr 29, 2026
Copy link
Copy Markdown
Collaborator

@aram356 aram356 left a comment

Choose a reason for hiding this comment

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

Summary

New trusted-server-cli crate adds ts config|audit|dev|auth fastly|provision fastly commands and switches ts audit to a Chromium-driven collector. The shape is solid — clean module split, trait-seamed credential store, comprehensive MockFastlyApi for the provision flow — but several issues block merge: clippy fails on the host target with -D warnings, the new HTTP clients have no timeouts, ts audit accepts arbitrary URL schemes (local-file disclosure via headless Chromium), and the repo's CI workflows don't actually run for this PR.

Blocking

🔧 wrench

  • Clippy fails on host target: clippy::needless_pass_by_value in build_audit_outputs (crates/trusted-server-cli/src/audit.rs:70). Inline comment.
  • No request/connect timeouts on Fastly API client (crates/trusted-server-cli/src/fastly/api.rs:107-113). Inline comment.
  • No request/connect timeouts on audit HTTP collector (crates/trusted-server-cli/src/audit/http_collector.rs:11-15). Inline comment.
  • ts audit accepts arbitrary URL schemesUrl::parse lets file:// / data: through, and validate_navigation_response (crates/trusted-server-cli/src/audit/browser_collector.rs:213-216) returns Ok(()) for any non-http(s) scheme. ts audit file:///etc/passwd can navigate headless Chromium to local files and serialize the rendered HTML into the on-disk artifact. Restrict scheme at parse time. Inline comment on crates/trusted-server-cli/src/lib.rs:218.
  • No Rust CI runs for this PR: .github/workflows/format.yml:6-7 and .github/workflows/test.yml:9-10 trigger only on pull_request: branches: [main]. PR #669 targets feature/ts-config-in-config-store, so neither cargo test, cargo clippy, nor the new host-CLI lanes have run on this commit. The push runs are recorded as failed with "log not found" — they never executed. Either retarget onto main, expand the branches: filter to include the stack base branch, or land the stack first. As direct evidence: the clippy break above was not caught because no clippy job ever ran here.

❓ question

  • --reuse-management-api-key privilege escalation (crates/trusted-server-cli/src/lib.rs:336-375). The flag plants a credential with full Fastly management privileges into the runtime secret store; if the worker ever leaks it, an attacker can mutate Fastly resources, not just sign requests. Is this a smoke-test convenience or a supported production path? Should --help explicitly call out the risk, and should it require confirmation even with --yes? Inline comment.

Non-blocking

🤔 thinking

  • Dead http_collector module (crates/trusted-server-cli/src/audit/http_collector.rs:9-79) — entire file is #[allow(dead_code)] with no consumer. Either wire it as a fallback when no browser is found, or delete it. Inline comment.
  • analyze_html only used by tests (crates/trusted-server-cli/src/audit.rs:60 and crates/trusted-server-cli/src/audit/analyzer.rs:117) — gated #[cfg_attr(not(test), allow(dead_code))]. Move both under #[cfg(test)] or rewrite the tests around analyze_collected_page. Inline comment.
  • No log:: macros anywhere in the CLI — CLAUDE.md mandates log macros for instrumentation; the CLI only writes to stdout/stderr. Browser audit steps and Fastly API requests would benefit from log::debug! / log::trace! behind RUST_LOG. Today there is no way to introspect what ts provision fastly apply is doing beyond final stdout/stderr.

♻️ refactor

  • Race condition in env-var tests (crates/trusted-server-cli/src/fastly/auth.rs:166-196) — parallel mutation of FASTLY_API_KEY across threads. Use serial_test::serial or a per-suite Mutex. Inline comment.
  • Repeated regex compilation (crates/trusted-server-cli/src/audit/analyzer.rs:201, 224) — GTM-[A-Z0-9]+$ is rebuilt on every inline-script and recompiled in two functions. Hoist to a LazyLock<Regex>. Inline comment.
  • Trim API key before persisting (crates/trusted-server-cli/src/fastly/auth.rs:122-133) — pasted tokens commonly arrive with trailing whitespace/newlines. Inline comment.
  • format! allocations in classify_party (crates/trusted-server-cli/src/audit/analyzer.rs:158-170) — two allocations per call, run for every asset URL; an allocation-free version using strip_suffix is straightforward. Inline comment.

🌱 seedling

  • Pin chromiumoxide via workspace deps (crates/trusted-server-cli/Cargo.toml:27) — only direct version pin in this crate; everything else is workspace = true. Inline comment.
  • ts audit lacks --json — every other validate/status/plan/apply command supports --json; audit doesn't. Consider adding for tooling parity.

⛏ nitpick

  • config validate --json returns unstructured error blob (crates/trusted-server-cli/src/config.rs:97-99) — multi-line Report debug dump shoved into a single JSON string. Inline comment.
  • navigator.webdriver spoofing (crates/trusted-server-cli/src/audit/browser_collector.rs:62-71) — bot-evasion technique that's likely unnecessary for auditing sites the operator owns. Either drop or document the specific failure mode it mitigates. Inline comment.
  • Reused cache-shared-key across wasm and host lanes (.github/workflows/format.yml:30,55, .github/workflows/test.yml:29,65) — distinct keys per target are safer under concurrent runs. Inline comment.
  • Public items without doc commentslib.rs:25,156,171, audit.rs:42,52, config.rs:13,19, dev.rs:13, error.rs:4, output.rs:8, fastly/auth.rs:57-75, fastly/api.rs:12-99. CLAUDE.md says every public item must have a doc comment. Most are effectively pub(crate) since the binary lives in the same crate — easiest fix is to downgrade visibility.

CI status

  • fmt: PASS (verified locally — workflow did not run on this commit)
  • clippy (workspace, wasm): not run
  • clippy (CLI, host): FAIL — see audit.rs:70 finding
  • rust tests (workspace): not run
  • rust tests (CLI, host): PASS locally (18/18) — workflow did not run on this commit
  • js tests: not run for this commit
  • analyze (javascript-typescript): PASS

}

fn build_audit_outputs(
collected: collector::CollectedPage,
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

🔧 wrench — Clippy fails on host with -D warnings: clippy::needless_pass_by_value. The body only borrows collected (analyze_collected_page(&collected), collected.final_url()), so this should take a reference.

Fix:

fn build_audit_outputs(
    collected: &collector::CollectedPage,
) -> Result<AuditOutputs, Report<CliError>> {

And update the call site at audit.rs:67 to build_audit_outputs(&collected).

Reproduced locally:

cargo clippy -p trusted-server-cli --target $(rustc -vV | sed -n 's/^host: //p') --all-targets -- -D warnings
error: this argument is passed by value, but not consumed in the function body
  --> crates/trusted-server-cli/src/audit.rs:70:16

.build()
.change_context(CliError::FastlyApi)?;
Ok(Self { client, api_key })
}
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

🔧 wrench — No request/connect timeouts on the Fastly API client. reqwest::blocking::Client has no default timeout, so a stalled Fastly endpoint will wedge ts provision fastly plan/apply indefinitely with no recovery path other than SIGINT.

Fix:

let client = Client::builder()
    .user_agent("trusted-server-cli/0.1")
    .connect_timeout(std::time::Duration::from_secs(10))
    .timeout(std::time::Duration::from_secs(60))
    .build()
    .change_context(CliError::FastlyApi)?;

.user_agent("trusted-server-cli/0.1")
.redirect(reqwest::redirect::Policy::limited(10))
.build()
.change_context(CliError::Audit)?;
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

🔧 wrench — Same as the Fastly client: no .timeout(...) / .connect_timeout(...). A slow target site will hang the audit. Add explicit timeouts.

let url = url::Url::parse(&args.url).map_err(|error| {
Report::new(CliError::Arguments)
.attach(format!("invalid audit URL `{}`: {error}", args.url))
})?;
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

🔧 wrenchUrl::parse accepts arbitrary schemes (file://, data:, chrome://, …). Combined with validate_navigation_response (browser_collector.rs:213-216) returning Ok(()) for any non-http(s) scheme, this lets ts audit file:///etc/passwd navigate Chromium to a local file and then serialize the rendered HTML into the audit artifact written to disk. Headless Chromium permits file:// unless explicitly disabled.

Fix — restrict scheme at parse time:

let url = url::Url::parse(&args.url).map_err(|error| {
    Report::new(CliError::Arguments)
        .attach(format!("invalid audit URL `{}`: {error}", args.url))
})?;
if !matches!(url.scheme(), "http" | "https") {
    return Err(Report::new(CliError::Arguments).attach(format!(
        "`ts audit` only supports http/https URLs, got `{}`",
        url.scheme()
    )));
}

}
if reuse_management_api_key {
return Ok(Some(management_api_key.to_string()));
}
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

question — Reusing the management API key as the runtime API key means edge code stores a credential with full Fastly management privileges. If the runtime token leaks (logs, error paths, future bug in the worker), an attacker can mutate Fastly resources, not just sign requests.

Questions:

  1. Is this intended only as a smoke-test convenience, or as a production-supported flow?
  2. If smoke-test only, can --help for --reuse-management-api-key flag the privilege-escalation risk explicitly?
  3. Should this require a confirmation prompt unless --yes is also set, so that operators can't accidentally promote their management token to the edge?

if asset_host == page_host
|| asset_host.ends_with(&format!(".{page_host}"))
|| page_host.ends_with(&format!(".{asset_host}"))
{
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

♻️ refactor — Two format!(".{page_host}") allocations per call, and this runs once per asset URL. Allocation-free version:

fn host_matches(page_host: &str, asset_host: &str) -> bool {
    asset_host == page_host
        || asset_host
            .strip_suffix(page_host)
            .is_some_and(|prefix| prefix.ends_with('.'))
        || page_host
            .strip_suffix(asset_host)
            .is_some_and(|prefix| prefix.ends_with('.'))
}

futures = { workspace = true }
tokio = { workspace = true, features = ["rt-multi-thread", "time"] }
which = { workspace = true }
chromiumoxide = "0.9.1"
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

🌱 seedling — Every other dependency in this crate uses workspace = true; only chromiumoxide = "0.9.1" is pinned at the crate level. Move to [workspace.dependencies] in the root Cargo.toml for consistency and centralized version bumps.

path: resolved_path,
config_hash: None,
errors: vec![format!("{error:?}")],
}
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

nitpickvec![format!("{error:?}")] puts a multi-line error_stack::Report debug dump into a single JSON string. Anything parsing the --json output gets an unstructured blob. Consider one entry per attached context layer or a structured {stage, message} shape so downstream tooling can pattern-match.

get: () => false,
});
"#,
)
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

nitpick — Overriding navigator.webdriver is bot-evasion. For an audit tool intended to run against sites the operator owns, this is unnecessary, and most sites don't gate solely on this flag. Either drop the override or add a // Why: comment explaining the specific failure mode it mitigates so future maintainers don't strip it as a security smell.

toolchain: ${{ steps.rust-version-cli.outputs.rust-version }}
target: wasm32-wasip1
components: "clippy, rustfmt"
cache-shared-key: cargo-${{ runner.os }}
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

nitpick — Same cache-shared-key: cargo-${{ runner.os }} is reused across the wasm-target workspace lane and the host-target CLI lane (here, line 55, and test.yml lines 29 and 65). Concurrent jobs may contend on cache writes. Distinct keys per target (cargo-${{ runner.os }}-wasm vs -host) are safer.

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.

Trusted server CLI implementation

2 participants