feat(preflight): gate scans on an exploit-workload readiness probe

This commit is contained in:
ezl-keygraph committed 2026-10-01 04:19:05 +05:30
1 parent a14c7944d8
commit ad2d069563
8 files changed
+291

No files matched your search

+78
View File
@@ -19,6 +19,7 @@ import { createHash } from 'node:crypto';
import fs from 'node:fs/promises';
import path from 'node:path';
import { ApplicationFailure, Context, heartbeat } from '@temporalio/activity';
import { resolveModelSelection } from '../ai/models.js';
import { syncPermissionSystemConfig } from '../ai/pi/permission-system.js';
import { writePlaywrightStealthConfig } from '../ai/playwright-config-writer.js';
import { AuditSession } from '../audit/index.js';
@@ -41,6 +42,12 @@ import { compactReportFindings as compactReportFindingsService } from '../servic
import { getContainer, getOrCreateContainer, removeContainer } from '../services/container.js';
import { classifyErrorForTemporal, PentestError } from '../services/error-handling.js';
import { RenumberError } from '../services/exact-output-commit.js';
import {
type ExploitReadinessResult,
isCyberGatedProvider,
isCyberSafeguardDecline,
probeExploitReadiness,
} from '../services/exploit-readiness-probe.js';
import { ExploitationCheckerService } from '../services/exploitation-checker.js';
import { renderFindingsFromQueues } from '../services/findings-renderer.js';
import { executeGitCommandWithRetry } from '../services/git-manager.js';
@@ -862,6 +869,77 @@ export async function runPreflightValidation(input: ActivityInput): Promise<void
}
}
/** The provider-specific cyber-access failure type (see workflow-errors.ts); provider is OpenAI or Anthropic. */
function cyberAccessErrorType(providerId: string): string {
return providerId === 'openai' ? 'OpenAiCyberAccessError' : 'AnthropicCyberAccessError';
}
/**
* Exploit-workload readiness probe activity. For OpenAI/Anthropic, hands the model a slice of the
* exploit agent's workload and gates on a decline (`stopReason: error`), failing the scan with the
* provider's own message. A setup/transport fault is not a decline and never gates.
*/
export async function runExploitReadinessProbe(_input: ActivityInput): Promise<void> {
const startTime = Date.now();
const attemptNumber = Context.current().info.attempt;
const heartbeatInterval = setInterval(() => {
const elapsed = Math.floor((Date.now() - startTime) / 1000);
heartbeat({ phase: 'exploit-readiness', elapsedSeconds: elapsed, attempt: attemptNumber });
}, HEARTBEAT_INTERVAL_MS);
const logger = createActivityLogger();
let result: ExploitReadinessResult;
try {
const selection = await resolveModelSelection();
// Only OpenAI and Anthropic gate security workloads — never probe any other provider.
if (!isCyberGatedProvider(selection.providerId)) {
logger.info(`Exploit-workload readiness: skipped (provider ${selection.providerId})`);
return;
}
logger.info('Checking exploit-workload readiness via pi...');
result = await probeExploitReadiness(selection.model, selection.modelRuntime, selection.providerId);
} catch (error) {
// Setup/transport fault, not a decline — never gates the scan.
const message = error instanceof Error ? error.message : String(error);
logger.info(`Exploit-workload readiness: probe skipped (${message.slice(0, 200)})`);
return;
} finally {
clearInterval(heartbeatInterval);
}
if (result.error !== undefined) {
logger.info(`Exploit-workload readiness: ${result.providerId} inconclusive (${result.error.slice(0, 200)})`);
return;
}
if (result.response?.stopReason === 'error') {
logger.info(
`Exploit-workload readiness: declined by ${result.providerId}: ${(result.response.errorMessage ?? '').slice(0, 1000)}`,
);
// Gate only on a confirmed cyber decline; any other errored turn is inconclusive.
if (!isCyberSafeguardDecline(result.providerId, result.response)) {
logger.info(`Exploit-workload readiness: ${result.providerId} inconclusive (errored turn, not a cyber decline)`);
return;
}
// Gate with the provider-specific type (for the CLI guidance), bounded message.
const message = truncateErrorMessage(`${result.providerId} declined the exploit workload`);
const failure = ApplicationFailure.nonRetryable(message, cyberAccessErrorType(result.providerId), [
{ phase: 'exploit-readiness', attemptNumber, elapsed: Date.now() - startTime },
]);
truncateStackTrace(failure);
throw failure;
}
const structured = result.structuredOutput !== undefined ? result.structuredValid : 'none';
logger.info(`Exploit-workload readiness: ${result.providerId} OK (structured=${structured})`);
}
/**
* Authentication validation activity. No-ops without an authentication
* block; otherwise surfaces a classified failure (failurePoint +
+3
View File
@@ -75,6 +75,7 @@ import {
runAuthVulnAgent,
runAuthzExploitAgent,
runAuthzVulnAgent,
runExploitReadinessProbe,
runInjectionExploitAgent,
runInjectionVulnAgent,
runMiscellaneousExploitAgent,
@@ -147,6 +148,7 @@ export const PENTEST_ACTIVITY_NAMES = Object.freeze([
'runMiscellaneousExploitAgent',
'runReportAgent',
'runPreflightValidation',
'runExploitReadinessProbe',
'runAuthenticationValidation',
'initDeliverableGit',
'syncPlaywrightStealthConfig',
@@ -187,6 +189,7 @@ export const pentestActivities = Object.freeze({
runMiscellaneousExploitAgent,
runReportAgent,
runPreflightValidation,
runExploitReadinessProbe,
runAuthenticationValidation,
initDeliverableGit,
syncPlaywrightStealthConfig,
@@ -36,6 +36,8 @@ const ERROR_TYPE_TO_CODE: Record<string, ErrorCode> = {
ReportSarifRenderError: ErrorCode.OUTPUT_VALIDATION_FAILED,
IncompatibleWorkspaceError: ErrorCode.CONFIG_VALIDATION_FAILED,
WorkspaceNotFoundError: ErrorCode.CONFIG_NOT_FOUND,
OpenAiCyberAccessError: ErrorCode.PROVIDER_CYBER_ACCESS_REQUIRED,
AnthropicCyberAccessError: ErrorCode.PROVIDER_CYBER_ACCESS_REQUIRED,
};
export function classifyErrorCode(error: unknown): ErrorCode | undefined {
@@ -64,6 +66,10 @@ const REMEDIATION_HINTS: Record<string, string> = {
IncompatibleWorkspaceError: 'start a new scan with a different -w name.',
WorkspaceNotFoundError: 'check the -w name against: shannon scans',
PipelineFailedError: 're-run the same -w to retry from the last checkpoint.',
OpenAiCyberAccessError:
'Your OpenAI organization must be approved for cyber use. Apply for Daybreak access at https://openai.com/daybreak, then retry.',
AnthropicCyberAccessError:
'Your Anthropic organization must complete cyber verification. See https://support.claude.com/en/articles/14604842-real-time-cyber-safeguards-on-claude-opus-and-sonnet, then retry.',
};
/**
@@ -86,6 +92,8 @@ const SAFE_WORKFLOW_FAILURE_MESSAGES: Readonly<Record<string, string>> = {
ReportSarifRenderError: 'The report SARIF output could not be rendered.',
IncompatibleWorkspaceError: 'This workspace cannot be resumed.',
WorkspaceNotFoundError: 'The requested workspace was not found.',
OpenAiCyberAccessError: 'OpenAI declined the security workload behind its cyber-access program.',
AnthropicCyberAccessError: 'Anthropic declined the security workload behind its cyber-access program.',
};
const WORKFLOW_PHASE_SET = new Set<string>(WORKFLOW_PHASES);
+3
View File
@@ -100,6 +100,8 @@ const PRODUCTION_RETRY = {
'InvalidTargetError',
'AuthLoginFailedError',
'PermanentError',
'OpenAiCyberAccessError',
'AnthropicCyberAccessError',
],
};
@@ -1338,6 +1340,7 @@ export async function pentestPipeline(input: PipelineInput): Promise<PipelineSta
state.currentPhase = 'preflight';
state.currentAgent = null;
await preflightActs.runPreflightValidation(activityInput);
await preflightActs.runExploitReadinessProbe(activityInput);
await preflightActs.syncPlaywrightStealthConfig(activityInput);
state.currentPhase = 'auth-validation';