Skip to content

Commit fd7a893

Browse files
authored
Extend sudoers resource to support all major OS paths (#6658)
Only Linux/macOS use /etc/sudoers. Make sure we find the file in the right place no matter what OS the user is on Signed-off-by: Tim Smith <tsmith84@gmail.com>
1 parent a23ecc6 commit fd7a893

File tree

4 files changed

+113
-8
lines changed

4 files changed

+113
-8
lines changed
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
// Copyright (c) Mondoo, Inc.
2+
// SPDX-License-Identifier: BUSL-1.1
3+
4+
package resources
5+
6+
import (
7+
"github.com/spf13/afero"
8+
"go.mondoo.com/mql/v13/providers-sdk/v1/inventory"
9+
"go.mondoo.com/mql/v13/providers/os/connection/shared"
10+
)
11+
12+
// mockConn implements shared.Connection with only the Asset() method populated.
13+
type mockConn struct {
14+
asset *inventory.Asset
15+
}
16+
17+
func (m *mockConn) ID() uint32 { return 0 }
18+
func (m *mockConn) ParentID() uint32 { return 0 }
19+
func (m *mockConn) RunCommand(command string) (*shared.Command, error) { return nil, nil }
20+
func (m *mockConn) FileInfo(path string) (shared.FileInfoDetails, error) {
21+
return shared.FileInfoDetails{}, nil
22+
}
23+
func (m *mockConn) FileSystem() afero.Fs { return nil }
24+
func (m *mockConn) Name() string { return "mock" }
25+
func (m *mockConn) Type() shared.ConnectionType { return "mock" }
26+
func (m *mockConn) Asset() *inventory.Asset { return m.asset }
27+
func (m *mockConn) UpdateAsset(asset *inventory.Asset) {}
28+
func (m *mockConn) Capabilities() shared.Capabilities { return 0 }
29+
30+
// connWithPlatform returns a mockConn with the given platform name set.
31+
func connWithPlatform(name string) *mockConn {
32+
return &mockConn{
33+
asset: &inventory.Asset{
34+
Platform: &inventory.Platform{
35+
Name: name,
36+
},
37+
},
38+
}
39+
}

providers/os/resources/sudoers.go

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,16 @@ import (
1717
"go.mondoo.com/mql/v13/types"
1818
)
1919

20-
const (
21-
defaultSudoersFile = "/etc/sudoers"
22-
)
20+
// sudoersPaths lists the sudoers file paths to check per platform.
21+
// Most systems use /etc/sudoers. BSD variants and AIX install sudo via
22+
// package managers to non-default prefixes, so /etc/sudoers does not exist.
23+
var sudoersPaths = map[string][]string{
24+
"freebsd": {"/usr/local/etc/sudoers"},
25+
"dragonflybsd": {"/usr/local/etc/sudoers"},
26+
"openbsd": {"/usr/local/etc/sudoers"},
27+
"netbsd": {"/usr/pkg/etc/sudoers"},
28+
"aix": {"/opt/freeware/etc/sudoers"},
29+
}
2330

2431
func initSudoers(runtime *plugin.Runtime, args map[string]*llx.RawData) (map[string]*llx.RawData, plugin.Resource, error) {
2532
if x, ok := args["path"]; ok {
@@ -80,15 +87,28 @@ func (sa *mqlSudoersAlias) id() (string, error) {
8087
return id, nil
8188
}
8289

90+
// sudoersPathsForPlatform returns the sudoers file paths to check for a given platform.
91+
func sudoersPathsForPlatform(conn shared.Connection) []string {
92+
asset := conn.Asset()
93+
if asset != nil && asset.Platform != nil {
94+
if paths, ok := sudoersPaths[asset.Platform.Name]; ok {
95+
return paths
96+
}
97+
}
98+
return []string{"/etc/sudoers"}
99+
}
100+
83101
// files returns the list of sudoers configuration files
84102
func (s *mqlSudoers) files() ([]any, error) {
85103
conn := s.MqlRuntime.Connection.(shared.Connection)
86104
visited := make(map[string]bool)
87105
var allFiles []any
88106
var errs []error
89107

90-
// Start with the main sudoers file
91-
s.collectSudoersFiles(conn, defaultSudoersFile, visited, &allFiles, &errs)
108+
// Try platform-specific sudoers paths
109+
for _, path := range sudoersPathsForPlatform(conn) {
110+
s.collectSudoersFiles(conn, path, visited, &allFiles, &errs)
111+
}
92112

93113
if len(errs) > 0 {
94114
return allFiles, errors.Join(errs...)
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
// Copyright (c) Mondoo, Inc.
2+
// SPDX-License-Identifier: BUSL-1.1
3+
4+
package resources
5+
6+
import (
7+
"testing"
8+
9+
"github.com/stretchr/testify/assert"
10+
"go.mondoo.com/mql/v13/providers-sdk/v1/inventory"
11+
)
12+
13+
func TestSudoersPathsForPlatform(t *testing.T) {
14+
tests := []struct {
15+
platform string
16+
expected []string
17+
}{
18+
{"freebsd", []string{"/usr/local/etc/sudoers"}},
19+
{"dragonflybsd", []string{"/usr/local/etc/sudoers"}},
20+
{"openbsd", []string{"/usr/local/etc/sudoers"}},
21+
{"netbsd", []string{"/usr/pkg/etc/sudoers"}},
22+
{"aix", []string{"/opt/freeware/etc/sudoers"}},
23+
{"debian", []string{"/etc/sudoers"}},
24+
{"ubuntu", []string{"/etc/sudoers"}},
25+
{"redhat", []string{"/etc/sudoers"}},
26+
{"macos", []string{"/etc/sudoers"}},
27+
}
28+
29+
for _, tt := range tests {
30+
t.Run(tt.platform, func(t *testing.T) {
31+
assert.Equal(t, tt.expected, sudoersPathsForPlatform(connWithPlatform(tt.platform)))
32+
})
33+
}
34+
35+
t.Run("nil platform", func(t *testing.T) {
36+
conn := &mockConn{asset: &inventory.Asset{}}
37+
assert.Equal(t, []string{"/etc/sudoers"}, sudoersPathsForPlatform(conn))
38+
})
39+
}

providers/os/resources/sudoers_test.go

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,16 +11,23 @@ import (
1111
)
1212

1313
func TestResource_Sudoers(t *testing.T) {
14-
// Uses the global 'x' tester from os_test.go (LinuxMock with arch.json)
15-
// For parsing unit tests, see sudoers/sudoers_test.go
16-
1714
t.Run("files are discovered", func(t *testing.T) {
1815
res := x.TestQuery(t, "sudoers.files.length")
1916
require.NotEmpty(t, res)
2017
require.NoError(t, res[0].Data.Error)
2118
assert.Equal(t, int64(1), res[0].Data.Value)
2219
})
2320

21+
t.Run("content aggregates all files", func(t *testing.T) {
22+
res := x.TestQuery(t, "sudoers.content")
23+
require.NotEmpty(t, res)
24+
require.NoError(t, res[0].Data.Error)
25+
content := res[0].Data.Value.(string)
26+
assert.Contains(t, content, "root ALL=(ALL:ALL) ALL")
27+
assert.Contains(t, content, "ADMINS ALL=(ALL) NOPASSWD: ALL")
28+
assert.Contains(t, content, "Defaults secure_path=")
29+
})
30+
2431
t.Run("userSpecs parsing", func(t *testing.T) {
2532
res := x.TestQuery(t, "sudoers.userSpecs.length")
2633
require.NotEmpty(t, res)

0 commit comments

Comments
 (0)