mirror of
https://github.com/KeygraphHQ/shannon.git
synced 2026-10-05 15:56:51 +02:00
feat(preflight): gate scans on an exploit-workload readiness probe
This commit is contained in:
1 parent
a14c7944d8
commit
ad2d069563
8 files changed
+291
No files matched your search
@@ -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 +
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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';
|
||||
|
||||
Reference in new issue
Block a user