Skip to content
Merged
2 changes: 1 addition & 1 deletion providers/os/connection/docker/file.go
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ func (f *File) ReadAt(b []byte, off int64) (n int, err error) {
}

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

func (f *File) Readdirnames(n int) ([]string, error) {
Expand Down
4 changes: 4 additions & 0 deletions providers/os/connection/docker/filesystem.go
Original file line number Diff line number Diff line change
Expand Up @@ -90,3 +90,7 @@ func (fs *FS) Chtimes(name string, atime time.Time, mtime time.Time) error {
func (fs *FS) Chown(name string, uid, gid int) error {
return errors.New("chown not implemented")
}

func (fs *FS) ReadDir(name string) ([]os.FileInfo, error) {
return fs.catFS.ReadDir(name)
}
6 changes: 6 additions & 0 deletions providers/os/connection/ssh/cat/cat.go
Original file line number Diff line number Diff line change
Expand Up @@ -107,3 +107,9 @@ func (cat *Fs) Chtimes(name string, atime time.Time, mtime time.Time) error {
func (cat *Fs) Chown(name string, uid, gid int) error {
return NotImplemented
}

func (cat *Fs) ReadDir(name string) ([]os.FileInfo, error) {
file := NewFile(cat, name, false)
defer file.Close()
return file.Readdir(-1) // -1 is ignored, we read all files
}
9 changes: 8 additions & 1 deletion providers/os/connection/ssh/cat/cat_file.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"encoding/base64"
"io"
"os"
"path/filepath"
"strings"

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

res = make([]os.FileInfo, len(names))
for i, name := range names {
res[i], err = f.catfs.Stat(name)
var statPath string
if filepath.IsAbs(name) {
statPath = name
} else {
statPath = filepath.Join(f.path, name)
}
res[i], err = f.catfs.Stat(statPath)
Comment on lines +99 to +105
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This function was not used until now, but I made the change so it's backward compatible.

if err != nil {
return nil, err
}
Expand Down
59 changes: 31 additions & 28 deletions providers/os/resources/sshd.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ package resources

import (
"errors"
"os"
"path/filepath"
"regexp"
"strings"
Expand Down Expand Up @@ -134,6 +135,12 @@ func (s *mqlSshdConfig) expandGlob(glob string) ([]string, error) {
for _, path := range paths {
files, err := afs.ReadDir(path)
if err != nil {
// If the directory doesn't exist, treat it as "no matches" (empty result)
// This is consistent with standard glob behavior where a non-existent directory
// results in an empty match set, not an error
if os.IsNotExist(err) {
continue
}
return nil, err
}

Expand Down Expand Up @@ -165,41 +172,37 @@ func (s *mqlSshdConfig) parse(file *mqlFile) error {
file.Path.Data: file,
}
var allContents strings.Builder
globPathContent := func(glob string) (string, error) {
paths, err := s.expandGlob(glob)
if err != nil {
return "", err
}

var content strings.Builder
for _, path := range paths {
file, ok := filesIdx[path]
if !ok {
raw, err := CreateResource(s.MqlRuntime, "file", map[string]*llx.RawData{
"path": llx.StringData(path),
})
if err != nil {
return "", err
}
file = raw.(*mqlFile)
filesIdx[path] = file
}

fileContent := file.GetContent()
if fileContent.Error != nil {
return "", fileContent.Error
// Function to get file content by path
fileContent := func(path string) (string, error) {
file, ok := filesIdx[path]
if !ok {
raw, err := CreateResource(s.MqlRuntime, "file", map[string]*llx.RawData{
"path": llx.StringData(path),
})
if err != nil {
return "", err
}
file = raw.(*mqlFile)
filesIdx[path] = file
}

content.WriteString(fileContent.Data)
content.WriteString("\n")
fileContent := file.GetContent()
if fileContent.Error != nil {
return "", fileContent.Error
}

res := content.String()
allContents.WriteString(res)
return res, nil
content := fileContent.Data + "\n"
allContents.WriteString(content)
return content, nil
}

// Function to expand glob patterns
globExpand := func(glob string) ([]string, error) {
return s.expandGlob(glob)
}

matchBlocks, err := sshd.ParseBlocks(file.Path.Data, globPathContent)
matchBlocks, err := sshd.ParseBlocksWithGlob(file.Path.Data, fileContent, globExpand)
// TODO: check if not ready on I/O
if err != nil {
s.Params = plugin.TValue[map[string]any]{Error: err, State: plugin.StateIsSet | plugin.StateIsNull}
Expand Down
209 changes: 187 additions & 22 deletions providers/os/resources/sshd/params.go
Original file line number Diff line number Diff line change
Expand Up @@ -85,23 +85,42 @@ func (m MatchBlocks) Flatten() map[string]any {

func mergeIncludedBlocks(matchConditions map[string]*MatchBlock, blocks MatchBlocks, curBlock string) {
for _, block := range blocks {
// meaning:
// 1. curBlock == "", we can always add all subblocks
// 2. if block == "", we can add it to whatever current block is
// 3. in all other cases the block criteria must match, or we move on
if block.Criteria != curBlock && curBlock != "" && block.Criteria != "" {
if block.Criteria == "" {
// Default block: merge into the current block
existing := matchConditions[curBlock]
if existing == nil {
existing = &MatchBlock{
Criteria: curBlock,
Params: map[string]any{},
Context: block.Context,
}
matchConditions[curBlock] = existing
}
if existing.Params == nil {
existing.Params = map[string]any{}
}
for k, v := range block.Params {
if _, ok := existing.Params[k]; !ok {
existing.Params[k] = v
}
}
continue
}

var existing *MatchBlock
if block.Criteria == "" {
existing = matchConditions[curBlock]
} else {
existing := matchConditions[block.Criteria]
if existing == nil {
matchConditions[block.Criteria] = block
continue
// Match block: always add to global map
existing, ok := matchConditions[block.Criteria]
if !ok {
// Create a new Match block
existing = &MatchBlock{
Criteria: block.Criteria,
Params: map[string]any{},
Context: block.Context,
}
matchConditions[block.Criteria] = existing
}

if existing.Params == nil {
existing.Params = map[string]any{}
}

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

func ParseBlocks(rootPath string, globPathContent func(string) (string, error)) (MatchBlocks, error) {
content, err := globPathContent(rootPath)
type (
fileContentFunc func(string) (content string, err error)
globExpandFunc func(string) (paths []string, err error)
)

// ParseBlocks parses a single SSH config file and returns the match blocks.
// The filePath should be the actual file path (not a glob pattern).
// For Include directives with glob patterns, use ParseBlocksWithGlob instead.
func ParseBlocks(filePath string, content string) (MatchBlocks, error) {
curBlock := &MatchBlock{
Criteria: "",
Params: map[string]any{},
Context: Context{
Path: filePath,
Range: llx.NewRange(),
curLine: 1,
},
}
matchConditions := map[string]*MatchBlock{
"": curBlock,
}

lines := strings.Split(content, "\n")
for curLineIdx, textLine := range lines {
l, err := ParseLine([]rune(textLine))
if err != nil {
return nil, err
}

key := l.key
if key == "" {
continue
}

// handle lower case entries and use proper ssh camel case
if sshKey, ok := SSH_Keywords[strings.ToLower(key)]; ok {
key = sshKey
}

if key == "Include" {
// Include directives are handled by ParseBlocksWithGlob
// This function only parses single files
log.Warn().Str("file", filePath).Msg("Include directive found in single-file parser, use ParseBlocksWithGlob instead")
continue
}

if key == "Match" {
// wrap up context on the previous block
curBlock.Context.Range = curBlock.Context.Range.AddLineRange(uint32(curBlock.Context.curLine), uint32(curLineIdx))
curBlock.Context.curLine = curLineIdx

// This key is stored in the condition of each block and can be accessed there.
condition := l.args
if b, ok := matchConditions[condition]; ok {
curBlock = b
} else {
curBlock = &MatchBlock{
Criteria: condition,
Params: map[string]any{},
Context: Context{
curLine: curLineIdx + 1,
Path: filePath,
Range: llx.NewRange(),
},
}
matchConditions[condition] = curBlock
}
continue
}

setParam(curBlock.Params, key, l.args)
}

keys := sortx.Keys(matchConditions)
res := make([]*MatchBlock, len(keys))
i := 0
for _, key := range keys {
res[i] = matchConditions[key]
i++
}

curBlock.Context.Range = curBlock.Context.Range.AddLineRange(uint32(curBlock.Context.curLine), uint32(len(lines)))

return res, nil
}

// ParseBlocksWithGlob parses SSH config files, expanding glob patterns in Include directives.
// It expands globs and calls ParseBlocksWithGlobRecursive for each matched file individually,
func ParseBlocksWithGlob(rootPath string, fileContent fileContentFunc, globExpand globExpandFunc) (MatchBlocks, error) {
// First, expand the root path if it's a glob
paths, err := globExpand(rootPath)
if err != nil {
return nil, err
}

// If no paths matched, check if rootPath was a single file (not a glob) or return empty blocks
if len(paths) == 0 {
// Check if rootPath contains a glob pattern
hasGlob := strings.Contains(rootPath, "*") || strings.Contains(rootPath, "?") || strings.Contains(rootPath, "[")
if !hasGlob {
_, err := fileContent(rootPath)
if err != nil {
return nil, err
}
}
return MatchBlocks{}, nil
}

// Parse each file individually and collect all blocks
// Each file maintains its own context (path, line numbers)
var allBlocks MatchBlocks

for i, path := range paths {
content, err := fileContent(path)
if err != nil {
if i == 0 && (len(paths) == 1 || path == rootPath) {
return nil, err
}
log.Warn().Err(err).Str("path", path).Msg("unable to read file")
continue
}

blocks, err := ParseBlocksWithGlobRecursive(path, content, fileContent, globExpand)
if err != nil {
log.Warn().Err(err).Str("path", path).Msg("unable to parse file")
continue
}

allBlocks = append(allBlocks, blocks...)
}

return allBlocks, nil
}

// ParseBlocksWithGlobRecursive parses a single file and recursively handles Include directives.
func ParseBlocksWithGlobRecursive(filePath string, content string, fileContent fileContentFunc, globExpand globExpandFunc) (MatchBlocks, error) {
curBlock := &MatchBlock{
Criteria: "",
Params: map[string]any{},
Context: Context{
Path: rootPath,
Path: filePath,
Range: llx.NewRange(),
curLine: 1,
},
Expand Down Expand Up @@ -150,15 +299,31 @@ func ParseBlocks(rootPath string, globPathContent func(string) (string, error))

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

for _, path := range paths {
subBlocks, err := ParseBlocks(path, globPathContent)
for _, includePath := range includePaths {
// Expand glob pattern if present
expandedPaths, err := globExpand(includePath)
if err != nil {
log.Warn().Err(err).Msg("unable to parse Include directive")
log.Warn().Err(err).Str("path", includePath).Msg("unable to expand Include directive")
continue
}
mergeIncludedBlocks(matchConditions, subBlocks, curBlock.Criteria)

// Parse each matched file individually
for _, expandedPath := range expandedPaths {
subContent, err := fileContent(expandedPath)
if err != nil {
log.Warn().Err(err).Str("path", expandedPath).Msg("unable to read included file")
continue
}

subBlocks, err := ParseBlocksWithGlobRecursive(expandedPath, subContent, fileContent, globExpand)
if err != nil {
log.Warn().Err(err).Str("path", expandedPath).Msg("unable to parse included file")
continue
}
mergeIncludedBlocks(matchConditions, subBlocks, curBlock.Criteria)
}
}
continue
}
Expand All @@ -179,7 +344,7 @@ func ParseBlocks(rootPath string, globPathContent func(string) (string, error))
Params: map[string]any{},
Context: Context{
curLine: curLineIdx + 1,
Path: rootPath,
Path: filePath,
Range: llx.NewRange(),
},
}
Expand Down
Loading
Loading