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 }