diff --git a/.github/actions/spelling/expect.txt b/.github/actions/spelling/expect.txt index 8eec53a13..d8fde7e83 100644 --- a/.github/actions/spelling/expect.txt +++ b/.github/actions/spelling/expect.txt @@ -75,6 +75,7 @@ ratebasedstatement regexmatchstatement regexpatternsetreferencestatement resourcegroup +rootfs rulegroup rulegroupreferencestatement Sas diff --git a/providers/os/resources/fstab.go b/providers/os/resources/fstab.go new file mode 100644 index 000000000..ef3cc6679 --- /dev/null +++ b/providers/os/resources/fstab.go @@ -0,0 +1,140 @@ +// Copyright (c) Mondoo, Inc. +// SPDX-License-Identifier: BUSL-1.1 +package resources + +import ( + "bufio" + "errors" + "io" + "strconv" + "strings" + + "go.mondoo.com/cnquery/v11/llx" + "go.mondoo.com/cnquery/v11/providers-sdk/v1/plugin" + "go.mondoo.com/cnquery/v11/providers/os/connection/shared" +) + +func initFstab(runtime *plugin.Runtime, args map[string]*llx.RawData) (map[string]*llx.RawData, plugin.Resource, error) { + if x, ok := args["path"]; ok { + path, ok := x.Value.(string) + if !ok || path == "" { + path = "/etc/fstab" + } + + f, err := CreateResource(runtime, "fstab", map[string]*llx.RawData{ + "path": llx.StringData(path), + }) + if err != nil { + return nil, nil, err + } + args["path"] = llx.StringData(path) + return args, f, nil + } + + args["path"] = llx.StringData("/etc/fstab") + return args, nil, nil +} + +func (f *mqlFstab) entries() ([]any, error) { + conn, ok := f.MqlRuntime.Connection.(shared.Connection) + if !ok { + return nil, errors.New("wrong connection type") + } + + fs := conn.FileSystem() + if fs == nil { + return nil, errors.New("filesystem not available") + } + + fstabFile, err := fs.Open(f.GetPath().Data) + if err != nil { + return nil, err + } + defer fstabFile.Close() + + entries, err := ParseFstab(fstabFile) + if err != nil { + return nil, err + } + + resources := []any{} + for _, entry := range entries { + resource, err := CreateResource(f.MqlRuntime, "fstab.entry", map[string]*llx.RawData{ + "device": llx.StringData(entry.Device), + "mountpoint": llx.StringData(entry.Mountpoint), + "fstype": llx.StringData(entry.Fstype), + "options": llx.StringData(entry.Options), + "dump": llx.IntDataPtr(entry.Dump), + "fsck": llx.IntDataPtr(entry.Fsck), + }) + if err != nil { + return nil, err + } + resources = append(resources, resource) + } + + return resources, nil +} + +type FstabEntry struct { + Device string + Mountpoint string + Fstype string + Options string + Dump *int + Fsck *int +} + +func ParseFstab(file io.Reader) ([]FstabEntry, error) { + scanner := bufio.NewScanner(file) + scanner.Split(bufio.ScanLines) + + var entries []FstabEntry + for scanner.Scan() { + line := scanner.Text() + // Skip comments and empty lines + if line == "" || line[0] == '#' { + continue + } + + record := strings.Fields(line) + if len(record) < 4 { + return nil, errors.New("invalid fstab entry") + } + + var dump *int + if len(record) >= 5 { + _dump, err := strconv.Atoi(record[4]) + if err != nil { + return nil, err + } + dump = &_dump + } + + var fsck *int + if len(record) >= 6 { + _fsck, err := strconv.Atoi(record[5]) + if err != nil { + return nil, err + } + fsck = &_fsck + } + + entry := FstabEntry{ + Device: record[0], + Mountpoint: record[1], + Fstype: record[2], + Options: record[3], + Dump: dump, + Fsck: fsck, + } + + entries = append(entries, entry) + } + + return entries, nil +} + +func (e *mqlFstabEntry) id() (string, error) { + return e.Device.Data, nil +} diff --git a/providers/os/resources/fstab_test.go b/providers/os/resources/fstab_test.go new file mode 100644 index 000000000..c712836cc --- /dev/null +++ b/providers/os/resources/fstab_test.go @@ -0,0 +1,160 @@ +// Copyright (c) Mondoo, Inc. +// SPDX-License-Identifier: BUSL-1.1 +package resources + +import ( + "strings" + "testing" + + "github.com/stretchr/testify/require" + "k8s.io/utils/ptr" +) + +func TestFstabEntries(t *testing.T) { + t.Run("valid", func(t *testing.T) { + testdata := `# +UUID=0a3407de-014b-458b-b5c1-848e92a327a3 / ext4 defaults 0 1 +UUID=f9fe0b69-a280-415d-a03a-a32752370dee none swap defaults 0 0 +UUID=b411dc99-f0a0-4c87-9e05-184977be8539 /home ext4 defaults 0 2` + + reader := strings.NewReader(testdata) + entries, err := ParseFstab(reader) + + require.NoError(t, err) + require.Len(t, entries, 3) + + require.Equal(t, FstabEntry{ + Device: "UUID=0a3407de-014b-458b-b5c1-848e92a327a3", + Mountpoint: "/", + Fstype: "ext4", + Options: "defaults", + Dump: ptr.To(0), + Fsck: ptr.To(1), + }, entries[0]) + require.Equal(t, FstabEntry{ + Device: "UUID=f9fe0b69-a280-415d-a03a-a32752370dee", + Mountpoint: "none", + Fstype: "swap", + Options: "defaults", + Dump: ptr.To(0), + Fsck: ptr.To(0), + }, entries[1]) + require.Equal(t, FstabEntry{ + Device: "UUID=b411dc99-f0a0-4c87-9e05-184977be8539", + Mountpoint: "/home", + Fstype: "ext4", + Options: "defaults", + Dump: ptr.To(0), + Fsck: ptr.To(2), + }, entries[2]) + }) + + t.Run("short", func(t *testing.T) { + testdata := `# +UUID=0a3407de-014b-458b-b5c1-848e92a327a3 / ext4 defaults +UUID=f9fe0b69-a280-415d-a03a-a32752370dee none swap defaults +UUID=b411dc99-f0a0-4c87-9e05-184977be8539 /home ext4 defaults` + + reader := strings.NewReader(testdata) + entries, err := ParseFstab(reader) + + require.NoError(t, err) + require.Len(t, entries, 3) + + require.Equal(t, FstabEntry{ + Device: "UUID=0a3407de-014b-458b-b5c1-848e92a327a3", + Mountpoint: "/", + Fstype: "ext4", + Options: "defaults", + }, entries[0]) + require.Equal(t, FstabEntry{ + Device: "UUID=f9fe0b69-a280-415d-a03a-a32752370dee", + Mountpoint: "none", + Fstype: "swap", + Options: "defaults", + }, entries[1]) + require.Equal(t, FstabEntry{ + Device: "UUID=b411dc99-f0a0-4c87-9e05-184977be8539", + Mountpoint: "/home", + Fstype: "ext4", + Options: "defaults", + }, entries[2]) + }) + + t.Run("valid (with tabs)", func(t *testing.T) { + testdata := `# +LABEL=cloudimg-rootfs / ext4 discard,commit=30,errors=remount-ro 0 1 +LABEL=BOOT /boot ext4 defaults 0 2 +LABEL=UEFI /boot/efi vfat umask=0077 0 1` + + reader := strings.NewReader(testdata) + entries, err := ParseFstab(reader) + + require.NoError(t, err) + require.Len(t, entries, 3) + + require.Equal(t, FstabEntry{ + Device: "LABEL=cloudimg-rootfs", + Mountpoint: "/", + Fstype: "ext4", + Options: "discard,commit=30,errors=remount-ro", + Dump: ptr.To(0), + Fsck: ptr.To(1), + }, entries[0]) + require.Equal(t, FstabEntry{ + Device: "LABEL=BOOT", + Mountpoint: "/boot", + Fstype: "ext4", + Options: "defaults", + Dump: ptr.To(0), + Fsck: ptr.To(2), + }, entries[1]) + require.Equal(t, FstabEntry{ + Device: "LABEL=UEFI", + Mountpoint: "/boot/efi", + Fstype: "vfat", + Options: "umask=0077", + Dump: ptr.To(0), + Fsck: ptr.To(1), + }, entries[2]) + }) + + t.Run("invalid (too short)", func(t *testing.T) { + testdata := `# +UUID=0a3407de-014b-458b-b5c1-848e92a327a3 / ext4 +UUID=f9fe0b69-a280-415d-a03a-a32752370dee none swap +UUID=b411dc99-f0a0-4c87-9e05-184977be8539 /home ext4` + + reader := strings.NewReader(testdata) + entries, err := ParseFstab(reader) + + require.Error(t, err) + require.Nil(t, entries) + }) + + t.Run("invalid (not numeric dump)", func(t *testing.T) { + testdata := `# +UUID=0a3407de-014b-458b-b5c1-848e92a327a3 / ext4 defaults 0 1 +UUID=f9fe0b69-a280-415d-a03a-a32752370dee none swap defaults 0 0 +UUID=b411dc99-f0a0-4c87-9e05-184977be8539 /home ext4 defaults A 2` // note the 'A' here + + reader := strings.NewReader(testdata) + entries, err := ParseFstab(reader) + + require.Error(t, err) + require.Nil(t, entries) + }) + + t.Run("invalid (not numeric fsck)", func(t *testing.T) { + testdata := `# +UUID=0a3407de-014b-458b-b5c1-848e92a327a3 / ext4 defaults 0 1 +UUID=f9fe0b69-a280-415d-a03a-a32752370dee none swap defaults 0 0 +UUID=b411dc99-f0a0-4c87-9e05-184977be8539 /home ext4 defaults 0 A` // note the 'A' here + + reader := strings.NewReader(testdata) + entries, err := ParseFstab(reader) + + require.Error(t, err) + require.Nil(t, entries) + }) +} diff --git a/providers/os/resources/os.go b/providers/os/resources/os.go index aafd9f634..db0e98144 100644 --- a/providers/os/resources/os.go +++ b/providers/os/resources/os.go @@ -680,3 +680,11 @@ func (s *mqlOsLinux) ip6tables() (*mqlIp6tables, error) { } return res.(*mqlIp6tables), nil } + +func (s *mqlOsLinux) fstab() (*mqlFstab, error) { + res, err := CreateResource(s.MqlRuntime, "fstab", map[string]*llx.RawData{}) + if err != nil { + return nil, err + } + return res.(*mqlFstab), nil +} diff --git a/providers/os/resources/os.lr b/providers/os/resources/os.lr index 39d701854..543388e57 100644 --- a/providers/os/resources/os.lr +++ b/providers/os/resources/os.lr @@ -310,6 +310,8 @@ os.linux { iptables() iptables // iptables firewall for IPv6 ip6tables() ip6tables + // /etc/fstab entries + fstab() fstab } // Operating system root certificates @@ -929,6 +931,28 @@ iptables.entry { chain string } +fstab @defaults("path") { + init(path? string) + path string + + entries() []fstab.entry +} + +private fstab.entry @defaults("device mountpoint") { + // Device referenced in the fstab, e.g., LABEL=rootfs + device string + // Mount point, e.g., '/' + mountpoint string + // File system type, e.g., ext4 + fstype string + // Mount options, e.g., defaults (`man fstab` for details) + options string + // Dump frequency (0 for full backup or an integer above 0, incremental backup, copies all files new or modified since the last dump of a lower level) + dump int + // File system check order, e.g., 1 + fsck int +} + // Process on this system process @defaults("executable pid state") { init(pid int) diff --git a/providers/os/resources/os.lr.go b/providers/os/resources/os.lr.go index 96ff8bc9e..c09d6d3dc 100644 --- a/providers/os/resources/os.lr.go +++ b/providers/os/resources/os.lr.go @@ -298,6 +298,14 @@ func init() { // to override args, implement: initIptablesEntry(runtime *plugin.Runtime, args map[string]*llx.RawData) (map[string]*llx.RawData, plugin.Resource, error) Create: createIptablesEntry, }, + "fstab": { + Init: initFstab, + Create: createFstab, + }, + "fstab.entry": { + // to override args, implement: initFstabEntry(runtime *plugin.Runtime, args map[string]*llx.RawData) (map[string]*llx.RawData, plugin.Resource, error) + Create: createFstabEntry, + }, "process": { Init: initProcess, Create: createProcess, @@ -858,6 +866,9 @@ var getDataFields = map[string]func(r plugin.Resource) *plugin.DataRes{ "os.linux.ip6tables": func(r plugin.Resource) *plugin.DataRes { return (r.(*mqlOsLinux).GetIp6tables()).ToDataRes(types.Resource("ip6tables")) }, + "os.linux.fstab": func(r plugin.Resource) *plugin.DataRes { + return (r.(*mqlOsLinux).GetFstab()).ToDataRes(types.Resource("fstab")) + }, "os.rootCertificates.files": func(r plugin.Resource) *plugin.DataRes { return (r.(*mqlOsRootCertificates).GetFiles()).ToDataRes(types.Array(types.Resource("file"))) }, @@ -1506,6 +1517,30 @@ var getDataFields = map[string]func(r plugin.Resource) *plugin.DataRes{ "iptables.entry.chain": func(r plugin.Resource) *plugin.DataRes { return (r.(*mqlIptablesEntry).GetChain()).ToDataRes(types.String) }, + "fstab.path": func(r plugin.Resource) *plugin.DataRes { + return (r.(*mqlFstab).GetPath()).ToDataRes(types.String) + }, + "fstab.entries": func(r plugin.Resource) *plugin.DataRes { + return (r.(*mqlFstab).GetEntries()).ToDataRes(types.Array(types.Resource("fstab.entry"))) + }, + "fstab.entry.device": func(r plugin.Resource) *plugin.DataRes { + return (r.(*mqlFstabEntry).GetDevice()).ToDataRes(types.String) + }, + "fstab.entry.mountpoint": func(r plugin.Resource) *plugin.DataRes { + return (r.(*mqlFstabEntry).GetMountpoint()).ToDataRes(types.String) + }, + "fstab.entry.fstype": func(r plugin.Resource) *plugin.DataRes { + return (r.(*mqlFstabEntry).GetFstype()).ToDataRes(types.String) + }, + "fstab.entry.options": func(r plugin.Resource) *plugin.DataRes { + return (r.(*mqlFstabEntry).GetOptions()).ToDataRes(types.String) + }, + "fstab.entry.dump": func(r plugin.Resource) *plugin.DataRes { + return (r.(*mqlFstabEntry).GetDump()).ToDataRes(types.Int) + }, + "fstab.entry.fsck": func(r plugin.Resource) *plugin.DataRes { + return (r.(*mqlFstabEntry).GetFsck()).ToDataRes(types.Int) + }, "process.pid": func(r plugin.Resource) *plugin.DataRes { return (r.(*mqlProcess).GetPid()).ToDataRes(types.Int) }, @@ -2807,6 +2842,10 @@ var setDataFields = map[string]func(r plugin.Resource, v *llx.RawData) bool { r.(*mqlOsLinux).Ip6tables, ok = plugin.RawToTValue[*mqlIp6tables](v.Value, v.Error) return }, + "os.linux.fstab": func(r plugin.Resource, v *llx.RawData) (ok bool) { + r.(*mqlOsLinux).Fstab, ok = plugin.RawToTValue[*mqlFstab](v.Value, v.Error) + return + }, "os.rootCertificates.__id": func(r plugin.Resource, v *llx.RawData) (ok bool) { r.(*mqlOsRootCertificates).__id, ok = v.Value.(string) return @@ -3855,6 +3894,46 @@ var setDataFields = map[string]func(r plugin.Resource, v *llx.RawData) bool { r.(*mqlIptablesEntry).Chain, ok = plugin.RawToTValue[string](v.Value, v.Error) return }, + "fstab.__id": func(r plugin.Resource, v *llx.RawData) (ok bool) { + r.(*mqlFstab).__id, ok = v.Value.(string) + return + }, + "fstab.path": func(r plugin.Resource, v *llx.RawData) (ok bool) { + r.(*mqlFstab).Path, ok = plugin.RawToTValue[string](v.Value, v.Error) + return + }, + "fstab.entries": func(r plugin.Resource, v *llx.RawData) (ok bool) { + r.(*mqlFstab).Entries, ok = plugin.RawToTValue[[]interface{}](v.Value, v.Error) + return + }, + "fstab.entry.__id": func(r plugin.Resource, v *llx.RawData) (ok bool) { + r.(*mqlFstabEntry).__id, ok = v.Value.(string) + return + }, + "fstab.entry.device": func(r plugin.Resource, v *llx.RawData) (ok bool) { + r.(*mqlFstabEntry).Device, ok = plugin.RawToTValue[string](v.Value, v.Error) + return + }, + "fstab.entry.mountpoint": func(r plugin.Resource, v *llx.RawData) (ok bool) { + r.(*mqlFstabEntry).Mountpoint, ok = plugin.RawToTValue[string](v.Value, v.Error) + return + }, + "fstab.entry.fstype": func(r plugin.Resource, v *llx.RawData) (ok bool) { + r.(*mqlFstabEntry).Fstype, ok = plugin.RawToTValue[string](v.Value, v.Error) + return + }, + "fstab.entry.options": func(r plugin.Resource, v *llx.RawData) (ok bool) { + r.(*mqlFstabEntry).Options, ok = plugin.RawToTValue[string](v.Value, v.Error) + return + }, + "fstab.entry.dump": func(r plugin.Resource, v *llx.RawData) (ok bool) { + r.(*mqlFstabEntry).Dump, ok = plugin.RawToTValue[int64](v.Value, v.Error) + return + }, + "fstab.entry.fsck": func(r plugin.Resource, v *llx.RawData) (ok bool) { + r.(*mqlFstabEntry).Fsck, ok = plugin.RawToTValue[int64](v.Value, v.Error) + return + }, "process.__id": func(r plugin.Resource, v *llx.RawData) (ok bool) { r.(*mqlProcess).__id, ok = v.Value.(string) return @@ -6780,6 +6859,7 @@ type mqlOsLinux struct { Unix plugin.TValue[*mqlOsUnix] Iptables plugin.TValue[*mqlIptables] Ip6tables plugin.TValue[*mqlIp6tables] + Fstab plugin.TValue[*mqlFstab] } // createOsLinux creates a new instance of this resource @@ -6867,6 +6947,22 @@ func (c *mqlOsLinux) GetIp6tables() *plugin.TValue[*mqlIp6tables] { }) } +func (c *mqlOsLinux) GetFstab() *plugin.TValue[*mqlFstab] { + return plugin.GetOrCompute[*mqlFstab](&c.Fstab, func() (*mqlFstab, error) { + if c.MqlRuntime.HasRecording { + d, err := c.MqlRuntime.FieldResourceFromRecording("os.linux", c.__id, "fstab") + if err != nil { + return nil, err + } + if d != nil { + return d.Value.(*mqlFstab), nil + } + } + + return c.fstab() + }) +} + // mqlOsRootCertificates for the os.rootCertificates resource type mqlOsRootCertificates struct { MqlRuntime *plugin.Runtime @@ -10596,6 +10692,141 @@ func (c *mqlIptablesEntry) GetChain() *plugin.TValue[string] { return &c.Chain } +// mqlFstab for the fstab resource +type mqlFstab struct { + MqlRuntime *plugin.Runtime + __id string + // optional: if you define mqlFstabInternal it will be used here + Path plugin.TValue[string] + Entries plugin.TValue[[]interface{}] +} + +// createFstab creates a new instance of this resource +func createFstab(runtime *plugin.Runtime, args map[string]*llx.RawData) (plugin.Resource, error) { + res := &mqlFstab{ + MqlRuntime: runtime, + } + + err := SetAllData(res, args) + if err != nil { + return res, err + } + + // to override __id implement: id() (string, error) + + if runtime.HasRecording { + args, err = runtime.ResourceFromRecording("fstab", res.__id) + if err != nil || args == nil { + return res, err + } + return res, SetAllData(res, args) + } + + return res, nil +} + +func (c *mqlFstab) MqlName() string { + return "fstab" +} + +func (c *mqlFstab) MqlID() string { + return c.__id +} + +func (c *mqlFstab) GetPath() *plugin.TValue[string] { + return &c.Path +} + +func (c *mqlFstab) GetEntries() *plugin.TValue[[]interface{}] { + return plugin.GetOrCompute[[]interface{}](&c.Entries, func() ([]interface{}, error) { + if c.MqlRuntime.HasRecording { + d, err := c.MqlRuntime.FieldResourceFromRecording("fstab", c.__id, "entries") + if err != nil { + return nil, err + } + if d != nil { + return d.Value.([]interface{}), nil + } + } + + return c.entries() + }) +} + +// mqlFstabEntry for the fstab.entry resource +type mqlFstabEntry struct { + MqlRuntime *plugin.Runtime + __id string + // optional: if you define mqlFstabEntryInternal it will be used here + Device plugin.TValue[string] + Mountpoint plugin.TValue[string] + Fstype plugin.TValue[string] + Options plugin.TValue[string] + Dump plugin.TValue[int64] + Fsck plugin.TValue[int64] +} + +// createFstabEntry creates a new instance of this resource +func createFstabEntry(runtime *plugin.Runtime, args map[string]*llx.RawData) (plugin.Resource, error) { + res := &mqlFstabEntry{ + MqlRuntime: runtime, + } + + err := SetAllData(res, args) + if err != nil { + return res, err + } + + if res.__id == "" { + res.__id, err = res.id() + if err != nil { + return nil, err + } + } + + if runtime.HasRecording { + args, err = runtime.ResourceFromRecording("fstab.entry", res.__id) + if err != nil || args == nil { + return res, err + } + return res, SetAllData(res, args) + } + + return res, nil +} + +func (c *mqlFstabEntry) MqlName() string { + return "fstab.entry" +} + +func (c *mqlFstabEntry) MqlID() string { + return c.__id +} + +func (c *mqlFstabEntry) GetDevice() *plugin.TValue[string] { + return &c.Device +} + +func (c *mqlFstabEntry) GetMountpoint() *plugin.TValue[string] { + return &c.Mountpoint +} + +func (c *mqlFstabEntry) GetFstype() *plugin.TValue[string] { + return &c.Fstype +} + +func (c *mqlFstabEntry) GetOptions() *plugin.TValue[string] { + return &c.Options +} + +func (c *mqlFstabEntry) GetDump() *plugin.TValue[int64] { + return &c.Dump +} + +func (c *mqlFstabEntry) GetFsck() *plugin.TValue[int64] { + return &c.Fsck +} + // mqlProcess for the process resource type mqlProcess struct { MqlRuntime *plugin.Runtime diff --git a/providers/os/resources/os.lr.manifest.yaml b/providers/os/resources/os.lr.manifest.yaml index 87fc897ab..d3ac9071d 100644 --- a/providers/os/resources/os.lr.manifest.yaml +++ b/providers/os/resources/os.lr.manifest.yaml @@ -386,6 +386,21 @@ resources: type: {} xdev: {} min_mondoo_version: 5.15.0 + fstab: + fields: + entries: {} + path: {} + min_mondoo_version: 9.0.0 + fstab.entry: + fields: + device: {} + dump: {} + fsck: {} + fstype: {} + mountpoint: {} + options: {} + is_private: true + min_mondoo_version: 9.0.0 group: fields: gid: {} @@ -673,6 +688,8 @@ resources: min_mondoo_version: 6.19.0 os.linux: fields: + fstab: + min_mondoo_version: 9.0.0 ip6tables: {} iptables: {} unix: {}