Skip to content

Commit

Permalink
✨ fstab parser (#4823)
Browse files Browse the repository at this point in the history
* feat: fstab parser

* allow "rootfs"

* nil-check FileSystem

* feat: path param

* handle wrong connection type

* docs

* Update providers/os/resources/os.lr

Co-authored-by: Letha <[email protected]>

* man ref

---------

Co-authored-by: Letha <[email protected]>
  • Loading branch information
slntopp and misterpantz authored Nov 8, 2024
1 parent 241213e commit 971a960
Show file tree
Hide file tree
Showing 7 changed files with 581 additions and 0 deletions.
1 change: 1 addition & 0 deletions .github/actions/spelling/expect.txt
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ ratebasedstatement
regexmatchstatement
regexpatternsetreferencestatement
resourcegroup
rootfs
rulegroup
rulegroupreferencestatement
Sas
Expand Down
140 changes: 140 additions & 0 deletions providers/os/resources/fstab.go
Original file line number Diff line number Diff line change
@@ -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
}
160 changes: 160 additions & 0 deletions providers/os/resources/fstab_test.go
Original file line number Diff line number Diff line change
@@ -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 := `# <device> <dir> <type> <options> <dump> <fsck>
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 := `# <device> <dir> <type> <options>
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 := `# <device> <dir> <type> <options> <dump> <fsck>
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 := `# <device> <dir> <type>
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 := `# <device> <dir> <type> <options> <dump> <fsck>
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 := `# <device> <dir> <type> <options> <dump> <fsck>
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)
})
}
8 changes: 8 additions & 0 deletions providers/os/resources/os.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
24 changes: 24 additions & 0 deletions providers/os/resources/os.lr
Original file line number Diff line number Diff line change
Expand Up @@ -310,6 +310,8 @@ os.linux {
iptables() iptables
// iptables firewall for IPv6
ip6tables() ip6tables
// /etc/fstab entries
fstab() fstab
}

// Operating system root certificates
Expand Down Expand Up @@ -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)
Expand Down
Loading

0 comments on commit 971a960

Please sign in to comment.