Skip to content

Commit ccb33e3

Browse files
committed
feat: allow security scan resource overrides
Signed-off-by: zebbern <185730623+zebbern@users.noreply.github.com>
1 parent 5e71828 commit ccb33e3

28 files changed

Lines changed: 614 additions & 136 deletions

docs/components/security.mdx

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,15 @@ Security components wrap popular open-source tools for subdomain discovery, DNS
77

88
---
99

10+
## Advanced: container resources
11+
12+
Docker-based security components run with tiered default limits (light / standard / heavy). On any Docker security step, enable **Override container resources** in the config panel to set custom `Container memory limit` (e.g. `4g`, `512m`) and/or `Container CPU limit` (e.g. `2`, `0.5`) for that node only. Leave the toggle off to keep defaults.
13+
14+
- Host Docker must have enough RAM/CPU for overrides; OOM kills typically show exit code **137**.
15+
- Worker caps: `SENTRIS_DOCKER_MAX_MEMORY` (default `8g`) and `SENTRIS_DOCKER_MAX_CPUS` (default `4`) in [`worker/.env`](../../worker/.env.example).
16+
17+
---
18+
1019
## Subdomain Discovery
1120

1221
### Subfinder

worker/.env.example

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,10 @@ OPENSEARCH_DASHBOARDS_URL=http://localhost/analytics
3939
# For multi-instance setups, use 9100 + instanceNum * 100
4040
# WORKER_HEALTH_PORT=9100
4141

42+
# Docker security component resource caps (optional)
43+
# SENTRIS_DOCKER_MAX_MEMORY=8g
44+
# SENTRIS_DOCKER_MAX_CPUS=4
45+
4246
# Kafka / Redpanda configuration for log and event ingestion
4347
LOG_KAFKA_BROKERS=localhost:9092
4448

worker/src/components/security/__tests__/security-component-catalog.spec.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,14 @@ describe('security component catalog', () => {
4545
expect(runner.memoryLimit).toBe(profile.memoryLimit);
4646
expect(runner.cpuLimit).toBe(profile.cpuLimit);
4747
expect(runner.pidsLimit).toBe(profile.pidsLimit);
48+
49+
const metadata = componentRegistry
50+
.listMetadata()
51+
.find((entry) => entry.definition.id === componentId);
52+
const parameterIds = metadata?.parameters.map((parameter) => parameter.id) ?? [];
53+
expect(parameterIds).toContain('overrideContainerResources');
54+
expect(parameterIds).toContain('containerMemoryLimit');
55+
expect(parameterIds).toContain('containerCpuLimit');
4856
}
4957
});
5058
});

worker/src/components/security/__tests__/security-docker-resources.test.ts

Lines changed: 90 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,20 @@
1-
import { describe, expect, it } from 'bun:test';
2-
import type { DockerRunnerConfig } from '@sentris/component-sdk';
1+
import { afterEach, describe, expect, it } from 'bun:test';
2+
import { ValidationError, type DockerRunnerConfig } from '@sentris/component-sdk';
33
import {
4+
getSecurityDockerMaxCpus,
5+
getSecurityDockerMaxMemory,
46
mergeSecurityDockerRunner,
7+
resolveSecurityDockerResourceOverrides,
58
SECURITY_DOCKER_RESOURCE_HEAVY,
69
SECURITY_DOCKER_RESOURCE_LIGHT,
710
} from '../security-docker-resources';
811

912
describe('security-docker-resources', () => {
13+
afterEach(() => {
14+
delete process.env.SENTRIS_DOCKER_MAX_MEMORY;
15+
delete process.env.SENTRIS_DOCKER_MAX_CPUS;
16+
});
17+
1018
it('mergeSecurityDockerRunner preserves resource limits from the base runner', () => {
1119
const baseRunner: DockerRunnerConfig = {
1220
kind: 'docker',
@@ -27,6 +35,86 @@ describe('security-docker-resources', () => {
2735
expect(merged.command).toEqual(['scan']);
2836
});
2937

38+
it('applies user memory override over tier defaults', () => {
39+
const baseRunner: DockerRunnerConfig = {
40+
kind: 'docker',
41+
...SECURITY_DOCKER_RESOURCE_LIGHT,
42+
image: 'example:latest',
43+
network: 'bridge',
44+
command: [],
45+
};
46+
47+
const merged = mergeSecurityDockerRunner(
48+
baseRunner,
49+
{ command: ['scan'] },
50+
{
51+
overrideContainerResources: true,
52+
containerMemoryLimit: '4g',
53+
},
54+
);
55+
56+
expect(merged.memoryLimit).toBe('4g');
57+
expect(merged.cpuLimit).toBe('1');
58+
});
59+
60+
it('ignores overrides when toggle is disabled', () => {
61+
expect(
62+
resolveSecurityDockerResourceOverrides({
63+
overrideContainerResources: false,
64+
containerMemoryLimit: '4g',
65+
}),
66+
).toEqual({});
67+
});
68+
69+
it('requires at least one limit when override is enabled', () => {
70+
expect(() =>
71+
resolveSecurityDockerResourceOverrides({
72+
overrideContainerResources: true,
73+
}),
74+
).toThrow(ValidationError);
75+
});
76+
77+
it('rejects invalid memory format', () => {
78+
expect(() =>
79+
resolveSecurityDockerResourceOverrides({
80+
overrideContainerResources: true,
81+
containerMemoryLimit: 'lots-of-ram',
82+
}),
83+
).toThrow(ValidationError);
84+
});
85+
86+
it('rejects memory above platform cap', () => {
87+
process.env.SENTRIS_DOCKER_MAX_MEMORY = '2g';
88+
89+
expect(() =>
90+
resolveSecurityDockerResourceOverrides({
91+
overrideContainerResources: true,
92+
containerMemoryLimit: '4g',
93+
}),
94+
).toThrow(ValidationError);
95+
});
96+
97+
it('rejects cpu above platform cap', () => {
98+
process.env.SENTRIS_DOCKER_MAX_CPUS = '2';
99+
100+
expect(() =>
101+
resolveSecurityDockerResourceOverrides({
102+
overrideContainerResources: true,
103+
containerCpuLimit: '4',
104+
}),
105+
).toThrow(ValidationError);
106+
});
107+
108+
it('reads platform caps from env with defaults', () => {
109+
expect(getSecurityDockerMaxMemory()).toBe('8g');
110+
expect(getSecurityDockerMaxCpus()).toBe('4');
111+
112+
process.env.SENTRIS_DOCKER_MAX_MEMORY = '6g';
113+
process.env.SENTRIS_DOCKER_MAX_CPUS = '3';
114+
expect(getSecurityDockerMaxMemory()).toBe('6g');
115+
expect(getSecurityDockerMaxCpus()).toBe('3');
116+
});
117+
30118
it('defines distinct light and heavy profiles', () => {
31119
expect(SECURITY_DOCKER_RESOURCE_LIGHT.memoryLimit).not.toBe(
32120
SECURITY_DOCKER_RESOURCE_HEAVY.memoryLimit,

worker/src/components/security/__tests__/subfinder.test.ts

Lines changed: 49 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { it, expect, beforeAll, afterEach, vi } from 'bun:test';
1+
import { it, expect, beforeAll, afterEach, vi, describe } from 'bun:test';
22
import * as sdk from '@sentris/component-sdk';
33
import { componentRegistry } from '../../index';
44
import type { SubfinderInput, SubfinderOutput } from '../subfinder';
@@ -171,3 +171,51 @@ dockerDescribe('subfinder component', () => {
171171
}
172172
});
173173
});
174+
175+
describe('subfinder container resources', () => {
176+
beforeAll(async () => {
177+
await import('../../index');
178+
});
179+
180+
afterEach(() => {
181+
vi.restoreAllMocks();
182+
});
183+
184+
it('passes custom container memory limits to the docker runner', async () => {
185+
const component = componentRegistry.get<SubfinderInput, SubfinderOutput>(
186+
'sentris.subfinder.run',
187+
);
188+
if (!component) throw new Error('Component not registered');
189+
190+
const context = sdk.createExecutionContext({
191+
runId: 'test-run',
192+
componentRef: 'subfinder-resource-test',
193+
});
194+
195+
const runnerSpy = vi.spyOn(sdk, 'runComponentWithRunner').mockResolvedValue({
196+
subdomains: [],
197+
rawOutput: '',
198+
domainCount: 1,
199+
subdomainCount: 0,
200+
});
201+
202+
await component.execute(
203+
{
204+
inputs: { domains: ['example.com'] },
205+
params: {
206+
overrideContainerResources: true,
207+
containerMemoryLimit: '4g',
208+
containerCpuLimit: '2',
209+
},
210+
},
211+
context,
212+
);
213+
214+
const runnerConfig = runnerSpy.mock.calls[0]?.[0];
215+
expect(runnerConfig).toBeDefined();
216+
if (runnerConfig && runnerConfig.kind === 'docker') {
217+
expect(runnerConfig.memoryLimit).toBe('4g');
218+
expect(runnerConfig.cpuLimit).toBe('2');
219+
}
220+
});
221+
});

worker/src/components/security/amass.ts

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ import {
1010
parameters,
1111
port,
1212
param,
13-
type DockerRunnerConfig,
1413
generateFindingHash,
1514
analyticsResultSchema,
1615
type AnalyticsResult,
@@ -20,6 +19,7 @@ import {
2019
import { IsolatedContainerVolume } from '../../utils/isolated-volume';
2120
import {
2221
mergeSecurityDockerRunner,
22+
securityDockerResourceParameterShape,
2323
SECURITY_DOCKER_RESOURCE_HEAVY,
2424
} from './security-docker-resources';
2525

@@ -61,6 +61,7 @@ const inputSchema = inputs({
6161
});
6262

6363
const parameterSchema = parameters({
64+
...securityDockerResourceParameterShape(),
6465
passive: param(
6566
z.boolean().default(true).describe('Use passive mode only (no DNS queries, faster)'),
6667
{
@@ -637,10 +638,14 @@ const definition = (defineComponent as any)({
637638
customFlags: customFlagArgs,
638639
});
639640

640-
const runnerConfig = mergeSecurityDockerRunner(baseRunner, {
641-
command: [...(baseRunner.command ?? []), ...amassArgs],
642-
volumes: [volume.getVolumeConfig(CONTAINER_INPUT_DIR, true)],
643-
});
641+
const runnerConfig = mergeSecurityDockerRunner(
642+
baseRunner,
643+
{
644+
command: [...(baseRunner.command ?? []), ...amassArgs],
645+
volumes: [volume.getVolumeConfig(CONTAINER_INPUT_DIR, true)],
646+
},
647+
parsedParams,
648+
);
644649

645650
try {
646651
const result = await runComponentWithRunner(

worker/src/components/security/checkov.ts

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ import { z } from 'zod';
22
import {
33
componentRegistry,
44
runComponentWithRunner,
5-
type DockerRunnerConfig,
65
ContainerError,
76
ComponentRetryPolicy,
87
defineComponent,
@@ -18,6 +17,7 @@ import {
1817
import { IsolatedContainerVolume } from '../../utils/isolated-volume';
1918
import {
2019
mergeSecurityDockerRunner,
20+
securityDockerResourceParameterShape,
2121
SECURITY_DOCKER_RESOURCE_STANDARD,
2222
} from './security-docker-resources';
2323
import { materializeFileBundle } from './bundle-files';
@@ -44,6 +44,7 @@ const inputSchema = inputs({
4444
});
4545

4646
const parameterSchema = parameters({
47+
...securityDockerResourceParameterShape(),
4748
framework: param(
4849
z
4950
.enum([
@@ -359,11 +360,15 @@ const definition = defineComponent({
359360
if (flag.length > 0) args.push(flag);
360361
}
361362

362-
const runnerConfig = mergeSecurityDockerRunner(baseRunner, {
363-
entrypoint: baseRunner.entrypoint,
364-
command: [...(baseRunner.command ?? []), ...args],
365-
volumes: [volume.getVolumeConfig(INPUT_DIR, true)],
366-
});
363+
const runnerConfig = mergeSecurityDockerRunner(
364+
baseRunner,
365+
{
366+
entrypoint: baseRunner.entrypoint,
367+
command: [...(baseRunner.command ?? []), ...args],
368+
volumes: [volume.getVolumeConfig(INPUT_DIR, true)],
369+
},
370+
parsedParams,
371+
);
367372

368373
try {
369374
const result = await runComponentWithRunner(

worker/src/components/security/dnsx.ts

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ import { z } from 'zod';
22
import {
33
componentRegistry,
44
runComponentWithRunner,
5-
type DockerRunnerConfig,
65
ContainerError,
76
ComponentRetryPolicy,
87
defineComponent,
@@ -18,6 +17,7 @@ import {
1817
import { IsolatedContainerVolume } from '../../utils/isolated-volume';
1918
import {
2019
mergeSecurityDockerRunner,
20+
securityDockerResourceParameterShape,
2121
SECURITY_DOCKER_RESOURCE_LIGHT,
2222
} from './security-docker-resources';
2323

@@ -66,6 +66,7 @@ const inputSchema = inputs({
6666
});
6767

6868
const parameterSchema = parameters({
69+
...securityDockerResourceParameterShape(),
6970
recordTypes: param(z.array(recordTypeEnum).default(['A']), {
7071
label: 'Record Types',
7172
editor: 'multi-select',
@@ -650,10 +651,14 @@ const definition = defineComponent({
650651
const volumeName = await volume.initialize(inputFiles);
651652
context.logger.info(`[DNSX] Created isolated volume: ${volumeName}`);
652653

653-
const runnerConfig = mergeSecurityDockerRunner(baseRunner, {
654-
command: [...(baseRunner.command ?? []), ...dnsxArgs],
655-
volumes: [volume.getVolumeConfig(CONTAINER_INPUT_DIR, true)],
656-
});
654+
const runnerConfig = mergeSecurityDockerRunner(
655+
baseRunner,
656+
{
657+
command: [...(baseRunner.command ?? []), ...dnsxArgs],
658+
volumes: [volume.getVolumeConfig(CONTAINER_INPUT_DIR, true)],
659+
},
660+
parsedParams,
661+
);
657662

658663
rawPayload = await runComponentWithRunner(
659664
runnerConfig,

worker/src/components/security/ffuf.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ import { z } from 'zod';
22
import {
33
componentRegistry,
44
runComponentWithRunner,
5-
type DockerRunnerConfig,
65
ContainerError,
76
ComponentRetryPolicy,
87
defineComponent,
@@ -18,6 +17,7 @@ import {
1817
import { IsolatedContainerVolume } from '../../utils/isolated-volume';
1918
import {
2019
mergeSecurityDockerRunner,
20+
securityDockerResourceParameterShape,
2121
SECURITY_DOCKER_RESOURCE_STANDARD,
2222
} from './security-docker-resources';
2323

@@ -51,6 +51,7 @@ const inputSchema = inputs({
5151
});
5252

5353
const parameterSchema = parameters({
54+
...securityDockerResourceParameterShape(),
5455
extensions: param(
5556
z.string().trim().optional().describe('Comma-separated file extensions (e.g., ".php,.html")'),
5657
{

0 commit comments

Comments
 (0)