Skip to content
Merged
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
47 changes: 41 additions & 6 deletions docs/development/component-development.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -615,14 +615,49 @@ Does your Docker image have a shell (/bin/sh)?
├─ YES → Use Shell Wrapper Pattern
│ entrypoint: 'sh', command: ['-c', 'tool "$@"', '--']
└─ NO (Distroless) → Does your tool have a -stream flag?
├─ YES → Use Direct Binary + Stream
│ entrypoint: 'tool', command: ['-stream', ...]
└─ NO → Rely on SDK stdin handling
Note: May have buffering issues
└─ NO (Distroless) → Use Default Entrypoint Pattern
Omit entrypoint, pass args via command: []
The image's built-in ENTRYPOINT handles execution.
```

### Distroless Images (Default Entrypoint Pattern)

Many ProjectDiscovery images (subfinder, dnsx, naabu, amass, notify) are **distroless** and do not contain `/bin/sh`. For these images, omit the `entrypoint` field entirely and let Docker use the image's default entrypoint:

```typescript
// ✅ CORRECT - Distroless image (no shell available)
runner: {
kind: 'docker',
image: 'ghcr.io/shipsecai/subfinder:latest',
// No entrypoint — uses image default (/usr/local/bin/subfinder)
command: [],
network: 'bridge',
}
```

```typescript
// ❌ WRONG - Distroless image with shell wrapper (exit code 127)
runner: {
kind: 'docker',
image: 'ghcr.io/shipsecai/subfinder:latest',
entrypoint: 'sh', // sh does not exist in distroless images!
command: ['-c', 'subfinder "$@"', '--'],
}
```

In the `execute()` function, append tool arguments directly to `command`:
```typescript
const runnerConfig: DockerRunnerConfig = {
...baseRunner,
command: [...(baseRunner.command ?? []), ...toolArgs],
};
```

<Note>
Distroless Go binaries (like ProjectDiscovery tools) handle PTY signals correctly.
Verified with `docker run --rm -t image args...` — output streams and exits cleanly.
</Note>

---

## File System Access
Expand Down
2 changes: 1 addition & 1 deletion scratch/opencode-mcp-test/run_test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ docker run --rm \
--network host \
-v "$DIR:/workspace" \
-e OPENROUTER_API_KEY="$OPENROUTER_API_KEY" \
ghcr.io/shipsecai/opencode:1.1.53 \
ghcr.io/shipsecai/opencode:latest \
run --log-level INFO "$(cat prompt.txt)"

# Kill MCP server
Expand Down
2 changes: 1 addition & 1 deletion worker/src/components/ai/__tests__/opencode.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ describe('shipsec.opencode.agent', () => {

expect(runSpy).toHaveBeenCalled();
const runnerCall = runSpy.mock.calls[0][0];
expect(runnerCall.image).toBe('ghcr.io/shipsecai/opencode:1.1.53');
expect(runnerCall.image).toBe('ghcr.io/shipsecai/opencode:latest');
expect(runnerCall.network).toBe('host');
expect(runnerCall.env.OPENAI_API_KEY).toBe('sk-test');
});
Expand Down
2 changes: 1 addition & 1 deletion worker/src/components/ai/opencode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ const definition = defineComponent({
category: 'ai',
runner: {
kind: 'docker',
image: 'ghcr.io/shipsecai/opencode:1.1.53',
image: 'ghcr.io/shipsecai/opencode:latest',
entrypoint: 'opencode', // We will override this in execution
network: 'host' as const, // Required to access localhost gateway
command: ['help'],
Expand Down
2 changes: 1 addition & 1 deletion worker/src/components/security/__tests__/amass.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ describe.skip('amass component', () => {

expect(component.runner.kind).toBe('docker');
if (component.runner.kind === 'docker') {
expect(component.runner.image).toBe('ghcr.io/shipsecai/amass:v5.0.1');
expect(component.runner.image).toBe('ghcr.io/shipsecai/amass:latest');
expect(component.runner.entrypoint).toBe('sh');
expect(component.runner.command).toBeInstanceOf(Array);
}
Expand Down
2 changes: 1 addition & 1 deletion worker/src/components/security/__tests__/dnsx.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -197,7 +197,7 @@ describe.skip('dnsx component', () => {

expect(component.runner.kind).toBe('docker');
if (component.runner.kind === 'docker') {
expect(component.runner.image).toBe('ghcr.io/shipsecai/dnsx:v1.2.2');
expect(component.runner.image).toBe('ghcr.io/shipsecai/dnsx:latest');
expect(component.runner.entrypoint).toBe('sh');
}
});
Expand Down
7 changes: 4 additions & 3 deletions worker/src/components/security/__tests__/naabu.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,9 +105,10 @@ describe('naabu component', () => {

expect(component.runner.kind).toBe('docker');
if (component.runner.kind === 'docker') {
expect(component.runner.image).toBe('ghcr.io/shipsecai/naabu:v2.3.7');
expect(component.runner.entrypoint).toBe('sh');
expect(component.runner.command).toBeInstanceOf(Array);
expect(component.runner.image).toBe('ghcr.io/shipsecai/naabu:latest');
// Distroless image — no entrypoint override, uses image default
expect(component.runner.entrypoint).toBeUndefined();
expect(component.runner.command).toEqual([]);
}
});
});
2 changes: 1 addition & 1 deletion worker/src/components/security/__tests__/subfinder.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,7 @@ describe.skip('subfinder component', () => {

expect(component.runner.kind).toBe('docker');
if (component.runner.kind === 'docker') {
expect(component.runner.image).toBe('ghcr.io/shipsecai/subfinder:v2.12.0');
expect(component.runner.image).toBe('ghcr.io/shipsecai/subfinder:latest');
}
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ describe('trufflehog component', () => {

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

Expand Down
17 changes: 5 additions & 12 deletions worker/src/components/security/amass.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import {
} from '@shipsec/component-sdk';
import { IsolatedContainerVolume } from '../../utils/isolated-volume';

const AMASS_IMAGE = 'ghcr.io/shipsecai/amass:v5.0.1';
const AMASS_IMAGE = 'ghcr.io/shipsecai/amass:latest';
const AMASS_TIMEOUT_SECONDS = (() => {
const raw = process.env.AMASS_TIMEOUT_SECONDS;
const parsed = raw ? Number.parseInt(raw, 10) : NaN;
Expand Down Expand Up @@ -470,19 +470,14 @@ const definition = (defineComponent as any)({
runner: {
kind: 'docker',
image: AMASS_IMAGE,
// IMPORTANT: Use shell wrapper for PTY compatibility
// Running CLI tools directly as entrypoint can cause them to hang with PTY (pseudo-terminal)
// The shell wrapper ensures proper TTY signal handling and clean exit
// See docs/component-development.md "Docker Entrypoint Pattern" for details
entrypoint: 'sh',
// The amass image is distroless (no shell available).
// Use the image's default entrypoint directly and pass args via command.
network: 'bridge',
timeoutSeconds: AMASS_TIMEOUT_SECONDS,
env: {
HOME: '/tmp',
},
// Shell wrapper pattern: sh -c 'amass "$@"' -- [args...]
// This allows dynamic args to be appended and properly passed to amass
command: ['-c', 'amass "$@"', '--'],
command: [],
},
inputs: inputSchema,
outputs: outputSchema,
Expand Down Expand Up @@ -643,9 +638,7 @@ const definition = (defineComponent as any)({
network: baseRunner.network,
timeoutSeconds: baseRunner.timeoutSeconds ?? AMASS_TIMEOUT_SECONDS,
env: { ...(baseRunner.env ?? {}) },
// Preserve the shell wrapper from baseRunner (sh -c 'amass "$@"' --)
entrypoint: baseRunner.entrypoint,
// Append amass arguments to shell wrapper command
// Pass amass CLI args directly (image default entrypoint is amass)
command: [...(baseRunner.command ?? []), ...amassArgs],
volumes: [volume.getVolumeConfig(CONTAINER_INPUT_DIR, true)],
};
Expand Down
23 changes: 8 additions & 15 deletions worker/src/components/security/dnsx.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ const recordTypeEnum = z.enum([

const outputModeEnum = z.enum(['silent', 'json']);

const DNSX_IMAGE = 'ghcr.io/shipsecai/dnsx:v1.2.2';
const DNSX_IMAGE = 'ghcr.io/shipsecai/dnsx:latest';
const DNSX_TIMEOUT_SECONDS = 180;
const INPUT_MOUNT_NAME = 'inputs';
const CONTAINER_INPUT_DIR = `/${INPUT_MOUNT_NAME}`;
Expand Down Expand Up @@ -483,19 +483,16 @@ const definition = defineComponent({
runner: {
kind: 'docker',
image: DNSX_IMAGE,
// IMPORTANT: Use shell wrapper for PTY compatibility
// Running CLI tools directly as entrypoint can cause them to hang with PTY (pseudo-terminal)
// The shell wrapper ensures proper TTY signal handling and clean exit
// See docs/component-development.md "Docker Entrypoint Pattern" for details
entrypoint: 'sh',
// The dnsx image is distroless (no shell available).
// Use the image's default entrypoint directly and pass args via command.
network: 'bridge',
timeoutSeconds: DNSX_TIMEOUT_SECONDS,
env: {
HOME: '/root',
// Image runs as nonroot — /root is not writable.
// Use /tmp so dnsx can create its config dir.
HOME: '/tmp',
},
// Shell wrapper pattern: sh -c 'dnsx "$@"' -- [args...]
// This allows dynamic args to be appended and properly passed to dnsx
command: ['-c', 'dnsx "$@"', '--'],
command: [],
},
inputs: inputSchema,
outputs: outputSchema,
Expand Down Expand Up @@ -655,11 +652,7 @@ const definition = defineComponent({
network: baseRunner.network,
timeoutSeconds: baseRunner.timeoutSeconds ?? DNSX_TIMEOUT_SECONDS,
env: { ...(baseRunner.env ?? {}) },
// Preserve the shell wrapper from baseRunner (sh -c 'dnsx "$@"' --)
// This is critical for PTY compatibility - do not override with 'dnsx'
entrypoint: baseRunner.entrypoint,
// Append dnsx arguments to shell wrapper command
// Resulting command: ['sh', '-c', 'dnsx "$@"', '--', ...dnsxArgs]
// Pass dnsx CLI args directly (image default entrypoint is dnsx)
command: [...(baseRunner.command ?? []), ...dnsxArgs],
volumes: [volume.getVolumeConfig(CONTAINER_INPUT_DIR, true)],
};
Expand Down
2 changes: 1 addition & 1 deletion worker/src/components/security/httpx.ts
Original file line number Diff line number Diff line change
Expand Up @@ -210,7 +210,7 @@ const definition = defineComponent({
category: 'security',
runner: {
kind: 'docker',
image: 'ghcr.io/shipsecai/httpx:v1.7.4',
image: 'ghcr.io/shipsecai/httpx:latest',
entrypoint: 'httpx',
network: 'bridge',
timeoutSeconds: dockerTimeoutSeconds,
Expand Down
Loading