fix(api): pentest retry follow-ups from production review#3400
Conversation
- 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".
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
There was a problem hiding this comment.
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
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.
|
Addressed in
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. |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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>
|
🎉 This PR is included in version 3.101.2 🎉 The release is available on GitHub release Your semantic-release bot 📦🚀 |
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
pentest.faileddeliveries each spawn a retry child for the same parent (duplicate scans + nondeterministic active-attempt pick). Added a unique index onretry_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.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."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)
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.
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.checks: []) on retry.fromScanParams; legacy or malformed params reveal “failed” immediately.Written for commit d0b3d4e. Summary will update on new commits.