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
8 changes: 8 additions & 0 deletions backend/secuscan/executor.py
Original file line number Diff line number Diff line change
Expand Up @@ -535,6 +535,14 @@ async def execute_task(self, task_id: str):
if not command:
raise ValueError("Failed to build command")

# Validate all command arguments against safe-mode + network policy
from .validation import validate_command_network_egress
cmd_valid, cmd_err = validate_command_network_egress(
command, safe_mode, plugin_id, task_id
)
if not cmd_valid:
raise ValueError(f"Command network egress validation failed: {cmd_err}")

# Apply Docker Sandboxing if enabled
if settings.docker_enabled:
# Validate the named Docker network exists before using it.
Expand Down
6 changes: 5 additions & 1 deletion backend/secuscan/validation.py
Original file line number Diff line number Diff line change
Expand Up @@ -583,8 +583,12 @@ def validate_command_network_egress(command: list[str], safe_mode: bool, plugin_
is_host = False
if not is_ip:
# Basic hostname check (with dots and valid characters, or 'localhost')
# Note: we require lowercase-only hostnames here so that dotted plugin
# parameters (e.g. "windows.pslist.PsList") are NOT misidentified as
# network destinations. Hostnames are case-insensitive per RFC 1123
# and are conventionally lowercase in practice.
if candidate.lower() == "localhost" or re.match(
r'^[a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?)+$',
r'^[a-z0-9]([a-z0-9\-]{0,61}[a-z0-9])?(\.[a-z0-9]([a-z0-9\-]{0,61}[a-z0-9])?)+$',
candidate
):
is_host = True
Expand Down
1 change: 1 addition & 0 deletions backend/secuscan/workflows.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ def _should_run(self, now: datetime, last_run_at: str | None, schedule_seconds:
return elapsed >= schedule_seconds
async def _run_workflow(self, workflow_id: str, steps: List[Dict[str, Any]]):
logger.info("Running workflow %s with %d step(s)", workflow_id, len(steps))
db = await get_db()
for step in steps:
plugin_id = step.get("plugin_id")
inputs = step.get("inputs") or {}
Expand Down
5 changes: 5 additions & 0 deletions frontend/testing/unit/pages/ToolConfigDynamic.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,11 @@ describe('ToolConfig dynamic schema flow', () => {
}),
true,
'quick',
expect.objectContaining({
scan_profile: 'standard',
validation_mode: 'proof',
evidence_level: 'standard',
}),
)
})
})
Expand Down
2 changes: 1 addition & 1 deletion frontend/testing/unit/pages/ToolConfigTimeout.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,6 @@ describe('ToolConfig timeout control', () => {
// min from field.validation
expect(input).toHaveAttribute('min', '30')
// max is min(field.validation.max, server default_timeout)
expect(input).toHaveAttribute('max', '600')
expect(input).toHaveAttribute('max', '7200')
})
})
10 changes: 9 additions & 1 deletion frontend/testing/unit/pages/Workflows.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,15 @@ describe('Workflows — create action', () => {
name: 'Nightly Scan',
schedule_seconds: 7200,
enabled: true,
steps: [{ plugin_id: '', inputs: {} }],
steps: [{
plugin_id: '',
inputs: {},
execution_context: {
scan_profile: 'standard',
validation_mode: 'proof',
evidence_level: 'standard',
},
}],
})
})
})
Expand Down
Loading