mirror of
https://github.com/KeygraphHQ/shannon.git
synced 2026-06-01 13:11:40 +02:00
feat: migrate to use MCP tools instead of helper scripts
This commit is contained in:
@@ -0,0 +1,45 @@
|
||||
/**
|
||||
* Shannon Helper MCP Server
|
||||
*
|
||||
* In-process MCP server providing save_deliverable and generate_totp tools
|
||||
* for Shannon penetration testing agents.
|
||||
*
|
||||
* Replaces bash script invocations with native tool access.
|
||||
*/
|
||||
|
||||
import { createSdkMcpServer } from '@anthropic-ai/claude-agent-sdk';
|
||||
import { saveDeliverableTool } from './tools/save-deliverable.js';
|
||||
import { generateTotpTool } from './tools/generate-totp.js';
|
||||
|
||||
/**
|
||||
* Create Shannon Helper MCP Server with target directory context
|
||||
*
|
||||
* @param {string} targetDir - The target repository directory where deliverables should be saved
|
||||
* @returns {Object} MCP server instance
|
||||
*/
|
||||
export function createShannonHelperServer(targetDir) {
|
||||
// Store target directory for tool access
|
||||
global.__SHANNON_TARGET_DIR = targetDir;
|
||||
|
||||
return createSdkMcpServer({
|
||||
name: 'shannon-helper',
|
||||
version: '1.0.0',
|
||||
tools: [saveDeliverableTool, generateTotpTool],
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Legacy export for backward compatibility
|
||||
* @deprecated Use createShannonHelperServer(targetDir) instead
|
||||
*/
|
||||
export const shannonHelperServer = createSdkMcpServer({
|
||||
name: 'shannon-helper',
|
||||
version: '1.0.0',
|
||||
tools: [saveDeliverableTool, generateTotpTool],
|
||||
});
|
||||
|
||||
// Export tools for direct usage if needed
|
||||
export { saveDeliverableTool, generateTotpTool };
|
||||
|
||||
// Export types for external use
|
||||
export * from './types/index.js';
|
||||
@@ -0,0 +1,137 @@
|
||||
/**
|
||||
* generate_totp MCP Tool
|
||||
*
|
||||
* Generates 6-digit TOTP codes for authentication.
|
||||
* Replaces tools/generate-totp-standalone.mjs bash script.
|
||||
* Based on RFC 6238 (TOTP) and RFC 4226 (HOTP).
|
||||
*/
|
||||
|
||||
import { tool } from '@anthropic-ai/claude-agent-sdk';
|
||||
import { createHmac } from 'crypto';
|
||||
import { z } from 'zod';
|
||||
import { createToolResult } from '../types/tool-responses.js';
|
||||
import { base32Decode, validateTotpSecret } from '../validation/totp-validator.js';
|
||||
import { createCryptoError, createGenericError } from '../utils/error-formatter.js';
|
||||
|
||||
/**
|
||||
* Input schema for generate_totp tool
|
||||
*/
|
||||
export const GenerateTotpInputSchema = z.object({
|
||||
secret: z
|
||||
.string()
|
||||
.min(1)
|
||||
.regex(/^[A-Z2-7]+$/i, 'Must be base32-encoded')
|
||||
.describe('Base32-encoded TOTP secret'),
|
||||
});
|
||||
|
||||
/**
|
||||
* Generate HOTP code (RFC 4226)
|
||||
* Ported from generate-totp-standalone.mjs (lines 74-99)
|
||||
*
|
||||
* @param {string} secret - Base32-encoded secret
|
||||
* @param {number} counter - Counter value
|
||||
* @param {number} [digits=6] - Number of digits in OTP
|
||||
* @returns {string} OTP code
|
||||
*/
|
||||
function generateHOTP(secret, counter, digits = 6) {
|
||||
const key = base32Decode(secret);
|
||||
|
||||
// Convert counter to 8-byte buffer (big-endian)
|
||||
const counterBuffer = Buffer.alloc(8);
|
||||
counterBuffer.writeBigUInt64BE(BigInt(counter));
|
||||
|
||||
// Generate HMAC-SHA1
|
||||
const hmac = createHmac('sha1', key);
|
||||
hmac.update(counterBuffer);
|
||||
const hash = hmac.digest();
|
||||
|
||||
// Dynamic truncation
|
||||
const offset = hash[hash.length - 1] & 0x0f;
|
||||
const code =
|
||||
((hash[offset] & 0x7f) << 24) |
|
||||
((hash[offset + 1] & 0xff) << 16) |
|
||||
((hash[offset + 2] & 0xff) << 8) |
|
||||
(hash[offset + 3] & 0xff);
|
||||
|
||||
// Generate digits
|
||||
const otp = (code % Math.pow(10, digits)).toString().padStart(digits, '0');
|
||||
return otp;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate TOTP code (RFC 6238)
|
||||
* Ported from generate-totp-standalone.mjs (lines 101-106)
|
||||
*
|
||||
* @param {string} secret - Base32-encoded secret
|
||||
* @param {number} [timeStep=30] - Time step in seconds
|
||||
* @param {number} [digits=6] - Number of digits in OTP
|
||||
* @returns {string} OTP code
|
||||
*/
|
||||
function generateTOTP(secret, timeStep = 30, digits = 6) {
|
||||
const currentTime = Math.floor(Date.now() / 1000);
|
||||
const counter = Math.floor(currentTime / timeStep);
|
||||
return generateHOTP(secret, counter, digits);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get seconds until TOTP code expires
|
||||
*
|
||||
* @param {number} [timeStep=30] - Time step in seconds
|
||||
* @returns {number} Seconds until expiration
|
||||
*/
|
||||
function getSecondsUntilExpiration(timeStep = 30) {
|
||||
const currentTime = Math.floor(Date.now() / 1000);
|
||||
return timeStep - (currentTime % timeStep);
|
||||
}
|
||||
|
||||
/**
|
||||
* generate_totp tool implementation
|
||||
*
|
||||
* @param {Object} args
|
||||
* @param {string} args.secret - Base32-encoded TOTP secret
|
||||
* @returns {Promise<Object>} Tool result
|
||||
*/
|
||||
export async function generateTotp(args) {
|
||||
try {
|
||||
const { secret } = args;
|
||||
|
||||
// Validate secret (throws on error)
|
||||
validateTotpSecret(secret);
|
||||
|
||||
// Generate TOTP code
|
||||
const totpCode = generateTOTP(secret);
|
||||
const expiresIn = getSecondsUntilExpiration();
|
||||
const timestamp = new Date().toISOString();
|
||||
|
||||
// Success response
|
||||
const successResponse = {
|
||||
status: 'success',
|
||||
message: 'TOTP code generated successfully',
|
||||
totpCode,
|
||||
timestamp,
|
||||
expiresIn,
|
||||
};
|
||||
|
||||
return createToolResult(successResponse);
|
||||
} catch (error) {
|
||||
// Check if it's a validation/crypto error
|
||||
if (error instanceof Error && (error.message.includes('base32') || error.message.includes('TOTP'))) {
|
||||
const errorResponse = createCryptoError(error.message, false);
|
||||
return createToolResult(errorResponse);
|
||||
}
|
||||
|
||||
// Generic error
|
||||
const errorResponse = createGenericError(error, false);
|
||||
return createToolResult(errorResponse);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Tool definition for MCP server - created using SDK's tool() function
|
||||
*/
|
||||
export const generateTotpTool = tool(
|
||||
'generate_totp',
|
||||
'Generates 6-digit TOTP code for authentication. Secret must be base32-encoded.',
|
||||
GenerateTotpInputSchema.shape,
|
||||
generateTotp
|
||||
);
|
||||
@@ -0,0 +1,6 @@
|
||||
/**
|
||||
* MCP Tools barrel export
|
||||
*/
|
||||
|
||||
export * from './save-deliverable.js';
|
||||
export * from './generate-totp.js';
|
||||
@@ -0,0 +1,85 @@
|
||||
/**
|
||||
* save_deliverable MCP Tool
|
||||
*
|
||||
* Saves deliverable files with automatic validation.
|
||||
* Replaces tools/save_deliverable.js bash script.
|
||||
*/
|
||||
|
||||
import { tool } from '@anthropic-ai/claude-agent-sdk';
|
||||
import { z } from 'zod';
|
||||
import { DeliverableType, DELIVERABLE_FILENAMES, isQueueType } from '../types/deliverables.js';
|
||||
import { createToolResult } from '../types/tool-responses.js';
|
||||
import { validateQueueJson } from '../validation/queue-validator.js';
|
||||
import { saveDeliverableFile } from '../utils/file-operations.js';
|
||||
import { createValidationError, createGenericError } from '../utils/error-formatter.js';
|
||||
|
||||
/**
|
||||
* Input schema for save_deliverable tool
|
||||
*/
|
||||
export const SaveDeliverableInputSchema = z.object({
|
||||
deliverable_type: z.nativeEnum(DeliverableType).describe('Type of deliverable to save'),
|
||||
content: z.string().min(1).describe('File content (markdown for analysis/evidence, JSON for queues)'),
|
||||
});
|
||||
|
||||
/**
|
||||
* save_deliverable tool implementation
|
||||
*
|
||||
* @param {Object} args
|
||||
* @param {string} args.deliverable_type - Type of deliverable to save
|
||||
* @param {string} args.content - File content
|
||||
* @returns {Promise<Object>} Tool result
|
||||
*/
|
||||
export async function saveDeliverable(args) {
|
||||
try {
|
||||
const { deliverable_type, content } = args;
|
||||
|
||||
// Validate queue JSON if applicable
|
||||
if (isQueueType(deliverable_type)) {
|
||||
const queueValidation = validateQueueJson(content);
|
||||
if (!queueValidation.valid) {
|
||||
const errorResponse = createValidationError(
|
||||
queueValidation.message,
|
||||
true,
|
||||
{
|
||||
deliverableType: deliverable_type,
|
||||
expectedFormat: '{"vulnerabilities": [...]}',
|
||||
}
|
||||
);
|
||||
return createToolResult(errorResponse);
|
||||
}
|
||||
}
|
||||
|
||||
// Get filename and save file
|
||||
const filename = DELIVERABLE_FILENAMES[deliverable_type];
|
||||
const filepath = saveDeliverableFile(filename, content);
|
||||
|
||||
// Success response
|
||||
const successResponse = {
|
||||
status: 'success',
|
||||
message: `Deliverable saved successfully: ${filename}`,
|
||||
filepath,
|
||||
deliverableType: deliverable_type,
|
||||
validated: isQueueType(deliverable_type),
|
||||
};
|
||||
|
||||
return createToolResult(successResponse);
|
||||
} catch (error) {
|
||||
const errorResponse = createGenericError(
|
||||
error,
|
||||
false,
|
||||
{ deliverableType: args.deliverable_type }
|
||||
);
|
||||
|
||||
return createToolResult(errorResponse);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Tool definition for MCP server - created using SDK's tool() function
|
||||
*/
|
||||
export const saveDeliverableTool = tool(
|
||||
'save_deliverable',
|
||||
'Saves deliverable files with automatic validation. Queue files must have {"vulnerabilities": [...]} structure.',
|
||||
SaveDeliverableInputSchema.shape,
|
||||
saveDeliverable
|
||||
);
|
||||
@@ -0,0 +1,107 @@
|
||||
/**
|
||||
* Deliverable Type Definitions
|
||||
*
|
||||
* Maps deliverable types to their filenames and defines validation requirements.
|
||||
* Must match the exact mappings from tools/save_deliverable.js.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} DeliverableType
|
||||
* @property {string} CODE_ANALYSIS
|
||||
* @property {string} RECON
|
||||
* @property {string} INJECTION_ANALYSIS
|
||||
* @property {string} INJECTION_QUEUE
|
||||
* @property {string} XSS_ANALYSIS
|
||||
* @property {string} XSS_QUEUE
|
||||
* @property {string} AUTH_ANALYSIS
|
||||
* @property {string} AUTH_QUEUE
|
||||
* @property {string} AUTHZ_ANALYSIS
|
||||
* @property {string} AUTHZ_QUEUE
|
||||
* @property {string} SSRF_ANALYSIS
|
||||
* @property {string} SSRF_QUEUE
|
||||
* @property {string} INJECTION_EVIDENCE
|
||||
* @property {string} XSS_EVIDENCE
|
||||
* @property {string} AUTH_EVIDENCE
|
||||
* @property {string} AUTHZ_EVIDENCE
|
||||
* @property {string} SSRF_EVIDENCE
|
||||
*/
|
||||
|
||||
export const DeliverableType = {
|
||||
// Pre-recon agent
|
||||
CODE_ANALYSIS: 'CODE_ANALYSIS',
|
||||
|
||||
// Recon agent
|
||||
RECON: 'RECON',
|
||||
|
||||
// Vulnerability analysis agents
|
||||
INJECTION_ANALYSIS: 'INJECTION_ANALYSIS',
|
||||
INJECTION_QUEUE: 'INJECTION_QUEUE',
|
||||
|
||||
XSS_ANALYSIS: 'XSS_ANALYSIS',
|
||||
XSS_QUEUE: 'XSS_QUEUE',
|
||||
|
||||
AUTH_ANALYSIS: 'AUTH_ANALYSIS',
|
||||
AUTH_QUEUE: 'AUTH_QUEUE',
|
||||
|
||||
AUTHZ_ANALYSIS: 'AUTHZ_ANALYSIS',
|
||||
AUTHZ_QUEUE: 'AUTHZ_QUEUE',
|
||||
|
||||
SSRF_ANALYSIS: 'SSRF_ANALYSIS',
|
||||
SSRF_QUEUE: 'SSRF_QUEUE',
|
||||
|
||||
// Exploitation agents
|
||||
INJECTION_EVIDENCE: 'INJECTION_EVIDENCE',
|
||||
XSS_EVIDENCE: 'XSS_EVIDENCE',
|
||||
AUTH_EVIDENCE: 'AUTH_EVIDENCE',
|
||||
AUTHZ_EVIDENCE: 'AUTHZ_EVIDENCE',
|
||||
SSRF_EVIDENCE: 'SSRF_EVIDENCE',
|
||||
};
|
||||
|
||||
/**
|
||||
* Hard-coded filename mappings from agent prompts
|
||||
* Must match tools/save_deliverable.js exactly
|
||||
*/
|
||||
export const DELIVERABLE_FILENAMES = {
|
||||
[DeliverableType.CODE_ANALYSIS]: 'code_analysis_deliverable.md',
|
||||
[DeliverableType.RECON]: 'recon_deliverable.md',
|
||||
[DeliverableType.INJECTION_ANALYSIS]: 'injection_analysis_deliverable.md',
|
||||
[DeliverableType.INJECTION_QUEUE]: 'injection_exploitation_queue.json',
|
||||
[DeliverableType.XSS_ANALYSIS]: 'xss_analysis_deliverable.md',
|
||||
[DeliverableType.XSS_QUEUE]: 'xss_exploitation_queue.json',
|
||||
[DeliverableType.AUTH_ANALYSIS]: 'auth_analysis_deliverable.md',
|
||||
[DeliverableType.AUTH_QUEUE]: 'auth_exploitation_queue.json',
|
||||
[DeliverableType.AUTHZ_ANALYSIS]: 'authz_analysis_deliverable.md',
|
||||
[DeliverableType.AUTHZ_QUEUE]: 'authz_exploitation_queue.json',
|
||||
[DeliverableType.SSRF_ANALYSIS]: 'ssrf_analysis_deliverable.md',
|
||||
[DeliverableType.SSRF_QUEUE]: 'ssrf_exploitation_queue.json',
|
||||
[DeliverableType.INJECTION_EVIDENCE]: 'injection_exploitation_evidence.md',
|
||||
[DeliverableType.XSS_EVIDENCE]: 'xss_exploitation_evidence.md',
|
||||
[DeliverableType.AUTH_EVIDENCE]: 'auth_exploitation_evidence.md',
|
||||
[DeliverableType.AUTHZ_EVIDENCE]: 'authz_exploitation_evidence.md',
|
||||
[DeliverableType.SSRF_EVIDENCE]: 'ssrf_exploitation_evidence.md',
|
||||
};
|
||||
|
||||
/**
|
||||
* Queue types that require JSON validation
|
||||
*/
|
||||
export const QUEUE_TYPES = [
|
||||
DeliverableType.INJECTION_QUEUE,
|
||||
DeliverableType.XSS_QUEUE,
|
||||
DeliverableType.AUTH_QUEUE,
|
||||
DeliverableType.AUTHZ_QUEUE,
|
||||
DeliverableType.SSRF_QUEUE,
|
||||
];
|
||||
|
||||
/**
|
||||
* Type guard to check if a deliverable type is a queue
|
||||
* @param {string} type - Deliverable type to check
|
||||
* @returns {boolean} True if the type is a queue type
|
||||
*/
|
||||
export function isQueueType(type) {
|
||||
return QUEUE_TYPES.includes(type);
|
||||
}
|
||||
|
||||
/**
|
||||
* @typedef {Object} VulnerabilityQueue
|
||||
* @property {Array<Object>} vulnerabilities - Array of vulnerability objects
|
||||
*/
|
||||
@@ -0,0 +1,6 @@
|
||||
/**
|
||||
* Type definitions barrel export
|
||||
*/
|
||||
|
||||
export * from './deliverables.js';
|
||||
export * from './tool-responses.js';
|
||||
@@ -0,0 +1,58 @@
|
||||
/**
|
||||
* Tool Response Type Definitions
|
||||
*
|
||||
* Defines structured response formats for MCP tools to ensure
|
||||
* consistent error handling and success reporting.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} ErrorResponse
|
||||
* @property {'error'} status
|
||||
* @property {string} message
|
||||
* @property {string} errorType - ValidationError, FileSystemError, CryptoError, etc.
|
||||
* @property {boolean} retryable
|
||||
* @property {Record<string, unknown>} [context]
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} SuccessResponse
|
||||
* @property {'success'} status
|
||||
* @property {string} message
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} SaveDeliverableResponse
|
||||
* @property {'success'} status
|
||||
* @property {string} message
|
||||
* @property {string} filepath
|
||||
* @property {string} deliverableType
|
||||
* @property {boolean} validated - true if queue JSON was validated
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} GenerateTotpResponse
|
||||
* @property {'success'} status
|
||||
* @property {string} message
|
||||
* @property {string} totpCode
|
||||
* @property {string} timestamp
|
||||
* @property {number} expiresIn - seconds until expiration
|
||||
*/
|
||||
|
||||
/**
|
||||
* Helper to create tool result from response
|
||||
* MCP tools should return this format
|
||||
*
|
||||
* @param {ErrorResponse | SaveDeliverableResponse | GenerateTotpResponse} response
|
||||
* @returns {{ content: Array<{ type: string; text: string }>; isError: boolean }}
|
||||
*/
|
||||
export function createToolResult(response) {
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify(response, null, 2),
|
||||
},
|
||||
],
|
||||
isError: response.status === 'error',
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
/**
|
||||
* Error Formatting Utilities
|
||||
*
|
||||
* Helper functions for creating structured error responses.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} ErrorResponse
|
||||
* @property {'error'} status
|
||||
* @property {string} message
|
||||
* @property {string} errorType
|
||||
* @property {boolean} retryable
|
||||
* @property {Record<string, unknown>} [context]
|
||||
*/
|
||||
|
||||
/**
|
||||
* Create a validation error response
|
||||
*
|
||||
* @param {string} message
|
||||
* @param {boolean} [retryable=true]
|
||||
* @param {Record<string, unknown>} [context]
|
||||
* @returns {ErrorResponse}
|
||||
*/
|
||||
export function createValidationError(message, retryable = true, context) {
|
||||
return {
|
||||
status: 'error',
|
||||
message,
|
||||
errorType: 'ValidationError',
|
||||
retryable,
|
||||
context,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a file system error response
|
||||
*
|
||||
* @param {string} message
|
||||
* @param {boolean} [retryable=false]
|
||||
* @param {Record<string, unknown>} [context]
|
||||
* @returns {ErrorResponse}
|
||||
*/
|
||||
export function createFileSystemError(message, retryable = false, context) {
|
||||
return {
|
||||
status: 'error',
|
||||
message,
|
||||
errorType: 'FileSystemError',
|
||||
retryable,
|
||||
context,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a crypto error response
|
||||
*
|
||||
* @param {string} message
|
||||
* @param {boolean} [retryable=false]
|
||||
* @param {Record<string, unknown>} [context]
|
||||
* @returns {ErrorResponse}
|
||||
*/
|
||||
export function createCryptoError(message, retryable = false, context) {
|
||||
return {
|
||||
status: 'error',
|
||||
message,
|
||||
errorType: 'CryptoError',
|
||||
retryable,
|
||||
context,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a generic error response
|
||||
*
|
||||
* @param {unknown} error
|
||||
* @param {boolean} [retryable=false]
|
||||
* @param {Record<string, unknown>} [context]
|
||||
* @returns {ErrorResponse}
|
||||
*/
|
||||
export function createGenericError(error, retryable = false, context) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
const errorType = error instanceof Error ? error.constructor.name : 'UnknownError';
|
||||
|
||||
return {
|
||||
status: 'error',
|
||||
message,
|
||||
errorType,
|
||||
retryable,
|
||||
context,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
/**
|
||||
* File Operations Utilities
|
||||
*
|
||||
* Handles file system operations for deliverable saving.
|
||||
* Ported from tools/save_deliverable.js (lines 117-130).
|
||||
*/
|
||||
|
||||
import { writeFileSync, mkdirSync } from 'fs';
|
||||
import { join } from 'path';
|
||||
|
||||
/**
|
||||
* Save deliverable file to deliverables/ directory
|
||||
*
|
||||
* @param {string} filename - Name of the file to save
|
||||
* @param {string} content - Content to write to the file
|
||||
* @returns {string} Full path to the saved file
|
||||
*/
|
||||
export function saveDeliverableFile(filename, content) {
|
||||
// Use target directory from global context (set by createShannonHelperServer)
|
||||
const targetDir = global.__SHANNON_TARGET_DIR || process.cwd();
|
||||
const deliverablesDir = join(targetDir, 'deliverables');
|
||||
const filepath = join(deliverablesDir, filename);
|
||||
|
||||
// Ensure deliverables directory exists
|
||||
try {
|
||||
mkdirSync(deliverablesDir, { recursive: true });
|
||||
} catch (error) {
|
||||
// Directory might already exist, ignore
|
||||
}
|
||||
|
||||
// Write file (atomic write - single operation)
|
||||
writeFileSync(filepath, content, 'utf8');
|
||||
|
||||
return filepath;
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
/**
|
||||
* Utilities barrel export
|
||||
*/
|
||||
|
||||
export * from './file-operations.js';
|
||||
export * from './error-formatter.js';
|
||||
@@ -0,0 +1,6 @@
|
||||
/**
|
||||
* Validation layer barrel export
|
||||
*/
|
||||
|
||||
export * from './queue-validator.js';
|
||||
export * from './totp-validator.js';
|
||||
@@ -0,0 +1,51 @@
|
||||
/**
|
||||
* Queue Validator
|
||||
*
|
||||
* Validates JSON structure for vulnerability queue files.
|
||||
* Ported from tools/save_deliverable.js (lines 56-75).
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} ValidationResult
|
||||
* @property {boolean} valid
|
||||
* @property {string} [message]
|
||||
* @property {Object} [data]
|
||||
*/
|
||||
|
||||
/**
|
||||
* Validate JSON structure for queue files
|
||||
* Queue files must have a 'vulnerabilities' array
|
||||
*
|
||||
* @param {string} content - JSON string to validate
|
||||
* @returns {ValidationResult} ValidationResult with valid flag, optional error message, and parsed data
|
||||
*/
|
||||
export function validateQueueJson(content) {
|
||||
try {
|
||||
const parsed = JSON.parse(content);
|
||||
|
||||
// Queue files must have a 'vulnerabilities' array
|
||||
if (!parsed.vulnerabilities) {
|
||||
return {
|
||||
valid: false,
|
||||
message: `Invalid queue structure: Missing 'vulnerabilities' property. Expected: {"vulnerabilities": [...]}`,
|
||||
};
|
||||
}
|
||||
|
||||
if (!Array.isArray(parsed.vulnerabilities)) {
|
||||
return {
|
||||
valid: false,
|
||||
message: `Invalid queue structure: 'vulnerabilities' must be an array. Expected: {"vulnerabilities": [...]}`,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
valid: true,
|
||||
data: parsed,
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
valid: false,
|
||||
message: `Invalid JSON: ${error instanceof Error ? error.message : String(error)}`,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
/**
|
||||
* TOTP Validator
|
||||
*
|
||||
* Validates TOTP secrets and provides base32 decoding.
|
||||
* Ported from tools/generate-totp-standalone.mjs (lines 43-72).
|
||||
*/
|
||||
|
||||
/**
|
||||
* Base32 decode function
|
||||
* Ported from generate-totp-standalone.mjs
|
||||
*
|
||||
* @param {string} encoded - Base32 encoded string
|
||||
* @returns {Buffer} Buffer containing decoded bytes
|
||||
*/
|
||||
export function base32Decode(encoded) {
|
||||
const alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';
|
||||
const cleanInput = encoded.toUpperCase().replace(/[^A-Z2-7]/g, '');
|
||||
|
||||
if (cleanInput.length === 0) {
|
||||
return Buffer.alloc(0);
|
||||
}
|
||||
|
||||
const output = [];
|
||||
let bits = 0;
|
||||
let value = 0;
|
||||
|
||||
for (const char of cleanInput) {
|
||||
const index = alphabet.indexOf(char);
|
||||
if (index === -1) {
|
||||
throw new Error(`Invalid base32 character: ${char}`);
|
||||
}
|
||||
|
||||
value = (value << 5) | index;
|
||||
bits += 5;
|
||||
|
||||
if (bits >= 8) {
|
||||
output.push((value >>> (bits - 8)) & 255);
|
||||
bits -= 8;
|
||||
}
|
||||
}
|
||||
|
||||
return Buffer.from(output);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate TOTP secret
|
||||
* Must be base32-encoded string
|
||||
*
|
||||
* @param {string} secret - Secret to validate
|
||||
* @returns {boolean} true if valid, throws Error if invalid
|
||||
*/
|
||||
export function validateTotpSecret(secret) {
|
||||
if (!secret || secret.length === 0) {
|
||||
throw new Error('TOTP secret cannot be empty');
|
||||
}
|
||||
|
||||
// Check if it's valid base32 (only A-Z and 2-7, case-insensitive)
|
||||
const base32Regex = /^[A-Z2-7]+$/i;
|
||||
if (!base32Regex.test(secret.replace(/[^A-Z2-7]/gi, ''))) {
|
||||
throw new Error('TOTP secret must be base32-encoded (characters A-Z and 2-7)');
|
||||
}
|
||||
|
||||
// Try to decode to ensure it's valid
|
||||
try {
|
||||
base32Decode(secret);
|
||||
} catch (error) {
|
||||
throw new Error(`Invalid TOTP secret: ${error instanceof Error ? error.message : String(error)}`);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
Reference in New Issue
Block a user