diff --git a/actions/client_info.go b/actions/client_info.go index 3735b88ea72..85d9c384e12 100644 --- a/actions/client_info.go +++ b/actions/client_info.go @@ -1,12 +1,13 @@ package actions import ( + "context" "runtime" "github.com/Showmax/go-fqdn" - "github.com/shirou/gopsutil/v3/host" actions_proto "www.velocidex.com/golang/velociraptor/actions/proto" config_proto "www.velocidex.com/golang/velociraptor/config/proto" + "www.velocidex.com/golang/velociraptor/vql/psutils" ) // Return essential information about the client used for indexing @@ -15,10 +16,11 @@ import ( // server periodically to avoid having to issue Generic.Client.Info // hunts all the time. func GetClientInfo( + ctx context.Context, config_obj *config_proto.Config) *actions_proto.ClientInfo { result := &actions_proto.ClientInfo{} - info, err := host.Info() + info, err := psutils.InfoWithContext(ctx) if err == nil { result = &actions_proto.ClientInfo{ Hostname: info.Hostname, diff --git a/actions/throttler.go b/actions/throttler.go index 243d88f3e21..cf59c401c0f 100644 --- a/actions/throttler.go +++ b/actions/throttler.go @@ -8,16 +8,17 @@ package actions import ( "context" "os" + "runtime" "sync" "time" "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/promauto" - "github.com/shirou/gopsutil/v3/cpu" - "github.com/shirou/gopsutil/v3/process" "www.velocidex.com/golang/velociraptor/utils" "www.velocidex.com/golang/vfilter" "www.velocidex.com/golang/vfilter/types" + + "www.velocidex.com/golang/velociraptor/vql/psutils" ) var ( @@ -56,7 +57,8 @@ type statsCollector struct { cond *sync.Cond id uint64 - proc *process.Process + // Our own pid so we can check stats etc + pid int32 samples [2]sample @@ -68,22 +70,12 @@ type statsCollector struct { number_of_cores float64 } -func newStatsCollector() (*statsCollector, error) { - proc, err := process.NewProcess(int32(os.Getpid())) - if err != nil || proc == nil { - return nil, err - } - - number_of_cores, err := cpu.Counts(true) - if err != nil || number_of_cores <= 0 { - return nil, err - } - +func newStatsCollector(ctx context.Context) (*statsCollector, error) { result := &statsCollector{ check_duration_msec: 300, id: utils.GetId(), - proc: proc, - number_of_cores: float64(number_of_cores), + pid: int32(os.Getpid()), + number_of_cores: float64(runtime.NumCPU()), } result.cond = sync.NewCond(&result.mu) @@ -136,15 +128,15 @@ func (self *statsCollector) GetAverageIOPS() float64 { // process. This is called not that frequently in order to minimize // the overheads of making a system call. func (self *statsCollector) getCpuTime(ctx context.Context) float64 { - cpu_time, err := self.proc.TimesWithContext(ctx) + cpu_time, err := psutils.TimesWithContext(ctx, self.pid) if err != nil { return 0 } - return cpu_time.Total() + return cpu_time.User + cpu_time.System } func (self *statsCollector) getIops(ctx context.Context) float64 { - counters, err := self.proc.IOCountersWithContext(ctx) + counters, err := psutils.IOCountersWithContext(ctx, self.pid) if err != nil { return 0 } @@ -259,7 +251,7 @@ func NewThrottler( if stats == nil { var err error - stats, err = newStatsCollector() + stats, err = newStatsCollector(ctx) if err != nil { return nil } @@ -298,12 +290,9 @@ func init() { return stats.GetAverageIOPS() } - proc, err := process.NewProcess(int32(os.Getpid())) - if err != nil || proc == nil { - return 0 - } - - counters, err := proc.IOCounters() + ctx := context.Background() + pid := int32(os.Getpid()) + counters, err := psutils.IOCountersWithContext(ctx, pid) if err != nil { return 0 } @@ -313,26 +302,19 @@ func init() { _ = prometheus.Register(promauto.NewGaugeFunc( prometheus.GaugeOpts{ Name: "process_cpu_used", - Help: "Current CPU utilization by this process", + Help: "Total CPU utilization by this process", }, func() float64 { if stats != nil { return stats.GetAverageCPULoad() } - proc, err := process.NewProcess(int32(os.Getpid())) - if err != nil || proc == nil { - return 0 - } - - number_of_cores, err := cpu.Counts(true) - if err != nil || number_of_cores <= 0 { - return 0 - } - - cpu_time, err := proc.CPUPercent() + ctx := context.Background() + pid := int32(os.Getpid()) + number_of_cores := runtime.NumCPU() + cpu_time, err := psutils.TimesWithContext(ctx, pid) if err != nil { return 0 } - return cpu_time / float64(number_of_cores) + return float64(cpu_time.User+cpu_time.System) / float64(number_of_cores) })) } diff --git a/bin/golden.go b/bin/golden.go index 2c346a6fbe1..3b17dd48940 100644 --- a/bin/golden.go +++ b/bin/golden.go @@ -35,7 +35,6 @@ import ( "github.com/Velocidex/yaml/v2" errors "github.com/go-errors/errors" "github.com/sergi/go-diff/diffmatchpatch" - "github.com/shirou/gopsutil/v3/process" "www.velocidex.com/golang/velociraptor/actions" actions_proto "www.velocidex.com/golang/velociraptor/actions/proto" config_proto "www.velocidex.com/golang/velociraptor/config/proto" @@ -49,6 +48,7 @@ import ( "www.velocidex.com/golang/velociraptor/utils" vql_subsystem "www.velocidex.com/golang/velociraptor/vql" "www.velocidex.com/golang/velociraptor/vql/acl_managers" + "www.velocidex.com/golang/velociraptor/vql/psutils" "www.velocidex.com/golang/velociraptor/vql/remapping" vfilter "www.velocidex.com/golang/vfilter" "www.velocidex.com/golang/vfilter/arg_parser" @@ -128,9 +128,9 @@ func makeCtxWithTimeout( // the goroutines and mutex and hard exit. case <-time.After(time.Second): if time.Now().Before(deadline) { - proc, _ := process.NewProcess(int32(os.Getpid())) - total_time, _ := proc.Percent(0) - memory, _ := proc.MemoryInfo() + pid := int32(os.Getpid()) + total_time, _ := psutils.TimesWithContext(ctx, pid) + memory, _ := psutils.MemoryInfoWithContext(ctx, pid) fmt.Printf("Not time to fire yet %v %v %v\n", time.Now(), total_time, memory) diff --git a/freebsd/velociraptor.rc b/docs/freebsd/velociraptor.rc similarity index 100% rename from freebsd/velociraptor.rc rename to docs/freebsd/velociraptor.rc diff --git a/executor/executor.go b/executor/executor.go index 366cc59551c..b324f916f2d 100644 --- a/executor/executor.go +++ b/executor/executor.go @@ -77,7 +77,7 @@ type ClientExecutor struct { } func (self *ClientExecutor) GetClientInfo() *actions_proto.ClientInfo { - return actions.GetClientInfo(self.config_obj) + return actions.GetClientInfo(self.ctx, self.config_obj) } func (self *ClientExecutor) FlowManager() *responder.FlowManager { diff --git a/uploads/file_based.go b/uploads/file_based.go index 0dffbd138c1..da9c52b7555 100644 --- a/uploads/file_based.go +++ b/uploads/file_based.go @@ -80,8 +80,7 @@ func (self *FileBasedUploader) Upload( atime time.Time, ctime time.Time, btime time.Time, - reader io.Reader) ( - *UploadResponse, error) { + reader io.Reader) (*UploadResponse, error) { if self.UploadDir == "" { scope.Log("UploadDir is not set") @@ -152,7 +151,10 @@ func (self *FileBasedUploader) Upload( } // It is not an error if we cant set the timestamps - best effort. - _ = setFileTimestamps(file_path, mtime, atime, ctime) + err = setFileTimestamps(file_path, mtime, atime, ctime) + if err != nil { + scope.Log("FileBasedUploader: %v", err) + } scope.Log("Uploaded %v (%v bytes)", file_path, offset) result = &UploadResponse{ @@ -171,8 +173,7 @@ func (self *FileBasedUploader) maybeCollectSparseFile( ctx context.Context, reader io.Reader, store_as_name *accessors.OSPath, - sanitized_name string) ( - *UploadResponse, error) { + sanitized_name string) (*UploadResponse, error) { // Can the reader produce ranges? range_reader, ok := reader.(RangeReader) diff --git a/vql/filesystem/filesystem.go b/vql/filesystem/filesystem.go index b047e939852..c640ad868ae 100644 --- a/vql/filesystem/filesystem.go +++ b/vql/filesystem/filesystem.go @@ -26,13 +26,13 @@ import ( "github.com/Velocidex/ordereddict" "github.com/go-errors/errors" - "github.com/shirou/gopsutil/v3/disk" "www.velocidex.com/golang/velociraptor/accessors" "www.velocidex.com/golang/velociraptor/acls" config_proto "www.velocidex.com/golang/velociraptor/config/proto" "www.velocidex.com/golang/velociraptor/glob" "www.velocidex.com/golang/velociraptor/vql" vql_subsystem "www.velocidex.com/golang/velociraptor/vql" + "www.velocidex.com/golang/velociraptor/vql/psutils" "www.velocidex.com/golang/vfilter" "www.velocidex.com/golang/vfilter/arg_parser" ) @@ -448,7 +448,7 @@ func init() { scope vfilter.Scope, args *ordereddict.Dict) []vfilter.Row { var result []vfilter.Row - partitions, err := disk.Partitions(true) + partitions, err := psutils.PartitionsWithContext(ctx) if err == nil { for _, item := range partitions { result = append(result, item) diff --git a/vql/filesystem/filesystems.go b/vql/filesystem/filesystems.go index 1a4b6612786..236c7115be1 100644 --- a/vql/filesystem/filesystems.go +++ b/vql/filesystem/filesystems.go @@ -21,18 +21,18 @@ import ( "context" "github.com/Velocidex/ordereddict" - "github.com/shirou/gopsutil/v3/disk" vql_subsystem "www.velocidex.com/golang/velociraptor/vql" + "www.velocidex.com/golang/velociraptor/vql/psutils" "www.velocidex.com/golang/vfilter" "www.velocidex.com/golang/vfilter/arg_parser" ) type ExtendedFileSystemInfo struct { - Partition disk.PartitionStat + Partition psutils.PartitionStat } -func (self ExtendedFileSystemInfo) Usage() *disk.UsageStat { - usage, err := disk.Usage(self.Partition.Mountpoint) +func (self ExtendedFileSystemInfo) Usage() *psutils.UsageStat { + usage, err := psutils.Usage(self.Partition.Mountpoint) if err != nil { return nil } @@ -40,15 +40,12 @@ func (self ExtendedFileSystemInfo) Usage() *disk.UsageStat { return usage } -/* func (self ExtendedFileSystemInfo) SerialNumber() string { - return disk.GetDiskSerialNumber(self.Partition.Device) + res, _ := psutils.SerialNumber(self.Partition.Device) + return res } -*/ -type PartitionsArgs struct { - All bool `vfilter:"optional,field=all,doc=If specified list all Partitions"` -} +type PartitionsArgs struct{} func init() { vql_subsystem.RegisterPlugin( @@ -67,7 +64,8 @@ func init() { return result } - if partitions, err := disk.Partitions(arg.All); err == nil { + partitions, err := psutils.PartitionsWithContext(ctx) + if err == nil { for _, item := range partitions { extended_info := ExtendedFileSystemInfo{item} result = append(result, extended_info) diff --git a/vql/functions/pskill.go b/vql/functions/pskill.go index bb86c544e18..8534ab4d1dd 100644 --- a/vql/functions/pskill.go +++ b/vql/functions/pskill.go @@ -21,10 +21,10 @@ import ( "context" "github.com/Velocidex/ordereddict" - "github.com/shirou/gopsutil/v3/process" "www.velocidex.com/golang/velociraptor/acls" "www.velocidex.com/golang/velociraptor/vql" vql_subsystem "www.velocidex.com/golang/velociraptor/vql" + "www.velocidex.com/golang/velociraptor/vql/psutils" "www.velocidex.com/golang/vfilter" "www.velocidex.com/golang/vfilter/arg_parser" ) @@ -54,13 +54,12 @@ func (self *PsKillFunction) Call(ctx context.Context, return vfilter.Null{} } - process_obj, err := process.NewProcess(int32(arg.Pid)) + err = psutils.Kill(int32(arg.Pid)) if err != nil { scope.Log("pskill: %v", err) return vfilter.Null{} } - - return process_obj.Kill() + return arg.Pid } func (self PsKillFunction) Info(scope vfilter.Scope, type_map *vfilter.TypeMap) *vfilter.FunctionInfo { diff --git a/vql/info.go b/vql/info.go index cf688339038..fe76bfadc0c 100644 --- a/vql/info.go +++ b/vql/info.go @@ -25,9 +25,9 @@ import ( fqdn "github.com/Showmax/go-fqdn" "github.com/Velocidex/ordereddict" - "github.com/shirou/gopsutil/v3/host" "www.velocidex.com/golang/velociraptor/acls" + "www.velocidex.com/golang/velociraptor/vql/psutils" "www.velocidex.com/golang/vfilter" "www.velocidex.com/golang/vfilter/arg_parser" ) @@ -36,7 +36,7 @@ var ( start_time = time.Now() ) -func GetInfo(host *host.InfoStat) *ordereddict.Dict { +func GetInfo(host *psutils.InfoStat) *ordereddict.Dict { me, _ := os.Executable() return ordereddict.NewDict(). Set("Hostname", host.Hostname). @@ -84,9 +84,9 @@ func init() { // It turns out that host.Info() is // actually rather slow so we cache it // in the scope cache. - info, ok := CacheGet(scope, "__info").(*host.InfoStat) + info, ok := CacheGet(scope, "__info").(*psutils.InfoStat) if !ok { - info, err = host.Info() + info, err = psutils.InfoWithContext(ctx) if err != nil { scope.Log("info: %s", err) return result diff --git a/vql/linux/connections.go b/vql/linux/connections.go index 44bd4372cdd..982ddba5b7d 100755 --- a/vql/linux/connections.go +++ b/vql/linux/connections.go @@ -23,14 +23,14 @@ import ( "syscall" "github.com/Velocidex/ordereddict" - "github.com/shirou/gopsutil/v3/net" "www.velocidex.com/golang/velociraptor/acls" "www.velocidex.com/golang/velociraptor/vql" vql_subsystem "www.velocidex.com/golang/velociraptor/vql" + "www.velocidex.com/golang/velociraptor/vql/psutils" "www.velocidex.com/golang/vfilter" ) -func makeDict(in net.ConnectionStat) *ordereddict.Dict { +func makeDict(in psutils.ConnectionStat) *ordereddict.Dict { var family, conn_type string switch in.Family { @@ -90,7 +90,8 @@ func init() { return result } - if cons, err := net.Connections("all"); err == nil { + cons, err := psutils.ConnectionsWithContext(ctx, "all") + if err == nil { for _, item := range cons { result = append(result, makeDict(item)) } diff --git a/vql/process.go b/vql/process.go index 7673d296f1a..60d8b87c0a5 100755 --- a/vql/process.go +++ b/vql/process.go @@ -18,16 +18,14 @@ along with this program. If not, see . */ -// This module is built on gopsutils but this is too slow and -// inefficient. Eventually we will remove it from the codebase. package vql import ( "context" "github.com/Velocidex/ordereddict" - "github.com/shirou/gopsutil/v3/process" "www.velocidex.com/golang/velociraptor/acls" + "www.velocidex.com/golang/velociraptor/vql/psutils" "www.velocidex.com/golang/vfilter" "www.velocidex.com/golang/vfilter/arg_parser" ) @@ -62,17 +60,17 @@ func init() { // If the user asked for one process // just return that one. if arg.Pid != 0 { - process_obj, err := process.NewProcess(int32(arg.Pid)) + process_obj, err := psutils.GetProcess(ctx, int32(arg.Pid)) if err == nil { - result = append(result, getProcessData(process_obj)) + result = append(result, process_obj) } return result } - processes, err := process.Processes() + processes, err := psutils.ListProcesses(ctx) if err == nil { for _, item := range processes { - result = append(result, getProcessData(item)) + result = append(result, item) } } return result @@ -81,40 +79,3 @@ func init() { Doc: "List processes", }) } - -// Only get a few fields from the process object otherwise we will -// spend too much time calling into virtual methods. -func getProcessData(process *process.Process) *ordereddict.Dict { - result := ordereddict.NewDict().SetCaseInsensitive(). - Set("Pid", process.Pid) - - name, _ := process.Name() - result.Set("Name", name) - - ppid, _ := process.Ppid() - result.Set("Ppid", ppid) - - // Make it compatible with the Windows pslist() - cmdline, _ := process.Cmdline() - result.Set("CommandLine", cmdline) - - create_time, _ := process.CreateTime() - result.Set("CreateTime", create_time) - - times, _ := process.Times() - result.Set("Times", times) - - exe, _ := process.Exe() - result.Set("Exe", exe) - - cwd, _ := process.Cwd() - result.Set("Cwd", cwd) - - user, _ := process.Username() - result.Set("Username", user) - - memory_info, _ := process.MemoryInfo() - result.Set("MemoryInfo", memory_info) - - return result -} diff --git a/vql/psutils/common.go b/vql/psutils/common.go new file mode 100644 index 00000000000..4abdd353491 --- /dev/null +++ b/vql/psutils/common.go @@ -0,0 +1,52 @@ +package psutils + +import ( + "fmt" + "os" +) + +// PathExistsWithContents returns the filename exists and it is not +// empty +func PathExistsWithContents(filename string) bool { + info, err := os.Stat(filename) + if err != nil { + return false + } + return info.Size() > 4 // at least 4 bytes +} + +func GetHostProc(pid int32) string { + return fmt.Sprintf("%s/%d", GetEnv("HOST_PROC", "/proc"), pid) +} + +func GetEnv(key string, dfault string) string { + value := os.Getenv(key) + if value == "" { + return dfault + } + + return value +} + +func ByteToString(orig []byte) string { + n := -1 + l := -1 + for i, b := range orig { + // skip left side null + if l == -1 && b == 0 { + continue + } + if l == -1 { + l = i + } + + if b == 0 { + break + } + n = i + 1 + } + if n == -1 { + return string(orig) + } + return string(orig[l:n]) +} diff --git a/vql/psutils/connections.go b/vql/psutils/connections.go new file mode 100644 index 00000000000..261cf125037 --- /dev/null +++ b/vql/psutils/connections.go @@ -0,0 +1 @@ +package psutils diff --git a/vql/psutils/cpu.go b/vql/psutils/cpu.go new file mode 100644 index 00000000000..f0ad7452cf9 --- /dev/null +++ b/vql/psutils/cpu.go @@ -0,0 +1,11 @@ +package psutils + +import ( + "context" + + "github.com/shirou/gopsutil/v3/cpu" +) + +func CountsWithContext(ctx context.Context, logical bool) (int, error) { + return cpu.CountsWithContext(ctx, logical) +} diff --git a/vql/psutils/disk.go b/vql/psutils/disk.go new file mode 100644 index 00000000000..24a88501826 --- /dev/null +++ b/vql/psutils/disk.go @@ -0,0 +1,42 @@ +package psutils + +import ( + "context" + + "github.com/shirou/gopsutil/v3/disk" +) + +type UsageStat struct { + disk.UsageStat +} + +type PartitionStat struct { + disk.PartitionStat +} + +func Usage(mount string) (*UsageStat, error) { + usage, err := disk.Usage(mount) + if err != nil { + return nil, err + } + + return &UsageStat{*usage}, nil +} + +func SerialNumber(disk_name string) (string, error) { + return disk.SerialNumber(disk_name) +} + +func PartitionsWithContext(ctx context.Context) ([]PartitionStat, error) { + res, err := disk.PartitionsWithContext(ctx, true) + if err != nil { + return nil, err + } + + result := make([]PartitionStat, 0, len(res)) + for _, i := range res { + result = append(result, PartitionStat{i}) + } + + return result, nil +} diff --git a/vql/psutils/doc.go b/vql/psutils/doc.go new file mode 100644 index 00000000000..37976548df4 --- /dev/null +++ b/vql/psutils/doc.go @@ -0,0 +1,16 @@ +package psutils + +/* + This is a wrapper package around gopsutils. gopsutils chooses to + shell out to various commands (ps, lsof etc) on some platforms which + makes it very hard to predict how expensive an operation is likely + to be. + + The purpose of this wrapper is to better control what operations are + allowed on different platforms to avoid this shelling behavior. + + We normally delegate through to gopsutils but in some situations we + would prefer to fail the operations rather than shell out to + external tools. Eventually we can remove all dependency on gopsutils + from this wrapper and simply reimplement the API surface we need. +*/ diff --git a/vql/psutils/gopsutil_LICENSE.txt b/vql/psutils/gopsutil_LICENSE.txt new file mode 100644 index 00000000000..6f06adcbff3 --- /dev/null +++ b/vql/psutils/gopsutil_LICENSE.txt @@ -0,0 +1,61 @@ +gopsutil is distributed under BSD license reproduced below. + +Copyright (c) 2014, WAKAYAMA Shirou +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + * Neither the name of the gopsutil authors nor the names of its contributors + may be used to endorse or promote products derived from this software without + specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +------- +internal/common/binary.go in the gopsutil is copied and modified from golang/encoding/binary.go. + + + +Copyright (c) 2009 The Go Authors. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + * Neither the name of Google Inc. nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. \ No newline at end of file diff --git a/vql/psutils/host.go b/vql/psutils/host.go new file mode 100644 index 00000000000..8136eeb839a --- /dev/null +++ b/vql/psutils/host.go @@ -0,0 +1,19 @@ +package psutils + +import ( + "context" + + "github.com/shirou/gopsutil/v3/host" +) + +type InfoStat struct { + host.InfoStat +} + +func InfoWithContext(ctx context.Context) (*InfoStat, error) { + res, err := host.InfoWithContext(ctx) + if err != nil { + return nil, err + } + return &InfoStat{InfoStat: *res}, err +} diff --git a/vql/psutils/net.go b/vql/psutils/net.go new file mode 100644 index 00000000000..0bcdcc11612 --- /dev/null +++ b/vql/psutils/net.go @@ -0,0 +1,25 @@ +package psutils + +import ( + "context" + + "github.com/shirou/gopsutil/v3/net" +) + +type ConnectionStat struct { + net.ConnectionStat +} + +func ConnectionsWithContext( + ctx context.Context, kind string) ([]ConnectionStat, error) { + res, err := net.ConnectionsWithContext(ctx, kind) + if err != nil { + return nil, err + } + + result := make([]ConnectionStat, 0, len(res)) + for _, i := range res { + result = append(result, ConnectionStat{ConnectionStat: i}) + } + return result, nil +} diff --git a/vql/psutils/process.go b/vql/psutils/process.go new file mode 100644 index 00000000000..cdc8ce201d3 --- /dev/null +++ b/vql/psutils/process.go @@ -0,0 +1,40 @@ +package psutils + +import ( + "errors" + "os" +) + +var ( + ErrorProcessNotRunning = errors.New("ErrorProcessNotRunning") + ErrorNotPermitted = errors.New("operation not permitted") + NotImplementedError = errors.New("NotImplementedError") +) + +type TimesStat struct { + CPU string `json:"cpu"` + User float64 `json:"user"` + System float64 `json:"system"` +} + +type MemoryInfoStat struct { + RSS uint64 `json:"rss"` // bytes + VMS uint64 `json:"vms"` // bytes + Swap uint64 `json:"swap"` // bytes +} + +type IOCountersStat struct { + ReadCount uint64 `json:"readCount"` + WriteCount uint64 `json:"writeCount"` + ReadBytes uint64 `json:"readBytes"` + WriteBytes uint64 `json:"writeBytes"` +} + +// This works on all OSs +func Kill(pid int32) error { + process, err := os.FindProcess(int(pid)) + if err != nil { + return err + } + return process.Kill() +} diff --git a/vql/psutils/process_darwin.go b/vql/psutils/process_darwin.go new file mode 100644 index 00000000000..b024ab1fba8 --- /dev/null +++ b/vql/psutils/process_darwin.go @@ -0,0 +1,114 @@ +//go:build darwin +// +build darwin + +package psutils + +import ( + "context" + "os/user" + "strconv" + "strings" + "time" + + "golang.org/x/sys/unix" + + "github.com/Velocidex/ordereddict" + "www.velocidex.com/golang/velociraptor/utils" +) + +func GetProcess(ctx context.Context, pid int32) (*ordereddict.Dict, error) { + proc, err := getKProc(pid) + if err != nil { + return nil, err + } + + return getProcessData(ctx, proc), nil +} + +func ListProcesses(ctx context.Context) ([]*ordereddict.Dict, error) { + result := []*ordereddict.Dict{} + processes, err := Processes() + if err != nil { + return nil, err + } + + for _, item := range processes { + if false { + utils.Debug(item) + } + result = append(result, getProcessData(ctx, &item)) + } + + return result, nil +} + +func Processes() ([]unix.KinfoProc, error) { + var ret []unix.KinfoProc + + kprocs, err := unix.SysctlKinfoProcSlice("kern.proc.all") + if err != nil { + return ret, err + } + + for _, proc := range kprocs { + ret = append(ret, proc) + } + + return ret, nil +} + +func getKProc(pid int32) (*unix.KinfoProc, error) { + return unix.SysctlKinfoProc("kern.proc.pid", int(pid)) +} + +func getProcessData(ctx context.Context, + proc *unix.KinfoProc) *ordereddict.Dict { + pid := proc.Proc.P_pid + + name, err := cmdNameWithContext(ctx, pid) + if err != nil { + name = ByteToString(proc.Proc.P_comm[:]) + } + + cmdline, err := cmdlineSliceWithContext(ctx, pid) + if err != nil { + cmdline = append(cmdline, name) + } + + times, _ := TimesWithContext(ctx, pid) + exe, _ := ExeWithContext(ctx, pid) + cwd, _ := CwdWithContext(ctx, pid) + + uid := proc.Eproc.Ucred.Uid + + username := "" + user, err := user.LookupId(strconv.Itoa(int(uid))) + if err == nil { + username = user.Username + } + + memory_info, _ := MemoryInfoWithContext(ctx, pid) + + result := ordereddict.NewDict(). + SetCaseInsensitive(). + Set("Pid", pid). + Set("Name", name). + Set("Ppid", proc.Eproc.Ppid). + + // Make it compatible with the Windows pslist() + Set("CommandLine", strings.Join(cmdline, " ")). + Set("_Argv", cmdline). + Set("CreateTime", time.Unix(proc.Proc.P_starttime.Sec, int64(proc.Proc.P_starttime.Usec)/1000)). + Set("Times", times). + Set("Exe", exe). + Set("Cwd", cwd). + Set("Uid", uid). + Set("Username", username). + Set("MemoryInfo", memory_info) + + return result +} + +func IOCountersWithContext(ctx context.Context, pid int32) (*IOCountersStat, error) { + return nil, NotImplementedError +} diff --git a/vql/psutils/process_darwin_cgo.go b/vql/psutils/process_darwin_cgo.go new file mode 100644 index 00000000000..e27e0171ce6 --- /dev/null +++ b/vql/psutils/process_darwin_cgo.go @@ -0,0 +1,216 @@ +//go:build darwin && cgo +// +build darwin,cgo + +package psutils + +// #include +// #include +// #include +// #include +// #include +// #include +// #include +import "C" + +import ( + "bytes" + "context" + "fmt" + "strings" + "syscall" + "unsafe" +) + +var ( + argMax int + timescaleToNanoSeconds float64 +) + +func init() { + argMax = getArgMax() + timescaleToNanoSeconds = getTimeScaleToNanoSeconds() +} + +func getArgMax() int { + var ( + mib = [...]C.int{C.CTL_KERN, C.KERN_ARGMAX} + argmax C.int + size C.size_t = C.ulong(unsafe.Sizeof(argmax)) + ) + retval := C.sysctl(&mib[0], 2, unsafe.Pointer(&argmax), &size, C.NULL, 0) + if retval == 0 { + return int(argmax) + } + return 0 +} + +func getTimeScaleToNanoSeconds() float64 { + var timeBaseInfo C.struct_mach_timebase_info + + C.mach_timebase_info(&timeBaseInfo) + + return float64(timeBaseInfo.numer) / float64(timeBaseInfo.denom) +} + +func ExeWithContext(ctx context.Context, pid int32) (string, error) { + var c C.char // need a var for unsafe.Sizeof need a var + const bufsize = C.PROC_PIDPATHINFO_MAXSIZE * unsafe.Sizeof(c) + buffer := (*C.char)(C.malloc(C.size_t(bufsize))) + defer C.free(unsafe.Pointer(buffer)) + + ret, err := C.proc_pidpath(C.int(pid), unsafe.Pointer(buffer), C.uint32_t(bufsize)) + if err != nil { + return "", err + } + if ret <= 0 { + return "", fmt.Errorf("unknown error: proc_pidpath returned %d", ret) + } + + return C.GoString(buffer), nil +} + +// CwdWithContext retrieves the Current Working Directory for the given process. +// It uses the proc_pidinfo from libproc and will only work for processes the +// EUID can access. Otherwise "operation not permitted" will be returned as the +// error. +// Note: This might also work for other *BSD OSs. +func CwdWithContext(ctx context.Context, pid int32) (string, error) { + const vpiSize = C.sizeof_struct_proc_vnodepathinfo + vpi := (*C.struct_proc_vnodepathinfo)(C.malloc(vpiSize)) + defer C.free(unsafe.Pointer(vpi)) + ret, err := C.proc_pidinfo(C.int(pid), C.PROC_PIDVNODEPATHINFO, 0, unsafe.Pointer(vpi), vpiSize) + if err != nil { + // fmt.Printf("ret: %d %T\n", ret, err) + if err == syscall.EPERM { + return "", ErrorNotPermitted + } + return "", err + } + if ret <= 0 { + return "", fmt.Errorf("unknown error: proc_pidinfo returned %d", ret) + } + if ret != C.sizeof_struct_proc_vnodepathinfo { + return "", fmt.Errorf("too few bytes; expected %d, got %d", vpiSize, ret) + } + return C.GoString(&vpi.pvi_cdir.vip_path[0]), err +} + +func procArgs(pid int32) ([]byte, int, error) { + var ( + mib = [...]C.int{C.CTL_KERN, C.KERN_PROCARGS2, C.int(pid)} + size C.size_t = C.ulong(argMax) + nargs C.int + result []byte + ) + procargs := (*C.char)(C.malloc(C.ulong(argMax))) + defer C.free(unsafe.Pointer(procargs)) + retval, err := C.sysctl(&mib[0], 3, unsafe.Pointer(procargs), &size, C.NULL, 0) + if retval == 0 { + C.memcpy(unsafe.Pointer(&nargs), unsafe.Pointer(procargs), C.sizeof_int) + result = C.GoBytes(unsafe.Pointer(procargs), C.int(size)) + // fmt.Printf("size: %d %d\n%s\n", size, nargs, hex.Dump(result)) + return result, int(nargs), nil + } + return nil, 0, err +} + +func cmdlineSliceWithContext(ctx context.Context, pid int32) ([]string, error) { + pargs, nargs, err := procArgs(pid) + if err != nil { + return nil, err + } + // The first bytes hold the nargs int, skip it. + args := bytes.Split((pargs)[C.sizeof_int:], []byte{0}) + var argStr string + // The first element is the actual binary/command path. + // command := args[0] + var argSlice []string + // var envSlice []string + // All other, non-zero elements are arguments. The first "nargs" elements + // are the arguments. Everything else in the slice is then the environment + // of the process. + for _, arg := range args[1:] { + argStr = string(arg[:]) + if len(argStr) > 0 { + if nargs > 0 { + argSlice = append(argSlice, argStr) + nargs-- + continue + } + break + // envSlice = append(envSlice, argStr) + } + } + return argSlice, err +} + +// cmdNameWithContext returns the command name (including spaces) without any arguments +func cmdNameWithContext(ctx context.Context, pid int32) (string, error) { + r, err := cmdlineSliceWithContext(ctx, pid) + if err != nil { + return "", err + } + + if len(r) == 0 { + return "", nil + } + + return r[0], err +} + +func CmdlineWithContext(ctx context.Context, pid int32) (string, error) { + r, err := cmdlineSliceWithContext(ctx, pid) + if err != nil { + return "", err + } + return strings.Join(r, " "), err +} + +func NumThreadsWithContext(ctx context.Context, pid int32) (int32, error) { + const tiSize = C.sizeof_struct_proc_taskinfo + ti := (*C.struct_proc_taskinfo)(C.malloc(tiSize)) + defer C.free(unsafe.Pointer(ti)) + + _, err := C.proc_pidinfo(C.int(pid), C.PROC_PIDTASKINFO, 0, unsafe.Pointer(ti), tiSize) + if err != nil { + return 0, err + } + + return int32(ti.pti_threadnum), nil +} + +func TimesWithContext(ctx context.Context, pid int32) (*TimesStat, error) { + const tiSize = C.sizeof_struct_proc_taskinfo + ti := (*C.struct_proc_taskinfo)(C.malloc(tiSize)) + defer C.free(unsafe.Pointer(ti)) + + _, err := C.proc_pidinfo(C.int(pid), C.PROC_PIDTASKINFO, 0, unsafe.Pointer(ti), tiSize) + if err != nil { + return nil, err + } + + ret := &TimesStat{ + CPU: "cpu", + User: float64(ti.pti_total_user) * timescaleToNanoSeconds / 1e9, + System: float64(ti.pti_total_system) * timescaleToNanoSeconds / 1e9, + } + return ret, nil +} + +func MemoryInfoWithContext(ctx context.Context, pid int32) (*MemoryInfoStat, error) { + const tiSize = C.sizeof_struct_proc_taskinfo + ti := (*C.struct_proc_taskinfo)(C.malloc(tiSize)) + defer C.free(unsafe.Pointer(ti)) + + _, err := C.proc_pidinfo(C.int(pid), C.PROC_PIDTASKINFO, 0, unsafe.Pointer(ti), tiSize) + if err != nil { + return nil, err + } + + ret := &MemoryInfoStat{ + RSS: uint64(ti.pti_resident_size), + VMS: uint64(ti.pti_virtual_size), + Swap: uint64(ti.pti_pageins), + } + return ret, nil +} diff --git a/vql/psutils/process_darwin_nocgo.go b/vql/psutils/process_darwin_nocgo.go new file mode 100644 index 00000000000..c00a8105ee7 --- /dev/null +++ b/vql/psutils/process_darwin_nocgo.go @@ -0,0 +1,32 @@ +//go:build darwin && !cgo +// +build darwin,!cgo + +package psutils + +import ( + "context" +) + +func cmdNameWithContext(ctx context.Context, pid int32) (string, error) { + return "", NotImplementedError +} + +func cmdlineSliceWithContext(ctx context.Context, pid int32) ([]string, error) { + return nil, NotImplementedError +} + +func TimesWithContext(ctx context.Context, pid int32) (*TimesStat, error) { + return nil, NotImplementedError +} + +func ExeWithContext(ctx context.Context, pid int32) (string, error) { + return "", NotImplementedError +} + +func CwdWithContext(ctx context.Context, pid int32) (string, error) { + return "", NotImplementedError +} + +func MemoryInfoWithContext(ctx context.Context, pid int32) (*MemoryInfoStat, error) { + return nil, NotImplementedError +} diff --git a/vql/psutils/process_posix.go b/vql/psutils/process_posix.go new file mode 100644 index 00000000000..ab1f748db40 --- /dev/null +++ b/vql/psutils/process_posix.go @@ -0,0 +1,119 @@ +//go:build linux || freebsd +// +build linux freebsd + +// This file is for operating systems with /proc + +package psutils + +import ( + "context" + + "github.com/Velocidex/ordereddict" + "github.com/shirou/gopsutil/v3/process" +) + +func GetProcess(ctx context.Context, pid int32) (*ordereddict.Dict, error) { + process_obj, err := process.NewProcessWithContext(ctx, pid) + if err != nil { + return nil, err + } + + return getProcessData(process_obj), nil +} + +func ListProcesses(ctx context.Context) ([]*ordereddict.Dict, error) { + result := []*ordereddict.Dict{} + processes, err := process.Processes() + if err != nil { + return nil, err + } + + for _, item := range processes { + result = append(result, getProcessData(item)) + } + + return result, nil +} + +// Only get a few fields from the process object otherwise we will +// spend too much time calling into virtual methods. +func getProcessData(process *process.Process) *ordereddict.Dict { + result := ordereddict.NewDict().SetCaseInsensitive(). + Set("Pid", process.Pid) + + name, _ := process.Name() + result.Set("Name", name) + + ppid, _ := process.Ppid() + result.Set("Ppid", ppid) + + // Make it compatible with the Windows pslist() + cmdline, _ := process.Cmdline() + result.Set("CommandLine", cmdline) + + create_time, _ := process.CreateTime() + result.Set("CreateTime", create_time) + + times, _ := process.Times() + result.Set("Times", times) + + exe, _ := process.Exe() + result.Set("Exe", exe) + + cwd, _ := process.Cwd() + result.Set("Cwd", cwd) + + user, _ := process.Username() + result.Set("Username", user) + + memory_info, _ := process.MemoryInfo() + result.Set("MemoryInfo", memory_info) + + return result +} + +func MemoryInfoWithContext(ctx context.Context, pid int32) (*MemoryInfoStat, error) { + delegate := &process.Process{Pid: pid} + mem, err := delegate.MemoryInfoWithContext(ctx) + if err != nil { + return nil, err + } + + ret := &MemoryInfoStat{ + RSS: mem.RSS, + VMS: mem.VMS, + Swap: mem.Swap, + } + + return ret, nil +} + +func TimesWithContext(ctx context.Context, pid int32) (*TimesStat, error) { + delegate := &process.Process{Pid: pid} + times, err := delegate.TimesWithContext(ctx) + if err != nil { + return nil, err + } + + return &TimesStat{ + CPU: times.CPU, + User: times.User, + System: times.System, + }, nil +} + +func IOCountersWithContext(ctx context.Context, pid int32) (*IOCountersStat, error) { + delegate := &process.Process{Pid: pid} + counters, err := delegate.IOCountersWithContext(ctx) + + if err != nil { + return nil, err + } + + return &IOCountersStat{ + ReadCount: counters.ReadCount, + WriteCount: counters.WriteCount, + ReadBytes: counters.ReadBytes, + WriteBytes: counters.WriteBytes, + }, nil +} diff --git a/vql/psutils/process_windows.go b/vql/psutils/process_windows.go new file mode 100644 index 00000000000..c21ac79a8d1 --- /dev/null +++ b/vql/psutils/process_windows.go @@ -0,0 +1,153 @@ +//go:build windows +// +build windows + +package psutils + +import ( + "context" + "fmt" + "syscall" + "unsafe" + + "golang.org/x/sys/windows" +) + +const ( + processQueryInformation = windows.PROCESS_QUERY_LIMITED_INFORMATION +) + +var ( + Modkernel32 = windows.NewLazySystemDLL("kernel32.dll") + procGetProcessIoCounters = Modkernel32.NewProc("GetProcessIoCounters") + + modpsapi = windows.NewLazySystemDLL("psapi.dll") + procGetProcessMemoryInfo = modpsapi.NewProc("GetProcessMemoryInfo") +) + +func PidExistsWithContext(ctx context.Context, pid int32) (bool, error) { + if pid == 0 { // special case for pid 0 System Idle Process + return true, nil + } + + if pid < 0 { + return false, fmt.Errorf("invalid pid %v", pid) + } + + if pid%4 != 0 { + // OpenProcess will succeed even on non-existing pid here https://devblogs.microsoft.com/oldnewthing/20080606-00/?p=22043 + // Valid pids are multiple of 4 so we can reject these immediately. + return false, nil + } + + h, err := windows.OpenProcess(windows.SYNCHRONIZE, false, uint32(pid)) + if err == windows.ERROR_ACCESS_DENIED { + return true, nil + } + + if err == windows.ERROR_INVALID_PARAMETER { + return false, nil + } + + if err != nil { + return false, err + } + + defer windows.CloseHandle(h) + event, err := windows.WaitForSingleObject(h, 0) + return event == uint32(windows.WAIT_TIMEOUT), err +} + +func TimesWithContext(ctx context.Context, pid int32) (*TimesStat, error) { + var times struct { + CreateTime syscall.Filetime + ExitTime syscall.Filetime + KernelTime syscall.Filetime + UserTime syscall.Filetime + } + + h, err := windows.OpenProcess(processQueryInformation, false, uint32(pid)) + if err != nil { + return nil, err + } + defer windows.CloseHandle(h) + + err = syscall.GetProcessTimes( + syscall.Handle(h), + ×.CreateTime, + ×.ExitTime, + ×.KernelTime, + ×.UserTime, + ) + + user := float64(times.UserTime.HighDateTime)*429.4967296 + float64(times.UserTime.LowDateTime)*1e-7 + kernel := float64(times.KernelTime.HighDateTime)*429.4967296 + float64(times.KernelTime.LowDateTime)*1e-7 + + return &TimesStat{ + User: user, + System: kernel, + }, nil +} + +func getProcessMemoryInfo(h windows.Handle, mem *PROCESS_MEMORY_COUNTERS) (err error) { + r1, _, e1 := syscall.Syscall(procGetProcessMemoryInfo.Addr(), 3, uintptr(h), uintptr(unsafe.Pointer(mem)), uintptr(unsafe.Sizeof(*mem))) + if r1 == 0 { + if e1 != 0 { + err = error(e1) + } else { + err = syscall.EINVAL + } + } + return +} + +func MemoryInfoWithContext(ctx context.Context, pid int32) (*MemoryInfoStat, error) { + var mem PROCESS_MEMORY_COUNTERS + + c, err := windows.OpenProcess(processQueryInformation, false, uint32(pid)) + if err != nil { + return nil, err + } + defer windows.CloseHandle(c) + if err := getProcessMemoryInfo(c, &mem); err != nil { + return nil, err + } + + ret := &MemoryInfoStat{ + RSS: uint64(mem.WorkingSetSize), + VMS: uint64(mem.PagefileUsage), + } + + return ret, nil +} + +func IOCountersWithContext(ctx context.Context, pid int32) (*IOCountersStat, error) { + // ioCounters is an equivalent representation of IO_COUNTERS in the Windows API. + // https://docs.microsoft.com/windows/win32/api/winnt/ns-winnt-io_counters + var ioCounters struct { + ReadOperationCount uint64 + WriteOperationCount uint64 + OtherOperationCount uint64 + ReadTransferCount uint64 + WriteTransferCount uint64 + OtherTransferCount uint64 + } + + c, err := windows.OpenProcess(processQueryInformation, false, uint32(pid)) + if err != nil { + return nil, err + } + defer windows.CloseHandle(c) + + ret, _, err := procGetProcessIoCounters.Call(uintptr(c), uintptr(unsafe.Pointer(&ioCounters))) + if ret == 0 { + return nil, err + } + stats := &IOCountersStat{ + ReadCount: ioCounters.ReadOperationCount, + ReadBytes: ioCounters.ReadTransferCount, + WriteCount: ioCounters.WriteOperationCount, + WriteBytes: ioCounters.WriteTransferCount, + } + + return stats, nil +} diff --git a/vql/psutils/process_windows_amd64.go b/vql/psutils/process_windows_amd64.go new file mode 100644 index 00000000000..5bd752fe537 --- /dev/null +++ b/vql/psutils/process_windows_amd64.go @@ -0,0 +1,17 @@ +//go:build (windows && amd64) || (windows && arm64) +// +build windows,amd64 windows,arm64 + +package psutils + +type PROCESS_MEMORY_COUNTERS struct { + CB uint32 + PageFaultCount uint32 + PeakWorkingSetSize uint64 + WorkingSetSize uint64 + QuotaPeakPagedPoolUsage uint64 + QuotaPagedPoolUsage uint64 + QuotaPeakNonPagedPoolUsage uint64 + QuotaNonPagedPoolUsage uint64 + PagefileUsage uint64 + PeakPagefileUsage uint64 +} diff --git a/vql/psutils/process_windows_i386.go b/vql/psutils/process_windows_i386.go new file mode 100644 index 00000000000..c75c41013fa --- /dev/null +++ b/vql/psutils/process_windows_i386.go @@ -0,0 +1,17 @@ +//go:build (windows && 386) || (windows && arm) +// +build windows,386 windows,arm + +package psutils + +type PROCESS_MEMORY_COUNTERS struct { + CB uint32 + PageFaultCount uint32 + PeakWorkingSetSize uint32 + WorkingSetSize uint32 + QuotaPeakPagedPoolUsage uint32 + QuotaPagedPoolUsage uint32 + QuotaPeakNonPagedPoolUsage uint32 + QuotaNonPagedPoolUsage uint32 + PagefileUsage uint32 + PeakPagefileUsage uint32 +} diff --git a/vql/tools/collector/collector_manager.go b/vql/tools/collector/collector_manager.go index e9d45e5c48e..accda7a6fea 100644 --- a/vql/tools/collector/collector_manager.go +++ b/vql/tools/collector/collector_manager.go @@ -7,7 +7,6 @@ import ( "time" "github.com/Velocidex/ordereddict" - "github.com/shirou/gopsutil/v3/host" "www.velocidex.com/golang/velociraptor/actions" actions_proto "www.velocidex.com/golang/velociraptor/actions/proto" api_proto "www.velocidex.com/golang/velociraptor/api/proto" @@ -27,6 +26,7 @@ import ( "www.velocidex.com/golang/velociraptor/utils" "www.velocidex.com/golang/velociraptor/vql" "www.velocidex.com/golang/velociraptor/vql/acl_managers" + "www.velocidex.com/golang/velociraptor/vql/psutils" "www.velocidex.com/golang/vfilter" "www.velocidex.com/golang/vfilter/types" ) @@ -200,7 +200,7 @@ func (self *collectionManager) storeHostInfo() error { version := config.GetVersion() var info_dict *ordereddict.Dict - host_info, err := host.Info() + host_info, err := psutils.InfoWithContext(self.ctx) if err != nil { info_dict = ordereddict.NewDict() } else {