Skip to content

Commit 4d4b9f8

Browse files
authored
🐛 Fix sshd sshd config check (globPathPattern used as file) (#6318)
* fix globPathPattern used as file * glob parser + tests ,address pr comments * CI lint * implement ReadDir * add comment * fix ReadDir not implemented on connection * fix test * fix test * match block edge case + tests
1 parent 7bf60b6 commit 4d4b9f8

File tree

14 files changed

+453
-58
lines changed

14 files changed

+453
-58
lines changed

providers/os/connection/docker/file.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ func (f *File) ReadAt(b []byte, off int64) (n int, err error) {
8484
}
8585

8686
func (f *File) Readdir(count int) (res []os.FileInfo, err error) {
87-
return nil, errors.New("not implemented")
87+
return f.catFs.ReadDir(f.path)
8888
}
8989

9090
func (f *File) Readdirnames(n int) ([]string, error) {

providers/os/connection/docker/filesystem.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,3 +90,7 @@ func (fs *FS) Chtimes(name string, atime time.Time, mtime time.Time) error {
9090
func (fs *FS) Chown(name string, uid, gid int) error {
9191
return errors.New("chown not implemented")
9292
}
93+
94+
func (fs *FS) ReadDir(name string) ([]os.FileInfo, error) {
95+
return fs.catFS.ReadDir(name)
96+
}

providers/os/connection/ssh/cat/cat.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,3 +107,9 @@ func (cat *Fs) Chtimes(name string, atime time.Time, mtime time.Time) error {
107107
func (cat *Fs) Chown(name string, uid, gid int) error {
108108
return NotImplemented
109109
}
110+
111+
func (cat *Fs) ReadDir(name string) ([]os.FileInfo, error) {
112+
file := NewFile(cat, name, false)
113+
defer file.Close()
114+
return file.Readdir(-1) // -1 is ignored, we read all files
115+
}

providers/os/connection/ssh/cat/cat_file.go

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"encoding/base64"
99
"io"
1010
"os"
11+
"path/filepath"
1112
"strings"
1213

1314
"github.com/cockroachdb/errors"
@@ -95,7 +96,13 @@ func (f *File) Readdir(count int) (res []os.FileInfo, err error) {
9596

9697
res = make([]os.FileInfo, len(names))
9798
for i, name := range names {
98-
res[i], err = f.catfs.Stat(name)
99+
var statPath string
100+
if filepath.IsAbs(name) {
101+
statPath = name
102+
} else {
103+
statPath = filepath.Join(f.path, name)
104+
}
105+
res[i], err = f.catfs.Stat(statPath)
99106
if err != nil {
100107
return nil, err
101108
}

providers/os/resources/sshd.go

Lines changed: 31 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ package resources
66

77
import (
88
"errors"
9+
"os"
910
"path/filepath"
1011
"regexp"
1112
"strings"
@@ -134,6 +135,12 @@ func (s *mqlSshdConfig) expandGlob(glob string) ([]string, error) {
134135
for _, path := range paths {
135136
files, err := afs.ReadDir(path)
136137
if err != nil {
138+
// If the directory doesn't exist, treat it as "no matches" (empty result)
139+
// This is consistent with standard glob behavior where a non-existent directory
140+
// results in an empty match set, not an error
141+
if os.IsNotExist(err) {
142+
continue
143+
}
137144
return nil, err
138145
}
139146

@@ -165,41 +172,37 @@ func (s *mqlSshdConfig) parse(file *mqlFile) error {
165172
file.Path.Data: file,
166173
}
167174
var allContents strings.Builder
168-
globPathContent := func(glob string) (string, error) {
169-
paths, err := s.expandGlob(glob)
170-
if err != nil {
171-
return "", err
172-
}
173-
174-
var content strings.Builder
175-
for _, path := range paths {
176-
file, ok := filesIdx[path]
177-
if !ok {
178-
raw, err := CreateResource(s.MqlRuntime, "file", map[string]*llx.RawData{
179-
"path": llx.StringData(path),
180-
})
181-
if err != nil {
182-
return "", err
183-
}
184-
file = raw.(*mqlFile)
185-
filesIdx[path] = file
186-
}
187175

188-
fileContent := file.GetContent()
189-
if fileContent.Error != nil {
190-
return "", fileContent.Error
176+
// Function to get file content by path
177+
fileContent := func(path string) (string, error) {
178+
file, ok := filesIdx[path]
179+
if !ok {
180+
raw, err := CreateResource(s.MqlRuntime, "file", map[string]*llx.RawData{
181+
"path": llx.StringData(path),
182+
})
183+
if err != nil {
184+
return "", err
191185
}
186+
file = raw.(*mqlFile)
187+
filesIdx[path] = file
188+
}
192189

193-
content.WriteString(fileContent.Data)
194-
content.WriteString("\n")
190+
fileContent := file.GetContent()
191+
if fileContent.Error != nil {
192+
return "", fileContent.Error
195193
}
196194

197-
res := content.String()
198-
allContents.WriteString(res)
199-
return res, nil
195+
content := fileContent.Data + "\n"
196+
allContents.WriteString(content)
197+
return content, nil
198+
}
199+
200+
// Function to expand glob patterns
201+
globExpand := func(glob string) ([]string, error) {
202+
return s.expandGlob(glob)
200203
}
201204

202-
matchBlocks, err := sshd.ParseBlocks(file.Path.Data, globPathContent)
205+
matchBlocks, err := sshd.ParseBlocksWithGlob(file.Path.Data, fileContent, globExpand)
203206
// TODO: check if not ready on I/O
204207
if err != nil {
205208
s.Params = plugin.TValue[map[string]any]{Error: err, State: plugin.StateIsSet | plugin.StateIsNull}

providers/os/resources/sshd/params.go

Lines changed: 187 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -85,23 +85,42 @@ func (m MatchBlocks) Flatten() map[string]any {
8585

8686
func mergeIncludedBlocks(matchConditions map[string]*MatchBlock, blocks MatchBlocks, curBlock string) {
8787
for _, block := range blocks {
88-
// meaning:
89-
// 1. curBlock == "", we can always add all subblocks
90-
// 2. if block == "", we can add it to whatever current block is
91-
// 3. in all other cases the block criteria must match, or we move on
92-
if block.Criteria != curBlock && curBlock != "" && block.Criteria != "" {
88+
if block.Criteria == "" {
89+
// Default block: merge into the current block
90+
existing := matchConditions[curBlock]
91+
if existing == nil {
92+
existing = &MatchBlock{
93+
Criteria: curBlock,
94+
Params: map[string]any{},
95+
Context: block.Context,
96+
}
97+
matchConditions[curBlock] = existing
98+
}
99+
if existing.Params == nil {
100+
existing.Params = map[string]any{}
101+
}
102+
for k, v := range block.Params {
103+
if _, ok := existing.Params[k]; !ok {
104+
existing.Params[k] = v
105+
}
106+
}
93107
continue
94108
}
95109

96-
var existing *MatchBlock
97-
if block.Criteria == "" {
98-
existing = matchConditions[curBlock]
99-
} else {
100-
existing := matchConditions[block.Criteria]
101-
if existing == nil {
102-
matchConditions[block.Criteria] = block
103-
continue
110+
// Match block: always add to global map
111+
existing, ok := matchConditions[block.Criteria]
112+
if !ok {
113+
// Create a new Match block
114+
existing = &MatchBlock{
115+
Criteria: block.Criteria,
116+
Params: map[string]any{},
117+
Context: block.Context,
104118
}
119+
matchConditions[block.Criteria] = existing
120+
}
121+
122+
if existing.Params == nil {
123+
existing.Params = map[string]any{}
105124
}
106125

107126
for k, v := range block.Params {
@@ -112,17 +131,147 @@ func mergeIncludedBlocks(matchConditions map[string]*MatchBlock, blocks MatchBlo
112131
}
113132
}
114133

115-
func ParseBlocks(rootPath string, globPathContent func(string) (string, error)) (MatchBlocks, error) {
116-
content, err := globPathContent(rootPath)
134+
type (
135+
fileContentFunc func(string) (content string, err error)
136+
globExpandFunc func(string) (paths []string, err error)
137+
)
138+
139+
// ParseBlocks parses a single SSH config file and returns the match blocks.
140+
// The filePath should be the actual file path (not a glob pattern).
141+
// For Include directives with glob patterns, use ParseBlocksWithGlob instead.
142+
func ParseBlocks(filePath string, content string) (MatchBlocks, error) {
143+
curBlock := &MatchBlock{
144+
Criteria: "",
145+
Params: map[string]any{},
146+
Context: Context{
147+
Path: filePath,
148+
Range: llx.NewRange(),
149+
curLine: 1,
150+
},
151+
}
152+
matchConditions := map[string]*MatchBlock{
153+
"": curBlock,
154+
}
155+
156+
lines := strings.Split(content, "\n")
157+
for curLineIdx, textLine := range lines {
158+
l, err := ParseLine([]rune(textLine))
159+
if err != nil {
160+
return nil, err
161+
}
162+
163+
key := l.key
164+
if key == "" {
165+
continue
166+
}
167+
168+
// handle lower case entries and use proper ssh camel case
169+
if sshKey, ok := SSH_Keywords[strings.ToLower(key)]; ok {
170+
key = sshKey
171+
}
172+
173+
if key == "Include" {
174+
// Include directives are handled by ParseBlocksWithGlob
175+
// This function only parses single files
176+
log.Warn().Str("file", filePath).Msg("Include directive found in single-file parser, use ParseBlocksWithGlob instead")
177+
continue
178+
}
179+
180+
if key == "Match" {
181+
// wrap up context on the previous block
182+
curBlock.Context.Range = curBlock.Context.Range.AddLineRange(uint32(curBlock.Context.curLine), uint32(curLineIdx))
183+
curBlock.Context.curLine = curLineIdx
184+
185+
// This key is stored in the condition of each block and can be accessed there.
186+
condition := l.args
187+
if b, ok := matchConditions[condition]; ok {
188+
curBlock = b
189+
} else {
190+
curBlock = &MatchBlock{
191+
Criteria: condition,
192+
Params: map[string]any{},
193+
Context: Context{
194+
curLine: curLineIdx + 1,
195+
Path: filePath,
196+
Range: llx.NewRange(),
197+
},
198+
}
199+
matchConditions[condition] = curBlock
200+
}
201+
continue
202+
}
203+
204+
setParam(curBlock.Params, key, l.args)
205+
}
206+
207+
keys := sortx.Keys(matchConditions)
208+
res := make([]*MatchBlock, len(keys))
209+
i := 0
210+
for _, key := range keys {
211+
res[i] = matchConditions[key]
212+
i++
213+
}
214+
215+
curBlock.Context.Range = curBlock.Context.Range.AddLineRange(uint32(curBlock.Context.curLine), uint32(len(lines)))
216+
217+
return res, nil
218+
}
219+
220+
// ParseBlocksWithGlob parses SSH config files, expanding glob patterns in Include directives.
221+
// It expands globs and calls ParseBlocksWithGlobRecursive for each matched file individually,
222+
func ParseBlocksWithGlob(rootPath string, fileContent fileContentFunc, globExpand globExpandFunc) (MatchBlocks, error) {
223+
// First, expand the root path if it's a glob
224+
paths, err := globExpand(rootPath)
117225
if err != nil {
118226
return nil, err
119227
}
120228

229+
// If no paths matched, check if rootPath was a single file (not a glob) or return empty blocks
230+
if len(paths) == 0 {
231+
// Check if rootPath contains a glob pattern
232+
hasGlob := strings.Contains(rootPath, "*") || strings.Contains(rootPath, "?") || strings.Contains(rootPath, "[")
233+
if !hasGlob {
234+
_, err := fileContent(rootPath)
235+
if err != nil {
236+
return nil, err
237+
}
238+
}
239+
return MatchBlocks{}, nil
240+
}
241+
242+
// Parse each file individually and collect all blocks
243+
// Each file maintains its own context (path, line numbers)
244+
var allBlocks MatchBlocks
245+
246+
for i, path := range paths {
247+
content, err := fileContent(path)
248+
if err != nil {
249+
if i == 0 && (len(paths) == 1 || path == rootPath) {
250+
return nil, err
251+
}
252+
log.Warn().Err(err).Str("path", path).Msg("unable to read file")
253+
continue
254+
}
255+
256+
blocks, err := ParseBlocksWithGlobRecursive(path, content, fileContent, globExpand)
257+
if err != nil {
258+
log.Warn().Err(err).Str("path", path).Msg("unable to parse file")
259+
continue
260+
}
261+
262+
allBlocks = append(allBlocks, blocks...)
263+
}
264+
265+
return allBlocks, nil
266+
}
267+
268+
// ParseBlocksWithGlobRecursive parses a single file and recursively handles Include directives.
269+
func ParseBlocksWithGlobRecursive(filePath string, content string, fileContent fileContentFunc, globExpand globExpandFunc) (MatchBlocks, error) {
121270
curBlock := &MatchBlock{
122271
Criteria: "",
123272
Params: map[string]any{},
124273
Context: Context{
125-
Path: rootPath,
274+
Path: filePath,
126275
Range: llx.NewRange(),
127276
curLine: 1,
128277
},
@@ -150,15 +299,31 @@ func ParseBlocks(rootPath string, globPathContent func(string) (string, error))
150299

151300
if key == "Include" {
152301
// FIXME: parse multi-keys properly
153-
paths := strings.Split(l.args, " ")
302+
includePaths := strings.Split(l.args, " ")
154303

155-
for _, path := range paths {
156-
subBlocks, err := ParseBlocks(path, globPathContent)
304+
for _, includePath := range includePaths {
305+
// Expand glob pattern if present
306+
expandedPaths, err := globExpand(includePath)
157307
if err != nil {
158-
log.Warn().Err(err).Msg("unable to parse Include directive")
308+
log.Warn().Err(err).Str("path", includePath).Msg("unable to expand Include directive")
159309
continue
160310
}
161-
mergeIncludedBlocks(matchConditions, subBlocks, curBlock.Criteria)
311+
312+
// Parse each matched file individually
313+
for _, expandedPath := range expandedPaths {
314+
subContent, err := fileContent(expandedPath)
315+
if err != nil {
316+
log.Warn().Err(err).Str("path", expandedPath).Msg("unable to read included file")
317+
continue
318+
}
319+
320+
subBlocks, err := ParseBlocksWithGlobRecursive(expandedPath, subContent, fileContent, globExpand)
321+
if err != nil {
322+
log.Warn().Err(err).Str("path", expandedPath).Msg("unable to parse included file")
323+
continue
324+
}
325+
mergeIncludedBlocks(matchConditions, subBlocks, curBlock.Criteria)
326+
}
162327
}
163328
continue
164329
}
@@ -179,7 +344,7 @@ func ParseBlocks(rootPath string, globPathContent func(string) (string, error))
179344
Params: map[string]any{},
180345
Context: Context{
181346
curLine: curLineIdx + 1,
182-
Path: rootPath,
347+
Path: filePath,
183348
Range: llx.NewRange(),
184349
},
185350
}

0 commit comments

Comments
 (0)