Skip to content

feat(remediate): --mark-compromised + shipper fix-forward (#98 PR 3)#131

Closed
EffortlessSteven wants to merge 2 commits into
feat/98-plan-yankfrom
feat/98-fix-forward
Closed

feat(remediate): --mark-compromised + shipper fix-forward (#98 PR 3)#131
EffortlessSteven wants to merge 2 commits into
feat/98-plan-yankfrom
feat/98-fix-forward

Conversation

@EffortlessSteven

Copy link
Copy Markdown
Member

Summary

Close the Remediate pillar's staged rollout:

  • PR 1 (#121) — `shipper yank` primitive + PackageYanked event
  • PR 2 (#129) — receipt schema (`compromised_at` / `compromised_by` / `superseded_by`) + `plan-yank` reverse-topological containment plan
  • PR 3 (this) — populating the compromise marker + fix-forward supersession plan

Base branch is #129, not main. Merges #129 first.

What's new

`shipper yank --mark-compromised`

New opt-in flag on the existing yank primitive. Mirrors the yank into
the receipt by amending `compromised_at` / `compromised_by` on the
matching `PackageReceipt`. Tolerant to missing receipt / missing
entry — yank always wins, mark is best-effort.

`shipper fix-forward --from-receipt `

New planning command. Reads a receipt, finds packages flagged
`compromised_at = Some(_)`, prints a topologically-ordered supersession
plan — dependencies first (opposite of plan-yank, because you're
publishing replacements here, not removing reachability).

Planning only — no Cargo.toml edits, no publish invocation. Honors
`--format text|json`.

Example

```
$ shipper yank --crate app --version 0.1.0
--reason "CVE-2026-0001" --mark-compromised
[warn] yanking app@0.1.0 ...
[info] marked app@0.1.0 compromised in .shipper/receipt.json
[info] yanked app@0.1.0 successfully...

$ shipper fix-forward

fix-forward plan — registry=crates-io, plan_id=abc

1 package(s) marked compromised

Steps:

1. For each crate below, bump the version in its Cargo.toml ...

3. Run `shipper publish` to ship the successors in topo order.

4. Once all successors are live, optionally run `shipper plan-yank

--compromised-only` to contain the compromised versions.

  1. app: 0.1.0 -> 0.1.0-next # CVE-2026-0001
    ```

Scope — explicitly NOT in this PR

  • Automated Cargo.toml bump. Workspace-edit territory; deserves its own PR with --dry-run / --apply / git-cleanliness guards.
  • Running the successor publish. `shipper publish` already does this; fix-forward just tells the operator when to run it.
  • Chaining `superseded_by` back to the original receipt. Needs post-publish receipt amendment from the successor run. Follow-on.
  • `release_compromised` top-level state. Per-package `compromised_at` covers the planning-level need.

Tests

  • 4 `fix_forward::tests`: compromised+Published filter / topo order preserved / empty-plan guidance / text render with reason + follow-up pointers
  • CLI snapshots for updated `yank --help` (now documents `--mark-compromised`) and new `fix-forward --help`

Test plan

Closes

Once this + #121 + #129 land, issue #98 Remediate is complete at the planning/primitive level. Cargo.toml edits and successor chaining are clean follow-ons.

@gemini-code-assist

Copy link
Copy Markdown

Warning

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

@coderabbitai

coderabbitai Bot commented Apr 17, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: d6b27f2c-c9db-4026-9f73-42066ad3a576

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/98-fix-forward

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@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: 8ee0c3d17d

ℹ️ 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 +85 to +86
fn is_compromised(p: &PackageReceipt) -> bool {
p.compromised_at.is_some() && matches!(p.state, PackageState::Published)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Include skipped published crates in compromised filter

build_plan currently drops any compromised package whose state is not exactly Published. That misses crates recorded as Skipped because they were already published, even though yank --mark-compromised can still mark those receipt entries and they may represent the compromised version you just yanked. In that scenario, fix-forward silently omits a real compromised crate and produces an incomplete supersession plan.

Useful? React with 👍 / 👎.

Comment on lines +843 to +845
let receipt_path = from_receipt.unwrap_or_else(|| {
opts.state_dir
.join(shipper::state::execution_state::RECEIPT_FILE)

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 Resolve default fix-forward receipt path from workspace root

The default --from-receipt path is derived directly from opts.state_dir, so a relative state dir is resolved against the current shell directory, not the workspace root used by publish/receipt generation. Running from outside the workspace (for example with --manifest-path /path/to/ws/Cargo.toml) will look up the wrong receipt.json and fail even though the workspace receipt exists.

Useful? React with 👍 / 👎.

EffortlessSteven added a commit that referenced this pull request Apr 17, 2026
Stitch the Remediate pillar together with a step-by-step operator
guide that walks through yank --mark-compromised → fix-forward plan →
publish → optional plan-yank finalize. Tied to docs/README.md's how-to
index so operators can find it without folklore.

Covers:
- 4-step remediation flow with concrete commands
- Worked example: single-CVE in a multi-crate workspace
- Explicit statement of what yank does NOT do (lockfile invalidation,
  version bumping, consumer notification)

No code changes. This only references commands added in this branch
stack (#121 yank, #129 plan-yank, #131 mark-compromised + fix-forward).
Close the remediation staged rollout: yank (PR 1) + plan-yank (PR 2) +
**fix-forward planning (this PR)** give operators the three building
blocks for handling a compromised release without improvising.

## What's new

### \`shipper yank --mark-compromised\`

New opt-in flag on the existing yank primitive. When set, after a
successful \`cargo yank\`, shipper also amends the matching
\`PackageReceipt\` in \`<state_dir>/receipt.json\` with:

- \`compromised_at = Utc::now()\`
- \`compromised_by = <--reason>\`

These are the fields added (additively) in PR 2 (#129). The amendment
lets downstream commands find compromised packages without scanning
events.jsonl.

Behaviour is tolerant:
- No receipt → warn + proceed (yank still succeeds)
- No matching package in receipt → warn + proceed
- Load/write errors → warn + proceed (yank already happened; a
  receipt-write failure doesn't retroactively unyank)

### \`shipper fix-forward --from-receipt <path>\`

New planning command. Reads a receipt, finds packages flagged
\`compromised_at = Some(_)\`, and prints an ordered supersession plan:

- Topological order (dependencies first, dependents last) — opposite
  of plan-yank, because fix-forward *publishes* replacements while
  plan-yank *removes* reachability.
- Suggested successor versions use a placeholder format (\`<old>-next\`)
  so the operator picks the real bump (patch / minor / major).
- Instructional preamble walks through the full remediation loop:
  bump Cargo.toml → commit → \`shipper publish\` → optional
  \`shipper plan-yank --compromised-only\` for containment.

Like plan-yank, fix-forward is **planning only**. It does not edit
Cargo.toml or invoke publish — those are operator steps.

## Tests

4 new tests in \`engine::fix_forward::tests\`:

- only compromised+Published packages produce steps (Failed-but-
  compromised packages are silently dropped — they were never shipped)
- topological order preserved from receipt.packages
- empty plan (no compromised packages) renders with guidance toward
  \`--mark-compromised\`
- text render enumerates steps with reason and points toward the
  publish + plan-yank follow-ups

Plus CLI snapshots for \`yank --help\` (gains \`--mark-compromised\`) and
\`fix-forward --help\`.

## Scope — explicitly NOT in this PR

- **Automated Cargo.toml bump.** Workspace-edit territory; needs its
  own PR with --dry-run, --apply, and git-cleanliness guards.
- **Publishing successors.** \`shipper publish\` already does this; fix-
  forward just tells the operator when to run it.
- **Chaining \`superseded_by\` back to the original receipt.** Requires
  post-publish receipt amendment from the successor run. Follow-on.
- **\`release_compromised\` top-level state.** The receipt-level marker.
  For now the per-package \`compromised_at\` fields cover the
  planning-level need.
Stitch the Remediate pillar together with a step-by-step operator
guide that walks through yank --mark-compromised → fix-forward plan →
publish → optional plan-yank finalize. Tied to docs/README.md's how-to
index so operators can find it without folklore.

Covers:
- 4-step remediation flow with concrete commands
- Worked example: single-CVE in a multi-crate workspace
- Explicit statement of what yank does NOT do (lockfile invalidation,
  version bumping, consumer notification)

No code changes. This only references commands added in this branch
stack (#121 yank, #129 plan-yank, #131 mark-compromised + fix-forward).
@EffortlessSteven EffortlessSteven deleted the branch feat/98-plan-yank April 18, 2026 00:11
EffortlessSteven added a commit that referenced this pull request Apr 18, 2026
Stitch the Remediate pillar together with a step-by-step operator
guide that walks through yank --mark-compromised → fix-forward plan →
publish → optional plan-yank finalize. Tied to docs/README.md's how-to
index so operators can find it without folklore.

Covers:
- 4-step remediation flow with concrete commands
- Worked example: single-CVE in a multi-crate workspace
- Explicit statement of what yank does NOT do (lockfile invalidation,
  version bumping, consumer notification)

No code changes. This only references commands added in this branch
stack (#121 yank, #129 plan-yank, #131 mark-compromised + fix-forward).
EffortlessSteven added a commit that referenced this pull request Apr 18, 2026
Stitch the Remediate pillar together with a step-by-step operator
guide that walks through yank --mark-compromised → fix-forward plan →
publish → optional plan-yank finalize. Tied to docs/README.md's how-to
index so operators can find it without folklore.

Covers:
- 4-step remediation flow with concrete commands
- Worked example: single-CVE in a multi-crate workspace
- Explicit statement of what yank does NOT do (lockfile invalidation,
  version bumping, consumer notification)

No code changes. This only references commands added in this branch
stack (#121 yank, #129 plan-yank, #131 mark-compromised + fix-forward).
EffortlessSteven added a commit that referenced this pull request Apr 18, 2026
…134)

* feat(remediate): --mark-compromised + shipper fix-forward (#98 PR 3)

Close the remediation staged rollout: yank (PR 1) + plan-yank (PR 2) +
**fix-forward planning (this PR)** give operators the three building
blocks for handling a compromised release without improvising.

## What's new

### \`shipper yank --mark-compromised\`

New opt-in flag on the existing yank primitive. When set, after a
successful \`cargo yank\`, shipper also amends the matching
\`PackageReceipt\` in \`<state_dir>/receipt.json\` with:

- \`compromised_at = Utc::now()\`
- \`compromised_by = <--reason>\`

These are the fields added (additively) in PR 2 (#129). The amendment
lets downstream commands find compromised packages without scanning
events.jsonl.

Behaviour is tolerant:
- No receipt → warn + proceed (yank still succeeds)
- No matching package in receipt → warn + proceed
- Load/write errors → warn + proceed (yank already happened; a
  receipt-write failure doesn't retroactively unyank)

### \`shipper fix-forward --from-receipt <path>\`

New planning command. Reads a receipt, finds packages flagged
\`compromised_at = Some(_)\`, and prints an ordered supersession plan:

- Topological order (dependencies first, dependents last) — opposite
  of plan-yank, because fix-forward *publishes* replacements while
  plan-yank *removes* reachability.
- Suggested successor versions use a placeholder format (\`<old>-next\`)
  so the operator picks the real bump (patch / minor / major).
- Instructional preamble walks through the full remediation loop:
  bump Cargo.toml → commit → \`shipper publish\` → optional
  \`shipper plan-yank --compromised-only\` for containment.

Like plan-yank, fix-forward is **planning only**. It does not edit
Cargo.toml or invoke publish — those are operator steps.

## Tests

4 new tests in \`engine::fix_forward::tests\`:

- only compromised+Published packages produce steps (Failed-but-
  compromised packages are silently dropped — they were never shipped)
- topological order preserved from receipt.packages
- empty plan (no compromised packages) renders with guidance toward
  \`--mark-compromised\`
- text render enumerates steps with reason and points toward the
  publish + plan-yank follow-ups

Plus CLI snapshots for \`yank --help\` (gains \`--mark-compromised\`) and
\`fix-forward --help\`.

## Scope — explicitly NOT in this PR

- **Automated Cargo.toml bump.** Workspace-edit territory; needs its
  own PR with --dry-run, --apply, and git-cleanliness guards.
- **Publishing successors.** \`shipper publish\` already does this; fix-
  forward just tells the operator when to run it.
- **Chaining \`superseded_by\` back to the original receipt.** Requires
  post-publish receipt amendment from the successor run. Follow-on.
- **\`release_compromised\` top-level state.** The receipt-level marker.
  For now the per-package \`compromised_at\` fields cover the
  planning-level need.

* docs: how-to for remediating a compromised release (#98)

Stitch the Remediate pillar together with a step-by-step operator
guide that walks through yank --mark-compromised → fix-forward plan →
publish → optional plan-yank finalize. Tied to docs/README.md's how-to
index so operators can find it without folklore.

Covers:
- 4-step remediation flow with concrete commands
- Worked example: single-CVE in a multi-crate workspace
- Explicit statement of what yank does NOT do (lockfile invalidation,
  version bumping, consumer notification)

No code changes. This only references commands added in this branch
stack (#121 yank, #129 plan-yank, #131 mark-compromised + fix-forward).
@codecov

codecov Bot commented Apr 18, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 66.22807% with 77 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
crates/shipper-cli/src/main.rs 0.00% 70 Missing ⚠️
crates/shipper/src/engine/fix_forward.rs 95.56% 7 Missing ⚠️

📢 Thoughts on this report? Let us know!

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.

1 participant