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

     1  // Copyright (c) Mondoo, Inc.
     2  // SPDX-License-Identifier: BUSL-1.1
     3  
     4  package services
     5  
     6  import (
     7  	"errors"
     8  	"fmt"
     9  	"io"
    10  	"io/fs"
    11  	"os"
    12  	"path"
    13  	"path/filepath"
    14  	"regexp"
    15  	"strings"
    16  
    17  	"github.com/coreos/go-systemd/unit"
    18  	"github.com/spf13/afero"
    19  	"go.mondoo.com/cnquery/providers/os/connection/shared"
    20  )
    21  
    22  var (
    23  	SYSTEMD_LIST_UNITS_REGEX = regexp.MustCompile(`(?m)^(?:[^\S\n]{2}|●[^\S\n]|)(\S+)(?:[^\S\n])+(loaded|not-found|masked)(?:[^\S\n])+(\S+)(?:[^\S\n])+(\S+)(?:[^\S\n])+(.+)$`)
    24  	serviceNameRegex         = regexp.MustCompile(`(.*)\.(service|target|socket)$`)
    25  	errIgnored               = errors.New("ignored")
    26  )
    27  
    28  func ResolveSystemdServiceManager(conn shared.Connection) OSServiceManager {
    29  	if !conn.Capabilities().Has(shared.Capability_RunCommand) {
    30  		return &SystemdFSServiceManager{Fs: conn.FileSystem()}
    31  	}
    32  	return &SystemDServiceManager{conn: conn}
    33  }
    34  
    35  // a line may be prefixed with nothing, whitespace or a dot
    36  func ParseServiceSystemDUnitFiles(input io.Reader) ([]*Service, error) {
    37  	var services []*Service
    38  	content, err := io.ReadAll(input)
    39  	if err != nil {
    40  		return nil, err
    41  	}
    42  
    43  	m := SYSTEMD_LIST_UNITS_REGEX.FindAllStringSubmatch(string(content), -1)
    44  	for i := range m {
    45  		name := m[i][1]
    46  		name = strings.Replace(name, ".service", "", 1)
    47  
    48  		s := &Service{
    49  			Name:      name,
    50  			Installed: m[i][2] == "loaded",
    51  			Running:   m[i][3] == "active",
    52  			// TODO: we may need to revist the enabled state
    53  			Enabled:     m[i][2] == "loaded",
    54  			Masked:      m[i][2] == "masked",
    55  			Description: m[i][5],
    56  			Type:        "systemd",
    57  		}
    58  		services = append(services, s)
    59  	}
    60  	return services, nil
    61  }
    62  
    63  // Newer linux systems use systemd as service manager
    64  type SystemDServiceManager struct {
    65  	conn shared.Connection
    66  }
    67  
    68  func (s *SystemDServiceManager) Name() string {
    69  	return "systemd Service Manager"
    70  }
    71  
    72  func (s *SystemDServiceManager) List() ([]*Service, error) {
    73  	c, err := s.conn.RunCommand("systemctl --all list-units --type service")
    74  	if err != nil {
    75  		return nil, err
    76  	}
    77  	return ParseServiceSystemDUnitFiles(c.Stdout)
    78  }
    79  
    80  type SystemdFSServiceManager struct {
    81  	Fs afero.Fs
    82  }
    83  
    84  // systemdUnitSearchPath is the order in which systemd looks up unit files
    85  // We ignore anything in /run as fs scans should not represent a running system
    86  // https://www.freedesktop.org/software/systemd/man/systemd.unit.html#Unit%20File%20Load%20Path
    87  var systemdUnitSearchPath = []string{
    88  	"/etc/systemd/system.control",
    89  	"/etc/systemd/system",
    90  	"/usr/local/lib/systemd/system",
    91  	"/usr/lib/systemd/system",
    92  }
    93  
    94  type unitInfo struct {
    95  	// name is the name of the unit without the type extension
    96  	name string
    97  	// uType is the type extension, for example service, target, etc
    98  	uType string
    99  	// description is the description that is provided in the unit section
   100  	description string
   101  	// deps is a list of all name.type values found in the Wants and Requires
   102  	// fields of the Unit section
   103  	deps []string
   104  	// Orderings is a list of all name.type values found in the Before and
   105  	// After fields of the Unit section
   106  	orderings []string
   107  	// masked is set to true of a unit is symlinked to /dev/null
   108  	masked bool
   109  	// missing is set to true if we have a dependency on a unit, but that
   110  	// unit was not found in the search path
   111  	missing bool
   112  	// isDep is true of this unit is found in the dependency tree starting
   113  	// from the default.target
   114  	isDep bool
   115  	// service is only set for socket units. It contains an optional name.target.
   116  	// If not provided, socketname.service is activated for the socket
   117  	service string
   118  }
   119  
   120  type stackEntry struct {
   121  	unit     string
   122  	critical bool
   123  }
   124  type stack []stackEntry
   125  
   126  func (s *stack) push(v stackEntry) {
   127  	*s = append(*s, v)
   128  }
   129  
   130  func (s *stack) pop() stackEntry {
   131  	n := len(*s) - 1
   132  	v := (*s)[n]
   133  	*s = (*s)[:n]
   134  	return v
   135  }
   136  
   137  func (s *stack) len() int {
   138  	return len(*s)
   139  }
   140  
   141  func (s *SystemdFSServiceManager) Name() string {
   142  	return "systemd FS Service Manager"
   143  }
   144  
   145  func (s *SystemdFSServiceManager) List() ([]*Service, error) {
   146  	enabledUnits, err := s.traverse()
   147  	if err != nil {
   148  		return nil, err
   149  	}
   150  	services := make([]*Service, 0, len(enabledUnits))
   151  	for _, v := range enabledUnits {
   152  		if v.uType != "service" {
   153  			continue
   154  		}
   155  		services = append(services, &Service{
   156  			Name:        v.name,
   157  			Type:        v.uType,
   158  			Description: v.description,
   159  			State:       ServiceUnknown,
   160  			Installed:   !v.missing,
   161  			Enabled:     !v.missing && v.isDep,
   162  			Masked:      v.masked,
   163  		})
   164  	}
   165  	return services, nil
   166  }
   167  
   168  // traverse traverses the root target and finds units. This implementation is
   169  // incomplete. It only looks at targets, services, and sockets, so at least
   170  // mounts and timers are missing. Also, handling of templates is probably not
   171  // fully correct. The implicit and default dependencies for types are also
   172  // not accounted for
   173  func (s *SystemdFSServiceManager) traverse() (map[string]*unitInfo, error) {
   174  	loadedUnits := map[string]*unitInfo{}
   175  	stack := new(stack)
   176  	stack.push(stackEntry{
   177  		critical: true,
   178  		unit:     "default.target",
   179  	})
   180  	for stack.len() > 0 {
   181  		u := stack.pop()
   182  		if l, ok := loadedUnits[u.unit]; ok {
   183  			if !l.isDep && u.critical {
   184  				// We need to revisit all the already loaded units
   185  				// and mark them as a dependency
   186  				l.isDep = true
   187  				for _, v := range l.deps {
   188  					stack.push(stackEntry{
   189  						unit:     v,
   190  						critical: true,
   191  					})
   192  				}
   193  			}
   194  			continue
   195  		}
   196  		uInfo, err := s.findUnit(u.unit)
   197  		if err != nil {
   198  			if errors.Is(err, errIgnored) {
   199  				continue
   200  			}
   201  			return nil, err
   202  		}
   203  		for _, d := range uInfo.deps {
   204  			stack.push(stackEntry{
   205  				unit:     d,
   206  				critical: u.critical,
   207  			})
   208  		}
   209  		for _, d := range uInfo.orderings {
   210  			stack.push(stackEntry{
   211  				unit:     d,
   212  				critical: false,
   213  			})
   214  		}
   215  		if uInfo.uType == "socket" {
   216  			d := uInfo.service
   217  			if d == "" {
   218  				d = fmt.Sprintf("%s.service", uInfo.name)
   219  			}
   220  			stack.push(stackEntry{
   221  				unit:     d,
   222  				critical: u.critical,
   223  			})
   224  		}
   225  		uInfo.isDep = uInfo.isDep || u.critical
   226  		loadedUnits[u.unit] = uInfo
   227  	}
   228  	return loadedUnits, nil
   229  }
   230  
   231  func (s *SystemdFSServiceManager) findUnit(unitName string) (*unitInfo, error) {
   232  	name, uType, ok := unitNameAndType(unitName)
   233  	if !ok {
   234  		return nil, errIgnored
   235  	}
   236  	uInfo := unitInfo{
   237  		name:  name,
   238  		uType: uType,
   239  	}
   240  
   241  	for _, p := range systemdUnitSearchPath {
   242  		var err error
   243  		var fsInfo fs.FileInfo
   244  
   245  		uName := unitName
   246  		unitPath := path.Join(p, unitName)
   247  
   248  		// We try to lstat if we can. We want to know if the file is
   249  		// a symlink because symlinks are aliases
   250  		if lstater, ok := s.Fs.(afero.Lstater); ok {
   251  			fsInfo, _, err = lstater.LstatIfPossible(unitPath)
   252  		} else {
   253  			fsInfo, err = s.Fs.Stat(unitPath)
   254  		}
   255  		if err != nil {
   256  			if os.IsNotExist(err) {
   257  				continue
   258  			}
   259  			return nil, err
   260  		}
   261  
   262  		findNames := []string{uName}
   263  		if fsInfo.Mode()&fs.ModeSymlink != 0 {
   264  			// If its a symlink, we need to get the real name
   265  			// TODO: check if this needs to be done recursively
   266  			if lr, ok := s.Fs.(afero.LinkReader); ok {
   267  				linkPath, err := lr.ReadlinkIfPossible(unitPath)
   268  				if err != nil {
   269  					return nil, err
   270  				}
   271  				if linkPath == "/dev/null" {
   272  					uInfo.masked = true
   273  					return &uInfo, nil
   274  				} else {
   275  					linkedName := path.Base(linkPath)
   276  					name, uType, ok := unitNameAndType(linkedName)
   277  					// The rules for aliasing only allow same type to same
   278  					// type. So foo.service -> bar.service, but not
   279  					// foo.service -> bar.socket
   280  					if !ok || (uInfo.uType != uType) {
   281  						return nil, fmt.Errorf("invalid unit %s", linkedName)
   282  					}
   283  					uInfo.name = name
   284  					findNames = append(findNames, linkedName)
   285  				}
   286  			}
   287  		}
   288  
   289  		if err := s.readUnit(unitPath, &uInfo); err != nil {
   290  			return nil, err
   291  		}
   292  
   293  		// We need to search for deps from directories based on both the
   294  		// real name and aliased name
   295  		for _, n := range findNames {
   296  			dirDeps, err := s.findDeps(n)
   297  			if err != nil {
   298  				return nil, err
   299  			}
   300  			uInfo.deps = append(uInfo.deps, dirDeps...)
   301  		}
   302  
   303  		return &uInfo, nil
   304  	}
   305  
   306  	uInfo.missing = true
   307  	return &uInfo, nil
   308  }
   309  
   310  // readUnit reads the unit file:
   311  // deps are pulled from the Wants and Requires keys of the Unit section
   312  // description is pulled from the Description key of the Unit section
   313  // orderings are pulled from the Before and After keys of the Unit section
   314  func (s *SystemdFSServiceManager) readUnit(unitPath string, uInfo *unitInfo) error {
   315  	// First resolve the symlink in case the unitPath is actually a symlink.
   316  	if lr, ok := s.Fs.(afero.LinkReader); ok {
   317  		linkPath, err := lr.ReadlinkIfPossible(unitPath)
   318  		if err == nil {
   319  			// If the linkPath is not absolute, use the directory of unitPath and append the
   320  			// filename.
   321  			if !filepath.IsAbs(linkPath) {
   322  				directory := filepath.Dir(unitPath)
   323  				linkPath = filepath.Join(directory, linkPath)
   324  			}
   325  			unitPath = linkPath
   326  		}
   327  	}
   328  
   329  	f, err := s.Fs.Open(unitPath)
   330  	if err != nil {
   331  		return err
   332  	}
   333  	opts, err := unit.Deserialize(f)
   334  	if err != nil {
   335  		return err
   336  	}
   337  
   338  	for _, o := range opts {
   339  		if o.Section == "Unit" && (o.Name == "Wants" || o.Name == "Requires") {
   340  			deps := strings.Fields(o.Value)
   341  			for _, d := range deps {
   342  				if serviceNameRegex.MatchString(d) {
   343  					uInfo.deps = append(uInfo.deps, d)
   344  				}
   345  			}
   346  		} else if o.Section == "Unit" && o.Name == "Description" {
   347  			uInfo.description = o.Value
   348  		} else if o.Section == "Unit" && (o.Name == "Before" || o.Name == "After") {
   349  			orderings := strings.Fields(o.Value)
   350  			for _, d := range orderings {
   351  				if serviceNameRegex.MatchString(d) {
   352  					uInfo.orderings = append(uInfo.orderings, d)
   353  				}
   354  			}
   355  		} else if o.Section == "Socket" && o.Name == "Service" {
   356  			uInfo.service = o.Value
   357  		}
   358  	}
   359  
   360  	return nil
   361  }
   362  
   363  // findDeps looks up the dependencies for the given unit name (foo.service)
   364  // by looking up all the links in the foo.service.wants and foo.service.requires
   365  // directories
   366  func (s *SystemdFSServiceManager) findDeps(unitName string) ([]string, error) {
   367  	deps := []string{}
   368  	for _, searchPath := range systemdUnitSearchPath {
   369  		paths := []string{
   370  			path.Join(searchPath, fmt.Sprintf("%s.wants", unitName)),
   371  			path.Join(searchPath, fmt.Sprintf("%s.requires", unitName)),
   372  		}
   373  
   374  		for _, p := range paths {
   375  			_, err := s.Fs.Stat(p)
   376  			if err != nil {
   377  				if os.IsNotExist(err) {
   378  					continue
   379  				}
   380  				return nil, err
   381  			}
   382  			unitLinks, err := afero.ReadDir(s.Fs, p)
   383  			if err != nil {
   384  				return nil, err
   385  			}
   386  			for _, unitLink := range unitLinks {
   387  				if serviceNameRegex.MatchString(unitLink.Name()) {
   388  					deps = append(deps, unitLink.Name())
   389  				}
   390  			}
   391  		}
   392  	}
   393  
   394  	return deps, nil
   395  }
   396  
   397  func unitNameAndType(n string) (name string, uType string, ok bool) {
   398  	matches := serviceNameRegex.FindStringSubmatch(n)
   399  	if len(matches) > 1 {
   400  		name = matches[1]
   401  	}
   402  	if len(matches) > 2 {
   403  		uType = matches[2]
   404  	}
   405  	if len(matches) == 3 {
   406  		ok = true
   407  	}
   408  	return
   409  }