Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
125 changes: 125 additions & 0 deletions apps/api/src/security-penetration-tests/pentest-lineage.util.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
import {
collapsedStatus,
MAX_ATTEMPTS,
RETRY_GRACE_MS,
} from './pentest-lineage.util';

describe('collapsedStatus', () => {
const now = 1_000_000_000_000;

it('passes through completed', () => {
expect(
collapsedStatus({
activeStatus: 'completed',
attemptNumber: 2,
failedAtMs: null,
nowMs: now,
}),
).toBe('completed');
});

it.each(['provisioning', 'cloning', 'running'] as const)(
'passes through in-progress status %s',
(status) => {
expect(
collapsedStatus({
activeStatus: status,
attemptNumber: 1,
failedAtMs: null,
nowMs: now,
}),
).toBe(status);
},
);

it('never masks cancelled (staff cancel is terminal, never retried)', () => {
expect(
collapsedStatus({
activeStatus: 'cancelled',
attemptNumber: 1,
failedAtMs: now - 1000,
nowMs: now,
}),
).toBe('cancelled');
});

describe('failed', () => {
it('masks a fresh failed non-final attempt as provisioning', () => {
expect(
collapsedStatus({
activeStatus: 'failed',
attemptNumber: 1,
failedAtMs: now - 5_000, // well within grace
nowMs: now,
}),
).toBe('provisioning');
});

it('masks the second attempt too', () => {
expect(
collapsedStatus({
activeStatus: 'failed',
attemptNumber: 2,
failedAtMs: now - 5_000,
nowMs: now,
}),
).toBe('provisioning');
});

it('reveals failed once the lineage is exhausted (final attempt)', () => {
expect(
collapsedStatus({
activeStatus: 'failed',
attemptNumber: MAX_ATTEMPTS,
failedAtMs: now - 5_000,
nowMs: now,
}),
).toBe('failed');
});

it('reveals failed after the grace window elapses without a retry', () => {
expect(
collapsedStatus({
activeStatus: 'failed',
attemptNumber: 1,
failedAtMs: now - (RETRY_GRACE_MS + 1),
nowMs: now,
}),
).toBe('failed');
});

it('reveals failed exactly at the grace-window boundary', () => {
expect(
collapsedStatus({
activeStatus: 'failed',
attemptNumber: 1,
failedAtMs: now - RETRY_GRACE_MS,
nowMs: now,
}),
).toBe('failed');
});

it('reveals failed when the failure time is unknown (cannot bound the mask)', () => {
expect(
collapsedStatus({
activeStatus: 'failed',
attemptNumber: 1,
failedAtMs: null,
nowMs: now,
}),
).toBe('failed');
});

it('reveals failed immediately when not retry-eligible (e.g. no scan params)', () => {
expect(
collapsedStatus({
activeStatus: 'failed',
attemptNumber: 1,
retryEligible: false,
failedAtMs: now - 5_000, // well within grace, but can't retry
nowMs: now,
}),
).toBe('failed');
});
});
});
73 changes: 73 additions & 0 deletions apps/api/src/security-penetration-tests/pentest-lineage.util.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
/**
* Pure helpers for the pentest retry-lineage feature.
*
* A scan is a "lineage": the original run plus up to `MAX_ATTEMPTS - 1`
* automatic retries, all sharing one root run id. Customers only ever see the
* root; our API resolves it to the active (highest-numbered) attempt and only
* reports a terminal failure once the whole lineage is exhausted.
*/

export type PentestRunStatus =
| 'provisioning'
| 'cloning'
| 'running'
| 'completed'
| 'failed'
| 'cancelled';

/** Original attempt + 2 automatic retries. */
export const MAX_ATTEMPTS = 3;

/**
* How long a failed non-final attempt is masked as in-progress while its
* retry is spawned. Comfortably covers provider webhook-delivery latency; if a
* retry never materializes within this window the real failure is revealed so
* the run can't hang on a fake "running" state forever.
*/
export const RETRY_GRACE_MS = 10 * 60 * 1000;

/**
* Computes the customer-facing status for a lineage from its active (highest)
* attempt. The active attempt is authoritative because we only ever retry
* `failed` runs — so no earlier attempt can be `completed`/`running`.
*
* - `completed` / in-progress statuses pass through unchanged.
* - `cancelled` is terminal and never retried → always revealed.
* - `failed`:
* - not retry-eligible (lineage exhausted, or the run can't actually be
* re-run — e.g. a pre-feature row with no stored scan params) → revealed
* immediately, so it's never masked as in-progress pointlessly.
* - otherwise masked as `provisioning` (a retry is expected) while inside
* the grace window; revealed once the window elapses without a retry
* appearing, or when the failure time is unknown (can't bound the mask).
*/
export function collapsedStatus(params: {
activeStatus: PentestRunStatus;
attemptNumber: number;
failedAtMs: number | null;
nowMs: number;
/** Whether this run can actually be auto-retried (has re-runnable params). */
retryEligible?: boolean;
maxAttempts?: number;
graceMs?: number;
}): PentestRunStatus {
const { activeStatus, attemptNumber, failedAtMs, nowMs } = params;
const maxAttempts = params.maxAttempts ?? MAX_ATTEMPTS;
const graceMs = params.graceMs ?? RETRY_GRACE_MS;
const retryEligible = params.retryEligible ?? true;

// Only `failed` triggers an auto-retry, so only `failed` is ever masked.
if (activeStatus !== 'failed') {
return activeStatus;
}

// Reveal immediately if no retry can happen — lineage exhausted or the run
// isn't re-runnable — so a failure that will never retry isn't masked.
if (attemptNumber >= maxAttempts || !retryEligible) {
return 'failed';
}
if (failedAtMs === null || nowMs - failedAtMs >= graceMs) {
return 'failed';
}
return 'provisioning';
}
111 changes: 111 additions & 0 deletions apps/api/src/security-penetration-tests/pentest-run-error.util.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import { toCustomerFacingError } from './pentest-run-error.util';

describe('toCustomerFacingError', () => {
const infraMessagePrefix =
'The scan was interrupted by a temporary infrastructure issue';

describe('infra / transient failures', () => {
const infraErrors = [
'Sandbox container deleted by Daytona infrastructure (age 35m). Desired-state=started but container gone — likely Daytona backup/restore race condition.',
'Run timed out — sandbox became unresponsive',
'Timed out with no sandbox after 9m',
'Hard timeout: 4m (cap 120m); Daytona unreachable',
];

it.each(infraErrors)(
'maps infra error to the transient message: %s',
(raw) => {
expect(toCustomerFacingError(raw)).toContain(infraMessagePrefix);
},
);

it('never echoes the raw provider text', () => {
const raw =
'Sandbox container deleted by Daytona infrastructure (age 35m). backup/restore race condition.';
const message = toCustomerFacingError(raw);
expect(message.toLowerCase()).not.toContain('daytona');
expect(message.toLowerCase()).not.toContain('sandbox');
expect(message.toLowerCase()).not.toContain('container');
});
});

describe('genuine runtime cap', () => {
it('humanizes a 720m cap as 12 hours', () => {
const message = toCustomerFacingError(
'Hard timeout: workflow ran 722m (cap 720m)',
);
expect(message).toContain('maximum runtime of 12 hours');
expect(message).toContain("won't be charged");
});

it('humanizes a 120m cap as 2 hours', () => {
const message = toCustomerFacingError(
'Hard timeout: workflow ran 130m (cap 120m)',
);
expect(message).toContain('maximum runtime of 2 hours');
});

it('renders a single-hour cap without pluralizing', () => {
const message = toCustomerFacingError(
'Hard timeout: workflow ran 61m (cap 60m)',
);
expect(message).toContain('maximum runtime of 1 hour');
expect(message).not.toContain('1 hours');
});

it('falls back to minutes for a non-hour cap', () => {
const message = toCustomerFacingError(
'Hard timeout: workflow ran 95m (cap 90m)',
);
expect(message).toContain('maximum runtime of 90 minutes');
});

it('renders a single-minute cap without pluralizing', () => {
const message = toCustomerFacingError(
'Hard timeout: workflow ran 2m (cap 1m)',
);
expect(message).toContain('maximum runtime of 1 minute');
expect(message).not.toContain('1 minutes');
});

it('falls back to the generic message for a malformed zero cap', () => {
const message = toCustomerFacingError(
'Hard timeout: workflow ran 0m (cap 0m)',
);
expect(message).not.toContain('maximum runtime');
expect(message).toContain("The scan couldn't be completed");
});

it('falls back to the generic message for an absurd oversized cap', () => {
const message = toCustomerFacingError(
'Hard timeout: workflow ran 1m (cap 999999999999999999999999m)',
);
expect(message).not.toContain('maximum runtime');
expect(message).toContain("The scan couldn't be completed");
});

it('classifies the "; Daytona unreachable" variant as infra, not runtime cap', () => {
const message = toCustomerFacingError(
'Hard timeout: 4m (cap 120m); Daytona unreachable',
);
expect(message).toContain(infraMessagePrefix);
expect(message).not.toContain('maximum runtime');
});
});

describe('unknown / empty', () => {
const genericPrefix = "The scan couldn't be completed";

it('returns the generic message for an unrecognized error', () => {
expect(
toCustomerFacingError('some brand new error we have not seen'),
).toContain(genericPrefix);
});

it('returns the generic message for null / undefined / empty', () => {
expect(toCustomerFacingError(null)).toContain(genericPrefix);
expect(toCustomerFacingError(undefined)).toContain(genericPrefix);
expect(toCustomerFacingError('')).toContain(genericPrefix);
});
});
});
Loading
Loading