Taco as interpreter + Cmd buf overflow error (#288)

* Taco as interpreter

* Working on code review comments

* Cmd buf overflow error

* DEV-2235 Refactored mocked fields

* Write to buffer data up to the overflow limit

* Fixing the test

* Update api-doc.yml

* Renamed taco to tacoscript

Co-authored-by: Thorsten Kramm <tk@system42.io>
This commit is contained in:
Andrey 2021-10-05 15:44:18 +03:00 committed by GitHub
parent 21312fe519
commit 063486bc2c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 310 additions and 133 deletions

View File

@ -694,8 +694,8 @@ paths:
description: "remote command to execute by the rport client. NOTE: if command limitation is enabled by an rport client then a full path command can be required to use. See https://oss.rport.io/docs/no06-command-execution.html for more details"
interpreter:
type: "string"
enum: [cmd, powershell]
description: "command interpreter to use to execute the command. Is applicable only for windows clients. If not set 'cmd' is used by default"
enum: [cmd, powershell, tacoscript]
description: "command interpreter to use to execute the command. If not set 'cmd' is used by default on Windows and /bin/sh on Linux"
cwd:
type: "string"
description: "current working directory for the executable command"
@ -762,8 +762,8 @@ paths:
description: "base64 encoded script which should be executed on a remote client"
interpreter:
type: "string"
enum: [ cmd, powershell ]
description: "command interpreter to use to execute the script. Is applicable only for windows clients. If not set 'cmd' is used by default"
enum: [ cmd, powershell, tacoscript ]
description: "command interpreter to use to execute the script. If not set 'cmd' is used by default on Windows and /bin/sh on Linux. For tacoscript interpreter you should install tacoscript binary from here: https://github.com/cloudradar-monitoring/tacoscript#installation. It should also be available in the system path."
cwd:
type: "string"
description: "current working directory for the executable script"
@ -2475,8 +2475,8 @@ definitions:
description: "list of client IDs where to run the script"
interpreter:
type: "string"
enum: [cmd, powershell]
description: "command interpreter to use to execute the script. Is applicable only for windows clients. If not set 'cmd' is used by default"
enum: [cmd, powershell, tacoscript]
description: "command interpreter to use to execute the script. If not set 'cmd' is used by default on Windows, and '/bin/sh' on Unix. For tacoscript interpreter you should install tacoscript binary from here: https://github.com/cloudradar-monitoring/tacoscript#installation. It should also be available in the system path."
timeout_sec:
type: "integer"
description: "timeout in seconds to observe the script execution on each client separately. If not set a default timeout (60 seconds) is used"
@ -2600,7 +2600,7 @@ definitions:
description: "User name who created this script"
interpreter:
type: "string"
description: "how will the script be executed on the client, e.g. /bin/sh, cmd.exe, powershell"
description: "how will the script be executed on the client, e.g. /bin/sh, cmd.exe, powershell, tacoscript"
cwd:
type: "string"
description: "current working directory, where the script should be executed"
@ -2621,7 +2621,7 @@ definitions:
description: "[required] text of the script"
interpreter:
type: "string"
description: "how will the script be executed on the client, e.g. /bin/sh, cmd.exe, powershell"
description: "how will the script be executed on the client, e.g. /bin/sh, cmd.exe, powershell, tacoscript"
cwd:
type: "string"
description: "current working directory, where the script should be executed"

View File

@ -1,7 +1,6 @@
package chclient
import (
"bytes"
"context"
"encoding/json"
"errors"
@ -10,6 +9,7 @@ import (
"os/exec"
"regexp"
"runtime"
"strings"
"time"
chshare "github.com/cloudradar-monitoring/rport/share"
@ -50,12 +50,6 @@ func (e *CmdExecutorImpl) Wait(cmd *exec.Cmd) error {
return cmd.Wait()
}
const (
unixShell = "/bin/sh"
cmdShell = "cmd"
powerShell = "powershell"
)
// now is used to stub time.Now in tests
var now = time.Now
@ -106,10 +100,10 @@ func (c *Client) HandleRunCmdRequest(ctx context.Context, reqPayload []byte) (*c
IsScript: job.IsScript,
}
cmd := c.cmdExec.New(ctx, execCtx)
stdOut := CapacityBuffer{capacity: c.config.RemoteCommands.SendBackLimit}
stdErr := CapacityBuffer{capacity: c.config.RemoteCommands.SendBackLimit}
cmd.Stdout = &stdOut
cmd.Stderr = &stdErr
stdOut := &CapacityBuffer{capacity: c.config.RemoteCommands.SendBackLimit}
stdErr := &CapacityBuffer{capacity: c.config.RemoteCommands.SendBackLimit}
cmd.Stdout = stdOut
cmd.Stderr = stdErr
c.Debugf("Generated command is %s, sysProcAttributes: %+v", cmd.String(), cmd.SysProcAttr)
@ -140,11 +134,12 @@ func (c *Client) HandleRunCmdRequest(ctx context.Context, reqPayload []byte) (*c
go func() { done <- c.cmdExec.Wait(cmd) }()
var status string
var execErr error
select {
case err := <-done:
if err != nil {
case execErr = <-done:
if execErr != nil {
status = models.JobStatusFailed
c.Errorf("failed to run command[jid=%q,pid=%d]:\ncmd:\n%s\nerr: %s", job.JID, res.Pid, job.Command, err)
c.Errorf("failed to run command[jid=%q,pid=%d]:\ncmd:\n%s\nerr: %s", job.JID, res.Pid, job.Command, execErr)
} else {
status = models.JobStatusSuccessful
}
@ -164,6 +159,8 @@ func (c *Client) HandleRunCmdRequest(ctx context.Context, reqPayload []byte) (*c
job.PID = &res.Pid
job.StartedAt = startedAt
job.Error = c.buildErrText(execErr, stdOut, stdErr)
job.Result = &models.JobResult{
StdOut: stdOut.String(),
StdErr: stdErr.String(),
@ -187,6 +184,22 @@ func (c *Client) HandleRunCmdRequest(ctx context.Context, reqPayload []byte) (*c
return res, nil
}
func (c *Client) buildErrText(execErr error, stdOut, stdErr *CapacityBuffer) string {
errs := make([]string, 0, 3)
if execErr != nil {
errs = append(errs, execErr.Error())
}
if stdOut.HasOverflow() {
errs = append(errs, fmt.Sprintf("overflow of stdOut buffer: %s", stdOut.GetOverflowMessage()))
}
if stdErr.HasOverflow() {
errs = append(errs, fmt.Sprintf("overflow of stdErr buffer: %s", stdErr.GetOverflowMessage()))
}
return strings.Join(errs, ", ")
}
func (c *Client) rmScriptIfNeeded(scriptPath string, isScript bool) {
if !isScript {
return
@ -202,11 +215,15 @@ func (c *Client) rmScriptIfNeeded(scriptPath string, isScript bool) {
// var is used to override in tests
var getInterpreter = func(inputInterpreter, os string, hasShebang bool) (string, error) {
if inputInterpreter == chshare.Tacoscript {
return inputInterpreter, nil
}
if os == "windows" {
switch inputInterpreter {
case "":
return cmdShell, nil
case cmdShell, powerShell:
return chshare.CmdShell, nil
case chshare.CmdShell, chshare.PowerShell:
return inputInterpreter, nil
}
return "", fmt.Errorf("invalid windows command interpreter: %q", inputInterpreter)
@ -219,7 +236,7 @@ var getInterpreter = func(inputInterpreter, os string, hasShebang bool) (string,
if inputInterpreter != "" {
return "", fmt.Errorf("for unix clients a command interpreter should not be specified, got: %q", inputInterpreter)
}
return unixShell, nil
return chshare.UnixShell, nil
}
// isAllowed returns true if a given command passes configured restrictions.
@ -252,22 +269,34 @@ func matchRegexp(cmd string, regexpList []*regexp.Regexp) bool {
}
type CapacityBuffer struct {
bytes.Buffer
capacity int
data []byte
capacity int
hasOverflow bool
}
func (b *CapacityBuffer) HasOverflow() bool {
return b.hasOverflow
}
func (b *CapacityBuffer) GetOverflowMessage() string {
return fmt.Sprintf("maximum send_back_limit of %d bytes exceeded", b.capacity)
}
func (b *CapacityBuffer) Write(p []byte) (n int, err error) {
freeCapacity := b.capacity - b.Len()
freeCapacity := b.capacity - len(b.data)
// do not write to buffer if no space left
if freeCapacity <= 0 {
return len(p), nil // pretend a successful write
}
// write to buffer only a part if exceeds the capacity
if len(p) > freeCapacity {
return b.Buffer.Write(p[:freeCapacity])
b.data = append(b.data, p[:freeCapacity]...)
b.hasOverflow = true
return 0, errors.New(b.GetOverflowMessage())
}
return b.Buffer.Write(p)
b.data = append(b.data, p...)
return len(p), nil
}
func (b *CapacityBuffer) String() string {
return string(b.data)
}

View File

@ -6,6 +6,8 @@ import (
"context"
"os/exec"
"strings"
chshare "github.com/cloudradar-monitoring/rport/share"
)
func (e *CmdExecutorImpl) New(ctx context.Context, execCtx *CmdExecutorContext) *exec.Cmd {
@ -16,7 +18,10 @@ func (e *CmdExecutorImpl) New(ctx context.Context, execCtx *CmdExecutorContext)
interpreter := execCtx.Interpreter
if interpreter != "" {
args = append(args, interpreter, "-c")
args = append(args, interpreter)
if interpreter != chshare.Tacoscript {
args = append(args, "-c")
}
}
commandStr := execCtx.Command

View File

@ -71,7 +71,8 @@ func (e *CmdExecutorMock) writeToStdOut(cmd *exec.Cmd) {
for _, s := range e.ReturnStdOut {
_, err := cmd.Stdout.Write([]byte(s))
if err != nil {
log.Fatalf("Failed to write data into stdout: %s", err)
log.Printf("Failed to write data into stdout: %s", err)
return
}
}
}
@ -82,7 +83,8 @@ func (e *CmdExecutorMock) writeToStdErr(cmd *exec.Cmd) {
for _, s := range e.ReturnStdErr {
_, err := cmd.Stderr.Write([]byte(s))
if err != nil {
log.Fatalf("Failed to write data into stderr: %s", err)
log.Printf("Failed to write data into stderr: %s", err)
return
}
}
}
@ -144,21 +146,21 @@ func TestGetInterpreter(t *testing.T) {
name: "windows, empty",
interpreter: "",
os: win,
wantInterpreter: cmdShell,
wantInterpreter: chshare.CmdShell,
wantErrContains: "",
},
{
name: "windows, cmd",
interpreter: cmdShell,
interpreter: chshare.CmdShell,
os: win,
wantInterpreter: cmdShell,
wantInterpreter: chshare.CmdShell,
wantErrContains: "",
},
{
name: "windows, powershell",
interpreter: powerShell,
interpreter: chshare.PowerShell,
os: win,
wantInterpreter: powerShell,
wantInterpreter: chshare.PowerShell,
wantErrContains: "",
},
{
@ -172,12 +174,12 @@ func TestGetInterpreter(t *testing.T) {
name: "unix, empty",
interpreter: "",
os: unix,
wantInterpreter: unixShell,
wantInterpreter: chshare.UnixShell,
wantErrContains: "",
},
{
name: "unix, non empty",
interpreter: unixShell,
interpreter: chshare.UnixShell,
os: unix,
wantInterpreter: "",
wantErrContains: "for unix clients a command interpreter should not be specified",
@ -186,7 +188,7 @@ func TestGetInterpreter(t *testing.T) {
name: "empty os, empty interpreter",
interpreter: "",
os: "",
wantInterpreter: unixShell,
wantInterpreter: chshare.UnixShell,
wantErrContains: "",
},
{
@ -198,17 +200,29 @@ func TestGetInterpreter(t *testing.T) {
{
name: "unix, hasShebang, interpreter not empty",
os: unix,
interpreter: unixShell,
interpreter: chshare.UnixShell,
wantInterpreter: "",
boolHasShebang: true,
},
{
name: "windows, hasShebang, interpreter not empty",
os: win,
interpreter: powerShell,
wantInterpreter: powerShell,
interpreter: chshare.PowerShell,
wantInterpreter: chshare.PowerShell,
boolHasShebang: true,
},
{
name: "windows, tacoscript interpreter",
os: win,
interpreter: chshare.Tacoscript,
wantInterpreter: chshare.Tacoscript,
},
{
name: "linux, tacoscript interpreter",
os: unix,
interpreter: chshare.Tacoscript,
wantInterpreter: chshare.Tacoscript,
},
}
for _, tc := range testCases {
@ -270,10 +284,10 @@ func TestHandleRunCmdRequestPositiveCase(t *testing.T) {
"cwd": "/root",
"timeout_sec": 60,
"multi_job_id":null,
"error":"",
"error":"%s",
`
wantJSONPart2 := `
"result": {
"result": {
"stdout": "output1output2output3",
"stderr": "error1error2"
}
@ -292,27 +306,27 @@ func TestHandleRunCmdRequestPositiveCase(t *testing.T) {
{
name: "limit is larger than stdout and stderr",
sendBackLimit: stdOutSize + 1,
wantJSON: wantJSONPart1 + wantJSONPart2,
wantJSON: fmt.Sprintf(wantJSONPart1, "") + wantJSONPart2,
},
{
name: "limit is equal to the larger output",
sendBackLimit: stdOutSize,
wantJSON: wantJSONPart1 + wantJSONPart2,
wantJSON: fmt.Sprintf(wantJSONPart1, "") + wantJSONPart2,
},
{
name: "limit is equal to the smaller output",
sendBackLimit: stdErrSize,
wantJSON: wantJSONPart1 + `
"result": {
"stdout": "output1outpu",
"stderr": "error1error2"
}
wantJSON: fmt.Sprintf(wantJSONPart1, "overflow of stdOut buffer: maximum send_back_limit of 12 bytes exceeded") + `
"result": {
"stdout": "output1outpu",
"stderr": "error1error2"
}
}`,
},
{
name: "limit is less than smaller output",
sendBackLimit: stdErrSize - 1,
wantJSON: wantJSONPart1 + `
wantJSON: fmt.Sprintf(wantJSONPart1, "overflow of stdOut buffer: maximum send_back_limit of 11 bytes exceeded, overflow of stdErr buffer: maximum send_back_limit of 11 bytes exceeded") + `
"result": {
"stdout": "output1outp",
"stderr": "error1error"
@ -322,12 +336,12 @@ func TestHandleRunCmdRequestPositiveCase(t *testing.T) {
{
name: "limit is zero",
sendBackLimit: 0,
wantJSON: wantJSONPart1 + `
"result": {
"stdout": "",
"stderr": ""
}
}`,
wantJSON: fmt.Sprintf(wantJSONPart1, "overflow of stdOut buffer: maximum send_back_limit of 0 bytes exceeded, overflow of stdErr buffer: maximum send_back_limit of 0 bytes exceeded") + `
"result": {
"stdout": "",
"stderr": ""
}
}`,
},
{
name: "command is not allowed",
@ -335,7 +349,8 @@ func TestHandleRunCmdRequestPositiveCase(t *testing.T) {
wantErrContains: "command is not allowed",
},
}
for _, tc := range testCases {
for i := range testCases {
tc := testCases[i]
t.Run(tc.name, func(t *testing.T) {
// given
c.config.RemoteCommands.SendBackLimit = tc.sendBackLimit

View File

@ -10,6 +10,8 @@ import (
"path/filepath"
"strings"
"syscall"
chshare "github.com/cloudradar-monitoring/rport/share"
)
func (e *CmdExecutorImpl) New(ctx context.Context, execCtx *CmdExecutorContext) *exec.Cmd {
@ -23,9 +25,9 @@ func (e *CmdExecutorImpl) New(ctx context.Context, execCtx *CmdExecutorContext)
}
switch execCtx.Interpreter {
case cmdShell:
case chshare.CmdShell:
return buildCmdInterpreterCmd(ctx, execCtx, interpreterPath)
case powerShell:
case chshare.PowerShell:
return buildPowershellCmd(ctx, execCtx, interpreterPath)
default:
return buildDefaultCmd(ctx, execCtx, interpreterPath)

View File

@ -25,7 +25,7 @@ curl -X POST 'http://localhost:3000/api/v1/library/scripts' \
### Params:
- _name_ any text to identify the script
- _interpreter_ how will the script be executed on the client, e.g. /bin/sh, cmd.exe, powershell
- _interpreter_ script syntax interpreter which is used for execution, possible values are sh, cmd.exe, powershell, tacoscript, default values: sh (under Linux) and cmd.exe (under Windows)
- _sudo_ true or false if this script should be executed under a sudo user
- _cwd_ an optional directory where the script will be executed
- _script_ the text of the script to execute
@ -264,3 +264,40 @@ Put an access token and a client ids in the corresponding fields. You can also p
Click Open to start websocket connection.
Put the input data in JSON format with the base64 encoded script to the input field and click Send. The payload will be transmitted via Websocket protocol. Once the clients finish the execution, they will send back the response which you'll see in the Output field.
### Execution of taco scripts
[tacoscript](https://github.com/cloudradar-monitoring/tacoscript) interpreter can be used to execute scripts in a Saltstack similar format for both Windows and Linux machines. Tacoscript interpreter doesn't require additional libraries or tools to be installed in the system and it has capabilities for:
- conditional execution depending on command exit codes, present/missing files, host system information (e.g. os version)
- installing/uninstalling/upgrading packages
- creating files
- dependant executions (e.g. script A depends on execution of script B etc)
- reserved values from the information about the host system
- reusable variables
To execute a taco script, you need to specify `tacoscript` as an interpreter, e.g.
```
curl -X POST 'http://localhost:3000/api/v1/clients/4943d682-7874-4f7a-999c-b4ff5493fc3f/scripts' \
-H 'Authorization: Bearer eyJhbGcidfasjflInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImdfasfdjoiMTEzMzkyNjMxNTA0MDYwOTU1MCJ9.JG4whDXeDKDuZqgVA \
-H 'Content-Type: application/x-www-form-urlencoded' \
--data-raw '{
"script": "IwojIEZpcnN0IGV4YW1wbGUgb2YgYSB0YWNvc2NyaXB0IGZvbGxvd2luZyB0aGUgc3ludGF4IG9mIFNhbHQKIyBidXQgbm90IGltcGxlbWVudGluZyBhbGwgb3B0aW9ucwojIGh0dHBzOi8vZG9jcy5zYWx0c3RhY2suY29tL2VuL2xhdGVzdC9yZWYvc3RhdGVzL2FsbC9zYWx0LnN0YXRlcy5jbWQuaHRtbAojCiMgdW5pcXVlIGlkIG9mIHRoZSB0YXNrLCBjYW4gYmUgYW55IHN0cmluZwpkYXRlIGNvbW1hbmQ6CiAgY21kLnJ1bjoKICAgIC0gbmFtZXM6CiAgICAgIC0gZGF0ZQo=",
"interpreter": "tacoscript"
}'
```
Where the base64 encoded script looks like this:
```
#
# First example of a tacoscript following the syntax of Salt
# but not implementing all options
# https://docs.saltstack.com/en/latest/ref/states/all/salt.states.cmd.html
#
# unique id of the task, can be any string
date command:
cmd.run:
- names:
- date
```
As a result this script will output the current date.
In order to execute taco scripts, there should be `tacoscript` binary avalable in the system path (see here [the installation instructions](https://github.com/cloudradar-monitoring/tacoscript#installation))

View File

@ -58,8 +58,6 @@ const (
minVersionScriptExecSupport = "0.1.35"
)
var validInputInterpreter = []string{"cmd", "powershell"}
var generateNewJobID = func() (string, error) {
return random.UUID4()
}
@ -1360,7 +1358,7 @@ func (al *APIListener) handleExecuteCommand(ctx context.Context, w http.Response
al.jsonErrorResponseWithTitle(w, http.StatusBadRequest, "Command cannot be empty.")
return
}
if err := validateInterpreter(executeInput.Interpreter); err != nil {
if err := validation.ValidateInterpreter(executeInput.Interpreter, executeInput.IsScript); err != nil {
al.jsonErrorResponseWithError(w, http.StatusBadRequest, "Invalid interpreter.", err)
return
}
@ -1511,18 +1509,6 @@ func (al *APIListener) handleExecuteScript(w http.ResponseWriter, req *http.Requ
al.handleExecuteCommand(req.Context(), w, execCmdInput)
}
func validateInterpreter(interpreter string) error {
if interpreter == "" {
return nil
}
for _, v := range validInputInterpreter {
if interpreter == v {
return nil
}
}
return fmt.Errorf("expected interpreter to be one of: %s, actual: %s", validInputInterpreter, interpreter)
}
func (al *APIListener) handleGetCommands(w http.ResponseWriter, req *http.Request) {
vars := mux.Vars(req)
cid := vars[routeParamClientID]
@ -1601,7 +1587,7 @@ func (al *APIListener) handlePostMultiClientCommand(w http.ResponseWriter, req *
al.jsonErrorResponseWithTitle(w, http.StatusBadRequest, "Command cannot be empty.")
return
}
if err := validateInterpreter(reqBody.Interpreter); err != nil {
if err := validation.ValidateInterpreter(reqBody.Interpreter, reqBody.IsScript); err != nil {
al.jsonErrorResponseWithError(w, http.StatusBadRequest, "Invalid interpreter.", err)
return
}
@ -2012,7 +1998,7 @@ func (al *APIListener) handleCommandsExecutionWS(
uiConnTS.WriteError("Command cannot be empty.", nil)
return
}
if err := validateInterpreter(inboundMsg.Interpreter); err != nil {
if err := validation.ValidateInterpreter(inboundMsg.Interpreter, inboundMsg.IsScript); err != nil {
uiConnTS.WriteError("Invalid interpreter", err)
return
}

View File

@ -700,7 +700,7 @@ func TestHandlePostCommand(t *testing.T) {
clients: []*clients.Client{c1},
wantStatusCode: http.StatusBadRequest,
wantErrTitle: "Invalid interpreter.",
wantErrDetail: "expected interpreter to be one of: [cmd powershell], actual: unsupported",
wantErrDetail: "expected interpreter to be one of: [cmd powershell tacoscript], actual: unsupported",
},
{
name: "valid cmd with no timeout",

View File

@ -67,14 +67,25 @@ func (e *Executor) createClientScriptPath(os, interpreter string) (string, error
if err != nil {
return "", err
}
if os == "windows" {
if interpreter == "powershell" {
return scriptName + ".ps1", nil
}
return scriptName + ".bat", nil
extension := e.getExtension(os, interpreter)
return scriptName + extension, nil
}
func (e *Executor) getExtension(os, interpreter string) string {
if interpreter == chshare.Tacoscript {
return ".yml"
}
return scriptName + ".sh", nil
if os == "windows" {
if interpreter == chshare.PowerShell {
return ".ps1"
}
return ".bat"
}
return ".sh"
}
const shebangPrefix = "#!"

View File

@ -4,59 +4,116 @@ import (
"encoding/json"
"strings"
"testing"
"time"
"github.com/cloudradar-monitoring/rport/server/api"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/cloudradar-monitoring/rport/server/clients"
chshare "github.com/cloudradar-monitoring/rport/share"
"github.com/cloudradar-monitoring/rport/share/comm"
"github.com/cloudradar-monitoring/rport/share/models"
"github.com/cloudradar-monitoring/rport/share/test"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestCreateScriptOnClient(t *testing.T) {
givenResp := &comm.CreateFileResponse{
FilePath: "/tmp/script.sh",
Sha256Hash: "a1159e9df3670d549d04524532629f5477ceb7deec9b45e47e8c009506ecb2c8", //sha256 of "pwd"
CreatedAt: time.Date(2001, 1, 1, 0, 0, 0, 0, time.UTC),
}
givenRespBytes, err := json.Marshal(givenResp)
require.NoError(t, err)
executor := NewExecutor(&chshare.Logger{})
conn := &test.ConnMock{
ReturnResponsePayload: givenRespBytes,
ReturnOk: true,
const defaultHash = "a1159e9df3670d549d04524532629f5477ceb7deec9b45e47e8c009506ecb2c8" //sha256 of "pwd"
const defaultScript = "pwd"
testCases := []struct {
name string
filePathWant string
Sha256HashToMocked string
clientOSKernelMocked string
scriptMocked string
fileExtensionWant string
interpreterMocked string
}{
{
name: "sh script linux",
filePathWant: "/tmp/script.sh",
Sha256HashToMocked: defaultHash,
clientOSKernelMocked: "linux",
scriptMocked: defaultScript,
fileExtensionWant: ".sh",
},
{
name: "tacoscript script linux",
interpreterMocked: chshare.Tacoscript,
filePathWant: "/tmp/taco_script.yml",
Sha256HashToMocked: defaultHash,
clientOSKernelMocked: "linux",
scriptMocked: defaultScript,
fileExtensionWant: ".yml",
},
{
name: "tacoscript windows",
interpreterMocked: chshare.Tacoscript,
filePathWant: "C:\\taco_script.yml",
Sha256HashToMocked: defaultHash,
clientOSKernelMocked: "windows",
scriptMocked: defaultScript,
fileExtensionWant: ".yml",
},
{
name: "powershell script windows",
interpreterMocked: chshare.PowerShell,
filePathWant: "C:\\ps_script.ps1",
Sha256HashToMocked: defaultHash,
clientOSKernelMocked: "windows",
scriptMocked: defaultScript,
fileExtensionWant: ".ps1",
},
{
name: "cmd script windows",
interpreterMocked: chshare.CmdShell,
filePathWant: "C:\\cmd_script.bat",
Sha256HashToMocked: defaultHash,
clientOSKernelMocked: "windows",
scriptMocked: defaultScript,
fileExtensionWant: ".bat",
},
}
cl := &clients.Client{
OSKernel: "linux",
ID: "123",
Connection: conn,
for i := range testCases {
tc := testCases[i]
t.Run(tc.name, func(t *testing.T) {
givenResp := &comm.CreateFileResponse{
FilePath: tc.filePathWant,
Sha256Hash: tc.Sha256HashToMocked,
}
givenRespBytes, err := json.Marshal(givenResp)
require.NoError(t, err)
executor := NewExecutor(&chshare.Logger{})
conn := &test.ConnMock{
ReturnResponsePayload: givenRespBytes,
ReturnOk: true,
}
cl := &clients.Client{
OSKernel: tc.clientOSKernelMocked,
Connection: conn,
}
inp := &api.ExecuteInput{
Script: tc.scriptMocked,
Interpreter: tc.interpreterMocked,
}
gotScriptPath, err := executor.CreateScriptOnClient(inp, cl)
require.NoError(t, err)
assert.Equal(t, tc.filePathWant, gotScriptPath)
fileInputGot := &models.File{}
_, _, fileInputBytes := conn.InputSendRequest()
err = json.Unmarshal(fileInputBytes, fileInputGot)
require.NoError(t, err)
assert.True(t, strings.HasSuffix(fileInputGot.Name, tc.fileExtensionWant))
assert.Equal(t, tc.scriptMocked, string(fileInputGot.Content))
assert.EqualValues(t, DefaultScriptFileMode, fileInputGot.Mode)
})
}
inp := &api.ExecuteInput{
Script: "pwd",
Cwd: "/home",
TimeoutSec: 1,
}
res, err := executor.CreateScriptOnClient(inp, cl)
require.NoError(t, err)
assert.Equal(t, "/tmp/script.sh", res)
fileInput := &models.File{}
_, _, fileInputBytes := conn.InputSendRequest()
err = json.Unmarshal(fileInputBytes, fileInput)
require.NoError(t, err)
assert.True(t, strings.HasSuffix(fileInput.Name, ".sh"))
assert.Equal(t, "pwd", string(fileInput.Content))
assert.EqualValues(t, 0744, fileInput.Mode)
}
func TestParsingShebangLine(t *testing.T) {

View File

@ -0,0 +1,27 @@
package validation
import (
"fmt"
chshare "github.com/cloudradar-monitoring/rport/share"
)
var validInputInterpreter = []string{chshare.CmdShell, chshare.PowerShell, chshare.Tacoscript}
func ValidateInterpreter(interpreter string, isScript bool) error {
if interpreter == "" {
return nil
}
if !isScript && interpreter == chshare.Tacoscript {
return fmt.Errorf("%s interpreter can't be used for commands execution", chshare.Tacoscript)
}
for _, v := range validInputInterpreter {
if interpreter == v {
return nil
}
}
return fmt.Errorf("expected interpreter to be one of: %s, actual: %s", validInputInterpreter, interpreter)
}

8
share/interpreter.go Normal file
View File

@ -0,0 +1,8 @@
package chshare
const (
UnixShell = "/bin/sh"
CmdShell = "cmd"
PowerShell = "powershell"
Tacoscript = "tacoscript"
)