diff --git a/actions/vql.go b/actions/vql.go index 111cd0e61..918e5043d 100644 --- a/actions/vql.go +++ b/actions/vql.go @@ -147,6 +147,7 @@ func (self VQLClientAction) StartQuery( builder := services.ScopeBuilder{ Config: &config_proto.Config{ + Client: config_obj.Client, Remappings: config_obj.Remappings, }, // Only provide the client config since we are running in diff --git a/artifacts/definitions/SUSE/Linux/Events/Services.yaml b/artifacts/definitions/SUSE/Linux/Events/Services.yaml index 7c903d9df..68c34370a 100644 --- a/artifacts/definitions/SUSE/Linux/Events/Services.yaml +++ b/artifacts/definitions/SUSE/Linux/Events/Services.yaml @@ -3,16 +3,48 @@ name: SUSE.Linux.Events.Services description: | This artifact collects new systemd services events. -required_permissions: - - EXECVE - -precondition: | - SELECT OS From info() where OS = 'linux' - type: CLIENT_EVENT sources: - - query: | + - precondition: | + SELECT OS FROM info() + WHERE OS = 'linux' AND version(plugin='systemctl') != Null + + query: | + LET serviceDetails(name) = SELECT + ExecMainPID, Description, ActiveState, + get(item=ExecStart, member="0.0") AS Process + FROM systemctl(command="show", unit=name, + properties=["ExecStart", "Description", "ExecMainPID", "ActiveState"]) + + LET serviceStartEvents = SELECT + timestamp(epoch=REALTIME_TIMESTAMP) AS Timestamp, + UNIT AS Service, + _UID AS UID, + { SELECT * FROM serviceDetails(name=UNIT) } AS details + FROM watch_journal() + WHERE SYSLOG_IDENTIFIER = "systemd" + AND JOB_TYPE = "start" + AND JOB_RESULT = "done" + AND _PID = "1" + AND UNIT =~ "service$" + + SELECT + Timestamp, + Service, + "root" AS User, + details.ExecMainPID AS PID, + details.Process AS Process, + details.Description AS Description, + details.ActiveState AS State + FROM serviceStartEvents + + + - precondition: | + SELECT OS FROM info() + WHERE OS = 'linux' AND version(plugin='systemctl') = Null + + query: | -- grok pattern to parse systemctl show output LET pattern = "%{NUMBER:pid}\n\{ path\=%{DATA:process} .*\n%{DATA:description}\n%{DATA:state}\n" @@ -44,4 +76,3 @@ sources: details.description AS Description, details.state AS State FROM serviceStartEvents - diff --git a/artifacts/definitions/SUSE/Linux/Events/Timers.yaml b/artifacts/definitions/SUSE/Linux/Events/Timers.yaml index c5cf4ef2c..28874accf 100644 --- a/artifacts/definitions/SUSE/Linux/Events/Timers.yaml +++ b/artifacts/definitions/SUSE/Linux/Events/Timers.yaml @@ -5,8 +5,8 @@ description: | type: CLIENT_EVENT sources: - - name: TimerStateChange - precondition: SELECT OS From info() where OS = 'linux' + - precondition: SELECT OS From info() where OS = 'linux' + name: TimerStateChange description: Collect event when a new timer is started or stopped query: | SELECT timestamp(string=REALTIME_TIMESTAMP) as Time, @@ -15,16 +15,32 @@ sources: FROM watch_journal() WHERE SYSLOG_IDENTIFIER = "systemd" AND CODE_FUNC = "job_emit_done_message" AND UNIT =~ ".*timer" - - name: TimerExecs - precondition: SELECT OS From info() where OS = 'linux' + - precondition: SELECT OS From info() where OS = 'linux' AND version(plugin='systemctl') != Null + name: TimerExecs + description: Collect systemd timer executions from journal + query: | + -- Returns a row if `service` was triggered by a timer. + LET timer_triggered(service) = SELECT * + FROM systemctl(command="show", unit=service, properties=["TriggeredBy"]) + WHERE len(list=TriggeredBy) > 0 + + LET timer_execs = SELECT * + FROM query(query='SELECT * FROM Artifact.SUSE.Linux.Events.Services()') + WHERE timer_triggered(service=Service) + + SELECT Timestamp, PID, User, Process as Cmd, Description + FROM timer_execs + + - precondition: SELECT OS From info() where OS = 'linux' AND version(plugin='systemctl') = Null + name: TimerExecs description: Collect systemd timer executions from journal query: | LET timers = SELECT parse_json_array(data=Stdout) AS list FROM execve(argv=['systemctl', 'list-timers', '--all', '-o', 'json', '--no-pager']) LET timer_execs = SELECT *, {SELECT activates from timers.list} AS activates - FROM Artifact.SUSE.Linux.Events.Services() - WHERE format(format="%s%s" , args=[Service, ".service"]) in activates + FROM query(query='SELECT * FROM Artifact.SUSE.Linux.Events.Services()') + WHERE Service in activates SELECT Timestamp, PID, User, Process as Cmd, Description FROM timer_execs diff --git a/docs/references/vql.yaml b/docs/references/vql.yaml index 82b40b772..f737f298e 100644 --- a/docs/references/vql.yaml +++ b/docs/references/vql.yaml @@ -5712,6 +5712,31 @@ are emitted. Any further queries are ignored. type: Plugin category: plugin +- name: systemctl + type: plugin + description: | + Get information about systemd services via dbus. + #### Example + ``` + SELECT Description, ActiveState + FROM systemctl(command="show", unit="bluetooth.service", properties=["Description", "ActiveState"])' + ``` + args: + - name: command + required: true + description: Command to run. Only "show" implemented. + type: string + - name: Unit + required: false + description: Unit to show. Required for show command. + type: string + - name: properties + required: false + description: List of properties to show with the show command. + repeated: true + category: linux + metadata: + permissions: MACHINE_STATE - name: tcpsnoop description: Report incoming/outgoing tcp connections type: Plugin diff --git a/go.mod b/go.mod index c560c851e..2c8e48bc0 100644 --- a/go.mod +++ b/go.mod @@ -119,6 +119,7 @@ require ( github.com/aquasecurity/libbpfgo/helpers v0.0.0-00010101000000-000000000000 github.com/clayscode/Go-Splunk-HTTP/splunk/v2 v2.0.1-0.20221027171526-76a36be4fa02 github.com/coreos/go-oidc/v3 v3.9.0 + github.com/coreos/go-systemd/v22 v22.5.0 github.com/djoreilly/go-rpmdb v0.0.0-20250618160408-ec049dd092e4 github.com/elastic/go-libaudit/v2 v2.3.1-0.20221118223002-d56d27cfa498 github.com/evanphx/json-patch/v5 v5.6.0 @@ -175,6 +176,7 @@ require ( github.com/eapache/queue v1.1.0 // indirect github.com/geoffgarside/ber v1.1.0 // indirect github.com/go-jose/go-jose/v3 v3.0.4 // indirect + github.com/godbus/dbus/v5 v5.1.0 // indirect github.com/golang/gddo v0.0.0-20210115222349-20d68f94ee1f // indirect github.com/golang/glog v1.1.0 // indirect github.com/golang/snappy v0.0.4 // indirect diff --git a/go.sum b/go.sum index 973f8998f..f10ea106f 100644 --- a/go.sum +++ b/go.sum @@ -175,6 +175,8 @@ github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWH github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/coreos/go-oidc/v3 v3.9.0 h1:0J/ogVOd4y8P0f0xUh8l9t07xRP/d8tccvjHl2dcsSo= github.com/coreos/go-oidc/v3 v3.9.0/go.mod h1:rTKz2PYwftcrtoCzV5g5kvfJoWcm0Mk8AF8y1iAQro4= +github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs= +github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/creack/pty v1.1.17 h1:QeVUsEDNrLBW4tMgZHvxy18sKtr6VI492kBhUfhDJNI= github.com/creack/pty v1.1.17/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= @@ -238,6 +240,9 @@ github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiU github.com/go-sql-driver/mysql v1.5.0 h1:ozyZYNQW3x3HtqT1jira07DN2PArx2v7/mN66gGcHOs= github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= github.com/go-stack/stack v1.6.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk= +github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/gogo/protobuf v1.3.0/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= github.com/golang-jwt/jwt v3.2.1+incompatible h1:73Z+4BJcrTC+KczS6WvTPvRGOp1WmfEP4Q1lOd9Z/+c= github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI= diff --git a/vql/linux/systemctl.go b/vql/linux/systemctl.go new file mode 100644 index 000000000..76d01c6a8 --- /dev/null +++ b/vql/linux/systemctl.go @@ -0,0 +1,107 @@ +//go:build linux + +package linux + +import ( + "context" + + "github.com/Velocidex/ordereddict" + "github.com/coreos/go-systemd/v22/dbus" + "www.velocidex.com/golang/velociraptor/acls" + "www.velocidex.com/golang/velociraptor/vql" + "www.velocidex.com/golang/vfilter" + "www.velocidex.com/golang/vfilter/arg_parser" +) + +type SystemctlPluginArgs struct { + Command string `vfilter:"required,field=command,doc=command to run"` + Unit string `vfilter:"optional,field=unit,doc=unit to show"` + Properties []string `vfilter:"optional,field=properties,doc=Properties to show"` +} + +type SystemctlPlugin struct{} + +func (s SystemctlPlugin) Info(scope vfilter.Scope, typeMap *vfilter.TypeMap) *vfilter.PluginInfo { + return &vfilter.PluginInfo{ + Name: "systemctl", + Doc: "Get information about systemd services via dbus.", + ArgType: typeMap.AddType(scope, &SystemctlPluginArgs{}), + Metadata: vql.VQLMetadata().Permissions(acls.MACHINE_STATE).Build(), + } +} + +func (s SystemctlPlugin) Call( + ctx context.Context, scope vfilter.Scope, args *ordereddict.Dict, +) <-chan vfilter.Row { + outputCh := make(chan vfilter.Row) + + go func() { + defer close(outputCh) + + err := vql.CheckAccess(scope, acls.MACHINE_STATE) + if err != nil { + scope.Log("systemctl plugin: checking access: %s", err) + return + } + + arg := SystemctlPluginArgs{} + err = arg_parser.ExtractArgsWithContext(ctx, scope, args, &arg) + if err != nil { + scope.Log("systemctl plugin: extracting args: %s", err) + return + } + + switch arg.Command { + case "show": + if arg.Unit == "" { + scope.Log("systemctl plugin: unit required for command show") + return + } + err := showProperties(ctx, scope, arg, outputCh) + if err != nil { + scope.Log("systemctl plugin: error showing properties: %v", err) + return + } + default: + scope.Log("systemctl plugin: invalid command: %s", arg.Command) + } + }() + + return outputCh +} + +func showProperties(ctx context.Context, scope vfilter.Scope, + arg SystemctlPluginArgs, outputCh chan vfilter.Row, +) error { + conn, err := dbus.NewSystemConnectionContext(ctx) + if err != nil { + return err + } + defer conn.Close() + + props, err := conn.GetUnitPropertiesContext(ctx, arg.Unit) + if err != nil { + return err + } + typeProps, err := conn.GetUnitTypePropertiesContext(ctx, arg.Unit, "Service") + if err != nil { + return err + } + + row := ordereddict.NewDict() + for _, p := range arg.Properties { + if v, ok := props[p]; ok { + row.Set(p, v) + continue + } + if v, ok := typeProps[p]; ok { + row.Set(p, v) + } + } + outputCh <- row + return nil +} + +func init() { + vql.RegisterPlugin(&SystemctlPlugin{}) +}