mirror of
https://github.com/openrport/openrport.git
synced 2025-10-26 11:27:11 +00:00
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:
parent
21312fe519
commit
063486bc2c
16
api-doc.yml
16
api-doc.yml
@ -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"
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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))
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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 = "#!"
|
||||
|
||||
@ -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) {
|
||||
|
||||
27
server/validation/interpreter.go
Normal file
27
server/validation/interpreter.go
Normal 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
8
share/interpreter.go
Normal file
@ -0,0 +1,8 @@
|
||||
package chshare
|
||||
|
||||
const (
|
||||
UnixShell = "/bin/sh"
|
||||
CmdShell = "cmd"
|
||||
PowerShell = "powershell"
|
||||
Tacoscript = "tacoscript"
|
||||
)
|
||||
Loading…
Reference in New Issue
Block a user