From 433319ff362ee960a95e87f5598a70c5a3370c9b Mon Sep 17 00:00:00 2001 From: addidea Date: Fri, 6 Mar 2026 00:58:26 +0800 Subject: [PATCH] fix: resolve issue #12 --- e2e-tests/studio-mcp/katana.e2e.test.ts | 15 ++++++++ .../components/__tests__/katana.unit.test.ts | 35 +++++++++++++++++++ studio/components/katana.ts | 34 ++++++++++++++++++ 3 files changed, 84 insertions(+) create mode 100644 e2e-tests/studio-mcp/katana.e2e.test.ts create mode 100644 studio/components/__tests__/katana.unit.test.ts create mode 100644 studio/components/katana.ts diff --git a/e2e-tests/studio-mcp/katana.e2e.test.ts b/e2e-tests/studio-mcp/katana.e2e.test.ts new file mode 100644 index 00000000..e5b0093c --- /dev/null +++ b/e2e-tests/studio-mcp/katana.e2e.test.ts @@ -0,0 +1,15 @@ +import { Katana } from '../../studio/components/katana'; + +describe('Katana component E2E tests', () => { + const katana = new Katana(); + + it('should run a Katana scan against a test URL', async () => { + const url = 'https://httpbin.org/get'; + const output = await katana.run({ url, depth: 1 }); + expect(output).toContain('http'); // Basic validation that Katana returned some output + }, 30000); // allow up to 30s for scan + + it('should throw an error for invalid URL', async () => { + await expect(katana.run({ url: 'invalid-url' })).rejects.toThrow(); + }); +}); \ No newline at end of file diff --git a/studio/components/__tests__/katana.unit.test.ts b/studio/components/__tests__/katana.unit.test.ts new file mode 100644 index 00000000..3c984b1e --- /dev/null +++ b/studio/components/__tests__/katana.unit.test.ts @@ -0,0 +1,35 @@ +import { Katana } from '../katana'; +import { exec } from 'child_process'; +import { jest } from '@jest/globals'; + +jest.mock('child_process', () => ({ + exec: jest.fn(), +})); + +const execMock = exec as unknown as jest.Mock; + +describe('Katana component unit tests', () => { + let katana: Katana; + + beforeEach(() => { + katana = new Katana(); + execMock.mockReset(); + }); + + it('should throw error if URL is missing', async () => { + await expect(katana.run({ url: '' })).rejects.toThrow('URL is required'); + }); + + it('should run katana with default options', async () => { + execMock.mockImplementation((cmd, cb) => cb(null, 'scan result', '')); + const output = await katana.run({ url: 'https://example.com' }); + expect(output).toBe('scan result'); + expect(execMock).toHaveBeenCalledWith('katana https://example.com', expect.any(Function)); + }); + + it('should include depth and output file arguments', async () => { + execMock.mockImplementation((cmd, cb) => cb(null, 'scan result', '')); + await katana.run({ url: 'https://example.com', depth: 3, outputFile: 'out.txt' }); + expect(execMock).toHaveBeenCalledWith('katana -depth 3 -o out.txt https://example.com', expect.any(Function)); + }); +}); \ No newline at end of file diff --git a/studio/components/katana.ts b/studio/components/katana.ts new file mode 100644 index 00000000..7f2cac78 --- /dev/null +++ b/studio/components/katana.ts @@ -0,0 +1,34 @@ +import { exec } from 'child_process'; +import { promisify } from 'util'; + +const execAsync = promisify(exec); + +export interface KatanaOptions { + url: string; + depth?: number; + outputFile?: string; +} + +export class Katana { + async run(options: KatanaOptions): Promise { + if (!options.url) { + throw new Error('URL is required to run Katana scan.'); + } + + const depthArg = options.depth ? `-depth ${options.depth}` : ''; + const outputArg = options.outputFile ? `-o ${options.outputFile}` : ''; + + const command = `katana ${depthArg} ${outputArg} ${options.url}`.trim(); + + try { + const { stdout, stderr } = await execAsync(command); + if (stderr) { + console.error('Katana error:', stderr); + } + return stdout; + } catch (err) { + console.error('Failed to run Katana:', err); + throw err; + } + } +} \ No newline at end of file