diff --git a/backend/secuscan/executor.py b/backend/secuscan/executor.py index 4317b476..1d9f2272 100644 --- a/backend/secuscan/executor.py +++ b/backend/secuscan/executor.py @@ -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. diff --git a/backend/secuscan/validation.py b/backend/secuscan/validation.py index 73a7e39b..6ca4920c 100644 --- a/backend/secuscan/validation.py +++ b/backend/secuscan/validation.py @@ -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 diff --git a/backend/secuscan/workflows.py b/backend/secuscan/workflows.py index eb98c598..c7ba88dc 100644 --- a/backend/secuscan/workflows.py +++ b/backend/secuscan/workflows.py @@ -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 {} diff --git a/frontend/testing/unit/pages/ToolConfigDynamic.test.tsx b/frontend/testing/unit/pages/ToolConfigDynamic.test.tsx index 6cd5a1da..03bfb13c 100644 --- a/frontend/testing/unit/pages/ToolConfigDynamic.test.tsx +++ b/frontend/testing/unit/pages/ToolConfigDynamic.test.tsx @@ -108,6 +108,11 @@ describe('ToolConfig dynamic schema flow', () => { }), true, 'quick', + expect.objectContaining({ + scan_profile: 'standard', + validation_mode: 'proof', + evidence_level: 'standard', + }), ) }) }) diff --git a/frontend/testing/unit/pages/ToolConfigTimeout.test.tsx b/frontend/testing/unit/pages/ToolConfigTimeout.test.tsx index 4c0b2213..513ca033 100644 --- a/frontend/testing/unit/pages/ToolConfigTimeout.test.tsx +++ b/frontend/testing/unit/pages/ToolConfigTimeout.test.tsx @@ -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') }) }) diff --git a/frontend/testing/unit/pages/Workflows.test.tsx b/frontend/testing/unit/pages/Workflows.test.tsx index 7c304da8..54628a33 100644 --- a/frontend/testing/unit/pages/Workflows.test.tsx +++ b/frontend/testing/unit/pages/Workflows.test.tsx @@ -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', + }, + }], }) }) })