mirror of
https://github.com/KeygraphHQ/shannon.git
synced 2026-10-12 02:47:08 +02:00
refactor: migrate to Turborepo + pnpm + Biome monorepo
Restructure into apps/worker, apps/cli, packages/mcp-server with Turborepo task orchestration, pnpm workspaces, Biome linting/formatting, and tsdown CLI bundling. Key changes: - src/ -> apps/worker/src/, cli/ -> apps/cli/, mcp-server/ -> packages/mcp-server/ - prompts/ and configs/ moved into apps/worker/ - npm replaced with pnpm, package-lock.json replaced with pnpm-lock.yaml - Dockerfile updated for pnpm-based builds - CLI logs command rewritten with chokidar for cross-platform reliability - Router health checking added for auto-detected router mode - Centralized path resolution via apps/worker/src/paths.ts
This commit is contained in:
1 parent
9b1abd9ec0
commit
181f24cfcc
141 files changed
+3717
-3997
No files matched your search
@@ -0,0 +1,128 @@
|
||||
// Copyright (C) 2025 Keygraph, Inc.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License version 3
|
||||
// as published by the Free Software Foundation.
|
||||
|
||||
/**
|
||||
* 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 { createHmac } from 'node:crypto';
|
||||
import { tool } from '@anthropic-ai/claude-agent-sdk';
|
||||
import { z } from 'zod';
|
||||
import { createToolResult, type GenerateTotpResponse, type ToolResult } from '../types/tool-responses.js';
|
||||
import { createCryptoError, createGenericError } from '../utils/error-formatter.js';
|
||||
import { base32Decode, validateTotpSecret } from '../validation/totp-validator.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'),
|
||||
});
|
||||
|
||||
export type GenerateTotpInput = z.infer<typeof GenerateTotpInputSchema>;
|
||||
|
||||
/**
|
||||
* Generate HOTP code (RFC 4226)
|
||||
* Ported from generate-totp-standalone.mjs (lines 74-99)
|
||||
*/
|
||||
function generateHOTP(secret: string, counter: number, digits: number = 6): string {
|
||||
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 % 10 ** digits).toString().padStart(digits, '0');
|
||||
return otp;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate TOTP code (RFC 6238)
|
||||
* Ported from generate-totp-standalone.mjs (lines 101-106)
|
||||
*/
|
||||
function generateTOTP(secret: string, timeStep: number = 30, digits: number = 6): string {
|
||||
const currentTime = Math.floor(Date.now() / 1000);
|
||||
const counter = Math.floor(currentTime / timeStep);
|
||||
return generateHOTP(secret, counter, digits);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get seconds until TOTP code expires
|
||||
*/
|
||||
function getSecondsUntilExpiration(timeStep: number = 30): number {
|
||||
const currentTime = Math.floor(Date.now() / 1000);
|
||||
return timeStep - (currentTime % timeStep);
|
||||
}
|
||||
|
||||
/**
|
||||
* generate_totp tool implementation
|
||||
*/
|
||||
export async function generateTotp(args: GenerateTotpInput): Promise<ToolResult> {
|
||||
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: GenerateTotpResponse = {
|
||||
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,159 @@
|
||||
// Copyright (C) 2025 Keygraph, Inc.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License version 3
|
||||
// as published by the Free Software Foundation.
|
||||
|
||||
/**
|
||||
* save_deliverable MCP Tool
|
||||
*
|
||||
* Saves deliverable files with automatic validation.
|
||||
* Replaces tools/save_deliverable.js bash script.
|
||||
*
|
||||
* Uses factory pattern to capture targetDir in closure, avoiding race conditions
|
||||
* when multiple workflows run in parallel.
|
||||
*/
|
||||
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { tool } from '@anthropic-ai/claude-agent-sdk';
|
||||
import { z } from 'zod';
|
||||
import { DELIVERABLE_FILENAMES, DeliverableType, isQueueType } from '../types/deliverables.js';
|
||||
import { createToolResult, type SaveDeliverableResponse, type ToolResult } from '../types/tool-responses.js';
|
||||
import { createGenericError, createValidationError } from '../utils/error-formatter.js';
|
||||
import { saveDeliverableFile } from '../utils/file-operations.js';
|
||||
import { validateQueueJson } from '../validation/queue-validator.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)
|
||||
.optional()
|
||||
.describe('File content (markdown for analysis/evidence, JSON for queues). Optional if file_path is provided.'),
|
||||
file_path: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe(
|
||||
'Path to a file whose contents should be used as the deliverable content. Relative paths are resolved against the deliverables directory. Use this instead of content for large reports to avoid output token limits.',
|
||||
),
|
||||
});
|
||||
|
||||
export type SaveDeliverableInput = z.infer<typeof SaveDeliverableInputSchema>;
|
||||
|
||||
/**
|
||||
* Check if a path is contained within a base directory.
|
||||
* Prevents path traversal attacks (e.g., ../../../etc/passwd).
|
||||
*/
|
||||
function isPathContained(basePath: string, targetPath: string): boolean {
|
||||
const resolvedBase = path.resolve(basePath);
|
||||
const resolvedTarget = path.resolve(targetPath);
|
||||
return resolvedTarget === resolvedBase || resolvedTarget.startsWith(resolvedBase + path.sep);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve deliverable content from either inline content or a file path.
|
||||
* Returns the content string on success, or a ToolResult error on failure.
|
||||
*/
|
||||
function resolveContent(args: SaveDeliverableInput, targetDir: string): string | ToolResult {
|
||||
if (args.content) {
|
||||
return args.content;
|
||||
}
|
||||
|
||||
if (!args.file_path) {
|
||||
return createToolResult(
|
||||
createValidationError('Either "content" or "file_path" must be provided', true, {
|
||||
deliverableType: args.deliverable_type,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
const resolvedPath = path.isAbsolute(args.file_path) ? args.file_path : path.resolve(targetDir, args.file_path);
|
||||
|
||||
// Security: Prevent path traversal outside targetDir
|
||||
if (!isPathContained(targetDir, resolvedPath)) {
|
||||
return createToolResult(
|
||||
createValidationError(`Path "${args.file_path}" resolves outside allowed directory`, false, {
|
||||
deliverableType: args.deliverable_type,
|
||||
allowedBase: targetDir,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
return fs.readFileSync(resolvedPath, 'utf-8');
|
||||
} catch (readError) {
|
||||
return createToolResult(
|
||||
createValidationError(
|
||||
`Failed to read file at ${resolvedPath}: ${readError instanceof Error ? readError.message : String(readError)}`,
|
||||
true,
|
||||
{ deliverableType: args.deliverable_type, filePath: resolvedPath },
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create save_deliverable handler with targetDir captured in closure.
|
||||
*
|
||||
* This factory pattern ensures each MCP server instance has its own targetDir,
|
||||
* preventing race conditions when multiple workflows run in parallel.
|
||||
*/
|
||||
function createSaveDeliverableHandler(targetDir: string) {
|
||||
return async function saveDeliverable(args: SaveDeliverableInput): Promise<ToolResult> {
|
||||
try {
|
||||
const { deliverable_type } = args;
|
||||
|
||||
const contentOrError = resolveContent(args, targetDir);
|
||||
if (typeof contentOrError !== 'string') {
|
||||
return contentOrError;
|
||||
}
|
||||
const content = contentOrError;
|
||||
|
||||
if (isQueueType(deliverable_type)) {
|
||||
const queueValidation = validateQueueJson(content);
|
||||
if (!queueValidation.valid) {
|
||||
return createToolResult(
|
||||
createValidationError(queueValidation.message ?? 'Invalid queue JSON', true, {
|
||||
deliverableType: deliverable_type,
|
||||
expectedFormat: '{"vulnerabilities": [...]}',
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const filename = DELIVERABLE_FILENAMES[deliverable_type];
|
||||
const filepath = saveDeliverableFile(targetDir, filename, content);
|
||||
|
||||
const successResponse: SaveDeliverableResponse = {
|
||||
status: 'success',
|
||||
message: `Deliverable saved successfully: ${filename}`,
|
||||
filepath,
|
||||
deliverableType: deliverable_type,
|
||||
validated: isQueueType(deliverable_type),
|
||||
};
|
||||
|
||||
return createToolResult(successResponse);
|
||||
} catch (error) {
|
||||
return createToolResult(createGenericError(error, false, { deliverableType: args.deliverable_type }));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Factory function to create save_deliverable tool with targetDir in closure
|
||||
*
|
||||
* Each MCP server instance should call this with its own targetDir to ensure
|
||||
* deliverables are saved to the correct workflow's directory.
|
||||
*/
|
||||
export function createSaveDeliverableTool(targetDir: string) {
|
||||
return tool(
|
||||
'save_deliverable',
|
||||
'Saves deliverable files with automatic validation. Queue files must have {"vulnerabilities": [...]} structure. For large reports, write the file to disk first then pass file_path instead of inline content to avoid output token limits.',
|
||||
SaveDeliverableInputSchema.shape,
|
||||
createSaveDeliverableHandler(targetDir),
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user