Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions e2e-tests/studio-mcp/katana.e2e.test.ts
Original file line number Diff line number Diff line change
@@ -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();
});
});
35 changes: 35 additions & 0 deletions studio/components/__tests__/katana.unit.test.ts
Original file line number Diff line number Diff line change
@@ -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));
});
});
34 changes: 34 additions & 0 deletions studio/components/katana.ts
Original file line number Diff line number Diff line change
@@ -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<string> {
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;
}
}
}