Skip to content

fix(api): pentest retry follow-ups from production review#3400

Merged
tofikwest merged 3 commits into
mainfrom
fix/pentest-retry-followups
Jul 14, 2026
Merged

fix(api): pentest retry follow-ups from production review#3400
tofikwest merged 3 commits into
mainfrom
fix/pentest-retry-followups

Conversation

@tofikwest

@tofikwest tofikwest commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

Follow-ups from the review on the production deploy PR (#3399) for the pentest auto-retry feature. Investigated all 5 findings; fixed the 4 real ones, left 1 that's a deliberate product decision.

Fixed

  • Concurrent duplicate failure webhooks → duplicate retries (P1). At-least-once webhook delivery could let two pentest.failed deliveries each spawn a retry child for the same parent (duplicate scans + nondeterministic active-attempt pick). Added a unique index on retry_of_provider_run_id (Postgres treats NULLs as distinct, so originals are unaffected) — at most one retry child per parent. The migration dedups first so it's deploy-safe. The losing concurrent spawn's ownership insert fails and is refunded by the existing error path.
  • Explicit empty check selection dropped on retry (P2). checks: [] was omitted on retry, letting the provider fall back to its default check set. Now the stored selection is preserved faithfully (including empty); stale entries are filtered out without dropping the whole list.
  • Legacy runs masked pointlessly (P2). A failure is now masked as in-progress only when the run is actually retry-eligible (under the attempt cap and has stored scan params). Pre-feature rows (no scan params) reveal their failure immediately instead of showing "provisioning" for the grace window.
  • Malformed zero runtime cap (P3). "cap 0m" produced "maximum runtime of its time limit"; a non-positive/non-finite cap now falls through to the generic message.

Not changed (deliberate)

  • "Retries all failures, including runtime-cap/config." This is the intended behavior — we chose to retry uniformly with the same parameters rather than classify transient vs permanent (agent runtime varies run-to-run, and it's a temporary posture while we're on the current sandbox provider). Not a bug.

Testing

npx jest src/security-penetration-tests → 151 tests green (isolation + serial), typecheck + prettier clean. Added coverage for empty-check preservation, retry-eligibility masking, and the zero-cap message.


Summary by cubic

Fixes pentest auto-retries to dedupe duplicates at the provider, preserve check selection, only mask failures when a retry can happen, and make billing reservations idempotent to avoid double charges. Tightens the runtime-cap message.

  • Bug Fixes
    • Dedupe concurrent failure webhooks at the provider (Idempotency-Key: retry:<parentRunId>) and make retry billing reservation idempotent (pending:retry:<parentRunId>), so duplicates create one scan and debit once; user-initiated creates send no key and keep a random reservation id.
    • Preserve an explicit empty check selection (checks: []) on retry.
    • Mask failures as “provisioning” only when retry-eligible: under the attempt cap and when stored params validate via fromScanParams; legacy or malformed params reveal “failed” immediately.
    • Show the generic error for non‑positive or absurd runtime caps; only positive, safe‑integer caps use the “maximum runtime” message.

Written for commit d0b3d4e. Summary will update on new commits.

Review in cubic

- Enforce one auto-retry child per parent (unique index on
  retry_of_provider_run_id, dedup-guarded migration) so concurrent duplicate
  failure webhooks can't create two retry scans or a nondeterministic active
  attempt.
- Preserve an explicit empty check selection ([]) on retry instead of dropping
  it (which let the provider fall back to its default check set).
- Only mask a failure as in-progress when the run is actually retry-eligible
  (under the attempt cap and has stored scan params), so pre-feature runs with
  no scan params reveal their failure immediately instead of showing
  provisioning for the grace window.
- Treat a malformed non-positive runtime cap (e.g. "cap 0m") as unrecognized
  and return the generic message instead of "maximum runtime of its time limit".
@vercel

vercel Bot commented Jul 14, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
comp-framework-editor Ready Ready Preview, Comment Jul 14, 2026 2:31am
2 Skipped Deployments
Project Deployment Actions Updated (UTC)
app Skipped Skipped Jul 14, 2026 2:31am
portal Skipped Skipped Jul 14, 2026 2:31am

Request Review

@cubic-dev-ai cubic-dev-ai 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.

All reported issues were addressed across 8 files

Confidence score: 5/5

  • Safe to merge after the addressed issues were fixed.

Reply with feedback, questions, or to request a fix.

Fix all with cubic | Re-trigger cubic

Comment thread packages/db/prisma/schema/security-penetration-test-run.prisma Outdated
Comment thread apps/api/src/security-penetration-tests/security-penetration-tests.service.ts Outdated
Comment thread apps/api/src/security-penetration-tests/pentest-run-error.util.ts Outdated
Replaces the DB unique-constraint approach with a Maced idempotency key so
concurrent duplicate failure webhooks dedupe at the provider — the key fix,
since the constraint only prevented duplicate rows AFTER both requests had
already launched a provider scan (orphaning the loser).

- Auto-retries pass a deterministic Idempotency-Key (`retry:<parentRunId>`);
  Maced returns the same run for concurrent duplicates, so only one scan runs
  and only one ownership row is written (via the existing providerRunId upsert).
- Drop the retry_of_provider_run_id unique index + its dedup migration
  (no longer needed, and the migration's row-delete could orphan provider runs).
- Retry-eligibility (for status masking) now reuses fromScanParams validation
  instead of a shallow null check, so malformed stored params reveal the failure
  immediately rather than masking it.
- Runtime-cap message now requires a safe integer cap, so absurd oversized caps
  fall through to the generic message.
@tofikwest

Copy link
Copy Markdown
Contributor Author

Addressed in 97820f454. Two of these (1 & 2) were genuinely right and pushed me to a better fix:

  • P1 (concurrent webhooks launch duplicate provider scans): Correct — the unique index only deduped DB rows after both requests had already called Maced, orphaning the loser's scan. Fixed properly with a Maced idempotency key (retry:<parentRunId>): concurrent duplicate pentest.failed webhooks now return the same provider run, so only one scan runs and the existing providerRunId upsert collapses the ownership rows.
  • P1 (migration DELETE orphans runs): Eliminated — with the idempotency key, duplicates never form, so I dropped the unique index and its dedup migration entirely. No destructive row-delete.
  • P2 (eligibility used shallow != null): Fixed — retry-eligibility (for status masking) now reuses fromScanParams validation, so malformed-but-non-null params reveal the failure immediately instead of masking it.
  • P3 (oversized cap → "1e+24 minutes"): Fixed — the runtime-cap message now requires Number.isSafeInteger; absurd caps fall through to the generic message.

Net effect: idempotency is now enforced at the provider (where duplicate work actually happens), not just at DB insert time, and the risky migration is gone.

Tests: 154 green (isolation + serial), typecheck + prettier clean. Added coverage for the idempotency key (present on retry, absent on user creates), malformed-params reveal, and the oversized-cap message.

@cubic-dev-ai cubic-dev-ai 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.

All reported issues were addressed across 6 files (changes from recent commits).

Reply with feedback, questions, or to request a fix.

Fix all with cubic | Re-trigger cubic

Concurrent duplicate failure webhooks converged on one provider run (via the
idempotency key) but still reserved two subscription allowances — each call
used a random reservation id, so only one landed on the shared ownership row
and the other allowance was orphaned (never refunded), double-charging the
customer. Retries now use a deterministic reservation id
(`pending:retry:<parentRunId>`); billing consumption is idempotent on it, so
duplicates debit exactly once.

@cubic-dev-ai cubic-dev-ai 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.

1 issue found across 2 files (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="apps/api/src/security-penetration-tests/security-penetration-tests.service.ts">

<violation number="1" location="apps/api/src/security-penetration-tests/security-penetration-tests.service.ts:363">
P1: A transient or ambiguous Maced failure in one duplicate webhook can refund the shared `pending:retry:<parent>` reservation while the concurrent call succeeds, leaving the retry with no net allowance charge. Reservation/refund state needs to be tied to the winning create outcome, or a shared reservation must not be refunded until the provider result is known.</violation>
</file>

Tip: Review your code locally with the cubic CLI to iterate faster.

Fix all with cubic | Re-trigger cubic

// so duplicates debit exactly once — otherwise two random ids would debit
// twice and only one would land on the shared ownership row, orphaning the
// other. User-initiated creates keep a unique random id.
const billingUsageSourceId = lineage.retryOfProviderRunId

@cubic-dev-ai cubic-dev-ai Bot Jul 14, 2026

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.

P1: A transient or ambiguous Maced failure in one duplicate webhook can refund the shared pending:retry:<parent> reservation while the concurrent call succeeds, leaving the retry with no net allowance charge. Reservation/refund state needs to be tied to the winning create outcome, or a shared reservation must not be refunded until the provider result is known.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/api/src/security-penetration-tests/security-penetration-tests.service.ts, line 363:

<comment>A transient or ambiguous Maced failure in one duplicate webhook can refund the shared `pending:retry:<parent>` reservation while the concurrent call succeeds, leaving the retry with no net allowance charge. Reservation/refund state needs to be tied to the winning create outcome, or a shared reservation must not be refunded until the provider result is known.</comment>

<file context>
@@ -354,7 +354,15 @@ export class SecurityPenetrationTestsService {
+    // so duplicates debit exactly once — otherwise two random ids would debit
+    // twice and only one would land on the shared ownership row, orphaning the
+    // other. User-initiated creates keep a unique random id.
+    const billingUsageSourceId = lineage.retryOfProviderRunId
+      ? `pending:retry:${lineage.retryOfProviderRunId}`
+      : `pending:${randomUUID()}`;
</file context>
Fix with cubic

@tofikwest tofikwest merged commit e6e9239 into main Jul 14, 2026
11 checks passed
@tofikwest tofikwest deleted the fix/pentest-retry-followups branch July 14, 2026 02:38
claudfuen pushed a commit that referenced this pull request Jul 14, 2026
## [3.101.2](v3.101.1...v3.101.2) (2026-07-14)

### Bug Fixes

* **api:** pentest retry follow-ups from production review ([#3400](#3400)) ([e6e9239](e6e9239))
* **api:** revert deterministic retry billing id (caused free-scan on redelivery) ([#3401](#3401)) ([6033674](6033674))
@claudfuen

Copy link
Copy Markdown
Contributor

🎉 This PR is included in version 3.101.2 🎉

The release is available on GitHub release

Your semantic-release bot 📦🚀

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants