Skip to content

fix: harden workflow command argument execution - #4

Merged
MKlolbullen merged 7 commits into
mainfrom
feature/typed-dag-runtime-v3
Aug 22, 2026
Merged

fix: harden workflow command argument execution#4
MKlolbullen merged 7 commits into
mainfrom
feature/typed-dag-runtime-v3

Conversation

@MKlolbullen

@MKlolbullen MKlolbullen commented Aug 22, 2026

Copy link
Copy Markdown
Owner

Summary

Fix the first runtime-hardening set after the DAG v3 / Mermaid work:

  1. Preserve quoted and escaped workflow arguments instead of using strings.Fields.
  2. Translate legacy > {{output}} / 1> {{output}} syntax into Termaid-managed stdout capture without invoking a shell.
  3. Replace stale tool-specific validation with generic executable validation, removing the Args[0] panic hazard and false rejections such as valid Nuclei defaults.

Safety / execution semantics

  • Commands are still launched directly with exec.CommandContext.
  • No implicit shell expansion, globbing, command substitution, or pipe interpretation was added.
  • Existing explicit bash -c "..." arguments remain supported because their quoted command body is now kept intact.
  • Arbitrary > filename destinations are rejected; managed legacy redirection must point at {{output}} / $(output).

Tests

Added coverage for quoted arguments, escaped spaces, empty quoted arguments, bash -c command bodies, malformed quoting, legacy and compact stdout redirects, tool-owned output paths, stdout -, arbitrary redirect rejection, nil tool configs, and executables with empty argument lists.

Summary by Sourcery

Harden workflow command preparation and validation while retaining direct, shell-free execution semantics.

Bug Fixes:

  • Preserve quoted, escaped, and empty workflow arguments while rejecting malformed quoting.
  • Translate managed legacy stdout redirection into direct runtime capture without shell execution.
  • Prevent nil or empty-argument tool configurations from causing validation panics or false rejections.

Enhancements:

  • Use generic executable validation instead of stale tool-specific flag checks.
  • Capture stdout consistently for streaming tools while respecting tool-owned output files.

Tests:

  • Add coverage for argument parsing, bash command bodies, managed redirects, output ownership, redirect rejection, and executable validation.

@sourcery-ai

sourcery-ai Bot commented Aug 22, 2026

Copy link
Copy Markdown

Reviewer's Guide

Introduces a hardened command argument preparation layer that preserves quoting/escaping, normalizes legacy stdout redirection into managed capture, updates DAG and tool runners to use this new API, and simplifies tool validation to generic executable checks while expanding test coverage for argument parsing, redirection, and validation edge cases.

Sequence diagram for hardened workflow command execution

sequenceDiagram
    participant Runner as WorkflowRunner
    participant Prep as CommandPreparation
    participant Process as DirectProcess
    participant Artifact as OutputArtifact

    Runner->>Prep: prepareCommandString(raw, domain, inputPath, outputFile)
    Prep->>Prep: splitCommandArgs(raw)
    Prep->>Prep: stripOutputRedirect(args)
    Prep-->>Runner: commandInvocation(Args, CaptureStdout)
    Runner->>Process: exec.CommandContext(command, Args)
    alt CaptureStdout
        Process-->>Artifact: stdout
    end
    Process-->>Runner: exit status and stderr
Loading

File-Level Changes

Change Details Files
Add a dedicated command argument parser and stdout-capture decision helper used by both DAG nodes and catalog tools.
  • Introduce commandInvocation struct to carry argv and stdout-capture decision.
  • Implement splitCommandArgs to parse human-editable command strings with shell-like quoting/escaping but no shell semantics.
  • Implement prepareCommandString/prepareCommandArgv to combine argument parsing, placeholder substitution, legacy stdout redirection handling, and capture decision.
  • Implement stripOutputRedirect/compactOutputRedirect/isOutputPlaceholder to translate legacy > {{output}} forms into managed stdout capture and reject arbitrary destinations.
internal/pipeline/command_args.go
Wire the new command preparation into DAG execution to preserve argument quoting and enable managed stdout capture for workflows.
  • Replace strings.Fields-based arg splitting with prepareCommandString in executeWorker.
  • Propagate prepareCommandString errors into node execution results and stderr.
  • Use commandInvocation.CaptureStdout instead of writesOwnFile for deciding when to create the node output file for stdout capture.
internal/pipeline/dag_runner.go
Update tool execution to use the shared command preparation logic and rely on a simplified, generic validation routine.
  • Use prepareCommandArgv for catalog tools, handling placeholder substitutions and legacy stdout redirects without invoking a shell.
  • Base stdout capture decision on commandInvocation.CaptureStdout, covering tools that use -o - or legacy redirection.
  • Relax validateTool to only enforce non-nil configuration, non-empty command, and existence in PATH, removing tool-specific flag checks and Args[0] assumptions.
  • Ensure RecordNodeOutput errors are ignored via _ = to avoid shadowing or dropped errors.
internal/pipeline/pipeline.go
Extend test coverage for argument parsing, stdout redirection semantics, stdout-capture behavior, and tool validation edge cases.
  • Add tests for splitCommandArgs covering quotes, escaped spaces, empty quoted args, bash -c bodies, and malformed quoting/escapes.
  • Add tests for prepareCommandString covering legacy and compact stdout redirects, tool-owned output paths, stdout dash capture, and arbitrary redirect rejection.
  • Add tests for validateTool to reject nil/empty tool configs and allow executables with empty argument lists.
internal/pipeline/command_args_test.go
internal/pipeline/tool_validation_test.go

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@coderabbitai

coderabbitai Bot commented Aug 22, 2026

Copy link
Copy Markdown

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: feab99c0-0f3a-42a7-bd9c-24806461ca23

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@MKlolbullen
MKlolbullen marked this pull request as ready for review August 22, 2026 09:15
@MKlolbullen
MKlolbullen merged commit 9832118 into main Aug 22, 2026
4 checks passed

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 2 issues

Prompt for AI Agents
Please address the comments from this code review:

## Individual Comments

### Comment 1
<location path="internal/pipeline/command_args.go" line_range="147-154" />
<code_context>
+		}
+
+		if quote == '"' {
+			switch r {
+			case '"':
+				quote = 0
+			case '\\':
+				escaped = true
+			default:
+				current.WriteRune(r)
+			}
+			tokenStarted = true
+			continue
</code_context>
<issue_to_address>
**issue (bug_risk):** Backslashes inside double-quoted arguments are always consumed, so an explicit command body such as `bash -c "printf '%s\\n' hi"` is passed to bash without the backslash and prints the wrong result. This violates the stated requirement that quoted command bodies remain intact.

**Triggers:** When a quoted workflow argument, especially a `bash -c` body, contains a backslash intended for the invoked program.

**Suggested fix:** Preserve backslashes inside double quotes except when they escape a supported quoting character, or implement the documented escaping rules explicitly.
</issue_to_address>

### Comment 2
<location path="internal/pipeline/pipeline.go" line_range="360-365" />
<code_context>
-		if !strings.Contains(tool.Args[0], "-w") {
-			return fmt.Errorf("gobuster requires a wordlist (-w)")
-		}
+	command := strings.TrimSpace(tool.Command)
+	if command == "" {
+		return fmt.Errorf("tool command is empty")
+	}
+	if _, err := exec.LookPath(command); err != nil {
+		return fmt.Errorf("command not found: %s (install it or check PATH)", command)
 	}
-
</code_context>
<issue_to_address>
**issue (bug_risk):** `validateTool` performs `LookPath` on the trimmed command but callers still execute `tool.Command` unchanged, so a command configured with surrounding whitespace passes validation and then fails at `exec.CommandContext` with an executable-not-found error.

**Triggers:** When a workflow tool command contains leading or trailing whitespace.

**Suggested fix:** Either reject whitespace-bearing command names or assign the trimmed command back to the invocation used by `exec.CommandContext`.

```suggestion
	command := strings.TrimSpace(tool.Command)
	if command == "" {
		return fmt.Errorf("tool command is empty")
	}
	if command != tool.Command {
		return fmt.Errorf("tool command contains surrounding whitespace")
	}
	if _, err := exec.LookPath(command); err != nil {
		return fmt.Errorf("command not found: %s (install it or check PATH)", command)
```
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment on lines +147 to +154
switch r {
case '"':
quote = 0
case '\\':
escaped = true
default:
current.WriteRune(r)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (bug_risk): Backslashes inside double-quoted arguments are always consumed, so an explicit command body such as bash -c "printf '%s\\n' hi" is passed to bash without the backslash and prints the wrong result. This violates the stated requirement that quoted command bodies remain intact.

Triggers: When a quoted workflow argument, especially a bash -c body, contains a backslash intended for the invoked program.

Suggested fix: Preserve backslashes inside double quotes except when they escape a supported quoting character, or implement the documented escaping rules explicitly.

Comment on lines +360 to +365
command := strings.TrimSpace(tool.Command)
if command == "" {
return fmt.Errorf("tool command is empty")
}
if _, err := exec.LookPath(command); err != nil {
return fmt.Errorf("command not found: %s (install it or check PATH)", command)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (bug_risk): validateTool performs LookPath on the trimmed command but callers still execute tool.Command unchanged, so a command configured with surrounding whitespace passes validation and then fails at exec.CommandContext with an executable-not-found error.

Triggers: When a workflow tool command contains leading or trailing whitespace.

Suggested fix: Either reject whitespace-bearing command names or assign the trimmed command back to the invocation used by exec.CommandContext.

Suggested change
command := strings.TrimSpace(tool.Command)
if command == "" {
return fmt.Errorf("tool command is empty")
}
if _, err := exec.LookPath(command); err != nil {
return fmt.Errorf("command not found: %s (install it or check PATH)", command)
command := strings.TrimSpace(tool.Command)
if command == "" {
return fmt.Errorf("tool command is empty")
}
if command != tool.Command {
return fmt.Errorf("tool command contains surrounding whitespace")
}
if _, err := exec.LookPath(command); err != nil {
return fmt.Errorf("command not found: %s (install it or check PATH)", command)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant