github.com/google/osv-scalibr@v0.4.1/detector/weakcredentials/etcshadow/etcshadow_test.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_test 16 17 import ( 18 "context" 19 "errors" 20 "io" 21 "io/fs" 22 "os" 23 "testing" 24 25 "github.com/google/go-cmp/cmp" 26 "github.com/google/go-cmp/cmp/cmpopts" 27 "github.com/google/osv-scalibr/detector/weakcredentials/etcshadow" 28 "github.com/google/osv-scalibr/extractor" 29 scalibrfs "github.com/google/osv-scalibr/fs" 30 "github.com/google/osv-scalibr/inventory" 31 "github.com/google/osv-scalibr/packageindex" 32 ) 33 34 // All users have the password "Password123" using distinct hashing algorithms. 35 var sampleEtcShadow = "" + 36 "user-yescrypt:$y$j9T$huXYrFRxr5.EtlA/GqJQg1$R36Nu5MbY5YM0SzRaWbBPyGpM7KMcWtbUmBq5gDZA9B\n" + 37 "user-gost-yescrypt:$gy$j9T$i.krMgTvuXE2doi6Hguka/$qwn482j7gJbWZNQ3cF0YdKAud.C3vUIorQGsF0ryox3\n" + 38 "user-scrypt:$7$CU..../....oupVTCfqrgm0HQkQR3JaB1$2m9CeDTqL8i5pMsc8E73A2bCIsvQPhntxBmSVlbrql2\n" + 39 "user-bcrypt:$2b$05$IYDlXvHmeORyyiUwu8KKuek2LE8VrxIYZ2skPvRDDNngpXJHRq7sG\n" + 40 "user-bcrypt-a:$2a$05$pRmHHyGfKl9/9AZLORG/neKW39VHGF4ptLT2MLq1BqQOnbwL6DQM6:3rdfield\n" + 41 "user-bcrypt-a\n" + // entry skipped, no ':' separator 42 "user-sha512crypt:$6$5dZ5RtTlA.rNzi8o$sE23IbqB0Q57/7nI2.AqazHUnWGP06HmkadfBJ90mHgAHkWVZteoaUWV25jITMIUXC/buIgZ9hU2JYQM5qGZn1\n" + 43 "user-sha256crypt:$5$bMDt75aAcRJMgynJ$7dvcQe0UPWAlpr4VFNQI2iDDUQLgwcaTOV5oQVSIR56\n" + 44 "user-sunmd5:$md5,rounds=46947$ieGPlcPv$$sJ4xQqZ5DHZu0Bma2EW/..\n" + 45 "user-md5crypt:$1$emQTNiRX$kZ2UzRTLgfsTBGS0M1OOb1\n" + 46 "user-NT-Hash:$3$$58a478135a93ac3bf058a5ea0e8fdb71\n" + 47 "user-bsdicrypt:_J9..Sc51o5Op8yDIuHc\n" + 48 "user-descrypt:chERDiI95PGCQ\n" + 49 "user-descrypt2:chERDiI95PGCQ:abc\n" + // entry with more than 2 fields 50 "" 51 52 // Minimal fake fs.FS implementation that supports reading from files a set content. 53 // Used to fake read from /etc/shadow a given set of password hashes. 54 type fakeFS struct { 55 files map[string]string 56 } 57 58 func (f fakeFS) Open(name string) (fs.File, error) { 59 if content, ok := f.files[name]; ok { 60 return &fakeFile{content, 0}, nil 61 } 62 return nil, os.ErrNotExist 63 } 64 func (fakeFS) ReadDir(name string) ([]fs.DirEntry, error) { 65 return nil, errors.New("not implemented") 66 } 67 func (fakeFS) Stat(name string) (fs.FileInfo, error) { 68 return nil, errors.New("not implemented") 69 } 70 71 type fakeFile struct { 72 content string 73 position int 74 } 75 76 func (f *fakeFile) Stat() (fs.FileInfo, error) { 77 return nil, nil 78 } 79 80 func (f *fakeFile) Read(buffer []byte) (count int, err error) { 81 size := copy(buffer, f.content[f.position:]) 82 if size > 0 { 83 f.position += size 84 return size, nil 85 } 86 return 0, io.EOF 87 } 88 89 func (*fakeFile) Close() error { 90 return nil 91 } 92 93 func TestScan(t *testing.T) { 94 wantTitle := "Ensure all users have strong passwords configured" 95 wantDesc := "The /etc/shadow file contains user account password hashes. " + 96 "These passwords must be strong and not easily guessable." 97 wantRec := "Run the following command to reset password for the reported users:\n" + 98 "# change password for USER: sudo passwd USER" 99 wantAdv := &inventory.GenericFindingAdvisory{ 100 ID: &inventory.AdvisoryID{ 101 Publisher: "SCALIBR", 102 Reference: "etc-shadow-weakcredentials", 103 }, 104 Title: wantTitle, 105 Description: wantDesc, 106 Recommendation: wantRec, 107 Sev: inventory.SeverityCritical, 108 } 109 110 px, _ := packageindex.New([]*extractor.Package{}) 111 testCases := []struct { 112 desc string 113 fsys scalibrfs.FS 114 wantFindings []*inventory.GenericFinding 115 wantErr error 116 }{ 117 { 118 desc: "File_doesn't_exist", 119 fsys: &fakeFS{}, 120 }, 121 { 122 desc: "File_empty", 123 fsys: &fakeFS{files: map[string]string{"etc/shadow": ""}}, 124 }, 125 { 126 desc: "File_with_incorrect_format", 127 fsys: &fakeFS{files: map[string]string{"etc/shadow": "x\ny\n"}}, 128 }, 129 { 130 desc: "File_without_hashes", 131 fsys: &fakeFS{files: map[string]string{"etc/shadow": "x:!:stuff\ny:*:stuff\nz:!!:stuff\n"}}, 132 }, 133 { 134 desc: "File_with_hashes,_some_cracked", 135 fsys: &fakeFS{files: map[string]string{"etc/shadow": sampleEtcShadow}}, 136 wantFindings: []*inventory.GenericFinding{{ 137 Adv: wantAdv, 138 Target: &inventory.GenericFindingTargetDetails{ 139 Extra: "/etc/shadow: The following users have weak passwords:\n" + 140 "user-bcrypt\n" + "user-bcrypt-a\n" + "user-sha512crypt\n", 141 }, 142 }}, 143 }, 144 } 145 146 for _, tc := range testCases { 147 t.Run(tc.desc, func(t *testing.T) { 148 detector := etcshadow.Detector{} 149 finding, err := detector.Scan(t.Context(), &scalibrfs.ScanRoot{FS: tc.fsys}, px) 150 if diff := cmp.Diff(tc.wantErr, err, cmpopts.EquateErrors()); diff != "" { 151 t.Fatalf("detector.Scan(%v): unexpected error (-want +got):\n%s", tc.fsys, diff) 152 } 153 if err == nil { 154 if diff := cmp.Diff(tc.wantFindings, finding.GenericFindings); diff != "" { 155 t.Errorf("detector.Scan(%v): unexpected findings (-want +got):\n%s", tc.fsys, diff) 156 } 157 } 158 }) 159 } 160 } 161 162 func TestScanCancelled(t *testing.T) { 163 px, _ := packageindex.New([]*extractor.Package{}) 164 detector := etcshadow.Detector{} 165 fsys := &fakeFS{files: map[string]string{"etc/shadow": sampleEtcShadow}} 166 ctx, cancelFunc := context.WithCancel(t.Context()) 167 cancelFunc() 168 finding, err := detector.Scan(ctx, &scalibrfs.ScanRoot{FS: fsys}, px) 169 if finding.GenericFindings != nil || !errors.Is(err, ctx.Err()) { 170 t.Errorf("expected scan to be cancelled") 171 } 172 }