Skip to content
Open
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
6 changes: 0 additions & 6 deletions .husky/prepare-commit-msg

This file was deleted.

1 change: 1 addition & 0 deletions worker/src/components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ import './security/trufflehog';
import './security/terminal-demo';
import './security/virustotal';
import './security/abuseipdb';
import './security/katana';
import './security/aws-mcp-group';

// GitHub components
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
/**
* Integration test for Katana component with real Docker execution
* Requires Docker daemon to be running
*/
import { describe, test, expect, beforeEach } from 'bun:test';
import { componentRegistry, createExecutionContext } from '@shipsec/component-sdk';
import type { ExecutionContext } from '@shipsec/component-sdk';
import type { KatanaInput, KatanaOutput } from '../katana';
import '../katana'; // Register the component

const enableDockerIntegration = process.env.ENABLE_DOCKER_TESTS === 'true';
const dockerAvailable = (() => {
try {
const result = Bun.spawnSync(['docker', 'version']);
return result.exitCode === 0;
} catch {
return false;
}
})();

const shouldRunDockerTests = enableDockerIntegration && dockerAvailable;
if (!shouldRunDockerTests) {
console.warn(
'Skipping katana integration tests. Ensure ENABLE_DOCKER_TESTS=true and Docker is available to enable.',
);
}

const dockerDescribe = shouldRunDockerTests ? describe : describe.skip;

dockerDescribe('Katana Integration (Docker)', () => {
let context: ExecutionContext;
const logs: string[] = [];

beforeEach(() => {
logs.length = 0;
context = createExecutionContext({
runId: 'test-run',
componentRef: 'shipsec.katana.crawl',
logCollector: (entry) => {
logs.push(`${entry.stream.toUpperCase()}: ${entry.message}`);
},
});
});

test('should crawl urls for a known domain using real katana', async () => {
const component = componentRegistry.get<KatanaInput, KatanaOutput>('shipsec.katana.crawl')!;
expect(component).toBeDefined();

const result = await component.execute(
{
inputs: { urls: ['https://example.com'] },
params: { depth: 1 },
},
context,
);

console.log('Katana result:', result);

// Verify output structure
expect(result).toHaveProperty('crawledUrls');
expect(result).toHaveProperty('rawOutput');
expect(result).toHaveProperty('targetCount');
expect(result).toHaveProperty('urlCount');
expect(Array.isArray(result.crawledUrls)).toBe(true);
expect(typeof result.rawOutput).toBe('string');
expect(typeof result.targetCount).toBe('number');
expect(typeof result.urlCount).toBe('number');

expect(result.targetCount).toBe(1);

// Check logs
expect(logs.some((log) => log.includes('katana'))).toBe(true);
}, 120000);
});
120 changes: 120 additions & 0 deletions worker/src/components/security/__tests__/katana.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
import { describe, it, expect, beforeAll, afterEach, vi, mock } from 'bun:test';
import * as sdk from '@shipsec/component-sdk';
import type { KatanaInput, KatanaOutput } from '../katana';

mock.module('../../../utils/isolated-volume', () => ({
IsolatedContainerVolume: class {
async initialize() {
return 'mock-volume';
}
getVolumeConfig(containerPath = '/inputs', readOnly = true) {
return { source: 'mock-volume', target: containerPath, readOnly };
}
async cleanup() {}
},
}));

let componentRegistry: typeof import('@shipsec/component-sdk').componentRegistry;

describe('katana component', () => {
beforeAll(async () => {
({ componentRegistry } = await import('../../index'));
});

afterEach(() => {
vi.restoreAllMocks();
});

it('should be registered', () => {
const component = componentRegistry.get<KatanaInput, KatanaOutput>('shipsec.katana.crawl');
expect(component).toBeDefined();
expect(component!.label).toBe('Katana Web Crawler');
expect(component!.category).toBe('security');
});

it('should normalise raw output returned as plain text', async () => {
const component = componentRegistry.get<KatanaInput, KatanaOutput>('shipsec.katana.crawl');
if (!component) throw new Error('Component not registered');

const context = sdk.createExecutionContext({
runId: 'test-run',
componentRef: 'katana-test',
});

const executePayload = {
inputs: {
urls: ['https://example.com'],
},
params: {},
};

vi.spyOn(sdk, 'runComponentWithRunner').mockResolvedValue(
'https://example.com/login\nhttps://example.com/dashboard',
);

const result = component.outputs.parse(await component.execute(executePayload, context));

expect(result.crawledUrls).toEqual([
'https://example.com/login',
'https://example.com/dashboard',
]);
expect(result.rawOutput).toBe('https://example.com/login\nhttps://example.com/dashboard');
expect(result.targetCount).toBe(1);
expect(result.urlCount).toBe(2);
});

it('should accept legacy "urls" string array and normalise', () => {
const component = componentRegistry.get<KatanaInput, KatanaOutput>('shipsec.katana.crawl');
if (!component) throw new Error('Component not registered');

const inputs = component.inputs.parse({ urls: 'https://example.com' });
expect(inputs.urls).toEqual(['https://example.com']);
});

it('should pass JS crawl and Headless flags when configured', async () => {
const component = componentRegistry.get<KatanaInput, KatanaOutput>('shipsec.katana.crawl');
if (!component) throw new Error('Component not registered');

const context = sdk.createExecutionContext({
runId: 'test-run',
componentRef: 'katana-test',
});

const executePayload = {
inputs: {
urls: ['https://example.com'],
},
params: {
jsCrawl: true,
headless: true,
depth: 2,
},
};

const runnerSpy = vi.spyOn(sdk, 'runComponentWithRunner').mockResolvedValue('');

await component.execute(executePayload, context);

expect(runnerSpy).toHaveBeenCalled();
const [runnerConfig] = runnerSpy.mock.calls[0];
expect(runnerConfig).toBeDefined();
if (runnerConfig && runnerConfig.kind === 'docker') {
const command = runnerConfig.command ?? [];
expect(command).toContain('-jc');
expect(command).toContain('-hl');
expect(command).toContain('-d');
const dIndex = command.indexOf('-d');
expect(command[dIndex + 1]).toBe('2');
}
});

it('should use docker runner config', () => {
const component = componentRegistry.get<KatanaInput, KatanaOutput>('shipsec.katana.crawl');
if (!component) throw new Error('Component not registered');

expect(component.runner.kind).toBe('docker');
if (component.runner.kind === 'docker') {
expect(component.runner.image).toBe('ghcr.io/shipsecai/katana:latest');
}
});
});
Loading