Files
buzz/crates/buzz-workflow/src/error.rs
T
9b0f744804 resolve findings (#3150)
Fixes all six HIGH findings from the buzz security report, one commit
per finding. Independently reviewed to approval by Max at `0158ae542`,
plus a deep isolated live pass (clean-room compose stack, weird ports,
full product matrix) at the same head — see the buzz-security thread for
evidence. `fe65c07c3` merges current `origin/main` on top (new commit,
no rebase), inheriting the nostr 0.44.6 bump (#3135) and relay-admin ban
gate (#3128).

## Findings and fixes

| Finding | Commit | Fix |
|---|---|---|
| 003 — quinn-proto RUSTSEC-2026-0185 | `e5dcdec72` | Bump quinn-proto
0.11.14 → 0.11.16 (lockfile-only) |
| 002/004 — linkify-it quadratic-parse DoS (GHSA-22p9-wv53-3rq4,
GHSA-v245-v573-v5vm) | `923b3c20f` | pnpm override `linkify-it: ^5.0.2`;
`pnpm why` confirms a single 5.0.2 copy |
| 001 — media reads served unauthenticated by default | `0f277e3e2` |
Helm `requireMediaGetAuth` defaults to `true` + rendered-chart test
pinning the default |
| 006 — removed workflow owners retain webhook-exfiltration authority |
`4749bd56c` | Fail-closed per-fire authority gate (current owner/admin
membership) on **all four** trigger doors (on_event, scheduler
pre-claim, manual trigger, webhook — masked as generic 404), save-time
gate for `call_webhook` defs, durable disable-on-removal wired to kinds
9001 + 9022 |
| 005 — git Smart-HTTP reads ignore channel membership | `e648f2dba` +
`0158ae542` | `authorize_git_read`: caller's **current active
membership** in the repo's bound channel, checked before any
hydration/subprocess on all three read doors (`info_refs` for both
services + `upload_pack` POST). Uniform generic 404 denials (no
membership probing), no repo-owner bypass, first-`buzz-channel`-tag
binding semantics fail closed on ambiguous duplicates (mutation-verified
test). Resolution follows the live kind:30617 announcement, so
deleted/replaced announcements deny immediately. The committed
`e2e-git-perms.sh` guest scenario previously asserted the vulnerability
— now asserts denial. |

## Behavior changes to be aware of

1. **Unbound repos fail closed for git reads.** `buzz repos create`
emits no `buzz-channel` tag, so CLI-created repos without a binding are
unreadable via git HTTP. Correct per finding 005's fail-closed posture;
a follow-up could bind CLI-created repos at creation time.
2. **006 is conservative:** a workflow disabled on owner removal does
not auto-re-enable if the owner is re-added — explicit re-enable
required.
3. Merge conflict resolution in `fe65c07c3`: kept main's
`@radix-ui/react-dismissable-layer` 1.1.19 bump alongside the linkify-it
security override (`pnpm-workspace.yaml` + lockfile).

## Verification at the merge head `fe65c07c3` (same shell)

- buzz-relay `--lib`: 761 passed / 1 failed — the lone red is the known
pre-existing `mesh_demo::demo_join_forwarded_arm_round_trips_echo` 504
flake, present on main
- SEC-005 module incl. PG behavioral matrix: 8/8 (removed-member,
never-member, owner-no-bypass, deleted-30617, malformed/ambiguous
binding, owner-mismatch all denied)
- buzz-workflow 153/0, buzz-db 84/0; `clippy --all-targets -D warnings`
+ `fmt --check` clean
- Desktop JS 3637/3637, tsc clean, biome clean,
file-size/px-text/pubkey-truncation gates clean
- `helm lint` + `helm unittest` (40/40) on `deploy/charts/buzz`
- All five pre-push hooks green (desktop-check, desktop-test,
rust-tests, desktop-tauri-test, branch-skew)

Prior review evidence at `0158ae542` (pre-merge): Max's independent
exact-head approval + clean-room live regression pass
(`WORK_LOGS/2026-07-27_SECURITY_HIGH_LIVE_TEST.md` in his workspace).
Max will re-run the deep local pass at this post-merge head before
merge.

---------

Signed-off-by: Tyler Longwell <tlongwell@block.xyz>
Signed-off-by: npub1qyvc0c5kl4gqv2fd97fsk46tu378sqgy35vc83rvgfwne90sel7s0ed67d <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@buzz.block.builderlab.xyz>
Co-authored-by: npub1qyvc0c5kl4gqv2fd97fsk46tu378sqgy35vc83rvgfwne90sel7s0ed67d <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@buzz.block.builderlab.xyz>
Co-authored-by: Tyler Longwell <tlongwell@block.xyz>
2026-07-27 14:18:24 -04:00

73 lines
2.2 KiB
Rust

//! Workflow error types.
use thiserror::Error;
/// Partial execution progress captured when a workflow step fails mid-run.
///
/// This allows callers to persist whatever trace was accumulated before the
/// error, rather than losing it when the in-memory `Vec` is dropped.
#[derive(Debug, Default)]
pub struct PartialProgress {
/// Index of the step that failed (0-based).
pub step_index: usize,
/// Trace entries for steps completed/skipped before the failure.
pub trace: Vec<serde_json::Value>,
}
/// Errors produced by the workflow engine.
#[derive(Debug, Error)]
pub enum WorkflowError {
/// The workflow YAML/JSON could not be parsed.
#[error("invalid YAML: {0}")]
InvalidYaml(#[from] serde_yaml::Error),
/// The workflow definition violates a semantic invariant.
#[error("invalid definition: {0}")]
InvalidDefinition(String),
/// An `if:` condition expression could not be evaluated.
#[error("condition evaluation error: {0}")]
ConditionError(String),
/// A template variable substitution failed.
#[error("template error: {0}")]
TemplateError(String),
/// A step exceeded its configured timeout.
#[error("step '{step_id}' timed out after {timeout_secs}s")]
StepTimeout {
/// The ID of the step that timed out.
step_id: String,
/// The timeout limit in seconds.
timeout_secs: u64,
},
/// An outbound webhook call failed.
#[error("webhook error: {0}")]
WebhookError(String),
/// The engine's concurrency limit was reached.
#[error("capacity exceeded")]
CapacityExceeded,
/// A database operation failed.
#[error("database error: {0}")]
Database(String),
/// The workflow's owner is not currently authorized to run it (removed
/// from the channel, insufficient role for the definition's actions, or
/// the authority lookup failed — all deny, fail-closed).
#[error("unauthorized: {0}")]
Unauthorized(String),
/// The action is defined but not yet implemented.
#[error("action not implemented: {0}")]
NotImplemented(String),
}
impl From<buzz_db::error::DbError> for WorkflowError {
fn from(e: buzz_db::error::DbError) -> Self {
WorkflowError::Database(e.to_string())
}
}