go.mondoo.com/cnquery@v0.0.0-20231005093811-59568235f6ea/providers/os/resources/sshd/include.go (about)

     1  // Copyright (c) Mondoo, Inc.
     2  // SPDX-License-Identifier: BUSL-1.1
     3  
     4  package sshd
     5  
     6  import (
     7  	"bufio"
     8  	"fmt"
     9  	"io"
    10  	"io/fs"
    11  	"path/filepath"
    12  	"regexp"
    13  	"strings"
    14  
    15  	"github.com/spf13/afero"
    16  	"go.mondoo.com/cnquery/providers/os/connection/shared"
    17  )
    18  
    19  var (
    20  	// includeStatement is a regexp for checking whether a given sshd configuration line
    21  	// is an 'Include' statement
    22  	includeStatement = regexp.MustCompile(`^[I|i]nclude\s+(.*)$`)
    23  	// includeStatementHasGlob is a regext for checking whether the contents of an 'Include'
    24  	// statement have a wildcard/glob (ie. a literal '*')
    25  	includeStatementHasGlob = regexp.MustCompile(`.*\*.*`)
    26  )
    27  
    28  // GetAllSshdIncludedFiles will return the list of dependent files referenced in the sshd
    29  // configuration file's 'Include' statements starting from the provided filePath parameter as
    30  // the beginning of the sshd configuration.
    31  func GetAllSshdIncludedFiles(filePath string, conn shared.Connection) ([]string, error) {
    32  	allFiles, _, err := readSshdConfig(filePath, conn)
    33  	return allFiles, err
    34  }
    35  
    36  // GetSshdUnifiedContent will return the unified sshd configuration content starting
    37  // from the provided filePath parameter as the beginning of the sshd configuration.
    38  func GetSshdUnifiedContent(filePath string, conn shared.Connection) (string, error) {
    39  	_, content, err := readSshdConfig(filePath, conn)
    40  	return content, err
    41  }
    42  
    43  // When an Include lists a relative path, it is interpreted as relative to /etc/ssh/
    44  const relativePathPrefix = "/etc/ssh/"
    45  
    46  func getBaseDirectory(filePath string) string {
    47  	baseDirectoryPath := filepath.Dir(filePath)
    48  	// insert the /etc/ssh path prefix if a relative path is specified
    49  	if baseDirectoryPath == "." {
    50  		baseDirectoryPath = relativePathPrefix
    51  	}
    52  	if !strings.HasPrefix(baseDirectoryPath, "/") {
    53  		baseDirectoryPath = relativePathPrefix + baseDirectoryPath
    54  	}
    55  
    56  	return baseDirectoryPath
    57  }
    58  
    59  func getFullPath(filePath string) string {
    60  	dir := getBaseDirectory(filePath)
    61  	fileName := filepath.Base(filePath)
    62  	return filepath.Join(dir, fileName)
    63  }
    64  
    65  // readSshdConfig will traverse the provided path to an sshd config file and return
    66  // the list of all depended files encountered while recursively traversing the
    67  // sshd 'Include' statements, and the unified sshd configuration where all the
    68  // sshd 'Include' statements have been replaced with the referenced file's content
    69  // in place of the 'Include'.
    70  func readSshdConfig(filePath string, conn shared.Connection) ([]string, string, error) {
    71  	allFiles := []string{}
    72  	var allContent strings.Builder
    73  
    74  	baseDirectoryPath := getBaseDirectory(filePath)
    75  
    76  	// First check if the Include path has a wildcard/glob
    77  	m := includeStatementHasGlob.FindStringSubmatch(filePath)
    78  	if m != nil {
    79  		glob := filepath.Base(filePath)
    80  
    81  		// List all the files in lexical order and check whether any match the glob
    82  		afs := &afero.Afero{Fs: conn.FileSystem()}
    83  
    84  		wErr := afs.Walk(baseDirectoryPath, func(path string, info fs.FileInfo, err error) error {
    85  			if err != nil {
    86  				return err
    87  			}
    88  			// don't recurse down further directories (as that matches sshd behavior)
    89  			if info.IsDir() {
    90  				return nil
    91  			}
    92  			match, err := filepath.Match(glob, info.Name())
    93  			if err != nil {
    94  				return err
    95  			}
    96  			if !match {
    97  				return nil
    98  			}
    99  
   100  			fullFilepath := filepath.Join(baseDirectoryPath, info.Name())
   101  
   102  			// Now search through that file for any more Include statements
   103  			files, content, err := readSshdConfig(fullFilepath, conn)
   104  			if err != nil {
   105  				return err
   106  			}
   107  			allFiles = append(allFiles, files...)
   108  			if _, err := allContent.WriteString(content); err != nil {
   109  				return err
   110  			}
   111  			return nil
   112  		})
   113  		if wErr != nil {
   114  			return nil, "", fmt.Errorf("error while walking through sshd config directory: %s", wErr)
   115  		}
   116  
   117  		return allFiles, allContent.String(), nil
   118  	}
   119  
   120  	// Now see if we're dealing with a directory
   121  	fullFilePath := getFullPath(filePath)
   122  	f, err := conn.FileSystem().Open(fullFilePath)
   123  	if err != nil {
   124  		return nil, "", err
   125  	}
   126  
   127  	fileInfo, err := f.Stat()
   128  	if err != nil {
   129  		return nil, "", err
   130  	}
   131  	if fileInfo.IsDir() {
   132  		// Again list all files in lexical order
   133  		afs := &afero.Afero{Fs: conn.FileSystem()}
   134  
   135  		wErr := afs.Walk(fullFilePath, func(path string, info fs.FileInfo, err error) error {
   136  			if err != nil {
   137  				return err
   138  			}
   139  
   140  			if info.IsDir() {
   141  				return nil
   142  			}
   143  			allFiles = append(allFiles, path)
   144  
   145  			// Now check this very file for any 'Include' statements
   146  			files, content, err := readSshdConfig(path, conn)
   147  			if err != nil {
   148  				return err
   149  			}
   150  			allFiles = append(allFiles, files...)
   151  			if _, err := allContent.WriteString(content); err != nil {
   152  				return err
   153  			}
   154  
   155  			return nil
   156  		})
   157  		if wErr != nil {
   158  			return nil, "", fmt.Errorf("error while walking through sshd config directory: %s", wErr)
   159  		}
   160  
   161  		return allFiles, allContent.String(), nil
   162  	}
   163  
   164  	// If here, we must be dealing with neither a wildcard nor directory
   165  	// so just consume the file's contents
   166  	allFiles = append(allFiles, fullFilePath)
   167  
   168  	rawFile, err := io.ReadAll(f)
   169  	if err != nil {
   170  		return nil, "", err
   171  	}
   172  
   173  	scanner := bufio.NewScanner(strings.NewReader(string(rawFile)))
   174  	for scanner.Scan() {
   175  		line := scanner.Text()
   176  		m := includeStatement.FindStringSubmatch(line)
   177  		if m != nil {
   178  			includeList := strings.Split(m[1], " ") // TODO: what about files with actual spaces in their names?
   179  			for _, file := range includeList {
   180  				files, content, err := readSshdConfig(file, conn)
   181  				if err != nil {
   182  					return nil, "", err
   183  				}
   184  				allFiles = append(allFiles, files...)
   185  				if _, err := allContent.WriteString(content); err != nil {
   186  					return nil, "", err
   187  				}
   188  			}
   189  			continue
   190  		}
   191  
   192  		if _, err := allContent.WriteString(line + "\n"); err != nil {
   193  			return nil, "", err
   194  		}
   195  	}
   196  	return allFiles, allContent.String(), nil
   197  }