From 473356a2d87d6f2aa16d44c1d45ab5180b6a85de Mon Sep 17 00:00:00 2001 From: Evan Gibler <20933572+egibs@users.noreply.github.com> Date: Wed, 25 Sep 2024 11:59:37 -0500 Subject: [PATCH] Add --processes flag to scan active process commands (#469) * Add --processes flag to scan active process commands Signed-off-by: egibs <20933572+egibs@users.noreply.github.com> * Fix Linux ps command Signed-off-by: egibs <20933572+egibs@users.noreply.github.com> * Avoid generating a report for malcontent when running a scan Signed-off-by: egibs <20933572+egibs@users.noreply.github.com> * Use gopsutil instead of parsing ps Signed-off-by: egibs <20933572+egibs@users.noreply.github.com> * Appease the linter Signed-off-by: egibs <20933572+egibs@users.noreply.github.com> * Re-add unique path functionality Signed-off-by: egibs <20933572+egibs@users.noreply.github.com> --------- Signed-off-by: egibs <20933572+egibs@users.noreply.github.com> --- go.mod | 8 ++++++ go.sum | 28 +++++++++++++++++-- malcontent.go | 49 ++++++++++++++++++++++++++++++-- pkg/action/process.go | 54 ++++++++++++++++++++++++++++++++++++ pkg/action/scan.go | 2 +- pkg/malcontent/malcontent.go | 1 + pkg/render/terminal_brief.go | 6 ++-- pkg/report/report.go | 9 ++++-- 8 files changed, 145 insertions(+), 12 deletions(-) create mode 100644 pkg/action/process.go diff --git a/go.mod b/go.mod index 00d9ae249..e44bf018f 100644 --- a/go.mod +++ b/go.mod @@ -11,6 +11,7 @@ require ( github.com/hillu/go-yara/v4 v4.3.3 github.com/liamg/magic v0.0.1 github.com/olekukonko/tablewriter v0.0.5 + github.com/shirou/gopsutil/v4 v4.24.8 github.com/ulikunitz/xz v0.5.12 github.com/urfave/cli/v2 v2.27.4 github.com/wk8/go-ordered-map/v2 v2.1.8 @@ -27,8 +28,10 @@ require ( github.com/docker/cli v27.1.2+incompatible // indirect github.com/docker/distribution v2.8.3+incompatible // indirect github.com/docker/docker-credential-helpers v0.8.2 // indirect + github.com/go-ole/go-ole v1.2.6 // indirect github.com/klauspost/compress v1.17.9 // indirect github.com/kr/pretty v0.2.1 // indirect + github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect github.com/mailru/easyjson v0.7.7 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect @@ -37,10 +40,15 @@ require ( github.com/opencontainers/go-digest v1.0.0 // indirect github.com/opencontainers/image-spec v1.1.0 // indirect github.com/pkg/errors v0.9.1 // indirect + github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect github.com/rivo/uniseg v0.4.7 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect + github.com/shoenig/go-m1cpu v0.1.6 // indirect github.com/sirupsen/logrus v1.9.3 // indirect + github.com/tklauser/go-sysconf v0.3.12 // indirect + github.com/tklauser/numcpus v0.6.1 // indirect github.com/vbatts/tar-split v0.11.5 // indirect github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect + github.com/yusufpapurcu/wmi v1.2.4 // indirect golang.org/x/sys v0.25.0 // indirect ) diff --git a/go.sum b/go.sum index 3d083e5a0..edb36523d 100644 --- a/go.sum +++ b/go.sum @@ -21,6 +21,9 @@ github.com/docker/docker-credential-helpers v0.8.2 h1:bX3YxiGzFP5sOXWc3bTPEXdEaZ github.com/docker/docker-credential-helpers v0.8.2/go.mod h1:P3ci7E3lwkZg6XiHdRKft1KckHiO9a2rNtyFbZ/ry9M= github.com/fatih/color v1.17.0 h1:GlRw1BRJxkpqUCBKzKOw098ed57fEsKeNjpTe3cSjK4= github.com/fatih/color v1.17.0/go.mod h1:YZ7TlrGPkiz6ku9fK3TLD/pl3CpsiFyu8N92HLgmosI= +github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY= +github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= +github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-containerregistry v0.20.2 h1:B1wPJ1SN/S7pB+ZAimcciVD+r+yV/l/DSArMxlbwseo= @@ -37,6 +40,8 @@ github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/liamg/magic v0.0.1 h1:Ru22ElY+sCh6RvRTWjQzKKCxsEco8hE0co8n1qe7TBM= github.com/liamg/magic v0.0.1/go.mod h1:yQkOmZZI52EA+SQ2xyHpVw8fNvTBruF873Y+Vt6S+fk= +github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4= +github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I= github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= @@ -59,17 +64,29 @@ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw= +github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/shirou/gopsutil/v4 v4.24.8 h1:pVQjIenQkIhqO81mwTaXjTzOMT7d3TZkf43PlVFHENI= +github.com/shirou/gopsutil/v4 v4.24.8/go.mod h1:wE0OrJtj4dG+hYkxqDH3QiBICdKSf04/npcvLLc/oRg= +github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFtM= +github.com/shoenig/go-m1cpu v0.1.6/go.mod h1:1JJMcUBvfNwpq05QDQVAnx3gUHr9IYF7GNg9SUEw2VQ= +github.com/shoenig/test v0.6.4 h1:kVTaSd7WLz5WZ2IaoM0RSzRsUD+m8wRR+5qvntpn4LU= +github.com/shoenig/test v0.6.4/go.mod h1:byHiCGXqrVaflBLAMq/srcZIHynQPQgeyvkvXnjqq0k= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= -github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU= +github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI= +github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk= +github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY= github.com/ulikunitz/xz v0.5.12 h1:37Nm15o69RwBkXM0J6A5OlE67RZTfzUxTj8fB3dfcsc= github.com/ulikunitz/xz v0.5.12/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= github.com/urfave/cli/v2 v2.27.4 h1:o1owoI+02Eb+K107p27wEX9Bb8eqIoZCfLXloLUSWJ8= @@ -80,15 +97,22 @@ github.com/wk8/go-ordered-map/v2 v2.1.8 h1:5h/BUHu93oj4gIdvHHHGsScSTMijfx5PeYkE/ github.com/wk8/go-ordered-map/v2 v2.1.8/go.mod h1:5nJHM5DyteebpVlHnWMV0rPz6Zp7+xBAnxjb1X5vnTw= github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4= github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM= +github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= +github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34= golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.24.0 h1:Mh5cbb+Zk2hqqXNO7S1iTjEphVL+jb8ZWaqh/g+JWkM= golang.org/x/term v0.24.0/go.mod h1:lOBK/LVxemqiMij05LGJ0tzNr8xlmwBRJ81PX6wVLH8= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= diff --git a/malcontent.go b/malcontent.go index 19c511692..d8f8c8c98 100644 --- a/malcontent.go +++ b/malcontent.go @@ -2,6 +2,8 @@ // SPDX-License-Identifier: Apache-2.0 // malcontent returns information about a file's capabilities +// +//nolint:cyclop // ignore complexity of 40 package main import ( @@ -78,6 +80,7 @@ var riskMap = map[string]int{ "critical": 4, } +//nolint:cyclop // ignore complexity of 40 func main() { returnCode := ExitOK defer func() { os.Exit(returnCode) }() @@ -355,6 +358,11 @@ func main() { Value: "", Usage: "Scan an image", }, + &cli.BoolFlag{ + Name: "processes", + Value: false, + Usage: "Scan the commands (paths) of running processes", + }, }, Action: func(c *cli.Context) error { // Handle edge cases @@ -363,9 +371,24 @@ func main() { switch { case c.String("image") != "": mc.OCI = true - case c.String("image") == "": + case c.String("image") == "" && !c.Bool("processes"): cmdArgs := c.Args().Slice() mc.ScanPaths = []string{cmdArgs[0]} + case c.Bool("processes"): + mc.Processes = true + } + + // When scanning processes, load all of the valid commands (paths) + // and store them as the ScanPaths + if mc.Processes { + processPaths, err := action.GetAllProcessPaths(ctx) + if err != nil { + returnCode = ExitActionFailed + return err + } + for _, p := range processPaths { + mc.ScanPaths = append(mc.ScanPaths, p.Path) + } } res, err = action.Scan(ctx, mc) @@ -415,6 +438,11 @@ func main() { Value: "", Usage: "Scan an image", }, + &cli.BoolFlag{ + Name: "processes", + Value: false, + Usage: "Scan the commands (paths) of running processes", + }, }, Action: func(c *cli.Context) error { mc.Scan = true @@ -424,9 +452,24 @@ func main() { switch { case c.String("image") != "": mc.OCI = true - case c.String("image") == "": + case c.String("image") == "" && !c.Bool("processes"): cmdArgs := c.Args().Slice() mc.ScanPaths = []string{cmdArgs[0]} + case c.Bool("processes"): + mc.Processes = true + } + + // When scanning processes, load all of the valid commands (paths) + // and store them as the ScanPaths + if mc.Processes { + processPaths, err := action.GetAllProcessPaths(ctx) + if err != nil { + returnCode = ExitActionFailed + return err + } + for _, p := range processPaths { + mc.ScanPaths = append(mc.ScanPaths, p.Path) + } } res, err = action.Scan(ctx, mc) @@ -444,7 +487,7 @@ func main() { } if res.Files.Len() > 0 { - fmt.Fprintf(os.Stderr, "\n\ntip: For detailed analysis, run: mal analyze \n") + fmt.Fprintf(os.Stderr, "\ntip: For detailed analysis, run: mal analyze \n") } return nil diff --git a/pkg/action/process.go b/pkg/action/process.go new file mode 100644 index 000000000..34dc89b1b --- /dev/null +++ b/pkg/action/process.go @@ -0,0 +1,54 @@ +package action + +import ( + "context" + "os" + + "github.com/shirou/gopsutil/v4/process" +) + +type Process struct { + PID int32 + Path string +} + +// GetAllProcessPaths is an exported function that returns a slice of Process PIDs and commands (path). +func GetAllProcessPaths(ctx context.Context) ([]Process, error) { + // Retrieve all of the active PIDs + procs, err := process.ProcessesWithContext(ctx) + if err != nil { + return nil, err + } + + // Store PIDs and their respective commands (paths) in a map of paths and their Process structs + processMap := make(map[string]Process) + for _, p := range procs { + path, err := p.Exe() + if err != nil { + return nil, err + } + if _, exists := processMap[path]; !exists && path != "" && isValidPath(path) { + processMap[path] = Process{ + PID: p.Pid, + Path: path, + } + } + } + + return procMapSlice(processMap), nil +} + +// procMapSlice converts a map of paths and their Process structs to a slice of Processes. +func procMapSlice(m map[string]Process) []Process { + result := make([]Process, 0, len(m)) + for _, v := range m { + result = append(result, v) + } + return result +} + +// isValidPath checks if the given path is valid. +func isValidPath(path string) bool { + _, err := os.Stat(path) + return err == nil +} diff --git a/pkg/action/scan.go b/pkg/action/scan.go index 75769ae42..04a70b688 100644 --- a/pkg/action/scan.go +++ b/pkg/action/scan.go @@ -324,7 +324,7 @@ func recursiveScan(ctx context.Context, c malcontent.Config) (*malcontent.Report // Add the sorted paths and file reports to the parent report and render the results for _, k := range pathKeys { - finding, ok := scanPathFindings.Load(k) + finding, ok := scanPathFindings.LoadAndDelete(k) if !ok { return nil, fmt.Errorf("could not load finding from sync map") } diff --git a/pkg/malcontent/malcontent.go b/pkg/malcontent/malcontent.go index aa41489f7..e99ba7669 100644 --- a/pkg/malcontent/malcontent.go +++ b/pkg/malcontent/malcontent.go @@ -28,6 +28,7 @@ type Config struct { MinRisk int OCI bool Output io.Writer + Processes bool QuantityIncreasesRisk bool Renderer Renderer Rules *yara.Rules diff --git a/pkg/render/terminal_brief.go b/pkg/render/terminal_brief.go index 2c35ee252..fce17f2b1 100644 --- a/pkg/render/terminal_brief.go +++ b/pkg/render/terminal_brief.go @@ -51,11 +51,11 @@ func (r TerminalBrief) File(_ context.Context, fr *malcontent.FileReport) error reasons := []string{} for _, b := range fr.Behaviors { - reasons = append(reasons, fmt.Sprintf("%s %s%s%s", color.HiYellowString(b.ID), color.HiBlackString("("), b.Description, color.HiBlackString(")"))) + reasons = append(reasons, fmt.Sprintf("%s %s%s%s\n", color.HiYellowString(b.ID), color.HiBlackString("("), b.Description, color.HiBlackString(")"))) } - fmt.Fprintf(r.w, "%s%s%s %s: %s", color.HiBlackString("["), briefRiskColor(fr.RiskLevel), color.HiBlackString("]"), color.HiGreenString(fr.Path), - strings.Join(reasons, color.HiBlackString(", "))) + fmt.Fprintf(r.w, "%s%s%s %s: \n%s%s\n", color.HiBlackString("["), briefRiskColor(fr.RiskLevel), color.HiBlackString("]"), color.HiGreenString(fr.Path), + color.HiBlackString("- "), strings.Join(reasons, color.HiBlackString("- "))) return nil } diff --git a/pkg/report/report.go b/pkg/report/report.go index d54ebaca8..69570c284 100644 --- a/pkg/report/report.go +++ b/pkg/report/report.go @@ -352,13 +352,13 @@ func Generate(ctx context.Context, path string, mrs yara.MatchRules, c malconten } riskCounts[risk]++ // The malcontent rule is classified as harmless - // This will prevent the rule from being filtered + // A !ignoreMalcontent condition will prevent the rule from being filtered // If running a scan as opposed to an analyze, // drop any matches that fall below the highest risk switch { case risk < minScore && !ignoreMalcontent: continue - case c.Scan && risk < highestRisk: + case c.Scan && risk < highestRisk && !ignoreMalcontent: continue } key = generateKey(m.Namespace, m.Rule) @@ -477,7 +477,10 @@ func Generate(ctx context.Context, path string, mrs yara.MatchRules, c malconten // TODO: If we match multiple rules within a single namespace, merge matchstrings } - if all(ignoreSelf, fr.IsMalcontent, ignoreMalcontent, filepath.Base(path) == "mal") { + // Check for both the full and shortened variants of malcontent + isMalBinary := (filepath.Base(path) == NAME || filepath.Base(path) == "mal") + + if all(ignoreSelf, fr.IsMalcontent, ignoreMalcontent, isMalBinary) { return malcontent.FileReport{}, nil }