github.com/google/osv-scalibr@v0.4.1/detector/weakcredentials/winlocal/samreg/samreg_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 samreg
    16  
    17  import (
    18  	"errors"
    19  	"runtime"
    20  	"strings"
    21  	"testing"
    22  
    23  	"github.com/google/go-cmp/cmp"
    24  	"github.com/google/osv-scalibr/common/windows/registry"
    25  	"github.com/google/osv-scalibr/testing/mockregistry"
    26  )
    27  
    28  func TestNewFromFile(t *testing.T) {
    29  	tests := []struct {
    30  		name        string
    31  		filepath    string
    32  		onGOOS      string
    33  		wantErr     bool
    34  		wantErrText string
    35  	}{
    36  		{
    37  			name:        "file_does_not_exist_returns_error_on_windows",
    38  			filepath:    "/some/path/that/does/not/exist",
    39  			onGOOS:      "windows",
    40  			wantErr:     true,
    41  			wantErrText: "The system cannot find the path specified",
    42  		},
    43  		{
    44  			name:        "file_does_not_exist_returns_error_on_linux",
    45  			filepath:    "/some/path/that/does/not/exist",
    46  			onGOOS:      "linux",
    47  			wantErr:     true,
    48  			wantErrText: "no such file or directory",
    49  		},
    50  		{
    51  			name:        "file_not_a_registry_returns_error_on_windows",
    52  			filepath:    "C:\\Windows\\System32\\cmd.exe",
    53  			onGOOS:      "windows",
    54  			wantErr:     true,
    55  			wantErrText: "File does not have registry magic.",
    56  		},
    57  		{
    58  			name:        "file_not_a_registry_returns_error_on_linux",
    59  			filepath:    "/dev/zero",
    60  			onGOOS:      "linux",
    61  			wantErr:     true,
    62  			wantErrText: "File does not have registry magic.",
    63  		},
    64  	}
    65  
    66  	for _, tc := range tests {
    67  		t.Run(tc.name, func(t *testing.T) {
    68  			if tc.onGOOS != runtime.GOOS {
    69  				t.Skipf("Skipping test %q for %q", tc.name, tc.onGOOS)
    70  			}
    71  
    72  			_, err := NewFromFile(tc.filepath)
    73  			if (err != nil) != tc.wantErr {
    74  				t.Errorf("NewFromFile(%q) unexpected error: %v", tc.filepath, err)
    75  			}
    76  
    77  			if tc.wantErr {
    78  				if !strings.Contains(err.Error(), tc.wantErrText) {
    79  					t.Errorf("NewFromFile(%q) unexpected error, got: %v, want: %v", tc.filepath, err, tc.wantErrText)
    80  				}
    81  
    82  				return
    83  			}
    84  		})
    85  	}
    86  }
    87  
    88  func TestUserRIDs(t *testing.T) {
    89  	tests := []struct {
    90  		name     string
    91  		registry *mockregistry.MockRegistry
    92  		want     []string
    93  		wantErr  error
    94  	}{
    95  		{
    96  			name: "list_users_from_SAM_succeeds",
    97  			registry: &mockregistry.MockRegistry{
    98  				Keys: map[string]registry.Key{
    99  					`SAM\Domains\Account\Users`: &mockregistry.MockKey{
   100  						KSubkeys: []registry.Key{
   101  							&mockregistry.MockKey{
   102  								KName: "Names",
   103  							},
   104  							&mockregistry.MockKey{
   105  								KName: "000003E9",
   106  							},
   107  							&mockregistry.MockKey{
   108  								KName: "000001F4",
   109  							},
   110  							&mockregistry.MockKey{
   111  								KName: "000003EA",
   112  							},
   113  						},
   114  					},
   115  				},
   116  			},
   117  			want: []string{"000003E9", "000001F4", "000003EA"},
   118  		},
   119  		{
   120  			name: "no_users_in_SAM_returns_empty_list",
   121  			registry: &mockregistry.MockRegistry{
   122  				Keys: map[string]registry.Key{
   123  					`SAM\Domains\Account\Users`: &mockregistry.MockKey{
   124  						KSubkeys: []registry.Key{
   125  							&mockregistry.MockKey{
   126  								KName: "Names",
   127  							},
   128  						},
   129  					},
   130  				},
   131  			},
   132  			want: []string{},
   133  		},
   134  		{
   135  			name:     "missing_user_key_returns_error",
   136  			registry: &mockregistry.MockRegistry{},
   137  			wantErr:  errFailedToParseUsers,
   138  		},
   139  	}
   140  
   141  	for _, tc := range tests {
   142  		t.Run(tc.name, func(t *testing.T) {
   143  			sam := SAMRegistry{tc.registry}
   144  
   145  			got, err := sam.UsersRIDs()
   146  			if !errors.Is(err, tc.wantErr) {
   147  				t.Fatalf("UserRIDs() returned an unexpected error: %v", err)
   148  			}
   149  
   150  			if tc.wantErr != nil {
   151  				return
   152  			}
   153  
   154  			if diff := cmp.Diff(tc.want, got); diff != "" {
   155  				t.Errorf("UserRIDs() returned an unexpected diff (-want +got): %v", diff)
   156  			}
   157  		})
   158  	}
   159  }
   160  
   161  func TestUserInfo(t *testing.T) {
   162  	tests := []struct {
   163  		name        string
   164  		registry    *mockregistry.MockRegistry
   165  		rid         string
   166  		wantErr     bool
   167  		wantErrText string
   168  	}{
   169  		{
   170  			name: "user_info_structure_parses_correctly",
   171  			rid:  "000001F4",
   172  			registry: &mockregistry.MockRegistry{
   173  				Keys: map[string]registry.Key{
   174  					`SAM\Domains\Account\Users\000001F4`: &mockregistry.MockKey{
   175  						KValues: []registry.Value{
   176  							&mockregistry.MockValue{
   177  								VName: "V",
   178  								VData: []byte(strings.Repeat("\x00", 0xCC)),
   179  							},
   180  							&mockregistry.MockValue{
   181  								VName: "F",
   182  								VData: []byte(""),
   183  							},
   184  						},
   185  					},
   186  				},
   187  			},
   188  			wantErr: false,
   189  		},
   190  		{
   191  			name:        "user_specific_key_missing_returns_error",
   192  			rid:         "000001F4",
   193  			registry:    &mockregistry.MockRegistry{},
   194  			wantErr:     true,
   195  			wantErrText: "SAM hive: failed to load user registry for RID",
   196  		},
   197  		{
   198  			name: "missing_v_structure_returns_error",
   199  			rid:  "000001F4",
   200  			registry: &mockregistry.MockRegistry{
   201  				Keys: map[string]registry.Key{
   202  					`SAM\Domains\Account\Users\000001F4`: &mockregistry.MockKey{
   203  						KValues: []registry.Value{
   204  							&mockregistry.MockValue{
   205  								VName: "F",
   206  								VData: []byte(""),
   207  							},
   208  						},
   209  					},
   210  				},
   211  			},
   212  			wantErr:     true,
   213  			wantErrText: "SAM hive: failed to find V or F structures for RID",
   214  		},
   215  		{
   216  			name: "V_structure_parse_failure_returns_error",
   217  			rid:  "000001F4",
   218  			registry: &mockregistry.MockRegistry{
   219  				Keys: map[string]registry.Key{
   220  					`SAM\Domains\Account\Users\000001F4`: &mockregistry.MockKey{
   221  						KValues: []registry.Value{
   222  							&mockregistry.MockValue{
   223  								VName: "V",
   224  								VData: []byte("\x00"),
   225  							},
   226  							&mockregistry.MockValue{
   227  								VName: "F",
   228  								VData: []byte(""),
   229  							},
   230  						},
   231  					},
   232  				},
   233  			},
   234  			wantErr:     true,
   235  			wantErrText: "unexpected EOF",
   236  		},
   237  		{
   238  			name: "missing_F_structure_returns_error",
   239  			rid:  "000001F4",
   240  			registry: &mockregistry.MockRegistry{
   241  				Keys: map[string]registry.Key{
   242  					`SAM\Domains\Account\Users\000001F4`: &mockregistry.MockKey{
   243  						KValues: []registry.Value{
   244  							&mockregistry.MockValue{
   245  								VName: "V",
   246  								VData: []byte(strings.Repeat("\x00", 0xCC)),
   247  							},
   248  						},
   249  					},
   250  				},
   251  			},
   252  			wantErr:     true,
   253  			wantErrText: "SAM hive: failed to find V or F structures for RID",
   254  		},
   255  	}
   256  
   257  	for _, tc := range tests {
   258  		t.Run(tc.name, func(t *testing.T) {
   259  			sam := SAMRegistry{tc.registry}
   260  
   261  			_, err := sam.UserInfo(tc.rid)
   262  			if (err != nil) != tc.wantErr {
   263  				t.Fatalf("UserInfo(%q) returned an unexpected error: %v", tc.rid, err)
   264  			}
   265  
   266  			if tc.wantErr {
   267  				if !strings.Contains(err.Error(), tc.wantErrText) {
   268  					t.Errorf("UserInfo(%q) unexpected error, got: %v, want: %v", tc.rid, err, tc.wantErrText)
   269  				}
   270  
   271  				return
   272  			}
   273  		})
   274  	}
   275  }
   276  
   277  func TestDeriveSyskey(t *testing.T) {
   278  	tests := []struct {
   279  		name     string
   280  		registry *mockregistry.MockRegistry
   281  		syskey   []byte
   282  		want     []byte
   283  		wantErr  error
   284  	}{
   285  		{
   286  			name: "syskey_derivation_succeeds",
   287  			registry: &mockregistry.MockRegistry{
   288  				Keys: map[string]registry.Key{
   289  					`SAM\Domains\Account`: &mockregistry.MockKey{
   290  						KValues: []registry.Value{
   291  							&mockregistry.MockValue{
   292  								VName: "F",
   293  								VData: []byte("\x02\x00\x01\x00\x00\x00\x00\x00\x40\x15\x3b\x97\x46\x9f\xce\x01\x26\x00\x00\x00\x00\x00\x00\x00\x00\x80\xa6\x0a\xff\xde\xff\xff\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x80\x00\xcc\x1d\xcf\xfb\xff\xff\xff\x00\xcc\x1d\xcf\xfb\xff\xff\xff\x00\x00\x00\x00\x00\x00\x00\x00\xe9\x03\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x03\x00\x00\x00\x01\x00\x00\x00\x00\x00\x01\x00\x01\x00\x00\x00\x38\x00\x00\x00\x23\x7e\xe9\x12\xa7\x34\xbf\x93\x18\x6e\xaa\xc1\x83\x07\x59\xa1\xd6\x96\xa6\x99\x6b\xa9\x41\x61\x44\x92\xb0\xfb\xd0\x0a\xe9\xa6\x37\xd6\x7c\xc6\x99\x2b\xc7\x12\xfe\x22\xa0\x17\x71\xce\xd3\xaa\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x38\x00\x00\x00\x3d\xfe\xe0\xd7\x20\xeb\x39\xc1\x44\x1c\x8d\x05\x29\xd6\x83\x47\x92\xa2\x29\x38\xfc\x9e\xa7\x29\xa9\x36\x7d\x4a\xfc\x6c\xe1\xb3\xd3\xac\xd4\xac\xe2\x5b\xab\xf9\xf8\x3f\x09\xe1\x91\x1a\x7d\xda\x00\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00"),
   294  							},
   295  						},
   296  					},
   297  				},
   298  			},
   299  			syskey: []byte("\x88\x93\xae\x93\x45\x13\xbd\xdd\x25\x47\x35\x16\x3e\x9d\x33\x00"),
   300  			want:   []byte("\x3d\x21\x2c\xe8\xa2\xda\x83\x43\xbd\xad\x1e\xf2\xcf\xb6\xb3\x1c"),
   301  		},
   302  		{
   303  			name: "missing_domain_key_returns_error",
   304  			registry: &mockregistry.MockRegistry{
   305  				Keys: map[string]registry.Key{},
   306  			},
   307  			wantErr: errFailedToOpenDomain,
   308  		},
   309  		{
   310  			name: "missing_domainF_structure_returns_error",
   311  			registry: &mockregistry.MockRegistry{
   312  				Keys: map[string]registry.Key{
   313  					`SAM\Domains\Account`: &mockregistry.MockKey{
   314  						KValues: []registry.Value{},
   315  					},
   316  				},
   317  			},
   318  			wantErr: errFailedToParseDomainF,
   319  		},
   320  		{
   321  			name: "error_from_derivation_propagates",
   322  			registry: &mockregistry.MockRegistry{
   323  				Keys: map[string]registry.Key{
   324  					`SAM\Domains\Account`: &mockregistry.MockKey{
   325  						KValues: []registry.Value{
   326  							&mockregistry.MockValue{
   327  								VName: "F",
   328  								VData: []byte(""),
   329  							},
   330  						},
   331  					},
   332  				},
   333  			},
   334  			wantErr: errDomainFTooShort,
   335  		},
   336  	}
   337  
   338  	for _, tc := range tests {
   339  		t.Run(tc.name, func(t *testing.T) {
   340  			sam := SAMRegistry{tc.registry}
   341  
   342  			got, err := sam.DeriveSyskey(tc.syskey)
   343  			if !errors.Is(err, tc.wantErr) {
   344  				t.Errorf("DeriveSyskey(%x) unexpected error, got: %v, want: %v", tc.syskey, err, tc.wantErr)
   345  			}
   346  
   347  			if tc.wantErr != nil {
   348  				return
   349  			}
   350  
   351  			if diff := cmp.Diff(tc.want, got); diff != "" {
   352  				t.Errorf("DeriveSyskey(%x) returned an unexpected diff (-want +got): %v", tc.syskey, diff)
   353  			}
   354  		})
   355  	}
   356  }