mirror of
https://github.com/KeygraphHQ/shannon.git
synced 2026-10-13 03:17:28 +02:00
feat(auth): auth-validation preflight + email_login credentials (#335)
* feat(preflight): add credential validation activity * refactor(preflight): tighten error retryability and dedup failure-point enum * refactor(preflight): extract resolvePromptDir helper and cap failure_detail at 250 chars * refactor(preflight): inline validator rules into intro paragraph * refactor(preflight): restyle validator prompt with XML tags and tool list * chore(preflight): bump auth validation timeout to 10 minutes * feat: provision playwright stealth config for browser auto-discovery * feat(stealth): strengthen browser fingerprint with chrome.runtime and realistic plugins * feat(prompts): add pipeline-testing stub for validate-authentication * refactor(stealth): swap zx for node:fs in playwright-config-writer * feat(auth): add email_login credentials with login-flow substitution * fix(auth): propagate email_login through credentials sanitizer * fix(config): drop dangerous-pattern check on credentials.password * feat(auth-validation): instruct agent to mask sensitive values in failure_detail * docs(auth): document email_login credentials for magic-link and email-OTP flows * docs(auth): add login_flow authoring guide with placeholder reference * feat(auth): make credentials.password optional for passwordless flows * docs(auth): drop redundant placeholder hint from login_flow examples
This commit is contained in:
1 parent
ca86c839cc
commit
1af42339b9
17 files changed
+568
-36
No files matched your search
@@ -18,6 +18,7 @@
|
||||
import fs from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
import { ApplicationFailure, Context, heartbeat } from '@temporalio/activity';
|
||||
import { writePlaywrightStealthConfig } from '../ai/playwright-config-writer.js';
|
||||
import { writeUserSettingsForCodePathAvoids } from '../ai/settings-writer.js';
|
||||
import { AuditSession } from '../audit/index.js';
|
||||
import type { ResumeAttempt } from '../audit/metrics-tracker.js';
|
||||
@@ -28,11 +29,12 @@ import { DEFAULT_DELIVERABLES_SUBDIR, deliverablesDir } from '../paths.js';
|
||||
import { getContainer, getOrCreateContainer, removeContainer } from '../services/container.js';
|
||||
import { classifyErrorForTemporal, PentestError } from '../services/error-handling.js';
|
||||
import { ExploitationCheckerService } from '../services/exploitation-checker.js';
|
||||
import { renderFindingsFromQueues } from '../services/findings-renderer.js';
|
||||
import { executeGitCommandWithRetry } from '../services/git-manager.js';
|
||||
import { runPreflightChecks } from '../services/preflight.js';
|
||||
import type { ExploitationDecision, VulnType } from '../services/queue-validation.js';
|
||||
import { renderFindingsFromQueues } from '../services/findings-renderer.js';
|
||||
import { assembleFinalReport, injectModelIntoReport } from '../services/reporting.js';
|
||||
import { validateAuthentication } from '../services/validate-authentication.js';
|
||||
import { AGENTS } from '../session-manager.js';
|
||||
import type { AgentName } from '../types/agents.js';
|
||||
import { ALL_AGENTS } from '../types/agents.js';
|
||||
@@ -184,11 +186,7 @@ async function runAgentActivity(agentName: AgentName, input: ActivityInput): Pro
|
||||
attemptNumber,
|
||||
...(input.apiKey !== undefined && { apiKey: input.apiKey }),
|
||||
...(input.providerConfig !== undefined && { providerConfig: input.providerConfig }),
|
||||
...(input.promptDir !== undefined && {
|
||||
promptDir: path.isAbsolute(input.promptDir)
|
||||
? input.promptDir
|
||||
: path.resolve(process.env.SHANNON_WORKER_ROOT ?? process.cwd(), input.promptDir),
|
||||
}),
|
||||
...(input.promptDir !== undefined && { promptDir: input.promptDir }),
|
||||
...(input.configYAML !== undefined && { configYAML: input.configYAML }),
|
||||
},
|
||||
auditSession,
|
||||
@@ -375,6 +373,95 @@ export async function runPreflightValidation(input: ActivityInput): Promise<void
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Authentication validation activity. No-ops without an authentication
|
||||
* block; otherwise surfaces a classified failure (failurePoint +
|
||||
* failureDetail in ApplicationFailure.details) on credential rejection.
|
||||
*/
|
||||
export async function runAuthenticationValidation(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: 'auth-validation', elapsedSeconds: elapsed, attempt: attemptNumber });
|
||||
}, HEARTBEAT_INTERVAL_MS);
|
||||
|
||||
try {
|
||||
const logger = createActivityLogger();
|
||||
|
||||
const sessionMetadata = buildSessionMetadata(input);
|
||||
const container = getOrCreateContainer(input.workflowId, sessionMetadata, buildContainerConfig(input));
|
||||
const configResult = await container.configLoader.loadOptional(input.configPath, undefined, input.configYAML);
|
||||
if (isErr(configResult)) {
|
||||
// runPreflightValidation already validated parsing, so this is unexpected.
|
||||
logger.warn(`runAuthenticationValidation: config load failed unexpectedly: ${configResult.error.message}`);
|
||||
return;
|
||||
}
|
||||
|
||||
const distributedConfig = configResult.value;
|
||||
if (!distributedConfig?.authentication) {
|
||||
logger.info('No authentication configured — skipping credential validation');
|
||||
return;
|
||||
}
|
||||
|
||||
const auditSession = new AuditSession(sessionMetadata);
|
||||
await auditSession.initialize(input.workflowId);
|
||||
|
||||
const result = await validateAuthentication({
|
||||
distributedConfig,
|
||||
repoPath: input.repoPath,
|
||||
webUrl: input.webUrl,
|
||||
logger,
|
||||
auditSession,
|
||||
attemptNumber,
|
||||
...(input.apiKey !== undefined && { apiKey: input.apiKey }),
|
||||
...(input.providerConfig !== undefined && { providerConfig: input.providerConfig }),
|
||||
...(input.deliverablesSubdir !== undefined && { deliverablesSubdir: input.deliverablesSubdir }),
|
||||
...(input.promptDir !== undefined && { promptDir: input.promptDir }),
|
||||
...(input.pipelineTestingMode !== undefined && { pipelineTestingMode: input.pipelineTestingMode }),
|
||||
});
|
||||
|
||||
if (isErr(result)) {
|
||||
const classified = classifyErrorForTemporal(result.error);
|
||||
const message = truncateErrorMessage(result.error.message);
|
||||
const ctx = result.error.context;
|
||||
const details = [
|
||||
{
|
||||
phase: 'auth-validation',
|
||||
attemptNumber,
|
||||
elapsed: Date.now() - startTime,
|
||||
...(ctx.failurePoint !== undefined && { failurePoint: ctx.failurePoint }),
|
||||
...(ctx.failureDetail !== undefined && { failureDetail: ctx.failureDetail }),
|
||||
},
|
||||
];
|
||||
|
||||
const failure = classified.retryable
|
||||
? ApplicationFailure.create({ message, type: classified.type, details })
|
||||
: ApplicationFailure.nonRetryable(message, classified.type, details);
|
||||
truncateStackTrace(failure);
|
||||
throw failure;
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof ApplicationFailure) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
const classified = classifyErrorForTemporal(error);
|
||||
const rawMessage = error instanceof Error ? error.message : String(error);
|
||||
const message = truncateErrorMessage(rawMessage);
|
||||
const details = [{ phase: 'auth-validation', attemptNumber, elapsed: Date.now() - startTime }];
|
||||
|
||||
const failure = classified.retryable
|
||||
? ApplicationFailure.create({ message, type: classified.type, details })
|
||||
: ApplicationFailure.nonRetryable(message, classified.type, details);
|
||||
truncateStackTrace(failure);
|
||||
throw failure;
|
||||
} finally {
|
||||
clearInterval(heartbeatInterval);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize a private git repository inside the workspace deliverables directory.
|
||||
* Idempotent — skips if .git already exists (resume case).
|
||||
@@ -400,6 +487,24 @@ export async function initDeliverableGit(input: ActivityInput): Promise<void> {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Drop a stealth cli.config.json into the repo's .playwright/ directory so
|
||||
* `playwright-cli open` auto-loads anti-detection defaults from the agent's
|
||||
* cwd (disables the Blink AutomationControlled flag, drops the
|
||||
* --enable-automation default, and overrides the HeadlessChrome user agent).
|
||||
*
|
||||
* No-op when the repo already has its own .playwright/cli.config.json.
|
||||
*/
|
||||
export async function syncPlaywrightStealthConfig(input: ActivityInput): Promise<void> {
|
||||
const logger = createActivityLogger();
|
||||
const { result, configPath } = await writePlaywrightStealthConfig(input.repoPath);
|
||||
if (result === 'skipped-existing') {
|
||||
logger.info(`Playwright stealth config: leaving existing ${configPath} in place`);
|
||||
} else {
|
||||
logger.info(`Playwright stealth config: wrote ${configPath}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sync code_path avoid rules into Claude's user-scope settings.json so the
|
||||
* SDK enforces them at the tool layer for every agent in this run.
|
||||
@@ -879,17 +984,7 @@ export async function generateReportOutputActivity(input: ActivityInput): Promis
|
||||
|
||||
const logger = createActivityLogger();
|
||||
|
||||
// Resolve promptDir against the worker root so providers are cwd-independent.
|
||||
const resolvedInput: ActivityInput = {
|
||||
...input,
|
||||
...(input.promptDir !== undefined && {
|
||||
promptDir: path.isAbsolute(input.promptDir)
|
||||
? input.promptDir
|
||||
: path.resolve(process.env.SHANNON_WORKER_ROOT ?? process.cwd(), input.promptDir),
|
||||
}),
|
||||
};
|
||||
|
||||
const result = await container.reportOutputProvider.generate(resolvedInput, logger);
|
||||
const result = await container.reportOutputProvider.generate(input, logger);
|
||||
if (result.outputPath) {
|
||||
logger.info(`Report output written to ${result.outputPath}`);
|
||||
}
|
||||
|
||||
@@ -76,6 +76,7 @@ const PRODUCTION_RETRY = {
|
||||
'ConfigurationError',
|
||||
'InvalidTargetError',
|
||||
'ExecutionLimitError',
|
||||
'AuthLoginFailedError',
|
||||
],
|
||||
};
|
||||
|
||||
@@ -134,6 +135,22 @@ const preflightActs = proxyActivities<typeof activities>({
|
||||
retry: PREFLIGHT_RETRY,
|
||||
});
|
||||
|
||||
// Credential rejection is not retryable; transient SDK errors get 3 attempts.
|
||||
const AUTH_VALIDATION_RETRY = {
|
||||
initialInterval: '10 seconds',
|
||||
maximumInterval: '1 minute',
|
||||
backoffCoefficient: 2,
|
||||
maximumAttempts: 3,
|
||||
nonRetryableErrorTypes: PRODUCTION_RETRY.nonRetryableErrorTypes,
|
||||
};
|
||||
|
||||
// Browser-driving validation measured at 60–180s; 10 min start-to-close leaves headroom for slow SSO/MFA flows.
|
||||
const authValidationActs = proxyActivities<typeof activities>({
|
||||
startToCloseTimeout: '10 minutes',
|
||||
heartbeatTimeout: '10 minutes',
|
||||
retry: AUTH_VALIDATION_RETRY,
|
||||
});
|
||||
|
||||
/**
|
||||
* Compute aggregated metrics from the current pipeline state.
|
||||
* Called on both success and failure to provide partial metrics.
|
||||
@@ -420,6 +437,18 @@ export async function pentestPipeline(input: PipelineInput): Promise<PipelineSta
|
||||
await preflightActs.runPreflightValidation(activityInput);
|
||||
log.info('Preflight validation passed');
|
||||
|
||||
// === Playwright stealth config ===
|
||||
// Write the playwright-cli config before any browser session opens so the
|
||||
// validator and downstream agents inherit anti-detection defaults.
|
||||
await preflightActs.syncPlaywrightStealthConfig(activityInput);
|
||||
|
||||
// === Authentication Validation ===
|
||||
state.currentPhase = 'auth-validation';
|
||||
state.currentAgent = 'validate-authentication';
|
||||
await authValidationActs.runAuthenticationValidation(activityInput);
|
||||
state.currentAgent = null;
|
||||
log.info('Authentication validation passed');
|
||||
|
||||
// === Initialize Deliverables Git ===
|
||||
await a.initDeliverableGit(activityInput);
|
||||
|
||||
|
||||
Reference in new issue
Block a user