← Back to Trust Center

Trust Artifact STRIDE per surface

Threat Models

STRIDE-aligned models for Pathfinder's four trust boundaries. Assets, vectors, mitigations, and owner pathkeepers per row.

Owning pathkeepers: domain/security + per-surface architecture pathkeepers

Jump to a surface:

IPC → MCP → HTTP → Connector →


IPC — Tauri IPC Surface

Surface: #[tauri::command] handlers reached via invoke() from the embedded React webview. ~180 commands across src-tauri/src/commands/.

Trust root: the macOS user account running the app.

In-scope adversaries:

  • A malicious or compromised npm package pulled into pathfinder-ui/ that runs JavaScript inside the Tauri webview.
  • An attacker who controls a workspace SQLite file path (e.g. swaps in a malicious DB before the user opens that workspace).
  • A future attacker who can reach the webview via tauri.conf.json URL allow-list expansion (today we ship local-content only).

Explicitly out of scope:

  • An attacker with arbitrary code execution as the local macOS user (they own the Keychain, the binary, and the trust root).
  • macOS-level lateral movement.

STRIDE

Threat Asset Vector Mitigation Owner pathkeeper
S Webview spoofs identity of user Connector OAuth tokens Malicious npm injects invoke('connection_save_oauth', ...) Tauri capabilities allow-list, command auth scope review (E3.2), audit_log_chain append-only proof of intent architecture/tauri-ipc, domain/security
T Tamper with SQLite mid-transaction Workspace DB Concurrent process writes while orchestrator runs SQLite WAL + per-workspace exclusive lock at open; integrity_check on every workspace_open architecture/db-migrations, domain/orchestrator
T Tamper with audit log Ed25519-signed Merkle chain Direct SQLite write outside the writer Chain seal forces prev_hash linkage; replay/verify detects gaps. Mitigation expanded by E1-S3. domain/audit-log-chain
R Repudiation of who triggered a migration Audit log User claims "I never approved this" Every migrate_* command writes an audit_log row with actor + intent; Ed25519 seal proves chain integrity domain/audit-log-chain
I Information disclosure via error strings Keychain secrets, OAuth tokens Error from a command leaks token in Result<_, String> error::redact() on every IPC return path; lint catches format!("{token}") patterns (E3.5) architecture/error-handling
D Denial of service via batch size Memory / SQLite Webview invokes migrate_run(batch_size=10_000_000) Each command clamps user inputs; orchestrator has memory ceiling + checkpoint pause domain/orchestrator
E Privilege escalation: webview invokes wrong command All commands npm package guesses an unprotected admin command Capability allow-list per Tauri webview window; lifecycle/code-reviewer blocks new commands w/o capability scope architecture/tauri-ipc

Notable hardening already in place

  • All IPC commands are async fn ... -> Result<T, String> — no panics cross the boundary.
  • Keychain access uses security-framework with prompt-on-first-use; the webview never sees raw secrets, only opaque connection IDs.
  • Workspace SQLite path is selected via macOS file picker (user-driven), not webview-driven, so a malicious page cannot pick a different DB.
  • Tauri v2 dragDropEnabled: false keeps the OS file-drop channel out of the webview pointer pipeline — see DND_MIGRATION_PLAN.md.

Gaps tracked by Enterprise Hardening Plan

  • E3.2 — Per-command capability allow-list audit. Today: every command is reachable from any window. Target: migration-write commands reachable only from the main window.
  • E3.5 — Lint that rejects format!("...{secret}...") patterns across IPC return paths.
  • E3.7 — IPC fuzz harness on the 20 highest-arity commands.

Review checklist for new IPC commands

When pathkeeper-steward or architecture/tauri-ipc reviews a new #[tauri::command]:

  1. Return type is Result<T, String> and String is redacted.
  2. All user-supplied integers/strings have explicit bounds.
  3. Capability allow-list entry added (tauri.conf.json).
  4. Audit-log write if the command mutates persistent state.
  5. Frontend wrapper in pathfinder-ui/src/lib/api.ts.
  6. This threat model refreshed if the command opens a new asset/vector.

MCP — MCP Server Surface

Surface: pathfinder-mcp binary, rmcp stdio transport. Consumed by a local MCP broker (Claude Desktop, IDE-side agent, etc.).

Trust root: the macOS user account that launches the MCP broker. The broker spawns pathfinder-mcp as a child process over stdio — no network listener is ever opened.

In-scope adversaries:

  • A malicious MCP client prompt (LLM-generated tool calls) that tries to drive Pathfinder into an unsafe state.
  • A malicious or compromised MCP broker that re-routes stdio.
  • A future attacker who induces the user to install a hostile claude_desktop_config.json block pointing at pathfinder-mcp with weaponized arguments.

Explicitly out of scope:

  • Network MCP transports (we do not ship one).
  • Multi-user broker scenarios.

STRIDE

Threat Asset Vector Mitigation Owner pathkeeper
S LLM spoofs user intent Migration approval LLM calls migrate_run "on behalf of" the user Two-tier tool set: read tools open, write tools require explicit confirmation flag in payload and UI consent recorded in audit log domain/mcp-server
T Tamper with stdio frame Tool result Broker truncates a JSON-RPC frame mid-stream rmcp framing has Content-Length validation; client-side broker reports parse errors loudly domain/mcp-server
T Tamper with API version Protocol compat Client sends spoofed api_version=2 pathfinder-mcp/src/api_version.rs::validate_api_version rejects unknown versions; see E1-A4 for the 90-day deprecation policy architecture/versioning
R Repudiation of LLM-triggered action Audit log "The LLM did it, not me" Every MCP tool call writes an audit_log row tagged actor=mcp:{client_name}; chain seal proves provenance domain/audit-log-chain
I Info disclosure via tool result Connector creds LLM asks "show me the Salesforce token" and tool returns it verbatim pathfinder-mcp/src/tools.rs never reads from Keychain on the read path; only connection IDs cross MCP domain/security
I Schema exfil via discovery Customer schemas LLM dumps every object schema and forwards via broker This is expected and authorized — the whole point of the MCP. Hardening is per-broker, not per-tool. We document this in the user-facing MCP guide. lifecycle/documentation
D Denial of service via long-running tool Process LLM calls fabric_run_pattern with a huge dataset Each tool has a per-call deadline; cancelable via stdio close domain/mcp-server
E Tool that should be admin-only is exposed unauthenticated All tools A new tool is added that wraps an unsafe IPC command Roster of MCP tools is allow-listed in pathfinder-mcp/src/tools.rs::register_tools; lifecycle/code-reviewer blocks new tools without a listed risk tier domain/mcp-server

Notable hardening already in place

  • Stdio-only transport. No TCP, no HTTP, no WebSocket. The attacker must already control the broker process.
  • API version gate. validate_api_version (added in E1-A4) refuses requests outside [MIN_SUPPORTED_API_VERSION, CURRENT_API_VERSION] and returns a structured unsupported_api_version error.
  • Tool registry is allow-list. Adding a new tool requires editing register_tools; the pathkeeper-coverage-analyst flags drift between registered tools and exposed Tauri commands.
  • No Keychain access from MCP path. MCP tools that need credentials go through the same connection-id indirection that IPC uses.

Gaps tracked by Enterprise Hardening Plan

  • E3.3 — Write tools currently honor a confirmed=true flag in the payload; we want a per-broker confirmation UX so the LLM cannot silently set the flag.
  • E3.6 — Fuzz the MCP JSON-RPC parser with malformed frames; the current test suite covers happy-path framing only.
  • E4.2 — Emit an mcp.tool_call metric per call, tagged client_name + tool + risk_tier, for post-hoc audit dashboards.

Review checklist for new MCP tools

  1. Risk tier set: read | write | admin.
  2. Write/admin tier requires explicit confirmation flag.
  3. Tool result schema documented (rmcp::schemars).
  4. Audit-log write on every call.
  5. api_version.rs updated if request/response shape changes.
  6. This threat model refreshed if a new asset is exposed.

HTTP — Outbound HTTP Surface

Surface: Outbound HTTP from the connector SDK (connectors/src/sdk/http.rs) to third-party SaaS APIs. There is no inbound HTTP listener in any Pathfinder process.

Trust root: TLS certificate chains via rustls + the OS trust store. Endpoints are pinned via per-connector base URLs configured at connection creation time.

In-scope adversaries:

  • A network attacker between the device and the SaaS API (corporate proxy, hostile coffee-shop wifi).
  • A compromised SaaS endpoint (Salesforce instance hijacked, RENXT hostile API response).
  • A malicious connector config that points at an attacker-controlled domain.

Explicitly out of scope:

  • DNS rebinding on inbound traffic — there is no inbound traffic.
  • Server-side authorization at the SaaS — that is the SaaS's job.

STRIDE

Threat Asset Vector Mitigation Owner pathkeeper
S TLS spoof OAuth tokens, customer data MITM swaps cert chain rustls with OS trust store, no custom CA injection; per-connector base URL is the only allowed origin domain/connector
S Connector points at attacker domain OAuth tokens User pastes hostile instance_url Base URL allow-list per connector type (Salesforce → *.my.salesforce.com, RENXT → api.sky.blackbaud.com, etc.); UI rejects unknown patterns domain/connector
T Hostile API response tampers extracted records Migration source data SaaS returns malformed JSON Schema-coerce step runs after extract; type mismatches surface as conflicts rather than silent data loss domain/connector, domain/quality
R Repudiation of an extracted record Audit log Customer claims "this record never came from RENXT" Extract phase writes a schema_snapshot (E1-A5) tagged with source connector + timestamp; chain seal proves it domain/schema-drift, domain/audit-log-chain
I Token leak in HTTP error log OAuth tokens reqwest debug log dumps Authorization header connectors/src/sdk/http.rs strips Authorization, Cookie, X-Api-Key before logging; covered by redact_headers test architecture/error-handling
I Token leak in URL OAuth tokens Connector author puts token in query string SDK exposes bearer(token) builder that pins to Authorization header; URL builder rejects ?access_token= patterns at lint time domain/connector
D Rate-limit storm at customer Customer SaaS quota Connector hammers API ignoring 429 SDK has shared RateLimiter + token-bucket; per-connector defaults in sdk/http.rs::default_rate_limit; respects Retry-After domain/connector, architecture/retry
D Slowloris on extract Process Hostile API drips bytes 1/sec reqwest::Client::builder().timeout(Duration::from_secs(60)) set workspace-wide; per-call deadlines override down domain/connector
E Connector escalates from read to write scope Target system A read-only OAuth scope gets upgraded to write OAuth scope is captured at connect time and pinned in the connection record; UI shows the granted scope before every migration domain/security, domain/connector

Notable hardening already in place

  • rustls-tls feature on reqwest — no OpenSSL exposure; sidesteps several alerts that closed during the H6 Dependabot triage.
  • Token transport. All known connectors put bearer tokens in the Authorization header, never in query strings or path segments.
  • Auth header redaction. Both the structured logger and the trace exporter mask Authorization / Cookie headers — verified by redact_headers test in sdk/http.rs.
  • Retry classifier. Network errors retry with jittered backoff; auth errors do not retry (avoids token-burn at SaaS).

Gaps tracked by Enterprise Hardening Plan

  • E3.8 — Per-connector base-URL allow-list is informal; tighten to a match-pattern in code and enforce at connection-save time.
  • E3.9 — Add a request-recording mock fixture so we can re-play any customer-supplied trace without going live to their SaaS.
  • E4.3 — Emit connector.request_duration_ms + .rate_limit_remaining metrics per (connector, endpoint), for support runbooks.

Review checklist for new connectors

  1. Base URL pinned at connection-save time (no runtime mutation).
  2. OAuth scope captured + persisted on the connection record.
  3. All API errors classified retriable | terminal | auth_revoked.
  4. Rate-limiter configured with the connector's documented quota.
  5. redact_headers covers any new auth header the SDK adds.
  6. This threat model refreshed if a new auth pattern is introduced.

Connector — Connector Surface

Surface: The 10 source/target connectors in connectors/src/. Each holds long-lived credentials (OAuth refresh tokens, API keys) and is the sole point of trust for extracted/loaded customer data.

Trust root: The macOS Keychain entry for each connection. The Keychain entry is opened via security-framework with prompt-on-first-use.

In-scope adversaries:

  • A user who connected to a malicious SaaS instance (intentionally or via social engineering).
  • A compromised SaaS account belonging to the legitimate customer.
  • A connector author (us) who accidentally ships a credential leak.

Explicitly out of scope:

  • SaaS-side authorization (their problem).
  • macOS Keychain compromise (root-level OS compromise; outside our trust boundary).

STRIDE

Threat Asset Vector Mitigation Owner pathkeeper
S Connection spoofing Keychain entry A different connector reuses another connector's Keychain key Key namespacing: pathfinder.<connector_type>.<connection_id>; collision lint covered by keychain::tests::reject_duplicate_service domain/security
T Tamper with stored connection config SQLite connections row Direct DB write swaps instance_url after consent connections table has last_verified_at + last_verified_url; mismatch on next test_connection forces re-consent domain/connector
T Tamper with field mappings during load Migration Hostile process edits field_mappings between approve and run Mapping checksum captured at approve; orchestrator verifies on run start; mapping_validate (E1-S4) covers the type-coerce surface domain/mapping, domain/transform-typecheck
R Repudiation: "I never approved this load" Migration Customer denies authorizing a load to their target Approval flow writes a row to audit_log with mapping checksum + actor; Ed25519 chain seal proves provenance (E1-S3) domain/audit-log-chain
I Credential leak in DB OAuth tokens A developer accidentally persists a token in connections.config_json connections schema has a secret_keys allow-list checked by connection_save — tokens cannot be written to DB; only Keychain domain/security
I Credential leak in logs OAuth tokens tracing::debug!("config: {:?}", config) dumps a token ConnectionConfig implements Debug manually to redact secret fields; covered by tests::debug_redacts_secrets domain/security, architecture/error-handling
I Credential leak in audit log OAuth tokens Audit writer persists raw params for a connection_save call audit_log_chain_writer::WriteParams serializer applies the same redactor used for Debug; pinned by property test domain/audit-log-chain
D Memory pressure from huge schemas Process RENXT returns 1M-field describe Schema-discovery pipeline truncates at 50K fields per object with a structured "schema too large" surface; covered by schema_too_large_returns_explicit_error domain/quality, domain/connector
D OAuth refresh storm Customer SaaS quota Refresh loop runs every retry Refresh has its own backoff cell distinct from request-level retry; if refresh returns invalid_grant, the connection is marked re_consent_required and stops retrying domain/connector
E Connector marks itself "verified" without verification All assets Author copies-pastes test_connection() { Ok(()) } from an example lifecycle/code-reviewer checklist requires a real round-trip; contract harness (E2.9) exercises the happy path per connector domain/connector

Notable hardening already in place

  • Keychain-only storage. Tokens never persist to SQLite or env vars. The connections row stores instance_url, client_id, refresh cookie ID (not the refresh token itself), and connector type.
  • No env-var fallback for production secrets. A few legacy connectors read from env vars for CI fixtures only; this is in scope for E3.4 to either delete or gate behind a #[cfg(test)].
  • Schema-coerce, not schema-trust. Every extracted record passes through pathfinder-core::transform typecheck (E1-S4); a hostile API cannot smuggle wrong-type data into the loader.
  • Contract harness (E2.9). Every connector must pass the same shared invariant sweep before merging.

Gaps tracked by Enterprise Hardening Plan

  • E3.4 — Full Keychain inventory + env-var cleanup audit.
  • E3.5 — Add secret_keys allow-list check to the codegen path so a new connector cannot accidentally widen the set.
  • E5.3 — Bench the OAuth refresh path under simulated 429 storms.

Review checklist for new connectors

  1. Credentials live in Keychain only; nothing secret in connections.config_json.
  2. Debug impl manually redacts every secret field.
  3. test_connection() actually hits the API and verifies a roundtrip.
  4. OAuth scope captured + persisted; UI shows it before every migration.
  5. Schema-discovery has an upper bound (no unbounded growth).
  6. Contract harness target added (see connectors/tests/contract.rs).
  7. This threat model refreshed if a new auth/secret pattern lands.

Have procurement questions?

Email security@colbysdatamovers.com or book a discovery call.

Book a discovery call →