Skip to content

Commit ec8e051

Browse files
authored
Osquerybeat: Implement host_users, host_groups, host_processes tables as a part of our osquery_extension. (#28434)
* Osquerybeat: Implement host_users, host_groups, host_processes tables as a part of our osquery_extension. * The expectation is that the /etc/passwd, /etc/group, /proc are avaiable in the container under /hostfs as: /hostfs/etc/passwd, /hostfs/etc/group, /hostfs/proc. The ELASTIC_OSQUERY_HOSTFS environment variable allows to change the default. * The on_disk column is always set to -1 * Remove erroneous comment line
1 parent d31cae9 commit ec8e051

18 files changed

Lines changed: 881 additions & 15 deletions

File tree

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
2+
// or more contributor license agreements. Licensed under the Elastic License;
3+
// you may not use this file except in compliance with the Elastic License.
4+
5+
package hostfs
6+
7+
import (
8+
"errors"
9+
"fmt"
10+
"os"
11+
"path/filepath"
12+
"strconv"
13+
)
14+
15+
const (
16+
defaultMount = "/hostfs"
17+
envHostFSOverride = "ELASTIC_OSQUERY_HOSTFS" // Allows to override the mount point for hostfs, default is /hostfs
18+
)
19+
20+
var (
21+
ErrMissingField = errors.New("missing/invalid field")
22+
ErrInvalidFieldType = errors.New("invalid field type")
23+
)
24+
25+
type ColumnType int
26+
27+
const (
28+
ColumnTypeString ColumnType = iota
29+
ColumnTypeInt
30+
ColumnTypeUint
31+
)
32+
33+
func (c ColumnType) String() string {
34+
return [...]string{"string", "int64", "uint64"}[c]
35+
}
36+
37+
type ColumnInfo struct {
38+
IndexFrom int
39+
Name string
40+
Type ColumnType
41+
Optional bool
42+
}
43+
44+
func GetPath(fp string) string {
45+
// Check the environment variable for override, otherwise use /hostfs as the mount root
46+
mountRoot := os.Getenv(envHostFSOverride)
47+
if mountRoot == "" {
48+
mountRoot = defaultMount
49+
}
50+
return filepath.Join(mountRoot, fp)
51+
}
52+
53+
type StringMap map[string]string
54+
55+
func (m StringMap) Set(fields []string, col ColumnInfo) error {
56+
if col.IndexFrom >= len(fields) {
57+
if !col.Optional {
58+
return fmt.Errorf("failed to read field at index: %d, when total number of fields is: %d, err: %w", col.IndexFrom, len(fields), ErrMissingField)
59+
}
60+
m[col.Name] = ""
61+
return nil
62+
}
63+
64+
var err error
65+
66+
sval := fields[col.IndexFrom]
67+
// Check that it is convertable to int type
68+
switch col.Type {
69+
case ColumnTypeUint:
70+
// For unsigned values (Apple) the number is parsed as signed int32 then converted to unsigned.
71+
// This is consistent with osquery `users` table data on Mac OS.
72+
// osquery> select * from users;
73+
// +------------+------------+------------+------------+------------------------+-------------------------------------------------+-------------------------------+------------------+--------------------------------------+-----------+
74+
// | uid | gid | uid_signed | gid_signed | username | description | directory | shell | uuid | is_hidden |
75+
// +------------+------------+------------+------------+------------------------+-------------------------------------------------+-------------------------------+------------------+--------------------------------------+-----------+
76+
// | 229 | 4294967294 | 229 | -2 | _avbdeviced | Ethernet AVB Device Daemon | /var/empty | /usr/bin/false | FFFFEEEE-DDDD-CCCC-BBBB-AAAA000000E5 | 0 |
77+
v, err := strconv.ParseInt(sval, 10, 32)
78+
if err == nil {
79+
n := uint32(v)
80+
sval = strconv.FormatUint(uint64(n), 10)
81+
}
82+
case ColumnTypeInt:
83+
_, err = strconv.ParseInt(sval, 10, 64)
84+
}
85+
86+
if err != nil {
87+
return fmt.Errorf("invalid field type at index: %d, expected %s, err: %w", col.IndexFrom, col.Type.String(), ErrInvalidFieldType)
88+
}
89+
90+
m[col.Name] = sval
91+
return nil
92+
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
2+
// or more contributor license agreements. Licensed under the Elastic License;
3+
// you may not use this file except in compliance with the Elastic License.
4+
5+
package hostfs
6+
7+
import (
8+
"bufio"
9+
"os"
10+
"strings"
11+
)
12+
13+
var columns = []ColumnInfo{
14+
{0, "groupname", ColumnTypeString, false},
15+
{2, "gid", ColumnTypeUint, false},
16+
{2, "gid_signed", ColumnTypeInt, false},
17+
}
18+
19+
func ReadGroup(fn string) ([]map[string]string, error) {
20+
f, err := os.Open(fn)
21+
if err != nil {
22+
return nil, err
23+
}
24+
defer f.Close()
25+
26+
var res []map[string]string
27+
scanner := bufio.NewScanner(f)
28+
for scanner.Scan() {
29+
line := scanner.Text()
30+
if strings.HasPrefix(line, "#") {
31+
continue
32+
}
33+
fields := strings.Split(line, ":")
34+
35+
rec := make(StringMap)
36+
37+
for _, col := range columns {
38+
err = rec.Set(fields, col)
39+
if err != nil {
40+
return nil, err
41+
}
42+
}
43+
44+
res = append(res, rec)
45+
}
46+
47+
if err := scanner.Err(); err != nil {
48+
return nil, err
49+
}
50+
51+
return res, nil
52+
}
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
2+
// or more contributor license agreements. Licensed under the Elastic License;
3+
// you may not use this file except in compliance with the Elastic License.
4+
5+
package hostfs
6+
7+
import (
8+
"bufio"
9+
"os"
10+
"strings"
11+
)
12+
13+
var passwdColumns = []ColumnInfo{
14+
{0, "username", ColumnTypeString, false},
15+
{2, "uid", ColumnTypeUint, false},
16+
{2, "uid_signed", ColumnTypeInt, false},
17+
{3, "gid", ColumnTypeUint, false},
18+
{3, "gid_signed", ColumnTypeInt, false},
19+
{4, "description", ColumnTypeString, false},
20+
{5, "directory", ColumnTypeString, false},
21+
{6, "shell", ColumnTypeString, false},
22+
{7, "uuid", ColumnTypeString, true},
23+
}
24+
25+
func ReadPasswd(fn string) ([]map[string]string, error) {
26+
f, err := os.Open(fn)
27+
if err != nil {
28+
return nil, err
29+
}
30+
defer f.Close()
31+
32+
var res []map[string]string
33+
scanner := bufio.NewScanner(f)
34+
for scanner.Scan() {
35+
line := scanner.Text()
36+
if strings.HasPrefix(line, "#") {
37+
continue
38+
}
39+
fields := strings.Split(line, ":")
40+
41+
rec := make(StringMap)
42+
43+
for _, col := range passwdColumns {
44+
err = rec.Set(fields, col)
45+
if err != nil {
46+
return nil, err
47+
}
48+
}
49+
50+
res = append(res, rec)
51+
}
52+
53+
if err := scanner.Err(); err != nil {
54+
return nil, err
55+
}
56+
57+
return res, nil
58+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
2+
// or more contributor license agreements. Licensed under the Elastic License;
3+
// you may not use this file except in compliance with the Elastic License.
4+
5+
package proc
6+
7+
import (
8+
"io/ioutil"
9+
"strings"
10+
)
11+
12+
func ReadCmdLine(root string, pid string) (string, error) {
13+
fn := getProcAttr(root, pid, "cmdline")
14+
15+
b, err := ioutil.ReadFile(fn)
16+
if err != nil {
17+
return "", err
18+
}
19+
20+
return strings.TrimSpace(string(b)), nil
21+
}
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
2+
// or more contributor license agreements. Licensed under the Elastic License;
3+
// you may not use this file except in compliance with the Elastic License.
4+
5+
package proc
6+
7+
import (
8+
"bytes"
9+
"io/ioutil"
10+
"strings"
11+
)
12+
13+
type ProcIO struct {
14+
ReadBytes string
15+
WriteBytes string
16+
CancelledWriteBytes string
17+
}
18+
19+
// ReadProcStat reads proccess stats from /proc/<pid>/io.
20+
// The parsing code logic is borrowed from osquery C++ implementation and translated to Go.
21+
// This makes the data returned from the `host_processes` table
22+
// consistent with data returned from the original osquery `processes` table.
23+
// https://github.com/osquery/osquery/blob/master/osquery/tables/system/linux/processes.cpp
24+
func ReadIO(root string, pid string) (procio ProcIO, err error) {
25+
// Proc IO example
26+
// rchar: 1527371144
27+
// wchar: 1495591102
28+
// syscr: 481186
29+
// syscw: 255942
30+
// read_bytes: 14401536
31+
// write_bytes: 815329280
32+
// cancelled_write_bytes: 40976384
33+
fn := getProcAttr(root, pid, "io")
34+
b, err := ioutil.ReadFile(fn)
35+
if err != nil {
36+
return
37+
}
38+
39+
lines := bytes.Split(b, []byte{'\n'})
40+
for _, line := range lines {
41+
detail := bytes.SplitN(line, []byte{':'}, 2)
42+
if len(detail) != 2 {
43+
continue
44+
}
45+
46+
k := strings.TrimSpace(bytesToString(detail[0]))
47+
switch k {
48+
case "read_bytes":
49+
procio.ReadBytes = strings.TrimSpace(bytesToString(detail[1]))
50+
case "write_bytes":
51+
procio.WriteBytes = strings.TrimSpace(bytesToString(detail[1]))
52+
case "cancelled_write_bytes":
53+
procio.CancelledWriteBytes = strings.TrimSpace(bytesToString(detail[1]))
54+
}
55+
}
56+
return procio, nil
57+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
2+
// or more contributor license agreements. Licensed under the Elastic License;
3+
// you may not use this file except in compliance with the Elastic License.
4+
5+
package proc
6+
7+
import (
8+
"os"
9+
)
10+
11+
func ReadLink(root string, pid string, attr string) (string, error) {
12+
fn := getProcAttr(root, pid, attr)
13+
14+
s, err := os.Readlink(fn)
15+
if err != nil {
16+
return "", err
17+
}
18+
return s, nil
19+
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
2+
// or more contributor license agreements. Licensed under the Elastic License;
3+
// you may not use this file except in compliance with the Elastic License.
4+
5+
package proc
6+
7+
import (
8+
"os"
9+
"path/filepath"
10+
"strconv"
11+
)
12+
13+
func List(root string) ([]string, error) {
14+
var pids []string
15+
16+
root = filepath.Join(root, "/proc")
17+
18+
dirs, err := os.ReadDir(root)
19+
20+
if err != nil {
21+
return nil, err
22+
}
23+
24+
for _, dir := range dirs {
25+
if !dir.IsDir() {
26+
continue
27+
}
28+
29+
name := dir.Name()
30+
// Check if directory is number
31+
_, err := strconv.Atoi(name)
32+
if err != nil {
33+
err = nil
34+
continue
35+
}
36+
pids = append(pids, name)
37+
}
38+
39+
return pids, nil
40+
}

0 commit comments

Comments
 (0)