-
Notifications
You must be signed in to change notification settings - Fork 88
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
providers/linux: Fix linux Capabilities #196
Conversation
💚 CLA has been signed |
4c0b74d
to
ca253f7
Compare
I believe this never worked, as readCapabilities() expected a Capabilities line but was passed the full file as input, it also would allocate its own CapabilityInfo and return it, even though it would fill one member. So change all this: o Scan the file line by line, as to avoid loading a full file into memory, then parse out only the desired lines and pass down. o Pass the structure as a parameter and let each member be filled. o Error out if we failed to report at least one of the capabilities. o Add a test against init(1) as it will """always""" be run as root and have all capabilities, test is a bit conservative but should be ok for now.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
two minor points which I don't deem as critical to action @haesbaert . Let me know your thoughts
|
||
err := parseKeyValue(content, ':', func(key, value []byte) error { | ||
func decodeCapabilityLine(content string, capInfo *types.CapabilityInfo) error { | ||
return parseKeyValue([]byte(content), ':', func(key, value []byte) error { | ||
var err error | ||
switch string(key) { | ||
case "CapInh": |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
although this code was already like that, I debate inside me if it would add more value having all Cap* strings defined as constants. Feel free to ignore
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ack, that makes sense, I was aiming to be conservative and using what's already there though.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I made a little snippet that gets all of the CAPs if you think it might be helpful here
package main
import (
"fmt"
"go/constant"
"go/types"
"os"
// "golang.org/x/sys/unix"
"golang.org/x/tools/go/packages"
)
func main() {
cfg := &packages.Config{Mode: packages.NeedTypes}
pkgs, err := packages.Load(cfg, "golang.org/x/sys/unix")
if err != nil {
fmt.Println(err)
os.Exit(1)
}
caps := map[string]uint64{}
for _, pkg := range pkgs {
scope := pkg.Types.Scope()
for _, t := range scope.Names() {
if len(t) > 4 && t[0:4] == "CAP_" {
t_v, err := types.Eval(pkg.Fset, pkg.Types, scope.Pos(), t)
if err == nil {
if flagbits, ok := constant.Uint64Val(t_v.Value); ok {
caps[t] = flagbits
}
}
}
}
}
for key, val := range caps {
fmt.Printf("%v: %v\n", key, val)
}
}
continue | ||
} | ||
err = decodeCapabilityLine(line, &capInfo) | ||
if err != nil && gotErr == nil { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
maybe we should do a "multierror" here? As it captures the error only for the first capability that fails and we lose the reason for any subsequent similar ones.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
hmm I'm ok with just returning the first one though, as it's 99.999999% (number took out of my hat) the reason of the possible subsequent failures
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Points of discussion from slack aside, I think this looks good :) 🚢
|
||
err := parseKeyValue(content, ':', func(key, value []byte) error { | ||
func decodeCapabilityLine(content string, capInfo *types.CapabilityInfo) error { | ||
return parseKeyValue([]byte(content), ':', func(key, value []byte) error { | ||
var err error | ||
switch string(key) { | ||
case "CapInh": |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I made a little snippet that gets all of the CAPs if you think it might be helpful here
package main
import (
"fmt"
"go/constant"
"go/types"
"os"
// "golang.org/x/sys/unix"
"golang.org/x/tools/go/packages"
)
func main() {
cfg := &packages.Config{Mode: packages.NeedTypes}
pkgs, err := packages.Load(cfg, "golang.org/x/sys/unix")
if err != nil {
fmt.Println(err)
os.Exit(1)
}
caps := map[string]uint64{}
for _, pkg := range pkgs {
scope := pkg.Types.Scope()
for _, t := range scope.Names() {
if len(t) > 4 && t[0:4] == "CAP_" {
t_v, err := types.Eval(pkg.Fset, pkg.Types, scope.Pos(), t)
if err == nil {
if flagbits, ok := constant.Uint64Val(t_v.Value); ok {
caps[t] = flagbits
}
}
}
}
}
for key, val := range caps {
fmt.Printf("%v: %v\n", key, val)
}
}
Include capabilities as described in: https://www.elastic.co/guide/en/ecs/master/ecs-process.html#field-process-thread-capabilities-effective Don't merge, this depends on two external PRs: elastic/go-sysinfo#196 elastic/go-sysinfo#197
Implements #36404 ECS: https://www.elastic.co/guide/en/ecs/master/ecs-process.html#field-process-thread-capabilities-effective Example output: ``` { "@timestamp": "2023-12-05T19:34:54.425Z", "@metadata": { "beat": "auditbeat", "type": "_doc", "version": "8.12.0" }, "process": { "thread": { "capabilities": { "effective": [ "CAP_DAC_READ_SEARCH", "CAP_SYS_RESOURCE" ], "permitted": [ "CAP_DAC_READ_SEARCH", "CAP_SYS_RESOURCE" ] } }, "entity_id": "DADEDQU03GoDNhc1", "pid": 2841325, "start": "2023-12-05T19:32:53.180Z", "args": [ "systemd-userwork: waiting..." ], ... ... ``` Don't merge, this depends on two external PRs: elastic/go-sysinfo#196 elastic/go-sysinfo#197 Next step is adding the same to add_process_metadata
Implements #36404 ECS: https://www.elastic.co/guide/en/ecs/master/ecs-process.html#field-process-thread-capabilities-effective Example output: ``` { "@timestamp": "2023-12-05T19:34:54.425Z", "@metadata": { "beat": "auditbeat", "type": "_doc", "version": "8.12.0" }, "process": { "thread": { "capabilities": { "effective": [ "CAP_DAC_READ_SEARCH", "CAP_SYS_RESOURCE" ], "permitted": [ "CAP_DAC_READ_SEARCH", "CAP_SYS_RESOURCE" ] } }, "entity_id": "DADEDQU03GoDNhc1", "pid": 2841325, "start": "2023-12-05T19:32:53.180Z", "args": [ "systemd-userwork: waiting..." ], ... ... ``` Implementation is pretty straightforward, go-sysinfo will parse /proc/$PID/status and fill in CapabilityInfo. Don't merge, this depends on two external PRs: elastic/go-sysinfo#196 elastic/go-sysinfo#197 Next step is adding the same to add_process_metadata
Implements #36404 ECS: https://www.elastic.co/guide/en/ecs/master/ecs-process.html#field-process-thread-capabilities-effective Example output: ``` { "@timestamp": "2023-12-05T19:34:54.425Z", "@metadata": { "beat": "auditbeat", "type": "_doc", "version": "8.12.0" }, "process": { "thread": { "capabilities": { "effective": [ "CAP_DAC_READ_SEARCH", "CAP_SYS_RESOURCE" ], "permitted": [ "CAP_DAC_READ_SEARCH", "CAP_SYS_RESOURCE" ] } }, "entity_id": "DADEDQU03GoDNhc1", "pid": 2841325, "start": "2023-12-05T19:32:53.180Z", "args": [ "systemd-userwork: waiting..." ], ... ... ``` Implementation is pretty straightforward, go-sysinfo will parse /proc/$PID/status and fill in CapabilityInfo. Don't merge, this depends on two external PRs: elastic/go-sysinfo#196 elastic/go-sysinfo#197 Next step is adding the same to add_process_metadata
Implements #36404 ECS: https://www.elastic.co/guide/en/ecs/master/ecs-process.html#field-process-thread-capabilities-effective Example output: ``` { "@timestamp": "2023-12-05T19:34:54.425Z", "@metadata": { "beat": "auditbeat", "type": "_doc", "version": "8.12.0" }, "process": { "thread": { "capabilities": { "effective": [ "CAP_DAC_READ_SEARCH", "CAP_SYS_RESOURCE" ], "permitted": [ "CAP_DAC_READ_SEARCH", "CAP_SYS_RESOURCE" ] } }, "entity_id": "DADEDQU03GoDNhc1", "pid": 2841325, "start": "2023-12-05T19:32:53.180Z", "args": [ "systemd-userwork: waiting..." ], ... ... ``` Implementation is pretty straightforward, go-sysinfo will parse /proc/$PID/status and fill in CapabilityInfo. Don't merge, this depends on two external PRs: elastic/go-sysinfo#196 elastic/go-sysinfo#197 Next step is adding the same to add_process_metadata
Implements #36404 ECS: https://www.elastic.co/guide/en/ecs/master/ecs-process.html#field-process-thread-capabilities-effective Example output: ``` { "@timestamp": "2023-12-05T19:34:54.425Z", "@metadata": { "beat": "auditbeat", "type": "_doc", "version": "8.12.0" }, "process": { "thread": { "capabilities": { "effective": [ "CAP_DAC_READ_SEARCH", "CAP_SYS_RESOURCE" ], "permitted": [ "CAP_DAC_READ_SEARCH", "CAP_SYS_RESOURCE" ] } }, "entity_id": "DADEDQU03GoDNhc1", "pid": 2841325, "start": "2023-12-05T19:32:53.180Z", "args": [ "systemd-userwork: waiting..." ], ... ... ``` Implementation is pretty straightforward, go-sysinfo will parse /proc/$PID/status and fill in CapabilityInfo. Don't merge, this depends on two external PRs: elastic/go-sysinfo#196 elastic/go-sysinfo#197 Next step is adding the same to add_process_metadata
😟 I checked the output of the tests on main and it reports a list of capabilities, so some part must have been working. https://github.com/elastic/go-sysinfo/actions/runs/7030012626/job/19128722216#step:8:56 |
func (p *process) Capabilities() (*types.CapabilityInfo, error) { | ||
content, err := ioutil.ReadFile(p.path("status")) | ||
var gotErr error | ||
f, err := os.OpenFile(p.path("status"), os.O_RDONLY, 0644) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think https://pkg.go.dev/os#Open would be more clear.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
absolutely, I'm new to Go so expect weirdness, I'll fix it.
func (p *process) Capabilities() (*types.CapabilityInfo, error) { | ||
content, err := ioutil.ReadFile(p.path("status")) | ||
var gotErr error |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Recommend moving the declaration down closer to first usage to minimize scope.
var capInfo types.CapabilityInfo | ||
for scanner.Scan() { | ||
line := scanner.Text() | ||
if len(line) != 24 || line[:3] != "Cap" { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Without ensuring the length of the line
, then line[:3]
could panic.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Agreed, but it does check: if len(line) != 24 || line[:3] != "Cap"
if err != nil && gotErr == nil { | ||
gotErr = err | ||
} | ||
} | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If the scanner encounters an error then Scan()
will return false, and the you will only see the error if you call https://pkg.go.dev/bufio#Scanner.Err. So this needs to check for that error after the loop exits.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Well spot, will fix it, many thanks
if err != nil { | ||
t.Fatal(err) | ||
} | ||
totalCaps := 41 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This seem brittle as it depends on the host operating system. The results could vary by kernel. Like if someone develops on an older OS then their tests might fail.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Agreed, I wasn't sure if this is worth or not
@@ -0,0 +1,3 @@ | |||
```release-note:bug | |||
linux: fix linux capabilities |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As a reader of the release notes, I think I would prefer to have a few more details about what was broken.
This is strange, the caller just does:
How would content be parseable by:
For sure I must be missing something |
My recommendation would be to create a failing testing that demonstrates the problem using main (before your changes).
The main branch implementation takes the full content, passes it to |
hA! I'm a dummy, apologies. I did test it before and it didn't work, maybe I missed something else, I'll check it. |
Running your
|
I just ran main against my auditbeat change and indeed it works, I've been chasing a red herring, sorry for the noise. |
I believe this never worked, as readCapabilities() expected a Capabilities line but was passed the full file as input, it also would allocate its own CapabilityInfo and return it, even though it would fill one member.
So change all this:
then parse out only the desired lines and pass down.
all capabilities, test is a bit conservative but should be ok for now.