mirror of
https://github.com/Velocidex/velociraptor.git
synced 2026-10-12 02:09:59 +00:00
Rework Shell GUI to utilize sessions. (#4839)
Implemented persistent Bash, Powershell, CMD and VQL shells Added full screen and transcript download. The transcript allows the user to download a textual transcript of the interactive session.
This commit is contained in:
+45
-26
@@ -45,40 +45,59 @@ func (self *Pipe) Stat() (os.FileInfo, error) {
|
||||
return nil, errors.New("Not implemented")
|
||||
}
|
||||
|
||||
// Read a non empty data from the query.
|
||||
func (self *Pipe) Read(buff []byte) (int, error) {
|
||||
select {
|
||||
|
||||
case <-self.ctx.Done():
|
||||
return 0, io.EOF
|
||||
|
||||
case row, ok := <-self.output_chan:
|
||||
if !ok {
|
||||
// Keep trying to read something until the first non empty result
|
||||
getrow:
|
||||
for {
|
||||
select {
|
||||
case <-self.ctx.Done():
|
||||
return 0, io.EOF
|
||||
}
|
||||
|
||||
switch t := row.(type) {
|
||||
case *ordereddict.Dict:
|
||||
for _, v := range t.Values() {
|
||||
switch t := v.(type) {
|
||||
case string:
|
||||
out := append([]byte(t), self.sep...)
|
||||
return utils.MemCpy(buff, out), nil
|
||||
case row, ok := <-self.output_chan:
|
||||
if !ok {
|
||||
return 0, io.EOF
|
||||
}
|
||||
|
||||
case []byte:
|
||||
return utils.MemCpy(buff,
|
||||
append(t, self.sep...)), nil
|
||||
switch t := row.(type) {
|
||||
case *ordereddict.Dict:
|
||||
for _, v := range t.Values() {
|
||||
switch t := v.(type) {
|
||||
case string:
|
||||
if t == "" {
|
||||
continue getrow
|
||||
}
|
||||
|
||||
default:
|
||||
data := fmt.Sprintf("%v", v)
|
||||
out := append([]byte(data), self.sep...)
|
||||
return utils.MemCpy(buff, out), nil
|
||||
out := append([]byte(t), self.sep...)
|
||||
return utils.MemCpy(buff, out), nil
|
||||
|
||||
case []byte:
|
||||
if len(t) == 0 {
|
||||
continue getrow
|
||||
}
|
||||
|
||||
return utils.MemCpy(buff,
|
||||
append(t, self.sep...)), nil
|
||||
|
||||
default:
|
||||
data := fmt.Sprintf("%v", v)
|
||||
if len(data) == 0 {
|
||||
continue getrow
|
||||
}
|
||||
out := append([]byte(data), self.sep...)
|
||||
return utils.MemCpy(buff, out), nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
data := fmt.Sprintf("%v", row)
|
||||
out := append([]byte(data), self.sep...)
|
||||
return utils.MemCpy(buff, out), nil
|
||||
data := fmt.Sprintf("%v", row)
|
||||
if len(data) == 0 {
|
||||
continue getrow
|
||||
}
|
||||
|
||||
out := append([]byte(data), self.sep...)
|
||||
return utils.MemCpy(buff, out), nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -17,9 +17,9 @@ import (
|
||||
"www.velocidex.com/golang/velociraptor/vtesting/assert"
|
||||
|
||||
// For execve and query
|
||||
_ "www.velocidex.com/golang/velociraptor/vql/common"
|
||||
_ "www.velocidex.com/golang/velociraptor/vql/protocols"
|
||||
_ "www.velocidex.com/golang/velociraptor/vql/tools"
|
||||
_ "www.velocidex.com/golang/velociraptor/vql/tools/shell"
|
||||
)
|
||||
|
||||
type ClientVQLTestSuite struct {
|
||||
|
||||
@@ -11,4 +11,10 @@ parameters:
|
||||
|
||||
sources:
|
||||
- query: |
|
||||
SELECT * FROM query(query=Command, env=dict(config=config))
|
||||
SELECT _SessionId, *
|
||||
FROM query(query=Command, env=dict(config=config))
|
||||
|
||||
- name: Overview
|
||||
query: |
|
||||
SELECT _SessionId, timestamp(epoch=now()) AS Timestamp, Command
|
||||
FROM scope()
|
||||
|
||||
@@ -11,6 +11,9 @@ description: |
|
||||
elevated permissions (specifically the `EXECVE` permission).
|
||||
Typically it is only available with the `administrator` role.
|
||||
|
||||
In recent Velociraptor releases, the artifact will create a
|
||||
persistent session.
|
||||
|
||||
required_permissions:
|
||||
- EXECVE
|
||||
|
||||
@@ -19,9 +22,52 @@ parameters:
|
||||
default: "ls -l /"
|
||||
|
||||
sources:
|
||||
- query: |
|
||||
- precondition: |
|
||||
SELECT * FROM info() WHERE version(function="shell_session")
|
||||
query: |
|
||||
// Get the core flow id to key a unique session off.
|
||||
LET FLOWID <= split(string=_SessionId, sep="/")[0]
|
||||
|
||||
// Newer clients have support for true shell sessions.
|
||||
LET Session <= shell_session(name=FLOWID, argv=["bash"])
|
||||
|
||||
LET _ <= shell_session_control(name=FLOWID, stdin=Command + "\n")
|
||||
|
||||
SELECT * FROM if(condition=NOT Session.IsRunning,
|
||||
then={
|
||||
SELECT Stdin AS Command,
|
||||
timestamp(epoch=now(ns=TRUE)) AS Timestamp,
|
||||
Stdout,
|
||||
NULL AS StdoutUpload,
|
||||
Stderr,
|
||||
NULL AS StderrUpload
|
||||
FROM foreach(row=Session.Query)
|
||||
})
|
||||
|
||||
notebook:
|
||||
- type: vql
|
||||
name: Transcript
|
||||
template: |
|
||||
/*
|
||||
# View session transcript
|
||||
*/
|
||||
LET Transcript = pipe(query={
|
||||
SELECT Command + Stdout AS Line
|
||||
FROM source()
|
||||
WHERE Line
|
||||
})
|
||||
|
||||
SELECT upload(accessor="pipe",
|
||||
file="Transcript", name="transcript.txt") AS Transcript
|
||||
FROM scope()
|
||||
|
||||
- precondition: |
|
||||
SELECT * FROM info() WHERE NOT version(function="shell_session")
|
||||
notebook:
|
||||
- type: none
|
||||
query: |
|
||||
LET SizeLimit <= 4096
|
||||
LET Now <= str(str=now())
|
||||
LET Now = str(str=now())
|
||||
|
||||
LET Output = SELECT "" AS Command,
|
||||
timestamp(epoch=now()) AS Timestamp,
|
||||
@@ -51,6 +97,15 @@ sources:
|
||||
FROM scope()
|
||||
}, b=Output)
|
||||
|
||||
resources:
|
||||
# By default the shell session is up for an hour or until cancelled
|
||||
# by the GUI.
|
||||
timeout: 3600
|
||||
|
||||
column_types:
|
||||
- name: StdoutUpload
|
||||
type: preview_upload
|
||||
- name: StderrUpload
|
||||
type: preview_upload
|
||||
- name: Transcript
|
||||
type: preview_upload
|
||||
|
||||
@@ -21,6 +21,11 @@ tools:
|
||||
serve_locally: true
|
||||
version: <VERSION>
|
||||
|
||||
- name: VelociraptorLinuxSumo
|
||||
url: https://github.com/Velocidex/velociraptor/releases/download/v<VERSION_BARE>/velociraptor-v<VERSION>-linux-amd64-sumo-musl
|
||||
serve_locally: true
|
||||
version: <VERSION>
|
||||
|
||||
# On MacOS we cannot embed the config in the binary so we use a
|
||||
# shell script stub instead. See
|
||||
# https://github.com/Velocidex/velociraptor/issues/2898
|
||||
|
||||
@@ -21,6 +21,11 @@ tools:
|
||||
serve_locally: true
|
||||
version: 0.76.5
|
||||
|
||||
- name: VelociraptorLinuxSumo
|
||||
url: https://github.com/Velocidex/velociraptor/releases/download/v0.76/velociraptor-v0.76.5-linux-amd64-sumo-musl
|
||||
serve_locally: true
|
||||
version: 0.76.5
|
||||
|
||||
# On MacOS we cannot embed the config in the binary so we use a
|
||||
# shell script stub instead. See
|
||||
# https://github.com/Velocidex/velociraptor/issues/2898
|
||||
|
||||
@@ -2,9 +2,10 @@ name: Server.Utils.CreateLinuxPackages
|
||||
description: |
|
||||
Builds Debian (.deb) and RPM packages with embedded client config for the current organization.
|
||||
|
||||
This artifact depends on the following tool:
|
||||
This artifact depends on the following tools:
|
||||
|
||||
* <velo-tool-viewer name="VelociraptorLinux" />
|
||||
* Clients <velo-tool-viewer name="VelociraptorLinux" />
|
||||
* Server <velo-tool-viewer name="VelociraptorLinuxSumo" />
|
||||
|
||||
You can replace this with suitable Velociraptor Linux build, or else
|
||||
the current release binary will be used by default.
|
||||
|
||||
@@ -20,15 +20,57 @@ description: |
|
||||
required_permissions:
|
||||
- EXECVE
|
||||
|
||||
precondition:
|
||||
SELECT OS From info() where OS = 'windows'
|
||||
|
||||
parameters:
|
||||
- name: Command
|
||||
default: "dir C:\\"
|
||||
|
||||
sources:
|
||||
- query: |
|
||||
- precondition: |
|
||||
SELECT * FROM info()
|
||||
WHERE OS = 'windows' AND version(function="shell_session")
|
||||
query: |
|
||||
// Get the core flow id to key a unique session off.
|
||||
LET FLOWID <= split(string=_SessionId, sep="/")[0]
|
||||
|
||||
// Newer clients have support for true shell sessions.
|
||||
LET Session <= shell_session(name=FLOWID, argv=["cmd.exe"])
|
||||
|
||||
LET _ <= shell_session_control(name=FLOWID, stdin=Command + "\n")
|
||||
|
||||
SELECT * FROM if(condition=NOT Session.IsRunning,
|
||||
then={
|
||||
SELECT Stdin AS Command,
|
||||
timestamp(epoch=now(ns=TRUE)) AS Timestamp,
|
||||
Stdout,
|
||||
NULL AS StdoutUpload,
|
||||
Stderr,
|
||||
NULL AS StderrUpload
|
||||
FROM foreach(row=Session.Query)
|
||||
})
|
||||
|
||||
notebook:
|
||||
- type: vql
|
||||
name: Transcript
|
||||
template: |
|
||||
/*
|
||||
# View session transcript
|
||||
*/
|
||||
LET Transcript = pipe(query={
|
||||
SELECT Command + Stdout AS Line
|
||||
FROM source()
|
||||
WHERE Line
|
||||
})
|
||||
|
||||
SELECT upload(accessor="pipe",
|
||||
file="Transcript", name="transcript.txt") AS Transcript
|
||||
FROM scope()
|
||||
|
||||
- precondition: |
|
||||
SELECT * FROM info()
|
||||
WHERE OS = "windows" AND NOT version(function="shell_session")
|
||||
notebook:
|
||||
- type: none
|
||||
query: |
|
||||
LET SizeLimit <= 4096
|
||||
LET Now <= str(str=now())
|
||||
|
||||
@@ -61,3 +103,7 @@ sources:
|
||||
column_types:
|
||||
- name: StdoutUpload
|
||||
type: preview_upload
|
||||
- name: StderrUpload
|
||||
type: preview_upload
|
||||
- name: Transcript
|
||||
type: preview_upload
|
||||
|
||||
@@ -31,9 +31,6 @@ description: |
|
||||
required_permissions:
|
||||
- EXECVE
|
||||
|
||||
precondition:
|
||||
SELECT OS From info() where OS = 'windows'
|
||||
|
||||
parameters:
|
||||
- name: Command
|
||||
default: "dir C:/"
|
||||
@@ -41,7 +38,53 @@ parameters:
|
||||
default: "C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe"
|
||||
|
||||
sources:
|
||||
- query: |
|
||||
- precondition: |
|
||||
SELECT * FROM info()
|
||||
WHERE OS = 'windows' AND version(function="shell_session")
|
||||
query: |
|
||||
// Get the core flow id to key a unique session off.
|
||||
LET FLOWID <= split(string=_SessionId, sep="/")[0]
|
||||
|
||||
// Newer clients have support for true shell sessions.
|
||||
LET Session <= shell_session(name=FLOWID,
|
||||
argv=[PowerShellExe, "-ExecutionPolicy", "Unrestricted"])
|
||||
|
||||
LET _ <= shell_session_control(name=FLOWID, stdin=Command + "\n")
|
||||
|
||||
SELECT * FROM if(condition=NOT Session.IsRunning,
|
||||
then={
|
||||
SELECT Stdin AS Command,
|
||||
timestamp(epoch=now(ns=TRUE)) AS Timestamp,
|
||||
Stdout,
|
||||
NULL AS StdoutUpload,
|
||||
Stderr,
|
||||
NULL AS StderrUpload
|
||||
FROM foreach(row=Session.Query)
|
||||
})
|
||||
|
||||
notebook:
|
||||
- type: vql
|
||||
name: Transcript
|
||||
template: |
|
||||
/*
|
||||
# View session transcript
|
||||
*/
|
||||
LET Transcript = pipe(query={
|
||||
SELECT Stdout AS Line
|
||||
FROM source()
|
||||
WHERE Line
|
||||
})
|
||||
|
||||
SELECT upload(accessor="pipe",
|
||||
file="Transcript", name="transcript.txt") AS Transcript
|
||||
FROM scope()
|
||||
|
||||
- precondition: |
|
||||
SELECT * FROM info()
|
||||
WHERE OS = "windows" AND NOT version(function="shell_session")
|
||||
notebook:
|
||||
- type: none
|
||||
query: |
|
||||
LET SizeLimit <= 4096
|
||||
LET Now <= str(str=now())
|
||||
|
||||
@@ -81,3 +124,5 @@ column_types:
|
||||
type: preview_upload
|
||||
- name: StderrUpload
|
||||
type: preview_upload
|
||||
- name: Transcript
|
||||
type: preview_upload
|
||||
|
||||
+7
File diff suppressed because one or more lines are too long
+3
-3
@@ -48,7 +48,7 @@ var (
|
||||
config_command = app.Command(
|
||||
"config", "Manipulate the configuration.")
|
||||
|
||||
config_command_org = config_command.Flag(
|
||||
org_id = app.Flag(
|
||||
"org", "Org ID to show").String()
|
||||
|
||||
config_show_command = config_command.Command(
|
||||
@@ -163,7 +163,7 @@ func doShowConfig() error {
|
||||
}
|
||||
defer sm.Close()
|
||||
|
||||
config_obj, err = maybeGetOrgConfig(*config_command_org, config_obj)
|
||||
config_obj, err = maybeGetOrgConfig(*org_id, config_obj)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -342,7 +342,7 @@ func doDumpClientConfig() error {
|
||||
}
|
||||
defer sm.Close()
|
||||
|
||||
config_obj, err = maybeGetOrgConfig(*config_command_org, config_obj)
|
||||
config_obj, err = maybeGetOrgConfig(*org_id, config_obj)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -151,6 +151,11 @@ func doLS(path, accessor string) error {
|
||||
}
|
||||
defer sm.Close()
|
||||
|
||||
config_obj, err = maybeGetOrgConfig(*org_id, config_obj)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
matches := accessor_reg.FindStringSubmatch(path)
|
||||
if matches != nil {
|
||||
accessor = matches[1]
|
||||
@@ -219,6 +224,11 @@ func doRM(path, accessor string) error {
|
||||
}
|
||||
defer sm.Close()
|
||||
|
||||
config_obj, err = maybeGetOrgConfig(*org_id, config_obj)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
matches := accessor_reg.FindStringSubmatch(path)
|
||||
if matches != nil {
|
||||
accessor = matches[1]
|
||||
@@ -281,6 +291,11 @@ func doCp(path, accessor string, dump_dir string) error {
|
||||
}
|
||||
defer sm.Close()
|
||||
|
||||
config_obj, err = maybeGetOrgConfig(*org_id, config_obj)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
matches := accessor_reg.FindStringSubmatch(path)
|
||||
if matches != nil {
|
||||
accessor = matches[1]
|
||||
@@ -385,6 +400,11 @@ func doCat(path, accessor_name string) error {
|
||||
}
|
||||
defer sm.Close()
|
||||
|
||||
config_obj, err = maybeGetOrgConfig(*org_id, config_obj)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
logger := &LogWriter{config_obj: config_obj}
|
||||
builder := services.ScopeBuilder{
|
||||
Config: config_obj,
|
||||
|
||||
+1
-3
@@ -33,8 +33,6 @@ var (
|
||||
grant_command = acl_command.Command(
|
||||
"grant", "Grant a principal a policy.")
|
||||
|
||||
grant_command_org = grant_command.Flag("org", "OrgID to grant").String()
|
||||
|
||||
grant_command_principal = grant_command.Arg(
|
||||
"principal", "Name of principal (User or cert) to grant.").
|
||||
Required().String()
|
||||
@@ -88,7 +86,7 @@ func doGrant() error {
|
||||
return err
|
||||
}
|
||||
|
||||
org_config_obj, err := org_manager.GetOrgConfig(*grant_command_org)
|
||||
org_config_obj, err := org_manager.GetOrgConfig(*org_id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
+3
-7
@@ -60,10 +60,6 @@ var (
|
||||
"timeout", "Time collection out after this many seconds.").
|
||||
Default("0").Float64()
|
||||
|
||||
query_org_id = query.Flag(
|
||||
"org", "The Org ID to target with this query").
|
||||
Default("root").String()
|
||||
|
||||
query_command_collect_cpu_limit = query.Flag(
|
||||
"cpu_limit", "A number between 0 to 100 representing maximum CPU utilization.").
|
||||
Default("0").Float64()
|
||||
@@ -312,16 +308,16 @@ func doQuery() error {
|
||||
logging.GetLogger(config_obj, &logging.ToolComponent).
|
||||
Info("API Client configuration loaded - will make gRPC connection.")
|
||||
return doRemoteQuery(
|
||||
config_obj, *format, *query_org_id, vql_queries, env)
|
||||
config_obj, *format, *org_id, vql_queries, env)
|
||||
}
|
||||
|
||||
if *query_org_id != "" {
|
||||
if *org_id != "" {
|
||||
org_manager, err := services.GetOrgManager()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
org_config_obj, err := org_manager.GetOrgConfig(*query_org_id)
|
||||
org_config_obj, err := org_manager.GetOrgConfig(*org_id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -1345,12 +1345,16 @@
|
||||
you will often need to specify a field name containing full
|
||||
stop. You can escape this using the backticks like the example above.
|
||||
type: Function
|
||||
version: 2
|
||||
version: 3
|
||||
args:
|
||||
- name: client_id
|
||||
type: string
|
||||
description: The client id to schedule a collection on
|
||||
required: true
|
||||
- name: flow_id
|
||||
type: string
|
||||
description: If a flow id is specified we do not create a new flow, but instead
|
||||
add the collection to this flow.
|
||||
- name: artifacts
|
||||
type: string
|
||||
description: A list of artifacts to collect
|
||||
@@ -2240,15 +2244,20 @@
|
||||
- windows_386_cgo
|
||||
- windows_amd64_cgo
|
||||
- name: encode
|
||||
description: Encodes a string as a different type. Currently supported types include
|
||||
'hex', 'base64'.
|
||||
description: |
|
||||
Encode an object as a string.
|
||||
|
||||
This function is an alias to `serialize()`
|
||||
type: Function
|
||||
version: 3
|
||||
args:
|
||||
- name: string
|
||||
type: Any
|
||||
description: The item to encode
|
||||
required: true
|
||||
- name: type
|
||||
type: string
|
||||
description: Encoding format (csv,json,yaml,hex,base64)
|
||||
required: true
|
||||
category: encode
|
||||
platforms:
|
||||
@@ -6307,6 +6316,11 @@
|
||||
|
||||
- [timestamp]({{< ref "/vql_reference/popular/timestamp/" >}})
|
||||
type: Function
|
||||
version: 2
|
||||
args:
|
||||
- name: ns
|
||||
type: bool
|
||||
description: Return the time in ns
|
||||
category: popular
|
||||
platforms:
|
||||
- darwin_amd64_cgo
|
||||
@@ -6514,7 +6528,7 @@
|
||||
type: Plugin
|
||||
category: developer
|
||||
metadata:
|
||||
permissions: MACHINE_STATE
|
||||
permissions: SERVER_ADMIN
|
||||
platforms:
|
||||
- darwin_amd64_cgo
|
||||
- darwin_arm64_cgo
|
||||
@@ -9704,6 +9718,8 @@
|
||||
WHERE DNSInfo AND Name
|
||||
})
|
||||
```
|
||||
|
||||
NOTE: An alternative to this plugin is a `Sigma Correlation rule`
|
||||
type: Plugin
|
||||
args:
|
||||
- name: query
|
||||
@@ -9736,9 +9752,12 @@
|
||||
example:
|
||||
|
||||
```vql
|
||||
SELECT * FROM Artifact.Linux.Search.FileFinder(SearchFilesGlobTable=serialize(format="csv",item=tlist),...)
|
||||
SELECT * FROM Artifact.Linux.Search.FileFinder(
|
||||
SearchFilesGlobTable=serialize(format="csv",item=tlist),...)
|
||||
```
|
||||
will pass a list in CSV format to the artifact's `SearchFilesGlobTable` parameter.
|
||||
|
||||
will pass a list in CSV format to the artifact's
|
||||
`SearchFilesGlobTable` parameter.
|
||||
|
||||
### See also
|
||||
|
||||
@@ -9874,6 +9893,55 @@
|
||||
- linux_amd64_cgo
|
||||
- windows_386_cgo
|
||||
- windows_amd64_cgo
|
||||
- name: shell_session
|
||||
description: Recreate or retrieve a shell session handle.
|
||||
type: Function
|
||||
version: 1
|
||||
args:
|
||||
- name: argv
|
||||
type: string
|
||||
description: Argv to run the command with.
|
||||
repeated: true
|
||||
required: true
|
||||
- name: env
|
||||
type: ordereddict.Dict
|
||||
description: Environment variables to launch with.
|
||||
- name: cwd
|
||||
type: string
|
||||
description: If specified we change to this working directory first.
|
||||
- name: secret
|
||||
type: string
|
||||
description: The name of a secret to use.
|
||||
- name: name
|
||||
type: string
|
||||
description: The name of the shell session. If the session already exists, we
|
||||
just return a handle to it.
|
||||
metadata:
|
||||
permissions: EXECVE
|
||||
platforms:
|
||||
- linux_amd64_cgo
|
||||
- name: shell_session_control
|
||||
description: Control a previously created shell session.
|
||||
type: Function
|
||||
version: 1
|
||||
args:
|
||||
- name: name
|
||||
type: string
|
||||
description: The name of the shell session.
|
||||
- name: stdin
|
||||
type: string
|
||||
description: Write this string to the session stdin.
|
||||
- name: close_stdin
|
||||
type: bool
|
||||
description: If specified we close the stdin of the session. This will usually
|
||||
terminate the session gracefully.
|
||||
- name: kill
|
||||
type: bool
|
||||
description: If specified we kill the session.
|
||||
metadata:
|
||||
permissions: EXECVE
|
||||
platforms:
|
||||
- linux_amd64_cgo
|
||||
- name: sigma
|
||||
description: |
|
||||
Evaluate sigma rules.
|
||||
|
||||
@@ -102,11 +102,6 @@ func getTypeFromComponents(components []string) ([]string, api.PathType) {
|
||||
return components, api.PATH_TYPE_FILESTORE_ANY
|
||||
}
|
||||
|
||||
// Client uploads are all untyped
|
||||
if len(components) > 4 && components[0] == "clients" {
|
||||
return components, api.PATH_TYPE_FILESTORE_ANY
|
||||
}
|
||||
|
||||
last_component := components[len(components)-1]
|
||||
|
||||
// Fallback, use the extension to deduce the type.
|
||||
|
||||
@@ -44,6 +44,31 @@ func TestFromGenericComponentList(t *testing.T) {
|
||||
client_path: "/notebooks/N.D55OJV2COB544/NC.D56I6S6LK00FI-D56I71V0BJULE/uploads/data/file.zip",
|
||||
path_type: api.PATH_TYPE_FILESTORE_ANY,
|
||||
},
|
||||
{
|
||||
// Client collection uploads
|
||||
components: []string{
|
||||
"clients", "C.d7f8859f5e0e01f7", "collections", "F.D55T34A0NIDTC",
|
||||
"uploads", "data", "file.json"},
|
||||
client_path: "/clients/C.d7f8859f5e0e01f7/collections/F.D55T34A0NIDTC/uploads/data/file.json",
|
||||
path_type: api.PATH_TYPE_FILESTORE_ANY,
|
||||
},
|
||||
{
|
||||
// Client collection jsonl logs
|
||||
components: []string{
|
||||
"clients", "C.d7f8859f5e0e01f7",
|
||||
"collections", "F.D55T34A0NIDTC", "logs.json"},
|
||||
client_path: "/clients/C.d7f8859f5e0e01f7/collections/F.D55T34A0NIDTC/logs.json",
|
||||
path_type: api.PATH_TYPE_FILESTORE_JSON,
|
||||
},
|
||||
{
|
||||
// Client collection jsonl logs
|
||||
components: []string{
|
||||
"clients", "C.d7f8859f5e0e01f7",
|
||||
"artifacts", "System.VFS.ListDirectory",
|
||||
"F.D55T34A0NIDTC", "Stats.json"},
|
||||
client_path: "/clients/C.d7f8859f5e0e01f7/artifacts/System.VFS.ListDirectory/F.D55T34A0NIDTC/Stats.json",
|
||||
path_type: api.PATH_TYPE_FILESTORE_JSON,
|
||||
},
|
||||
{
|
||||
// Client notebook attachments are always PATH_TYPE_FILESTORE_ANY
|
||||
components: []string{
|
||||
|
||||
@@ -312,7 +312,8 @@ func (self *ClientFlowRunner) ProcessSingleMessage(
|
||||
}
|
||||
|
||||
if msg.LogMessage != nil {
|
||||
err := self.LogMessage(ctx, client_id, flow_id, msg.LogMessage)
|
||||
err := self.LogMessage(
|
||||
ctx, client_id, flow_id, child_flow_id, msg.LogMessage)
|
||||
if err != nil {
|
||||
return fmt.Errorf("LogMessage: %w", err)
|
||||
}
|
||||
@@ -824,7 +825,7 @@ func (self *ClientFlowRunner) processAlert(
|
||||
}
|
||||
|
||||
func (self *ClientFlowRunner) LogMessage(
|
||||
ctx context.Context, client_id, flow_id string,
|
||||
ctx context.Context, client_id, flow_id, child_flow_id string,
|
||||
msg *crypto_proto.LogMessage) error {
|
||||
|
||||
flow_path_manager := paths.NewFlowPathManager(client_id, flow_id).Log()
|
||||
@@ -840,7 +841,14 @@ func (self *ClientFlowRunner) LogMessage(
|
||||
}
|
||||
defer rs_writer.Close()
|
||||
|
||||
_ = rs_writer.SetStartRow(int64(msg.Id))
|
||||
// If this is a child flow, append to the result set's end
|
||||
// otherwise, write at the specified offset.
|
||||
if child_flow_id == "" {
|
||||
_ = rs_writer.SetStartRow(int64(msg.Id))
|
||||
|
||||
} else {
|
||||
_ = rs_writer.SetStartRow(rs_writer.TotalRows())
|
||||
}
|
||||
|
||||
// The JSON payload from the client.
|
||||
payload := artifacts.DeobfuscateString(self.config_obj, msg.Jsonl)
|
||||
|
||||
@@ -22,6 +22,7 @@ import FullScreenTable from './components/notebooks/table_view.jsx';
|
||||
import FullScreenNotebook from './components/notebooks/full_notebook.jsx';
|
||||
import FullScreenHuntNotebook from './components/hunts/hunt-full-notebook.jsx';
|
||||
import FullScreenFlowNotebook from './components/flows/flow-full-notebook.jsx';
|
||||
import { ShellViewerFullScreen } from "./components/clients/shell-viewer.jsx";
|
||||
import ArtifactInspector from './components/artifacts/artifacts.jsx';
|
||||
import UserInspector from './components/users/user-inspector.jsx';
|
||||
import VeloHunts from './components/hunts/hunts.jsx';
|
||||
@@ -44,7 +45,7 @@ import Navbar from 'react-bootstrap/Navbar';
|
||||
import Nav from 'react-bootstrap/Nav';
|
||||
|
||||
import SidebarKeyNavigator from './components/sidebar/hotkeys.jsx';
|
||||
import {getItem, setItem, schema} from './components/core/storage.jsx';
|
||||
import {setItem, schema} from './components/core/storage.jsx';
|
||||
|
||||
import './themes/no-theme.css';
|
||||
import './themes/veloci-light.css';
|
||||
@@ -253,6 +254,10 @@ class App extends Component {
|
||||
<SnackbarProvider>
|
||||
<SidebarKeyNavigator client={this.state.client}/>
|
||||
<Switch>
|
||||
<Route path="/fullscreen/shell/:artifact/:client_id/:flow_id">
|
||||
<ShellViewerFullScreen />
|
||||
</Route>
|
||||
|
||||
<Route path="/fullscreen/notebooks/:notebook_id">
|
||||
<FullScreenNotebook />
|
||||
</Route>
|
||||
|
||||
@@ -9,7 +9,6 @@ import Col from 'react-bootstrap/Col';
|
||||
import Button from 'react-bootstrap/Button';
|
||||
import InputGroup from 'react-bootstrap/InputGroup';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import classNames from "classnames";
|
||||
import api from '../core/api-service.jsx';
|
||||
import {CancelToken} from 'axios';
|
||||
import T from '../i8n/i8n.jsx';
|
||||
@@ -71,7 +70,6 @@ export default class ArtifactsUpload extends React.Component {
|
||||
{file: this.state.pack_file},
|
||||
{name: "ArtifactPack", type: "upload_file"}).then(
|
||||
response=>{
|
||||
let url = response.data.url;
|
||||
this.setState({
|
||||
loading: false,
|
||||
upload_info: response.data,
|
||||
@@ -114,7 +112,6 @@ export default class ArtifactsUpload extends React.Component {
|
||||
vfs_path: response.data.vfs_path,
|
||||
uploaded: uploaded});
|
||||
}).catch(err=>{
|
||||
console.log(err);
|
||||
this.setState({loading: false});
|
||||
});
|
||||
};
|
||||
@@ -130,8 +127,6 @@ export default class ArtifactsUpload extends React.Component {
|
||||
};
|
||||
api.post("v1/LoadArtifactPack", request,
|
||||
this.source.token).then(response => {
|
||||
console.log(response);
|
||||
|
||||
if (response.data.cancel) {
|
||||
return ;
|
||||
}
|
||||
|
||||
@@ -4,8 +4,7 @@ import _ from 'lodash';
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {CancelToken} from 'axios';
|
||||
import { cleanupHTML } from '../core/sanitize.jsx';
|
||||
import parseHTML from '../core/sanitize.jsx';
|
||||
import parseHTML, { cleanupHTML } from '../core/sanitize.jsx';
|
||||
|
||||
import api from '../core/api-service.jsx';
|
||||
import { VeloLineChart, VeloTimeChart } from './line-charts.jsx';
|
||||
|
||||
@@ -409,9 +409,13 @@ class VeloHostInfo extends Component {
|
||||
}
|
||||
|
||||
if (this.state.mode === 'shell') {
|
||||
let system = this.props.client && this.props.client.os_info &&
|
||||
this.props.client.os_info.system;
|
||||
return (
|
||||
<div className="client-details shell">
|
||||
<ShellViewer client={this.props.client} />
|
||||
<ShellViewer
|
||||
system={system}
|
||||
client_id={this.props.client.client_id} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -25,6 +25,8 @@ class VeloClientSearch extends Component {
|
||||
static propTypes = {
|
||||
// Update the applications' search parameter.
|
||||
setSearch: PropTypes.func.isRequired,
|
||||
|
||||
history: PropTypes.any,
|
||||
};
|
||||
|
||||
componentDidMount = () => {
|
||||
|
||||
@@ -28,8 +28,14 @@
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.cell-toolbar.not_selected {
|
||||
display: none;
|
||||
.cell-toolbar {
|
||||
width: 100%;
|
||||
position: static;
|
||||
background: var(--color-canvas-background);
|
||||
}
|
||||
|
||||
.cell-toolbar .btn-toolbar {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.notebook-output.collapsed {
|
||||
@@ -46,6 +52,16 @@
|
||||
max-height: 200em;
|
||||
}
|
||||
|
||||
.shell-output-container {
|
||||
height: 100%;
|
||||
overflow-y: hidden;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.fullscreen .shell-output-container {
|
||||
height: 85vh;
|
||||
}
|
||||
|
||||
div.dt-buttons {
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
@@ -77,10 +93,6 @@ div.dt-buttons {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.shell-cell.expanded {
|
||||
max-height: 200em;
|
||||
}
|
||||
|
||||
.shell-command {
|
||||
padding: 5px;
|
||||
}
|
||||
@@ -93,10 +105,10 @@ div.dt-buttons {
|
||||
display: hidden;
|
||||
}
|
||||
|
||||
.notebook-output pre, .notebook-input pre {
|
||||
.shell-output-container pre {
|
||||
display: block;
|
||||
padding: 8px;
|
||||
margin: 0 0 8.5px;
|
||||
margin-bottom: 0px;
|
||||
font-size: 12px;
|
||||
line-height: 1.31;
|
||||
word-break: break-all;
|
||||
@@ -106,21 +118,17 @@ div.dt-buttons {
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.shell-output-container .notebook-input pre {
|
||||
.shell-output-container .accordion-header pre {
|
||||
color: var(--color-sidebar-disabled-foreground);
|
||||
margin: 0;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
.shell-output-container .notebook-output pre {
|
||||
.shell-output-container .accordion-body pre {
|
||||
color: var(--color-foreground);
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
.shell-results {
|
||||
overflow-y: none;
|
||||
}
|
||||
|
||||
.shell-command .velo-ace-editor {
|
||||
flex: auto;
|
||||
width: 1%;
|
||||
@@ -142,8 +150,42 @@ div.dt-buttons {
|
||||
display: inline-flex;
|
||||
}
|
||||
|
||||
.shell-info i {
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.shell-output-container {
|
||||
max-height: 50vh;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
.shell-output-container .row {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.shell-results {
|
||||
max-height: 70vh;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.shell-cell {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.stderr {
|
||||
color: var(--color-warning-color);
|
||||
}
|
||||
|
||||
.shell-vql-cell .velo-ace-editor {
|
||||
height: 38px;
|
||||
width: 70vw;
|
||||
}
|
||||
|
||||
.fullscreen .shell-vql-cell.shell-output-container {
|
||||
height: 65vh;
|
||||
}
|
||||
|
||||
.fullscreen .vql-shell-input.velo-ace-editor {
|
||||
height: 15vh;
|
||||
width: 70vw;
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -642,11 +642,12 @@ class VeloPagedTable extends Component {
|
||||
}
|
||||
|
||||
let page_size = getItem(schema.CurrentPageSizeKey) || 10;
|
||||
let transform = this.props.params && this.props.params.transform;
|
||||
this.setState({
|
||||
guid: getID(),
|
||||
page_size: page_size,
|
||||
transform: transform || {},
|
||||
});
|
||||
|
||||
this.fetchRows();
|
||||
}
|
||||
|
||||
|
||||
@@ -13,7 +13,6 @@ import VeloTimestamp from "../utils/time.jsx";
|
||||
import URLViewer from "../utils/url.jsx";
|
||||
import VeloAce, { SettingsButton } from '../core/ace.jsx';
|
||||
import VeloValueRenderer from '../utils/value.jsx';
|
||||
import { NavLink } from "react-router-dom";
|
||||
import ClientLink from '../clients/client-link.jsx';
|
||||
import FlowLink from '../flows/flow-link.jsx';
|
||||
import { HexViewPopup } from '../utils/hex.jsx';
|
||||
|
||||
@@ -62,10 +62,6 @@ class _UserSettings extends React.Component {
|
||||
|
||||
let current_theme = this.state.traits.theme;
|
||||
|
||||
this.setState({
|
||||
traits: traits,
|
||||
messages: response.data.messages || []});
|
||||
|
||||
// Only update the theme if it changed.
|
||||
if (current_theme !== traits.theme) {
|
||||
document.body.classList.remove('no-theme');
|
||||
@@ -86,6 +82,14 @@ class _UserSettings extends React.Component {
|
||||
document.body.classList.add("veloci-light");
|
||||
}
|
||||
}
|
||||
|
||||
if(_.isEqual(traits, this.state.traits)) {
|
||||
return;
|
||||
};
|
||||
|
||||
this.setState({
|
||||
traits: traits,
|
||||
messages: response.data.messages || []});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -16,7 +16,6 @@ import Alert from 'react-bootstrap/Alert';
|
||||
import Accordion from 'react-bootstrap/Accordion';
|
||||
import Badge from 'react-bootstrap/Badge';
|
||||
import parseHTML from '../core/sanitize.jsx';
|
||||
import { cleanupHTML } from '../core/sanitize.jsx';
|
||||
import hljs from 'highlight.js';
|
||||
import ToolViewer from '../tools/tool-viewer.jsx';
|
||||
import markdownit from 'markdown-it';
|
||||
@@ -34,6 +33,7 @@ class Admonition extends Component {
|
||||
static propTypes = {
|
||||
type: PropTypes.string,
|
||||
caption: PropTypes.string,
|
||||
children: PropTypes.node,
|
||||
}
|
||||
|
||||
renderType = ()=>{
|
||||
|
||||
@@ -48,7 +48,7 @@ export default class Image extends Component {
|
||||
|
||||
render() {
|
||||
return <>
|
||||
<a className="image-popup"
|
||||
<a className="image-popup" role="button"
|
||||
onClick={()=>this.setState({expanded: true})}>
|
||||
<span><img alt={this.props.alt} src={this.props.src} /></span>
|
||||
<span className="figcaption">{this.props.alt}</span>
|
||||
|
||||
@@ -21,7 +21,7 @@ import T from '../i8n/i8n.jsx';
|
||||
import ToolTip from '../widgets/tooltip.jsx';
|
||||
import VeloLog from "../widgets/logs.jsx";
|
||||
import { EditNotebook } from '../notebooks/new-notebook.jsx';
|
||||
import {getItem, setItem, schema} from '../core/storage.jsx';
|
||||
import {setItem, schema} from '../core/storage.jsx';
|
||||
import Select from 'react-select';
|
||||
import { withRouter } from "react-router-dom";
|
||||
|
||||
|
||||
@@ -10,14 +10,8 @@ import FlowLogs from './flow-logs.jsx';
|
||||
import FlowNotebook from './flow-notebook.jsx';
|
||||
import { withRouter } from "react-router-dom";
|
||||
import {getItem, setItem, schema} from '../core/storage.jsx';
|
||||
|
||||
// Typically subclassable by actual components.
|
||||
import api from '../core/api-service.jsx';
|
||||
|
||||
import {CancelToken} from 'axios';
|
||||
|
||||
const POLL_TIME = 5000;
|
||||
|
||||
class FlowInspector extends React.Component {
|
||||
static propTypes = {
|
||||
flow: PropTypes.object,
|
||||
|
||||
@@ -32,7 +32,6 @@ import DeleteNotebookDialog from '../notebooks/notebook-delete.jsx';
|
||||
import ExportNotebook from '../notebooks/export-notebook.jsx';
|
||||
import T from '../i8n/i8n.jsx';
|
||||
import UserConfig from '../core/user.jsx';
|
||||
import {getItem, setItem} from '../core/storage.jsx';
|
||||
import api from '../core/api-service.jsx';
|
||||
import {CancelToken} from 'axios';
|
||||
|
||||
|
||||
@@ -154,7 +154,7 @@ class NewNotebookLaunch extends React.Component {
|
||||
|
||||
export class NewNotebook extends React.Component {
|
||||
static propTypes = {
|
||||
notebook_paramerers: PropTypes.object,
|
||||
notebook_parameters: PropTypes.object,
|
||||
parameters: PropTypes.object,
|
||||
closeDialog: PropTypes.func.isRequired,
|
||||
updateNotebooks: PropTypes.func.isRequired,
|
||||
@@ -201,12 +201,10 @@ export class NewNotebook extends React.Component {
|
||||
}
|
||||
|
||||
setParameters = (params) => {
|
||||
console.log("setParameters", params);
|
||||
this.setState({parameters: params});
|
||||
}
|
||||
|
||||
setNotebookParameters = (params) => {
|
||||
console.log("setNotebookParameters", params);
|
||||
this.setState({notebook_parameters: params});
|
||||
}
|
||||
|
||||
|
||||
@@ -1,12 +1,9 @@
|
||||
import _ from 'lodash';
|
||||
|
||||
import "./notebook-navigator.css";
|
||||
import Button from 'react-bootstrap/Button';
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import T from '../i8n/i8n.jsx';
|
||||
import VeloTable, { getFormatter } from '../core/table.jsx';
|
||||
import VeloTimestamp from "../utils/time.jsx";
|
||||
import classNames from "classnames";
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { withRouter } from "react-router-dom";
|
||||
|
||||
@@ -184,10 +184,12 @@ export default class NotebookRenderer extends React.Component {
|
||||
}
|
||||
|
||||
getRef = cell_id=>{
|
||||
let res = this.state.refs[cell_id];
|
||||
let refs = this.state.refs;
|
||||
let res = refs[cell_id];
|
||||
if(!res) {
|
||||
res = React.createRef();
|
||||
this.state.refs[cell_id] = res;
|
||||
refs[cell_id] = res;
|
||||
this.setState({refs: refs});
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ import {CancelToken} from 'axios';
|
||||
import api from '../core/api-service.jsx';
|
||||
import Spinner from '../utils/spinner.jsx';
|
||||
import { withRouter } from "react-router-dom";
|
||||
import {getItem, setItem, schema} from '../core/storage.jsx';
|
||||
import {setItem, schema} from '../core/storage.jsx';
|
||||
|
||||
// Poll for new notebooks list.
|
||||
const POLL_TIME = 5000;
|
||||
|
||||
@@ -79,7 +79,7 @@ class ServerInfo extends Component {
|
||||
return (
|
||||
<div className="client-details shell">
|
||||
<ShellViewer
|
||||
client={{client_id: "server"}}
|
||||
client_id={"server"}
|
||||
default_shell="Bash"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -10,7 +10,7 @@ import classNames from "classnames";
|
||||
import { NavLink } from "react-router-dom";
|
||||
import T from '../i8n/i8n.jsx';
|
||||
import ToolTip from '../widgets/tooltip.jsx';
|
||||
import {getItem, setItem, schema} from '../core/storage.jsx';
|
||||
import {getItem, schema} from '../core/storage.jsx';
|
||||
|
||||
import { EncodePathInURL } from '../utils/paths.jsx';
|
||||
|
||||
@@ -126,11 +126,6 @@ class VeloNavigator extends Component {
|
||||
disabled = "disabled";
|
||||
};
|
||||
|
||||
let vfs_path = "";
|
||||
if (this.props.vfs_path.length) {
|
||||
vfs_path = this.props.vfs_path;
|
||||
}
|
||||
|
||||
// Only show the user management screen if the user is an
|
||||
// admin in this org.
|
||||
let user_is_admin = this.context.traits &&
|
||||
|
||||
@@ -1,13 +1,9 @@
|
||||
import _ from 'lodash';
|
||||
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import Button from 'react-bootstrap/Button';
|
||||
import UserConfig from '../core/user.jsx';
|
||||
import Modal from 'react-bootstrap/Modal';
|
||||
import T from '../i8n/i8n.jsx';
|
||||
import Alert from 'react-bootstrap/Alert';
|
||||
import VeloLog from "../widgets/logs.jsx";
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import VeloPagedTable from '../core/paged-table.jsx';
|
||||
import ButtonGroup from 'react-bootstrap/ButtonGroup';
|
||||
|
||||
@@ -21,7 +21,7 @@ import {CancelToken} from 'axios';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import VeloForm from "../forms/form.jsx";
|
||||
import FlowLink from "../flows/flow-link.jsx";
|
||||
import {getItem, setItem, schema} from '../core/storage.jsx';
|
||||
import {setItem, schema} from '../core/storage.jsx';
|
||||
|
||||
import T from '../i8n/i8n.jsx';
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ import { EncodePathInURL, DecodePathInURL, SplitPathComponents, Join } from '../
|
||||
import api from '../core/api-service.jsx';
|
||||
import { withRouter } from "react-router-dom";
|
||||
import { JSONparse } from '../utils/json_parse.jsx';
|
||||
import {getItem, setItem, schema} from '../core/storage.jsx';
|
||||
import {setItem, schema} from '../core/storage.jsx';
|
||||
|
||||
class VeloFileTree extends Component {
|
||||
static contextType = UserConfig;
|
||||
|
||||
@@ -415,7 +415,7 @@ class InspectDialog extends React.Component {
|
||||
size={this.props.size}/>
|
||||
}
|
||||
</Tab>
|
||||
<Tab eventKey="details" title={T("Details")}>
|
||||
<Tab eventKey="details" title={T("Download")}>
|
||||
{ this.state.tab === "details" &&
|
||||
<>
|
||||
<Download fs_components={components}
|
||||
|
||||
@@ -29,6 +29,7 @@ import { faHome, faCrosshairs, faWrench, faEye, faServer, faBook, faLaptop,
|
||||
faChevronRight, faEllipsis, faLayerGroup, faBullseye, faPersonRunning,
|
||||
faQuestion, faCalendarPlus, faForwardFast, faBackwardFast, faSliders,
|
||||
faRepeat, faBorderAll, faBell, faCircleQuestion, faLightbulb, faBomb,
|
||||
faAngleDown, faAngleUp,
|
||||
} from '@fortawesome/free-solid-svg-icons';
|
||||
|
||||
import { faSquare, faSquareCheck, faSquareMinus,
|
||||
@@ -54,7 +55,7 @@ library.add(faHome, faCrosshairs, faWrench, faEye, faServer, faBook, faLaptop,
|
||||
faEllipsis, faLayerGroup, faBullseye, faPersonRunning, faQuestion,
|
||||
faCalendarPlus, faForwardFast, faBackwardFast, faSquareCheck,
|
||||
faSquare, faSquareMinus, faSliders, faRepeat, faBorderAll, faBell,
|
||||
faCircleQuestion, faLightbulb, faBomb,
|
||||
faCircleQuestion, faLightbulb, faBomb, faAngleDown, faAngleUp,
|
||||
);
|
||||
|
||||
ReactDOM.render(
|
||||
|
||||
+17
-11
@@ -69,7 +69,7 @@ type FlowContext struct {
|
||||
error_message string // If an error occurs trap the error message
|
||||
|
||||
last_stats_timestamp uint64
|
||||
frequency_msec uint64
|
||||
frequency_ns uint64
|
||||
|
||||
// We ensure to only send the final flow complete message
|
||||
// once. This will trigger a System.Flow.Completion event on the
|
||||
@@ -93,26 +93,32 @@ func newFlowContext(ctx context.Context,
|
||||
|
||||
// How often do we send a FlowStat message to sync the server's
|
||||
// flow progress stat.
|
||||
frequency_msec := uint64(5000)
|
||||
frequency := 5 * time.Second
|
||||
if config_obj != nil &&
|
||||
config_obj.Client != nil &&
|
||||
config_obj.Client.DefaultServerFlowStatsUpdate > 0 {
|
||||
frequency_msec = config_obj.Client.DefaultServerFlowStatsUpdate
|
||||
frequency = time.Second * time.Duration(
|
||||
config_obj.Client.DefaultServerFlowStatsUpdate)
|
||||
}
|
||||
|
||||
if req.FlowRequest.FlowUpdateTime > 0 {
|
||||
frequency_msec = req.FlowRequest.FlowUpdateTime
|
||||
frequency = time.Second * time.Duration(
|
||||
req.FlowRequest.FlowUpdateTime)
|
||||
}
|
||||
|
||||
// Default is set by config file
|
||||
batch_delay := uint64(5000)
|
||||
batch_delay := 5 * time.Second
|
||||
if config_obj != nil &&
|
||||
config_obj.Frontend != nil &&
|
||||
config_obj.Frontend.Resources != nil &&
|
||||
config_obj.Frontend.Resources.DefaultLogBatchTime > 0 {
|
||||
batch_delay = config_obj.Frontend.Resources.DefaultLogBatchTime
|
||||
batch_delay = time.Millisecond *
|
||||
time.Duration(config_obj.Frontend.Resources.DefaultLogBatchTime)
|
||||
}
|
||||
|
||||
if req.FlowRequest.LogBatchTime > 0 {
|
||||
batch_delay = req.FlowRequest.LogBatchTime
|
||||
batch_delay = time.Millisecond *
|
||||
time.Duration(req.FlowRequest.LogBatchTime)
|
||||
}
|
||||
|
||||
// Allow the flow to be cancelled.
|
||||
@@ -123,7 +129,7 @@ func newFlowContext(ctx context.Context,
|
||||
wg: &sync.WaitGroup{},
|
||||
output: output,
|
||||
req: req.FlowRequest,
|
||||
frequency_msec: frequency_msec,
|
||||
frequency_ns: uint64(frequency.Nanoseconds()),
|
||||
config_obj: config_obj,
|
||||
flow_id: flow_id,
|
||||
owner: owner,
|
||||
@@ -140,7 +146,7 @@ func newFlowContext(ctx context.Context,
|
||||
case <-sub_ctx.Done():
|
||||
return
|
||||
|
||||
case <-time.After(time.Duration(batch_delay) * time.Millisecond):
|
||||
case <-time.After(batch_delay):
|
||||
stats := self.MaybeSendStats()
|
||||
if stats != nil {
|
||||
select {
|
||||
@@ -436,10 +442,10 @@ func (self *FlowContext) MaybeSendStats() *crypto_proto.VeloMessage {
|
||||
return nil
|
||||
}
|
||||
|
||||
now := uint64(utils.GetTime().Now().UnixNano() / 1000)
|
||||
now := uint64(utils.GetTime().Now().UnixNano())
|
||||
last_timestamp := self.last_stats_timestamp
|
||||
if self.isFlowComplete() ||
|
||||
now-last_timestamp > self.frequency_msec {
|
||||
now-last_timestamp > self.frequency_ns {
|
||||
self.last_stats_timestamp = now
|
||||
return self.getStats()
|
||||
}
|
||||
|
||||
@@ -125,6 +125,10 @@ func (self *ResultSetWriterImpl) WriteJSONL(serialized []byte, total_rows uint64
|
||||
total_rows = countLines(serialized)
|
||||
}
|
||||
|
||||
if total_rows > constants.MAX_ROW_LIMIT {
|
||||
return utils.MemoryError
|
||||
}
|
||||
|
||||
if len(serialized) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -118,7 +118,9 @@ package launcher
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/go-errors/errors"
|
||||
"google.golang.org/protobuf/proto"
|
||||
@@ -200,9 +202,10 @@ func (self *Launcher) CompileCollectorArgs(
|
||||
|
||||
// Batching control
|
||||
var local_cpu_limit float32
|
||||
var max_batch_wait, max_batch_rows uint64
|
||||
var max_batch_wait uint64 // seconds
|
||||
var max_batch_rows uint64
|
||||
var max_batch_row_buffer uint64
|
||||
var local_timeout uint64
|
||||
var local_timeout uint64 // seconds
|
||||
|
||||
if config_obj != nil && config_obj.Defaults != nil {
|
||||
max_batch_rows = config_obj.Defaults.MaxRows
|
||||
@@ -211,7 +214,8 @@ func (self *Launcher) CompileCollectorArgs(
|
||||
}
|
||||
|
||||
if collector_request.AllowCustomOverrides {
|
||||
artifact, _ = repository.Get(ctx, config_obj, "Custom."+spec.Artifact)
|
||||
artifact, _ = repository.Get(
|
||||
ctx, config_obj, "Custom."+spec.Artifact)
|
||||
}
|
||||
|
||||
if artifact == nil {
|
||||
@@ -666,7 +670,12 @@ func (self *Launcher) WriteArtifactCollectionRecord(
|
||||
// The session_id we send to the client.
|
||||
var client_session_id string
|
||||
|
||||
if session_id == "" {
|
||||
// If the session_id starts with a / it is a relative child flow.
|
||||
if strings.HasPrefix(session_id, "/S") {
|
||||
session_id = utils.NewFlowId(client_id)
|
||||
client_session_id = session_id + "/S"
|
||||
|
||||
} else if session_id == "" {
|
||||
session_id = utils.NewFlowId(client_id)
|
||||
client_session_id = session_id
|
||||
|
||||
@@ -686,13 +695,15 @@ func (self *Launcher) WriteArtifactCollectionRecord(
|
||||
}
|
||||
|
||||
// How long to batch log messages for on the client.
|
||||
batch_delay := uint64(2000)
|
||||
batch_delay := time.Second * 2
|
||||
if collector_request.LogBatchTime > 0 {
|
||||
batch_delay = collector_request.LogBatchTime
|
||||
batch_delay = time.Second * time.Duration(
|
||||
collector_request.LogBatchTime)
|
||||
} else if config_obj.Frontend != nil &&
|
||||
config_obj.Frontend.Resources != nil &&
|
||||
config_obj.Frontend.Resources.DefaultLogBatchTime > 0 {
|
||||
batch_delay = config_obj.Frontend.Resources.DefaultLogBatchTime
|
||||
batch_delay = time.Second * time.Duration(
|
||||
config_obj.Frontend.Resources.DefaultLogBatchTime)
|
||||
}
|
||||
|
||||
// Compile all the requests into specific tasks to be sent to the
|
||||
@@ -701,7 +712,7 @@ func (self *Launcher) WriteArtifactCollectionRecord(
|
||||
SessionId: client_session_id,
|
||||
RequestId: constants.ProcessVQLResponses,
|
||||
FlowRequest: &crypto_proto.FlowRequest{
|
||||
LogBatchTime: batch_delay,
|
||||
LogBatchTime: uint64(batch_delay.Seconds()),
|
||||
MaxRows: collector_request.MaxRows,
|
||||
MaxLogs: collector_request.MaxLogs,
|
||||
MaxUploadBytes: collector_request.MaxUploadBytes,
|
||||
@@ -751,8 +762,10 @@ func (self *Launcher) WriteArtifactCollectionRecord(
|
||||
}
|
||||
|
||||
if existing_flow != nil {
|
||||
collection_context.PreviousFlows = append(
|
||||
collection_context.PreviousFlows, existing_flow.Context)
|
||||
previous_flows := existing_flow.Context.PreviousFlows
|
||||
existing_flow.Context.PreviousFlows = nil
|
||||
previous_flows = append(previous_flows, existing_flow.Context)
|
||||
collection_context.PreviousFlows = previous_flows
|
||||
}
|
||||
|
||||
// Record the tasks for provenance of what we actually did.
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"www.velocidex.com/golang/vfilter"
|
||||
)
|
||||
|
||||
func SendToOutput(
|
||||
ctx context.Context,
|
||||
output_chan chan vfilter.Row,
|
||||
event interface{}) bool {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return false
|
||||
case output_chan <- vfilter.Row(event):
|
||||
return true
|
||||
}
|
||||
}
|
||||
+57
-18
@@ -27,7 +27,7 @@ type EncodeFunction struct{}
|
||||
func (self *EncodeFunction) Call(ctx context.Context,
|
||||
scope vfilter.Scope,
|
||||
args *ordereddict.Dict) vfilter.Any {
|
||||
defer vql_subsystem.RegisterMonitor(ctx, "encode", args)()
|
||||
defer vql_subsystem.RegisterMonitor(ctx, "serialize", args)()
|
||||
|
||||
arg := &EncodeFunctionArgs{}
|
||||
err := arg_parser.ExtractArgsWithContext(ctx, scope, args, arg)
|
||||
@@ -36,10 +36,17 @@ func (self *EncodeFunction) Call(ctx context.Context,
|
||||
return vfilter.Null{}
|
||||
}
|
||||
|
||||
result := arg.Item
|
||||
switch t := result.(type) {
|
||||
return _encode("serialize", ctx, scope, arg.Item, arg.Format)
|
||||
}
|
||||
|
||||
func _encode(
|
||||
name string,
|
||||
ctx context.Context, scope vfilter.Scope,
|
||||
item vfilter.Any, format string) vfilter.Any {
|
||||
|
||||
switch t := item.(type) {
|
||||
case types.LazyExpr:
|
||||
result = t.Reduce(ctx)
|
||||
item = t.Reduce(ctx)
|
||||
|
||||
case types.StoredQuery:
|
||||
result_rows := []vfilter.Row{}
|
||||
@@ -47,53 +54,54 @@ func (self *EncodeFunction) Call(ctx context.Context,
|
||||
result_rows = append(result_rows, row)
|
||||
}
|
||||
|
||||
result = result_rows
|
||||
item = result_rows
|
||||
}
|
||||
|
||||
switch arg.Format {
|
||||
switch format {
|
||||
case "", "json":
|
||||
opts := vql_subsystem.EncOptsFromScope(scope)
|
||||
serialized_content, err := json.MarshalIndentWithOptions(result, opts)
|
||||
serialized_content, err := json.MarshalIndentWithOptions(item, opts)
|
||||
if err != nil {
|
||||
scope.Log("serialize: %v", err)
|
||||
scope.Log("%s: %v", name, err)
|
||||
return vfilter.Null{}
|
||||
}
|
||||
|
||||
return string(serialized_content)
|
||||
|
||||
case "yaml":
|
||||
serialized, err := yaml.Marshal(result)
|
||||
serialized, err := yaml.Marshal(item)
|
||||
if err != nil {
|
||||
scope.Log("serialize: %v", err)
|
||||
scope.Log("%v: %v", name, err)
|
||||
return vfilter.Null{}
|
||||
}
|
||||
return string(serialized)
|
||||
|
||||
case "hex":
|
||||
switch t := result.(type) {
|
||||
switch t := item.(type) {
|
||||
case []byte:
|
||||
return hex.EncodeToString(t)
|
||||
case string:
|
||||
return hex.EncodeToString([]byte(t))
|
||||
default:
|
||||
scope.Log("serialize: Unsupported type for hex encoding %T", result)
|
||||
scope.Log("%s: Unsupported type for hex encoding %T", name, item)
|
||||
return vfilter.Null{}
|
||||
}
|
||||
|
||||
case "base64":
|
||||
switch t := result.(type) {
|
||||
switch t := item.(type) {
|
||||
case []byte:
|
||||
return base64.RawStdEncoding.EncodeToString(t)
|
||||
case string:
|
||||
return base64.RawStdEncoding.EncodeToString([]byte(t))
|
||||
default:
|
||||
scope.Log("serialize: Unsupported type for base64 encoding %T", result)
|
||||
scope.Log("%v: Unsupported type for base64 encoding %T",
|
||||
name, item)
|
||||
return vfilter.Null{}
|
||||
}
|
||||
|
||||
case "csv":
|
||||
// Not actually a slice.
|
||||
if reflect.TypeOf(result).Kind() != reflect.Slice {
|
||||
if reflect.TypeOf(item).Kind() != reflect.Slice {
|
||||
return vfilter.Null{}
|
||||
}
|
||||
|
||||
@@ -105,7 +113,7 @@ func (self *EncodeFunction) Call(ctx context.Context,
|
||||
true, /* write_headers */
|
||||
json.DefaultEncOpts())
|
||||
|
||||
result_rows_value := reflect.ValueOf(result)
|
||||
result_rows_value := reflect.ValueOf(item)
|
||||
for i := 0; i < result_rows_value.Len(); i++ {
|
||||
csv_writer.Write(result_rows_value.Index(i).Interface())
|
||||
}
|
||||
@@ -114,7 +122,7 @@ func (self *EncodeFunction) Call(ctx context.Context,
|
||||
return buff.String()
|
||||
|
||||
default:
|
||||
scope.Log("serialize: Unknown format %s", arg.Format)
|
||||
scope.Log("%s: Unknown format %s", name, format)
|
||||
}
|
||||
return vfilter.Null{}
|
||||
}
|
||||
@@ -122,11 +130,42 @@ func (self *EncodeFunction) Call(ctx context.Context,
|
||||
func (self EncodeFunction) Info(scope vfilter.Scope, type_map *vfilter.TypeMap) *vfilter.FunctionInfo {
|
||||
return &vfilter.FunctionInfo{
|
||||
Name: "serialize",
|
||||
Doc: "Encode an object as a string (csv or json).",
|
||||
Doc: "Encode an object as a string (json, yaml, hex, base64, csv). This function is an alias to `encode`",
|
||||
ArgType: type_map.AddType(scope, &EncodeFunctionArgs{}),
|
||||
}
|
||||
}
|
||||
|
||||
type _EncodeOverrideArgs struct {
|
||||
String types.Any `vfilter:"required,field=string,doc=The item to encode"`
|
||||
Type string `vfilter:"required,field=type,doc=Encoding format (csv,json,yaml,hex,base64)"`
|
||||
}
|
||||
|
||||
type EncodeOverride struct{}
|
||||
|
||||
func (self EncodeOverride) Call(
|
||||
ctx context.Context,
|
||||
scope types.Scope,
|
||||
args *ordereddict.Dict) types.Any {
|
||||
arg := &_EncodeOverrideArgs{}
|
||||
err := arg_parser.ExtractArgsWithContext(ctx, scope, args, arg)
|
||||
if err != nil {
|
||||
scope.Log("encode: %s", err.Error())
|
||||
return types.Null{}
|
||||
}
|
||||
|
||||
return _encode("encode", ctx, scope, arg.String, arg.Type)
|
||||
}
|
||||
|
||||
func (self EncodeOverride) Info(scope vfilter.Scope, type_map *vfilter.TypeMap) *vfilter.FunctionInfo {
|
||||
return &vfilter.FunctionInfo{
|
||||
Name: "encode",
|
||||
Doc: "Encode an object as a string (json, yaml, hex, base64, csv).",
|
||||
ArgType: type_map.AddType(scope, &_EncodeOverrideArgs{}),
|
||||
Version: 3,
|
||||
}
|
||||
}
|
||||
|
||||
func init() {
|
||||
vql_subsystem.RegisterFunction(&EncodeFunction{})
|
||||
vql_subsystem.OverrideFunction(&EncodeOverride{})
|
||||
}
|
||||
|
||||
@@ -272,19 +272,36 @@ func (self _ParseFloat) Info(scope vfilter.Scope, type_map *vfilter.TypeMap) *vf
|
||||
}
|
||||
}
|
||||
|
||||
type _NowArgs struct {
|
||||
NS bool `vfilter:"optional,field=ns,doc=Return the time in ns"`
|
||||
}
|
||||
|
||||
type _Now struct{}
|
||||
|
||||
func (self _Now) Call(
|
||||
ctx context.Context,
|
||||
scope vfilter.Scope,
|
||||
args *ordereddict.Dict) vfilter.Any {
|
||||
|
||||
arg := &_NowArgs{}
|
||||
err := arg_parser.ExtractArgsWithContext(ctx, scope, args, arg)
|
||||
if err != nil {
|
||||
scope.Log("now: %s", err.Error())
|
||||
return vfilter.Null{}
|
||||
}
|
||||
|
||||
if arg.NS {
|
||||
return time.Now().UnixNano()
|
||||
}
|
||||
return time.Now().Unix()
|
||||
}
|
||||
|
||||
func (self _Now) Info(scope vfilter.Scope, type_map *vfilter.TypeMap) *vfilter.FunctionInfo {
|
||||
return &vfilter.FunctionInfo{
|
||||
Name: "now",
|
||||
Doc: "Returns current time in seconds since epoch.",
|
||||
Name: "now",
|
||||
Doc: "Returns current time in seconds since epoch.",
|
||||
ArgType: type_map.AddType(scope, &_NowArgs{}),
|
||||
Version: 2,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -115,7 +115,11 @@ func (self CreatePackagePlugin) Call(ctx context.Context,
|
||||
}
|
||||
|
||||
if arg.Target == "" {
|
||||
arg.Target = "VelociraptorLinux"
|
||||
if arg.Server {
|
||||
arg.Target = "VelociraptorLinuxSumo"
|
||||
} else {
|
||||
arg.Target = "VelociraptorLinux"
|
||||
}
|
||||
}
|
||||
|
||||
config_obj, ok := vql_subsystem.GetServerConfig(scope)
|
||||
|
||||
@@ -15,7 +15,7 @@ GNU Affero General Public License for more details.
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package common
|
||||
package shell
|
||||
|
||||
import (
|
||||
"context"
|
||||
@@ -51,17 +51,18 @@ type ShellPluginArgs struct {
|
||||
}
|
||||
|
||||
type ShellResult struct {
|
||||
Stdin string
|
||||
Stdout string
|
||||
Stderr string
|
||||
ReturnCode int64
|
||||
Complete bool
|
||||
}
|
||||
|
||||
type ShellPlugin struct {
|
||||
type ExecvePlugin struct {
|
||||
pipeReader pipeReaderFunc
|
||||
}
|
||||
|
||||
func (self *ShellPlugin) maybeForceSecrets(
|
||||
func maybeForceSecrets(
|
||||
ctx context.Context, scope vfilter.Scope,
|
||||
arg *ShellPluginArgs) error {
|
||||
|
||||
@@ -87,7 +88,7 @@ func (self *ShellPlugin) maybeForceSecrets(
|
||||
return utils.SecretsEnforced
|
||||
}
|
||||
|
||||
func (self ShellPlugin) Call(
|
||||
func (self ExecvePlugin) Call(
|
||||
ctx context.Context,
|
||||
scope vfilter.Scope,
|
||||
args *ordereddict.Dict) <-chan vfilter.Row {
|
||||
@@ -117,7 +118,7 @@ func (self ShellPlugin) Call(
|
||||
return
|
||||
}
|
||||
|
||||
err = self.maybeForceSecrets(ctx, scope, arg)
|
||||
err = maybeForceSecrets(ctx, scope, arg)
|
||||
if err != nil {
|
||||
scope.Error("execve: %v", err)
|
||||
return
|
||||
@@ -125,7 +126,7 @@ func (self ShellPlugin) Call(
|
||||
|
||||
original_args := append([]string{}, arg.Argv...)
|
||||
if arg.Secret != "" {
|
||||
arg, err = self.mergeSecretToRequest(ctx, scope, arg, arg.Secret)
|
||||
arg, err = mergeSecretToRequest(ctx, scope, arg, arg.Secret)
|
||||
if err != nil {
|
||||
scope.Log("ERROR:execve: %v", err)
|
||||
return
|
||||
@@ -323,7 +324,7 @@ func (self ShellPlugin) Call(
|
||||
return output_chan
|
||||
}
|
||||
|
||||
func (self ShellPlugin) mergeSecretToRequest(
|
||||
func mergeSecretToRequest(
|
||||
ctx context.Context, scope vfilter.Scope,
|
||||
arg *ShellPluginArgs, secret_name string) (*ShellPluginArgs, error) {
|
||||
|
||||
@@ -386,7 +387,7 @@ func (self ShellPlugin) mergeSecretToRequest(
|
||||
return new_arg, nil
|
||||
}
|
||||
|
||||
func (self ShellPlugin) Info(scope vfilter.Scope, type_map *vfilter.TypeMap) *vfilter.PluginInfo {
|
||||
func (self ExecvePlugin) Info(scope vfilter.Scope, type_map *vfilter.TypeMap) *vfilter.PluginInfo {
|
||||
return &vfilter.PluginInfo{
|
||||
Name: "execve",
|
||||
Doc: "Execute the commands given by argv.",
|
||||
@@ -502,7 +503,7 @@ func defaultPipeReader(
|
||||
}
|
||||
|
||||
func init() {
|
||||
vql_subsystem.RegisterPlugin(&ShellPlugin{
|
||||
vql_subsystem.RegisterPlugin(&ExecvePlugin{
|
||||
pipeReader: defaultPipeReader,
|
||||
})
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package common
|
||||
package shell
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
@@ -0,0 +1,65 @@
|
||||
package shell
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"www.velocidex.com/golang/velociraptor/utils"
|
||||
)
|
||||
|
||||
const (
|
||||
BUFFER_SIZE = 1024 * 1024
|
||||
)
|
||||
|
||||
func pumpToPipeFromInput(
|
||||
ctx context.Context,
|
||||
wg *sync.WaitGroup,
|
||||
input chan string,
|
||||
cb func(message string)) {
|
||||
defer wg.Done()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
|
||||
case in, ok := <-input:
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
cb(in)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func pumpFromPipeToOutput(
|
||||
ctx context.Context,
|
||||
wg *sync.WaitGroup,
|
||||
pipe io.Reader,
|
||||
cb func(message []byte)) {
|
||||
|
||||
defer wg.Done()
|
||||
|
||||
buff := make([]byte, BUFFER_SIZE)
|
||||
for !utils.IsCtxDone(ctx) {
|
||||
n, err := pipe.Read(buff)
|
||||
if err != nil { // EOF on pipe close.
|
||||
if n > 0 {
|
||||
cb(buff[:n])
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// If the buffer is not full, wait for it be filled. Sleep a
|
||||
// small amount to allow the pipe to fill up so we dont end up
|
||||
// making lots of small reads and generating many small rows.
|
||||
if n < BUFFER_SIZE {
|
||||
time.Sleep(300 * time.Millisecond)
|
||||
}
|
||||
|
||||
cb(buff[:n])
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
package shell
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
vfilter "www.velocidex.com/golang/vfilter"
|
||||
"www.velocidex.com/golang/vfilter/types"
|
||||
)
|
||||
|
||||
func (self *ShellSession) IsRunning() bool {
|
||||
self.mu.Lock()
|
||||
defer self.mu.Unlock()
|
||||
|
||||
return self._IsRunning
|
||||
}
|
||||
|
||||
func (self *ShellSession) Query() types.StoredQuery {
|
||||
return &SessionQuery{
|
||||
session: self,
|
||||
}
|
||||
}
|
||||
|
||||
// Set the session into the running state and return the previous
|
||||
// state.
|
||||
func (self *ShellSession) startRunning() bool {
|
||||
self.mu.Lock()
|
||||
defer self.mu.Unlock()
|
||||
|
||||
if self._IsRunning {
|
||||
return true
|
||||
}
|
||||
|
||||
self._IsRunning = true
|
||||
return false
|
||||
}
|
||||
|
||||
type SessionQuery struct {
|
||||
session *ShellSession
|
||||
}
|
||||
|
||||
// The session is a query and can be SELECTed from. Only a single
|
||||
// query can select from the session and this query controls the
|
||||
// lifetime of the session - When that query exists the subprocess is
|
||||
// terminated and the session is unmounted.
|
||||
func (self *SessionQuery) Eval(
|
||||
ctx context.Context, scope vfilter.Scope) <-chan vfilter.Row {
|
||||
output_chan := make(chan types.Row)
|
||||
|
||||
// Only one runner is allowed
|
||||
is_running := self.session.startRunning()
|
||||
if is_running {
|
||||
scope.Log("shell_session: Session %v is already running",
|
||||
self.session.Name)
|
||||
close(output_chan)
|
||||
return output_chan
|
||||
}
|
||||
|
||||
return self.session.output
|
||||
}
|
||||
|
||||
func (self *ShellSession) Close() {
|
||||
self.owner.Remove(self.Name)
|
||||
|
||||
self.stdin.Close()
|
||||
self.cancel()
|
||||
self.wg.Wait()
|
||||
self.command.Wait()
|
||||
close(self.output)
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
package shell
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/Velocidex/ordereddict"
|
||||
"www.velocidex.com/golang/velociraptor/acls"
|
||||
"www.velocidex.com/golang/velociraptor/artifacts"
|
||||
vql_subsystem "www.velocidex.com/golang/velociraptor/vql"
|
||||
vfilter "www.velocidex.com/golang/vfilter"
|
||||
"www.velocidex.com/golang/vfilter/arg_parser"
|
||||
)
|
||||
|
||||
type ShellSessionControlArgs struct {
|
||||
Name string `vfilter:"optional,field=name,doc=The name of the shell session."`
|
||||
Stdin string `vfilter:"optional,field=stdin,doc=Write this string to the session stdin."`
|
||||
StdinClose bool `vfilter:"optional,field=close_stdin,doc=If specified we close the stdin of the session. This will usually terminate the session gracefully."`
|
||||
Kill bool `vfilter:"optional,field=kill,doc=If specified we kill the session."`
|
||||
}
|
||||
|
||||
type ShellSessionControl struct{}
|
||||
|
||||
func (self *ShellSessionControl) Call(ctx context.Context,
|
||||
scope vfilter.Scope,
|
||||
args *ordereddict.Dict) vfilter.Any {
|
||||
err := vql_subsystem.CheckAccess(scope, acls.EXECVE)
|
||||
if err != nil {
|
||||
scope.Log("shell_session_control: %v", err)
|
||||
return vfilter.Null{}
|
||||
}
|
||||
|
||||
// Check the config if we are allowed to execve at all.
|
||||
config_obj, ok := artifacts.GetConfig(scope)
|
||||
if ok && config_obj.PreventExecve {
|
||||
scope.Log("shell_session_control: Not allowed to execve by configuration.")
|
||||
return vfilter.Null{}
|
||||
}
|
||||
|
||||
arg := &ShellSessionControlArgs{}
|
||||
err = arg_parser.ExtractArgsWithContext(ctx, scope, args, arg)
|
||||
if err != nil {
|
||||
scope.Error("shell_session_control: %v", err)
|
||||
return vfilter.Null{}
|
||||
}
|
||||
|
||||
session, pres := shellManager.GetByName(arg.Name)
|
||||
if !pres {
|
||||
scope.Error("shell_session_control: session %v not known", arg.Name)
|
||||
return vfilter.Null{}
|
||||
}
|
||||
|
||||
if arg.Stdin != "" {
|
||||
session.WriteStdin(ctx, arg.Stdin)
|
||||
}
|
||||
|
||||
if arg.StdinClose {
|
||||
session.CloseStdin()
|
||||
}
|
||||
|
||||
return session
|
||||
}
|
||||
|
||||
func (self ShellSessionControl) Info(scope vfilter.Scope, type_map *vfilter.TypeMap) *vfilter.FunctionInfo {
|
||||
return &vfilter.FunctionInfo{
|
||||
Name: "shell_session_control",
|
||||
Doc: "Control a previously created shell session.",
|
||||
ArgType: type_map.AddType(scope, &ShellSessionControlArgs{}),
|
||||
Metadata: vql_subsystem.VQLMetadata().Permissions(acls.EXECVE).Build(),
|
||||
Version: 1,
|
||||
}
|
||||
}
|
||||
|
||||
func init() {
|
||||
vql_subsystem.RegisterFunction(&ShellSessionControl{})
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
//go:build !windows
|
||||
// +build !windows
|
||||
|
||||
package shell
|
||||
|
||||
import (
|
||||
"os/exec"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
// For posix systems we want the child to run in a different process
|
||||
// group so it can not capture our signals.
|
||||
func UpdateCommandForOS(command *exec.Cmd) {
|
||||
command.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}
|
||||
}
|
||||
@@ -0,0 +1,295 @@
|
||||
package shell
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os/exec"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/Velocidex/ordereddict"
|
||||
"www.velocidex.com/golang/velociraptor/acls"
|
||||
"www.velocidex.com/golang/velociraptor/artifacts"
|
||||
vql_subsystem "www.velocidex.com/golang/velociraptor/vql"
|
||||
vfilter "www.velocidex.com/golang/vfilter"
|
||||
"www.velocidex.com/golang/vfilter/arg_parser"
|
||||
"www.velocidex.com/golang/vfilter/types"
|
||||
)
|
||||
|
||||
var (
|
||||
shellManager = &ShellManager{
|
||||
sessions: map[string]*ShellSession{},
|
||||
}
|
||||
)
|
||||
|
||||
type ShellManager struct {
|
||||
mu sync.Mutex
|
||||
sessions map[string]*ShellSession
|
||||
}
|
||||
|
||||
func (self *ShellManager) Remove(name string) {
|
||||
self.mu.Lock()
|
||||
defer self.mu.Unlock()
|
||||
|
||||
delete(self.sessions, name)
|
||||
}
|
||||
|
||||
func (self *ShellManager) GetByName(name string) (*ShellSession, bool) {
|
||||
self.mu.Lock()
|
||||
defer self.mu.Unlock()
|
||||
|
||||
res, pres := self.sessions[name]
|
||||
return res, pres
|
||||
}
|
||||
|
||||
func (self *ShellManager) Get(
|
||||
ctx context.Context, scope vfilter.Scope,
|
||||
args *StartShellFunctionArgs) (*ShellSession, error) {
|
||||
self.mu.Lock()
|
||||
defer self.mu.Unlock()
|
||||
|
||||
res, pres := self.sessions[args.Name]
|
||||
if pres {
|
||||
return res, nil
|
||||
}
|
||||
|
||||
res, err := self.newShellSession(ctx, scope, args)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
self.sessions[args.Name] = res
|
||||
return res, err
|
||||
}
|
||||
|
||||
func (self *ShellManager) mergeSecretToRequest(
|
||||
ctx context.Context, scope vfilter.Scope,
|
||||
arg *StartShellFunctionArgs, secret_name string) (*StartShellFunctionArgs, error) {
|
||||
execve_args := &ShellPluginArgs{
|
||||
Argv: arg.Argv,
|
||||
Env: arg.Env,
|
||||
Cwd: arg.Cwd,
|
||||
Secret: arg.Secret}
|
||||
|
||||
res, err := mergeSecretToRequest(ctx, scope, execve_args, arg.Secret)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &StartShellFunctionArgs{
|
||||
Argv: res.Argv,
|
||||
Env: res.Env,
|
||||
Cwd: res.Cwd,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (self *ShellManager) newShellSession(
|
||||
ctx context.Context, scope vfilter.Scope,
|
||||
arg *StartShellFunctionArgs) (res *ShellSession, err error) {
|
||||
|
||||
if len(arg.Argv) == 0 {
|
||||
return nil, errors.New("Argv not provided")
|
||||
}
|
||||
|
||||
original_args := append([]string{}, arg.Argv...)
|
||||
if arg.Secret != "" {
|
||||
arg, err = self.mergeSecretToRequest(
|
||||
ctx, scope, arg, arg.Secret)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
output_chan := make(chan types.Row)
|
||||
wg := &sync.WaitGroup{}
|
||||
|
||||
sub_ctx, cancel := context.WithCancel(ctx)
|
||||
err = scope.AddDestructor(cancel)
|
||||
if err != nil {
|
||||
cancel()
|
||||
return
|
||||
}
|
||||
|
||||
scope.Log("shell_session: Running external command %v", original_args)
|
||||
command := exec.CommandContext(sub_ctx, arg.Argv[0], arg.Argv[1:]...)
|
||||
if arg.Env != nil {
|
||||
for _, i := range arg.Env.Items() {
|
||||
command.Env = append(command.Env,
|
||||
fmt.Sprintf("%s=%s", i.Key, i.Value))
|
||||
}
|
||||
}
|
||||
command.Dir = arg.Cwd
|
||||
command.WaitDelay = time.Second
|
||||
command.Cancel = func() error {
|
||||
cancel()
|
||||
return nil
|
||||
}
|
||||
UpdateCommandForOS(command)
|
||||
|
||||
stdin_pipe, err := command.StdinPipe()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
stdout_pipe, err := command.StdoutPipe()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
stderr_pipe, err := command.StderrPipe()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
wg.Add(3)
|
||||
go pumpFromPipeToOutput(sub_ctx, wg, stdout_pipe, func(data []byte) {
|
||||
select {
|
||||
case <-sub_ctx.Done():
|
||||
return
|
||||
case output_chan <- &ShellResult{
|
||||
Stdout: string(data),
|
||||
}:
|
||||
}
|
||||
})
|
||||
|
||||
go pumpFromPipeToOutput(sub_ctx, wg, stderr_pipe, func(data []byte) {
|
||||
select {
|
||||
case <-sub_ctx.Done():
|
||||
return
|
||||
case output_chan <- &ShellResult{
|
||||
Stderr: string(data),
|
||||
}:
|
||||
}
|
||||
})
|
||||
|
||||
// Also report any stdin messages to the output channel. This
|
||||
// ensures they are delivered in order.
|
||||
input_chan := make(chan string, 10)
|
||||
go pumpToPipeFromInput(sub_ctx, wg, input_chan, func(data string) {
|
||||
select {
|
||||
case <-sub_ctx.Done():
|
||||
return
|
||||
case output_chan <- &ShellResult{
|
||||
Stdin: data,
|
||||
}:
|
||||
stdin_pipe.Write([]byte(data))
|
||||
}
|
||||
})
|
||||
|
||||
err = command.Start()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
res = &ShellSession{
|
||||
Name: arg.Name,
|
||||
wg: wg,
|
||||
owner: self,
|
||||
cancel: cancel,
|
||||
input: input_chan,
|
||||
output: output_chan,
|
||||
command: command,
|
||||
stdin: stdin_pipe,
|
||||
stdout: stdout_pipe,
|
||||
stderr: stderr_pipe,
|
||||
}
|
||||
|
||||
go func() {
|
||||
wg.Wait()
|
||||
res.Close()
|
||||
}()
|
||||
|
||||
return res, nil
|
||||
}
|
||||
|
||||
// A shell is a persistent session
|
||||
type ShellSession struct {
|
||||
mu sync.Mutex
|
||||
|
||||
wg *sync.WaitGroup
|
||||
cancel func()
|
||||
_IsRunning bool
|
||||
Name string
|
||||
owner *ShellManager
|
||||
|
||||
// Shell output will be pushed to this channel. SELECTing from
|
||||
// ShellSession will read from this channel.
|
||||
stdin io.WriteCloser
|
||||
stdout io.ReadCloser
|
||||
stderr io.ReadCloser
|
||||
command *exec.Cmd
|
||||
|
||||
input chan string
|
||||
output chan vfilter.Row
|
||||
}
|
||||
|
||||
func (self *ShellSession) WriteStdin(ctx context.Context, data string) {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case self.input <- data:
|
||||
}
|
||||
}
|
||||
|
||||
func (self *ShellSession) CloseStdin() {
|
||||
self.stdin.Close()
|
||||
}
|
||||
|
||||
type StartShellFunctionArgs struct {
|
||||
Argv []string `vfilter:"required,field=argv,doc=Argv to run the command with."`
|
||||
Env *ordereddict.Dict `vfilter:"optional,field=env,doc=Environment variables to launch with."`
|
||||
Cwd string `vfilter:"optional,field=cwd,doc=If specified we change to this working directory first."`
|
||||
Secret string `vfilter:"optional,field=secret,doc=The name of a secret to use."`
|
||||
Name string `vfilter:"optional,field=name,doc=The name of the shell session. If the session already exists, we just return a handle to it."`
|
||||
}
|
||||
|
||||
type StartShellFunction struct{}
|
||||
|
||||
func (self StartShellFunction) Call(ctx context.Context,
|
||||
scope vfilter.Scope,
|
||||
args *ordereddict.Dict) vfilter.Any {
|
||||
|
||||
err := vql_subsystem.CheckAccess(scope, acls.EXECVE)
|
||||
if err != nil {
|
||||
scope.Log("shell_session: %v", err)
|
||||
return vfilter.Null{}
|
||||
}
|
||||
|
||||
// Check the config if we are allowed to execve at all.
|
||||
config_obj, ok := artifacts.GetConfig(scope)
|
||||
if ok && config_obj.PreventExecve {
|
||||
scope.Log("shell_session: Not allowed to execve by configuration.")
|
||||
return vfilter.Null{}
|
||||
}
|
||||
|
||||
arg := &StartShellFunctionArgs{}
|
||||
err = arg_parser.ExtractArgsWithContext(ctx, scope, args, arg)
|
||||
if err != nil {
|
||||
scope.Error("shell_session: %v", err)
|
||||
return vfilter.Null{}
|
||||
}
|
||||
|
||||
res, err := shellManager.Get(ctx, scope, arg)
|
||||
if err != nil {
|
||||
scope.Error("shell_session: %v", err)
|
||||
return vfilter.Null{}
|
||||
}
|
||||
|
||||
return res
|
||||
}
|
||||
|
||||
func (self StartShellFunction) Info(scope vfilter.Scope, type_map *vfilter.TypeMap) *vfilter.FunctionInfo {
|
||||
return &vfilter.FunctionInfo{
|
||||
Name: "shell_session",
|
||||
Doc: "Recreate or retrieve a shell session handle.",
|
||||
ArgType: type_map.AddType(scope, &StartShellFunctionArgs{}),
|
||||
Metadata: vql_subsystem.VQLMetadata().Permissions(acls.EXECVE).Build(),
|
||||
Version: 1,
|
||||
}
|
||||
}
|
||||
|
||||
func init() {
|
||||
vql_subsystem.RegisterFunction(&StartShellFunction{})
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
//go:build windows
|
||||
// +build windows
|
||||
|
||||
package shell
|
||||
|
||||
import (
|
||||
"os/exec"
|
||||
)
|
||||
|
||||
func UpdateCommandForOS(command *exec.Cmd) {
|
||||
}
|
||||
@@ -51,4 +51,5 @@ import (
|
||||
_ "www.velocidex.com/golang/velociraptor/vql/tools/packaging"
|
||||
_ "www.velocidex.com/golang/velociraptor/vql/tools/process"
|
||||
_ "www.velocidex.com/golang/velociraptor/vql/tools/rsyslog"
|
||||
_ "www.velocidex.com/golang/velociraptor/vql/tools/shell"
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user