github.com/google/osv-scalibr@v0.4.1/detector/weakcredentials/etcshadow/etcshadow.go (about) 1 // Copyright 2025 Google LLC 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 // Package etcshadow implements a detector for weak/guessable passwords stored in /etc/shadow. 16 package etcshadow 17 18 import ( 19 "bufio" 20 "context" 21 "errors" 22 "fmt" 23 "io/fs" 24 "os" 25 "sort" 26 "strings" 27 28 "github.com/google/osv-scalibr/detector" 29 scalibrfs "github.com/google/osv-scalibr/fs" 30 "github.com/google/osv-scalibr/inventory" 31 "github.com/google/osv-scalibr/packageindex" 32 "github.com/google/osv-scalibr/plugin" 33 ) 34 35 const ( 36 // Name of the detector. 37 Name = "weakcredentials/etcshadow" 38 ) 39 40 // Detector is a SCALIBR Detector for weak/guessable passwords from /etc/shadow. 41 type Detector struct{} 42 43 // New returns a detector. 44 func New() detector.Detector { 45 return &Detector{} 46 } 47 48 // Name of the detector. 49 func (Detector) Name() string { return Name } 50 51 // Version of the detector. 52 func (Detector) Version() int { return 0 } 53 54 // Requirements of the detector. 55 func (Detector) Requirements() *plugin.Capabilities { return &plugin.Capabilities{OS: plugin.OSUnix} } 56 57 // RequiredExtractors returns an empty list as there are no dependencies. 58 func (Detector) RequiredExtractors() []string { return []string{} } 59 60 // DetectedFinding returns generic vulnerability information about what is detected. 61 func (d Detector) DetectedFinding() inventory.Finding { 62 return d.findingForTarget(nil) 63 } 64 65 func (d Detector) findingForTarget(target *inventory.GenericFindingTargetDetails) inventory.Finding { 66 return inventory.Finding{GenericFindings: []*inventory.GenericFinding{{ 67 Adv: &inventory.GenericFindingAdvisory{ 68 ID: &inventory.AdvisoryID{ 69 Publisher: "SCALIBR", 70 Reference: "etc-shadow-weakcredentials", 71 }, 72 Title: "Ensure all users have strong passwords configured", 73 Description: "The /etc/shadow file contains user account password hashes. " + 74 "These passwords must be strong and not easily guessable.", 75 Recommendation: "Run the following command to reset password for the reported users:\n" + 76 "# change password for USER: sudo passwd USER", 77 Sev: inventory.SeverityCritical, 78 }, 79 Target: target, 80 }}} 81 } 82 83 // Scan starts the scan. 84 func (d Detector) Scan(ctx context.Context, scanRoot *scalibrfs.ScanRoot, px *packageindex.PackageIndex) (inventory.Finding, error) { 85 f, err := scanRoot.FS.Open("etc/shadow") 86 if err != nil { 87 if errors.Is(err, os.ErrNotExist) { 88 // File doesn't exist, check not applicable. 89 return inventory.Finding{}, nil 90 } 91 return inventory.Finding{}, err 92 } 93 defer f.Close() 94 95 users, err := parseShadowFile(f) 96 if err != nil { 97 return inventory.Finding{}, err 98 } 99 100 cracker := NewPasswordCracker() 101 102 // When looking at password hashes we strictly focus on hash strings 103 // with the format $ALGO$SALT$HASH. There are many other things we choose 104 // not to check for the sake of simplicity (e.g. hash strings preceded 105 // by one or two ! characters are for locked logins - password can still be weak 106 // and running as user can be done locally with the 'su' command). 107 var problemUsers []string 108 for user, hash := range users { 109 if ctx.Err() != nil { 110 return inventory.Finding{}, ctx.Err() 111 } 112 if _, err := cracker.Crack(ctx, hash); err == nil { // if cracked 113 // Report only user name to avoid PII leakage. 114 problemUsers = append(problemUsers, user) 115 } 116 } 117 118 if len(problemUsers) == 0 { 119 return inventory.Finding{}, nil 120 } 121 122 // Sort users to avoid non-determinism in the processing order from users map. 123 sort.Strings(problemUsers) 124 buf := new(strings.Builder) 125 _, _ = fmt.Fprintln(buf, "The following users have weak passwords:") 126 for _, u := range problemUsers { 127 _, _ = fmt.Fprintln(buf, u) 128 } 129 problemDescription := buf.String() 130 target := &inventory.GenericFindingTargetDetails{Extra: "/etc/shadow: " + problemDescription} 131 return d.findingForTarget(target), nil 132 } 133 134 func parseShadowFile(f fs.File) (map[string]string, error) { 135 users := make(map[string]string) 136 scanner := bufio.NewScanner(f) 137 for scanner.Scan() { 138 fields := strings.Split(scanner.Text(), ":") 139 if len(fields) >= 2 { 140 users[fields[0]] = fields[1] 141 } 142 } 143 144 if err := scanner.Err(); err != nil { 145 return nil, err 146 } 147 148 return users, nil 149 }