diff --git a/artifacts/definitions/Server/Utils/CreateCollector.yaml b/artifacts/definitions/Server/Utils/CreateCollector.yaml
index bff11bd9eef..5730abae27b 100644
--- a/artifacts/definitions/Server/Utils/CreateCollector.yaml
+++ b/artifacts/definitions/Server/Utils/CreateCollector.yaml
@@ -66,6 +66,7 @@ parameters:
- ZIP
- GCS
- S3
+ - SFTP
- name: target_args
description: Type Dependent args
@@ -143,6 +144,19 @@ parameters:
name=name,
credentials=TargetArgs.GCSKey)
+ - name: SFTPCollection
+ type: hidden
+ default : |
+ LET upload_file(filename, name, accessor) = upload_sftp(
+ file=filename,
+ accessor=accessor,
+ name=name,
+ user=TargetArgs.user,
+ path=TargetArgs.path,
+ privatekey=TargetArgs.privatekey,
+ endpoint=TargetArgs.endpoint,
+ hostkey = TargetArgs.hostkey)
+
- name: CloudCollection
type: hidden
default: |
@@ -294,7 +308,8 @@ sources:
a = { SELECT StandardCollection AS Value FROM scope() WHERE target = "ZIP" },
b = { SELECT S3Collection + CloudCollection AS Value FROM scope() WHERE target = "S3" },
c = { SELECT GCSCollection + CloudCollection AS Value FROM scope() WHERE target = "GCS" },
- d = { SELECT "" AS Value FROM scope() WHERE log(message="Unknown collection type " + target) }
+ d = { SELECT SFTPCollection + CloudCollection AS Value FROM scope() WHERE target = "SFTP" },
+ e = { SELECT "" AS Value FROM scope() WHERE log(message="Unknown collection type " + target) }
)
LET definitions <= SELECT * FROM chain(
diff --git a/go.mod b/go.mod
index cbd7c59ad8b..2b2d53bca64 100644
--- a/go.mod
+++ b/go.mod
@@ -74,6 +74,7 @@ require (
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e // indirect
github.com/olekukonko/tablewriter v0.0.4
github.com/pkg/errors v0.9.1
+ github.com/pkg/sftp v1.12.0 // indirect
github.com/pkg/term v0.0.0-20190109203006-aa71e9d9e942 // indirect
github.com/pquerna/cachecontrol v0.0.0-20200921180117-858c6e7e6b7e // indirect
github.com/processout/grpc-go-pool v1.2.1
diff --git a/go.sum b/go.sum
index d83220c3c4e..e963615d9f9 100644
--- a/go.sum
+++ b/go.sum
@@ -303,6 +303,8 @@ github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+o
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/konsorten/go-windows-terminal-sequences v1.0.3 h1:CE8S1cTafDpPvMhIxNJKvHsGVBgn1xWYf1NbHQhywc8=
github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
+github.com/kr/fs v0.1.0 h1:Jskdu9ieNAYnjxsi0LbQp1ulIKZV1LAFgK1tWhpZgl8=
+github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.2.0 h1:s5hAObm+yFO5uHYt5dYjxi2rXrsnmRpJx4OYvIWUaQs=
@@ -369,6 +371,8 @@ github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
+github.com/pkg/sftp v1.12.0 h1:/f3b24xrDhkhddlaobPe2JgBqfdt+gC/NYl0QY9IOuI=
+github.com/pkg/sftp v1.12.0/go.mod h1:fUqqXB5vEgVCZ131L+9say31RAri6aF6KDViawhxKK8=
github.com/pkg/term v0.0.0-20190109203006-aa71e9d9e942 h1:A7GG7zcGjl3jqAqGPmcNjd/D9hzL95SuoOQAaFNdLU0=
github.com/pkg/term v0.0.0-20190109203006-aa71e9d9e942/go.mod h1:eCbImbZ95eXtAUIbLAuAVnBnwf83mjf6QIVH8SHYwqQ=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
diff --git a/gui/velociraptor/src/components/flows/offline-collector.js b/gui/velociraptor/src/components/flows/offline-collector.js
index 68e41a5fd44..28e24dad1cd 100644
--- a/gui/velociraptor/src/components/flows/offline-collector.js
+++ b/gui/velociraptor/src/components/flows/offline-collector.js
@@ -83,6 +83,7 @@ class OfflineCollectorParameters extends React.Component {
+
@@ -185,7 +186,100 @@ class OfflineCollectorParameters extends React.Component {
/>
+
+
+ Skip Cert Verification
+
+ {
+ this.props.parameters.target_args.noverifycert = e.target.value;
+ this.props.setParameters(this.props.parameters);
+ }}
+ >
+
+
+
+
+
+
+
+
+ >
+ }
+
+ { this.props.parameters.target === "SFTP" && <>
+
+ Upload Path
+
+ {
+ this.props.parameters.target_args.path = e.target.value;
+ this.props.setParameters(this.props.parameters);
+ }}
+ />
+
+
+
+
+ Private Key
+
+ {
+ this.props.parameters.target_args.privatekey = e.target.value;
+ this.props.setParameters(this.props.parameters);
+ }}
+ />
+
+
+
+
+ User
+
+ {
+ this.props.parameters.target_args.user = e.target.value;
+ this.props.setParameters(this.props.parameters);
+ }}
+ />
+
+
+
+
+ Endpoint
+
+ {
+ this.props.parameters.target_args.endpoint = e.target.value;
+ this.props.setParameters(this.props.parameters);
+ }}
+ />
+
+
+
+
+ Host Key
+
+ {
+ this.props.parameters.target_args.hostkey = e.target.value;
+ this.props.setParameters(this.props.parameters);
+ }}
+ />
+
+
>
+
}
diff --git a/vql/tools/s3_upload.go b/vql/tools/s3_upload.go
index 921b96db0bc..41b89450f5b 100644
--- a/vql/tools/s3_upload.go
+++ b/vql/tools/s3_upload.go
@@ -82,7 +82,12 @@ func (self *S3UploadFunction) Call(ctx context.Context,
upload_response, err := upload_S3(
sub_ctx, scope, file,
arg.Bucket,
- arg.Name, arg.CredentialsKey, arg.CredentialsSecret, arg.Region, arg.Endpoint, arg.NoVerifyCert)
+ arg.Name,
+ arg.CredentialsKey,
+ arg.CredentialsSecret,
+ arg.Region,
+ arg.Endpoint,
+ arg.NoVerifyCert)
if err != nil {
scope.Log("upload_S3: %v", err)
// Relay the error in the UploadResponse
@@ -97,7 +102,11 @@ func (self *S3UploadFunction) Call(ctx context.Context,
func upload_S3(ctx context.Context, scope *vfilter.Scope,
reader glob.ReadSeekCloser,
bucket, name string,
- credentialsKey string, credentialsSecret string, region string, endpoint string, NoVerifyCert bool) (
+ credentialsKey string,
+ credentialsSecret string,
+ region string,
+ endpoint string,
+ NoVerifyCert bool) (
*api.UploadResponse, error) {
scope.Log("upload_S3: Uploading %v to %v", name, bucket)
diff --git a/vql/tools/sftp_upload.go b/vql/tools/sftp_upload.go
new file mode 100644
index 00000000000..333aa2f1bfd
--- /dev/null
+++ b/vql/tools/sftp_upload.go
@@ -0,0 +1,223 @@
+//+build extras
+
+package tools
+
+import (
+ "encoding/base64"
+ "errors"
+ "fmt"
+ "io"
+ "net"
+ "path/filepath"
+
+ "github.com/Velocidex/ordereddict"
+ "github.com/pkg/sftp"
+ "golang.org/x/crypto/ssh"
+ "golang.org/x/net/context"
+ "www.velocidex.com/golang/velociraptor/file_store/api"
+ "www.velocidex.com/golang/velociraptor/glob"
+ vql_subsystem "www.velocidex.com/golang/velociraptor/vql"
+ "www.velocidex.com/golang/vfilter"
+)
+
+type SFTPUploadArgs struct {
+ File string `vfilter:"required,field=file,doc=The file to upload"`
+ Name string `vfilter:"optional,field=name,doc=The name of the file that should be stored on the server"`
+ User string `vfilter:"required,field=user,doc=The username to connect to the endpoint with"`
+ Path string `vfilter:"required,field=path,doc=Path on server to upload file to"`
+ Accessor string `vfilter:"optional,field=accessor,doc=The accessor to use"`
+ PrivateKey string `vfilter:"required,field=privatekey,doc=The private key to use"`
+ Endpoint string `vfilter:"required,field=endpoint,doc=The Endpoint to use"`
+ HostKey string `vfilter:"optional,field=hostkey,doc=Host key to verify. Blank to disable"`
+}
+
+type SFTPUploadFunction struct{}
+
+func (self *SFTPUploadFunction) Call(ctx context.Context,
+ scope *vfilter.Scope,
+ args *ordereddict.Dict) vfilter.Any {
+
+ arg := &SFTPUploadArgs{}
+ err := vfilter.ExtractArgs(scope, args, arg)
+ if err != nil {
+ scope.Log("upload_sftp: %s", err.Error())
+ return vfilter.Null{}
+ }
+
+ err = vql_subsystem.CheckFilesystemAccess(scope, arg.Accessor)
+ if err != nil {
+ scope.Log("upload_SFTP: %s", err)
+ return vfilter.Null{}
+ }
+
+ accessor, err := glob.GetAccessor(arg.Accessor, scope)
+ if err != nil {
+ scope.Log("upload_SFTP: %v", err)
+ return vfilter.Null{}
+ }
+
+ file, err := accessor.Open(arg.File)
+ if err != nil {
+ scope.Log("upload_SFTP: Unable to open %s: %s",
+ arg.File, err.Error())
+ return &vfilter.Null{}
+ }
+ defer file.Close()
+
+ if arg.Name == "" {
+ arg.Name = arg.File
+ }
+
+ stat, err := file.Stat()
+ if err != nil {
+ scope.Log("upload_SFTP: Unable to stat %s: %v",
+ arg.File, err)
+ } else if !stat.IsDir() {
+ // Abort uploading when the scope is destroyed.
+ sub_ctx, cancel := context.WithCancel(ctx)
+ scope.AddDestructor(func() {
+ cancel()
+ })
+
+ upload_response, err := upload_SFTP(
+ sub_ctx, scope, file,
+ arg.User,
+ arg.Path,
+ arg.Name,
+ arg.PrivateKey,
+ arg.Endpoint,
+ arg.HostKey)
+ if err != nil {
+ scope.Log("upload_SFTP: %v", err)
+ // Relay the error in the UploadResponse
+ return upload_response
+ }
+ return upload_response
+ }
+
+ return vfilter.Null{}
+}
+
+func keyString(k ssh.PublicKey) string {
+ return k.Type() + " " + base64.StdEncoding.EncodeToString(k.Marshal())
+}
+
+func hostkeycallback(trustedkey string) ssh.HostKeyCallback {
+ return func(_ string, _ net.Addr, k ssh.PublicKey) error {
+ ks := keyString(k)
+ if trustedkey != ks {
+ return fmt.Errorf("SSH-key verification: expected %s but got %s", trustedkey, ks)
+ }
+ return nil
+ }
+}
+
+func getSFTPClient(scope *vfilter.Scope, user string, privateKey string, endpoint string, hostKey string) (*sftp.Client, error) {
+ cacheKey := fmt.Sprintf("%s %s", user, endpoint)
+ client := vql_subsystem.CacheGet(scope, cacheKey)
+ if client == nil {
+ signer, err := ssh.ParsePrivateKey([]byte(privateKey))
+ if err != nil {
+ vql_subsystem.CacheSet(scope, cacheKey, err)
+ return nil, err
+ }
+ var clientConfig *ssh.ClientConfig
+ if hostKey == "" {
+ clientConfig = &ssh.ClientConfig{
+ User: user,
+ Auth: []ssh.AuthMethod{
+ ssh.PublicKeys(signer),
+ },
+ HostKeyCallback: ssh.InsecureIgnoreHostKey(),
+ }
+ } else {
+ clientConfig = &ssh.ClientConfig{
+ User: user,
+ Auth: []ssh.AuthMethod{
+ ssh.PublicKeys(signer),
+ },
+ HostKeyCallback: hostkeycallback(hostKey),
+ }
+ }
+
+ conn, err := ssh.Dial("tcp", endpoint, clientConfig)
+ if err != nil {
+ vql_subsystem.CacheSet(scope, cacheKey, err)
+ return nil, err
+ }
+ client, err := sftp.NewClient(conn)
+ if err != nil {
+ vql_subsystem.CacheSet(scope, cacheKey, err)
+ return nil, err
+ }
+ vql_subsystem.AddGlobalDestructor(scope, func() {
+ conn.Close()
+ client.Close()
+ })
+ vql_subsystem.CacheSet(scope, cacheKey, client)
+ return client, nil
+ }
+ switch t := client.(type) {
+ case error:
+ return nil, t
+ case *sftp.Client:
+ return t, nil
+ default:
+ return nil, errors.New("Error")
+ }
+}
+
+func upload_SFTP(ctx context.Context, scope *vfilter.Scope,
+ reader io.Reader,
+ user, path, name string,
+ privateKey string, endpoint string, hostKey string) (
+ *api.UploadResponse, error) {
+
+ scope.Log("upload_SFTP: Uploading %v to %v", name, endpoint)
+ client, err := getSFTPClient(scope, user, privateKey, endpoint, hostKey)
+ if err != nil {
+ return &api.UploadResponse{
+ Error: err.Error(),
+ }, err
+ }
+
+ fpath := filepath.Join(path, name)
+ file, err := client.Create(fpath)
+ if err != nil {
+ return &api.UploadResponse{
+ Error: err.Error(),
+ }, err
+ }
+ defer file.Close()
+ if _, err := file.ReadFrom(reader); err != nil {
+ return &api.UploadResponse{
+ Error: err.Error(),
+ }, err
+ }
+
+ check, err := client.Lstat(fpath)
+ if err != nil {
+ return &api.UploadResponse{
+ Error: err.Error(),
+ }, err
+ }
+
+ response := &api.UploadResponse{
+ Path: fpath,
+ Size: uint64(check.Size()),
+ }
+ return response, nil
+}
+
+func (self SFTPUploadFunction) Info(
+ scope *vfilter.Scope, type_map *vfilter.TypeMap) *vfilter.FunctionInfo {
+ return &vfilter.FunctionInfo{
+ Name: "upload_sftp",
+ Doc: "Upload files to SFTP.",
+ ArgType: type_map.AddType(scope, &SFTPUploadArgs{}),
+ }
+}
+
+func init() {
+ vql_subsystem.RegisterFunction(&SFTPUploadFunction{})
+}