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  }