github.com/google/osv-scalibr@v0.4.1/detector/misc/dockersocket/dockersocket_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  //go:build !windows
    16  
    17  package dockersocket
    18  
    19  import (
    20  	"fmt"
    21  	"io"
    22  	"io/fs"
    23  	"strings"
    24  	"syscall"
    25  	"testing"
    26  	"testing/fstest"
    27  	"time"
    28  
    29  	"github.com/google/go-cmp/cmp"
    30  	"github.com/google/osv-scalibr/inventory"
    31  	"github.com/google/osv-scalibr/packageindex"
    32  	"github.com/google/osv-scalibr/plugin"
    33  )
    34  
    35  // Helper functions for generating expected test issues
    36  
    37  func expectSocketWorldReadable(perms fs.FileMode) string {
    38  	return fmt.Sprintf("Docker socket is world-readable (permissions: %03o)", perms.Perm())
    39  }
    40  
    41  func expectSocketWorldWritable(perms fs.FileMode) string {
    42  	return fmt.Sprintf("Docker socket is world-writable (permissions: %03o)", perms.Perm())
    43  }
    44  
    45  func expectSocketNonRootOwner(uid uint32) string {
    46  	return fmt.Sprintf("Docker socket owner is not root (uid: %d)", uid)
    47  }
    48  
    49  func expectInsecureTCPBinding(host string) string {
    50  	return fmt.Sprintf("Insecure TCP binding in daemon.json: %q (consider using TLS)", host)
    51  }
    52  
    53  func expectInsecureSystemdBinding(path, line string) string {
    54  	return fmt.Sprintf("Insecure TCP binding in %q: %q (missing TLS)", path, line)
    55  }
    56  
    57  // fakeFileInfo implements fs.FileInfo for testing
    58  type fakeFileInfo struct {
    59  	name    string
    60  	size    int64
    61  	mode    fs.FileMode
    62  	modTime time.Time
    63  	isDir   bool
    64  	sys     any
    65  }
    66  
    67  func (f fakeFileInfo) Name() string       { return f.name }
    68  func (f fakeFileInfo) Size() int64        { return f.size }
    69  func (f fakeFileInfo) Mode() fs.FileMode  { return f.mode }
    70  func (f fakeFileInfo) ModTime() time.Time { return f.modTime }
    71  func (f fakeFileInfo) IsDir() bool        { return f.isDir }
    72  func (f fakeFileInfo) Sys() any           { return f.sys }
    73  
    74  // fakeFile implements fs.File for testing
    75  type fakeFile struct {
    76  	*fstest.MapFile
    77  
    78  	info   fakeFileInfo
    79  	offset int
    80  }
    81  
    82  func (f fakeFile) Stat() (fs.FileInfo, error) {
    83  	return f.info, nil
    84  }
    85  
    86  func (f fakeFile) Close() error {
    87  	return nil
    88  }
    89  
    90  func (f *fakeFile) Read(b []byte) (int, error) {
    91  	if f.offset >= len(f.Data) {
    92  		return 0, io.EOF
    93  	}
    94  	n := copy(b, f.Data[f.offset:])
    95  	f.offset += n
    96  	return n, nil
    97  }
    98  
    99  func TestDockerSocketPermissions(t *testing.T) {
   100  	tests := []struct {
   101  		name        string
   102  		socketPerms fs.FileMode
   103  		uid         uint32
   104  		gid         uint32
   105  		wantIssues  []string
   106  	}{
   107  		{
   108  			name:        "secure socket permissions",
   109  			socketPerms: 0660, // rw-rw----
   110  			uid:         0,    // root
   111  			gid:         999,  // docker group
   112  			wantIssues:  nil,
   113  		},
   114  		{
   115  			name:        "world-readable socket",
   116  			socketPerms: 0664, // rw-rw-r--
   117  			uid:         0,
   118  			gid:         999,
   119  			wantIssues:  []string{expectSocketWorldReadable(0664)},
   120  		},
   121  		{
   122  			name:        "world-writable socket",
   123  			socketPerms: 0666, // rw-rw-rw-
   124  			uid:         0,
   125  			gid:         999,
   126  			wantIssues:  []string{expectSocketWorldReadable(0666), expectSocketWorldWritable(0666)},
   127  		},
   128  		{
   129  			name:        "non-root owner",
   130  			socketPerms: 0660,
   131  			uid:         1000, // non-root
   132  			gid:         999,
   133  			wantIssues:  []string{expectSocketNonRootOwner(1000)},
   134  		},
   135  	}
   136  
   137  	for _, tt := range tests {
   138  		t.Run(tt.name, func(t *testing.T) {
   139  			stat := &syscall.Stat_t{
   140  				Uid: tt.uid,
   141  				Gid: tt.gid,
   142  			}
   143  
   144  			fsys := fstest.MapFS{}
   145  
   146  			// Override the file with our custom info
   147  			file := fakeFile{
   148  				MapFile: &fstest.MapFile{Data: []byte{}},
   149  				info: fakeFileInfo{
   150  					name:    "docker.sock",
   151  					mode:    tt.socketPerms,
   152  					modTime: time.Now(),
   153  					sys:     stat,
   154  				},
   155  			}
   156  
   157  			// Create a custom filesystem that returns our fake file
   158  			customFS := &testFS{
   159  				MapFS:    fsys,
   160  				sockFile: file,
   161  			}
   162  
   163  			d := &Detector{}
   164  			finding, err := d.ScanFS(t.Context(), customFS, &packageindex.PackageIndex{})
   165  
   166  			if err != nil {
   167  				t.Errorf("ScanFS() returned error: %v", err)
   168  			}
   169  
   170  			var actualIssues []string
   171  			if len(finding.GenericFindings) > 0 {
   172  				// Extract issues from the Extra field
   173  				extra := finding.GenericFindings[0].Target.Extra
   174  				if extra != "" {
   175  					actualIssues = strings.Split(extra, "; ")
   176  				}
   177  			}
   178  
   179  			// Filter to only socket-related issues for this test
   180  			var socketIssues []string
   181  			for _, issue := range actualIssues {
   182  				if strings.Contains(issue, "Docker socket") {
   183  					socketIssues = append(socketIssues, issue)
   184  				}
   185  			}
   186  
   187  			if diff := cmp.Diff(tt.wantIssues, socketIssues); diff != "" {
   188  				t.Errorf("Socket permissions test mismatch (-want +got):\n%s", diff)
   189  			}
   190  		})
   191  	}
   192  }
   193  
   194  // testFS wraps fstest.MapFS to return our custom file for docker.sock
   195  type testFS struct {
   196  	fstest.MapFS
   197  
   198  	sockFile fakeFile
   199  }
   200  
   201  func (t *testFS) Open(name string) (fs.File, error) {
   202  	if name == "var/run/docker.sock" {
   203  		return &t.sockFile, nil
   204  	}
   205  	return t.MapFS.Open(name)
   206  }
   207  
   208  func TestDockerDaemonConfig(t *testing.T) {
   209  	tests := []struct {
   210  		name       string
   211  		config     string
   212  		wantIssues []string
   213  	}{
   214  		{
   215  			name:       "secure config - no hosts",
   216  			config:     `{}`,
   217  			wantIssues: nil,
   218  		},
   219  		{
   220  			name:       "secure config - unix socket only",
   221  			config:     `{"hosts": ["unix:///var/run/docker.sock"]}`,
   222  			wantIssues: nil,
   223  		},
   224  		{
   225  			name:       "insecure config - tcp without tls",
   226  			config:     `{"hosts": ["tcp://0.0.0.0:2375"]}`,
   227  			wantIssues: []string{expectInsecureTCPBinding("tcp://0.0.0.0:2375")},
   228  		},
   229  		{
   230  			name:       "mixed config - both secure and insecure",
   231  			config:     `{"hosts": ["unix:///var/run/docker.sock", "tcp://0.0.0.0:2375"]}`,
   232  			wantIssues: []string{expectInsecureTCPBinding("tcp://0.0.0.0:2375")},
   233  		},
   234  		{
   235  			name:       "multiple insecure hosts",
   236  			config:     `{"hosts": ["tcp://0.0.0.0:2375", "tcp://127.0.0.1:2376"]}`,
   237  			wantIssues: []string{expectInsecureTCPBinding("tcp://0.0.0.0:2375"), expectInsecureTCPBinding("tcp://127.0.0.1:2376")},
   238  		},
   239  		{
   240  			name:       "invalid json",
   241  			config:     `{invalid json}`,
   242  			wantIssues: nil, // Should not error on invalid JSON
   243  		},
   244  	}
   245  
   246  	for _, tt := range tests {
   247  		t.Run(tt.name, func(t *testing.T) {
   248  			fsys := fstest.MapFS{
   249  				"etc/docker/daemon.json": &fstest.MapFile{
   250  					Data: []byte(tt.config),
   251  				},
   252  			}
   253  
   254  			d := &Detector{}
   255  			finding, err := d.ScanFS(t.Context(), fsys, &packageindex.PackageIndex{})
   256  
   257  			if err != nil {
   258  				t.Errorf("ScanFS() returned error: %v", err)
   259  			}
   260  
   261  			var actualIssues []string
   262  			if len(finding.GenericFindings) > 0 {
   263  				// Extract issues from the Extra field
   264  				extra := finding.GenericFindings[0].Target.Extra
   265  				if extra != "" {
   266  					actualIssues = strings.Split(extra, "; ")
   267  				}
   268  			}
   269  
   270  			// Filter to only daemon config related issues for this test
   271  			var daemonIssues []string
   272  			for _, issue := range actualIssues {
   273  				if strings.Contains(issue, "daemon.json") {
   274  					daemonIssues = append(daemonIssues, issue)
   275  				}
   276  			}
   277  
   278  			if diff := cmp.Diff(tt.wantIssues, daemonIssues); diff != "" {
   279  				t.Errorf("Daemon config test mismatch (-want +got):\n%s", diff)
   280  			}
   281  		})
   282  	}
   283  }
   284  
   285  func TestSystemdServiceConfig(t *testing.T) {
   286  	tests := []struct {
   287  		name        string
   288  		serviceFile string
   289  		wantIssues  []string
   290  	}{
   291  		{
   292  			name: "secure_service_-_unix_socket_only",
   293  			serviceFile: `[Unit]
   294  Description=Docker Application Container Engine
   295  
   296  [Service]
   297  ExecStart=/usr/bin/dockerd -H unix:///var/run/docker.sock
   298  
   299  [Install]
   300  WantedBy=multi-user.target`,
   301  			wantIssues: nil,
   302  		},
   303  		{
   304  			name: "insecure_service_-_tcp_without_tls",
   305  			serviceFile: `[Unit]
   306  Description=Docker Application Container Engine
   307  
   308  [Service]
   309  ExecStart=/usr/bin/dockerd -H tcp://0.0.0.0:2375
   310  
   311  [Install]
   312  WantedBy=multi-user.target`,
   313  			wantIssues: []string{expectInsecureSystemdBinding("etc/systemd/system/docker.service", "ExecStart=/usr/bin/dockerd -H tcp://0.0.0.0:2375")},
   314  		},
   315  		{
   316  			name: "secure_service_-_tcp_with_tls",
   317  			serviceFile: `[Unit]
   318  Description=Docker Application Container Engine
   319  
   320  [Service]
   321  ExecStart=/usr/bin/dockerd -H tcp://0.0.0.0:2376 --tls --tlscert=/path/to/cert.pem --tlskey=/path/to/key.pem
   322  
   323  [Install]
   324  WantedBy=multi-user.target`,
   325  			wantIssues: nil,
   326  		},
   327  		{
   328  			name: "multiple_ExecStart_lines_-_some_insecure",
   329  			serviceFile: `[Unit]
   330  Description=Docker Application Container Engine
   331  
   332  [Service]
   333  ExecStart=/usr/bin/dockerd -H unix:///var/run/docker.sock
   334  ExecStart=/usr/bin/dockerd -H tcp://0.0.0.0:2375
   335  
   336  [Install]
   337  WantedBy=multi-user.target`,
   338  			wantIssues: []string{expectInsecureSystemdBinding("etc/systemd/system/docker.service", "ExecStart=/usr/bin/dockerd -H tcp://0.0.0.0:2375")},
   339  		},
   340  	}
   341  
   342  	for _, tt := range tests {
   343  		t.Run(tt.name, func(t *testing.T) {
   344  			fsys := fstest.MapFS{
   345  				"etc/systemd/system/docker.service": &fstest.MapFile{
   346  					Data: []byte(tt.serviceFile),
   347  				},
   348  			}
   349  
   350  			d := &Detector{}
   351  			finding, err := d.ScanFS(t.Context(), fsys, &packageindex.PackageIndex{})
   352  
   353  			if err != nil {
   354  				t.Errorf("ScanFS() returned error: %v", err)
   355  			}
   356  
   357  			var actualIssues []string
   358  			if len(finding.GenericFindings) > 0 {
   359  				// Extract issues from the Extra field
   360  				extra := finding.GenericFindings[0].Target.Extra
   361  				if extra != "" {
   362  					actualIssues = strings.Split(extra, "; ")
   363  				}
   364  			}
   365  
   366  			// Filter to only systemd service related issues for this test
   367  			var systemdIssues []string
   368  			for _, issue := range actualIssues {
   369  				if strings.Contains(issue, "systemd") || strings.Contains(issue, ".service") {
   370  					systemdIssues = append(systemdIssues, issue)
   371  				}
   372  			}
   373  
   374  			if diff := cmp.Diff(tt.wantIssues, systemdIssues); diff != "" {
   375  				t.Errorf("Systemd service test mismatch (-want +got):\n%s", diff)
   376  			}
   377  		})
   378  	}
   379  }
   380  
   381  func TestSystemdServiceConfig_MultiplePaths(t *testing.T) {
   382  	// Test that the detector checks all possible systemd service paths
   383  	insecureService := `[Unit]
   384  Description=Docker Application Container Engine
   385  
   386  [Service]
   387  ExecStart=/usr/bin/dockerd -H tcp://0.0.0.0:2375
   388  
   389  [Install]
   390  WantedBy=multi-user.target`
   391  
   392  	tests := []struct {
   393  		name       string
   394  		files      map[string]string
   395  		wantIssues []string
   396  	}{
   397  		{
   398  			name: "service_in_/etc/systemd/system",
   399  			files: map[string]string{
   400  				"etc/systemd/system/docker.service": insecureService,
   401  			},
   402  			wantIssues: []string{expectInsecureSystemdBinding("etc/systemd/system/docker.service", "ExecStart=/usr/bin/dockerd -H tcp://0.0.0.0:2375")},
   403  		},
   404  		{
   405  			name: "service_in_/lib/systemd/system",
   406  			files: map[string]string{
   407  				"lib/systemd/system/docker.service": `[Service]
   408  ExecStart=/usr/bin/dockerd -H tcp://127.0.0.1:2376`,
   409  			},
   410  			wantIssues: []string{expectInsecureSystemdBinding("lib/systemd/system/docker.service", "ExecStart=/usr/bin/dockerd -H tcp://127.0.0.1:2376")},
   411  		},
   412  		{
   413  			name: "service_in_/usr/lib/systemd/system",
   414  			files: map[string]string{
   415  				"usr/lib/systemd/system/docker.service": `[Service]
   416  ExecStart=/usr/bin/dockerd -H tcp://192.168.1.1:2377`,
   417  			},
   418  			wantIssues: []string{expectInsecureSystemdBinding("usr/lib/systemd/system/docker.service", "ExecStart=/usr/bin/dockerd -H tcp://192.168.1.1:2377")},
   419  		},
   420  		{
   421  			name: "multiple_service_files_with_issues",
   422  			files: map[string]string{
   423  				"etc/systemd/system/docker.service": insecureService,
   424  				"lib/systemd/system/docker.service": `[Service]
   425  ExecStart=/usr/bin/dockerd -H tcp://10.0.0.1:2378`,
   426  			},
   427  			wantIssues: []string{
   428  				expectInsecureSystemdBinding("etc/systemd/system/docker.service", "ExecStart=/usr/bin/dockerd -H tcp://0.0.0.0:2375"),
   429  				expectInsecureSystemdBinding("lib/systemd/system/docker.service", "ExecStart=/usr/bin/dockerd -H tcp://10.0.0.1:2378"),
   430  			},
   431  		},
   432  	}
   433  
   434  	for _, tt := range tests {
   435  		t.Run(tt.name, func(t *testing.T) {
   436  			fsys := fstest.MapFS{}
   437  			for path, content := range tt.files {
   438  				fsys[path] = &fstest.MapFile{Data: []byte(content)}
   439  			}
   440  
   441  			d := &Detector{}
   442  			finding, err := d.ScanFS(t.Context(), fsys, &packageindex.PackageIndex{})
   443  
   444  			if err != nil {
   445  				t.Errorf("ScanFS() returned error: %v", err)
   446  			}
   447  
   448  			var actualIssues []string
   449  			if len(finding.GenericFindings) > 0 {
   450  				// Extract issues from the Extra field
   451  				extra := finding.GenericFindings[0].Target.Extra
   452  				if extra != "" {
   453  					actualIssues = strings.Split(extra, "; ")
   454  				}
   455  			}
   456  
   457  			// Filter to only systemd service related issues for this test
   458  			var systemdIssues []string
   459  			for _, issue := range actualIssues {
   460  				if strings.Contains(issue, "systemd") || strings.Contains(issue, ".service") {
   461  					systemdIssues = append(systemdIssues, issue)
   462  				}
   463  			}
   464  
   465  			if diff := cmp.Diff(tt.wantIssues, systemdIssues); diff != "" {
   466  				t.Errorf("Multiple paths test mismatch (-want +got):\n%s", diff)
   467  			}
   468  		})
   469  	}
   470  }
   471  
   472  func TestScanFS_NoDocker(t *testing.T) {
   473  	// Test with no Docker installation (no socket, no config files)
   474  	fsys := fstest.MapFS{}
   475  
   476  	d := &Detector{}
   477  	finding, err := d.ScanFS(t.Context(), fsys, &packageindex.PackageIndex{})
   478  
   479  	if err != nil {
   480  		t.Errorf("ScanFS() returned error: %v", err)
   481  	}
   482  
   483  	if len(finding.GenericFindings) != 0 {
   484  		t.Errorf("ScanFS() returned findings when no Docker is installed, got: %v", finding)
   485  	}
   486  }
   487  
   488  func TestScanFS_Integration(t *testing.T) {
   489  	tests := []struct {
   490  		name              string
   491  		setupFS           func() fs.FS
   492  		wantFindingCount  int
   493  		wantSeverity      inventory.SeverityEnum
   494  		wantIssuesContain []string
   495  	}{
   496  		{
   497  			name: "socket_with_world-readable_and_insecure_daemon_config",
   498  			setupFS: func() fs.FS {
   499  				stat := &syscall.Stat_t{Uid: 0, Gid: 999}
   500  				fsys := fstest.MapFS{
   501  					"etc/docker/daemon.json": &fstest.MapFile{
   502  						Data: []byte(`{"hosts": ["tcp://0.0.0.0:2375"]}`),
   503  					},
   504  				}
   505  				return &testFS{
   506  					MapFS: fsys,
   507  					sockFile: fakeFile{
   508  						MapFile: &fstest.MapFile{Data: []byte{}},
   509  						info: fakeFileInfo{
   510  							name:    "docker.sock",
   511  							mode:    0664, // world-readable
   512  							modTime: time.Now(),
   513  							sys:     stat,
   514  						},
   515  					},
   516  				}
   517  			},
   518  			wantFindingCount: 1,
   519  			wantSeverity:     inventory.SeverityHigh,
   520  			wantIssuesContain: []string{
   521  				"Docker socket is world-readable",
   522  				"Insecure TCP binding in daemon.json",
   523  			},
   524  		},
   525  		{
   526  			name: "multiple_insecure_systemd_services",
   527  			setupFS: func() fs.FS {
   528  				insecureService := `[Service]
   529  ExecStart=/usr/bin/dockerd -H tcp://0.0.0.0:2375`
   530  				return fstest.MapFS{
   531  					"etc/systemd/system/docker.service": &fstest.MapFile{Data: []byte(insecureService)},
   532  					"lib/systemd/system/docker.service": &fstest.MapFile{Data: []byte(insecureService)},
   533  				}
   534  			},
   535  			wantFindingCount: 1,
   536  			wantSeverity:     inventory.SeverityHigh,
   537  			wantIssuesContain: []string{
   538  				"Insecure TCP binding in \"etc/systemd/system/docker.service\"",
   539  				"Insecure TCP binding in \"lib/systemd/system/docker.service\"",
   540  			},
   541  		},
   542  		{
   543  			name: "comprehensive_security_issues",
   544  			setupFS: func() fs.FS {
   545  				stat := &syscall.Stat_t{Uid: 1000, Gid: 999} // non-root owner
   546  				fsys := fstest.MapFS{
   547  					"etc/docker/daemon.json": &fstest.MapFile{
   548  						Data: []byte(`{"hosts": ["tcp://0.0.0.0:2375", "tcp://127.0.0.1:2376"]}`),
   549  					},
   550  					"etc/systemd/system/docker.service": &fstest.MapFile{
   551  						Data: []byte(`[Service]
   552  ExecStart=/usr/bin/dockerd -H tcp://0.0.0.0:2377`),
   553  					},
   554  				}
   555  				return &testFS{
   556  					MapFS: fsys,
   557  					sockFile: fakeFile{
   558  						MapFile: &fstest.MapFile{Data: []byte{}},
   559  						info: fakeFileInfo{
   560  							name:    "docker.sock",
   561  							mode:    0666, // world-readable and writable
   562  							modTime: time.Now(),
   563  							sys:     stat,
   564  						},
   565  					},
   566  				}
   567  			},
   568  			wantFindingCount: 1,
   569  			wantSeverity:     inventory.SeverityHigh,
   570  			wantIssuesContain: []string{
   571  				"Docker socket is world-readable",
   572  				"Docker socket is world-writable",
   573  				"Docker socket owner is not root",
   574  				"tcp://0.0.0.0:2375",
   575  				"tcp://127.0.0.1:2376",
   576  				"tcp://0.0.0.0:2377",
   577  			},
   578  		},
   579  	}
   580  
   581  	for _, tt := range tests {
   582  		t.Run(tt.name, func(t *testing.T) {
   583  			d := &Detector{}
   584  			finding, err := d.ScanFS(t.Context(), tt.setupFS(), &packageindex.PackageIndex{})
   585  
   586  			if err != nil {
   587  				t.Errorf("ScanFS() returned error: %v", err)
   588  			}
   589  
   590  			if len(finding.GenericFindings) != tt.wantFindingCount {
   591  				t.Errorf("ScanFS() expected %d findings, got %d", tt.wantFindingCount, len(finding.GenericFindings))
   592  			}
   593  
   594  			if tt.wantFindingCount > 0 && len(finding.GenericFindings) > 0 {
   595  				if finding.GenericFindings[0].Adv.Sev != tt.wantSeverity {
   596  					t.Errorf("ScanFS() expected %v severity, got %v", tt.wantSeverity, finding.GenericFindings[0].Adv.Sev)
   597  				}
   598  
   599  				// Check that all expected issue substrings are present in the target extra field
   600  				extra := finding.GenericFindings[0].Target.Extra
   601  				for _, expectedSubstring := range tt.wantIssuesContain {
   602  					if !contains(extra, expectedSubstring) {
   603  						t.Errorf("ScanFS() expected issues to contain %q, but got: %s", expectedSubstring, extra)
   604  					}
   605  				}
   606  			}
   607  		})
   608  	}
   609  }
   610  
   611  // Helper function to check if a string contains a substring
   612  func contains(s, substr string) bool {
   613  	return strings.Contains(s, substr)
   614  }
   615  
   616  func TestDetectorInterface(t *testing.T) {
   617  	d := New()
   618  
   619  	if d.Name() != Name {
   620  		t.Errorf("Name() = %q, want %q", d.Name(), Name)
   621  	}
   622  
   623  	if d.Version() != 0 {
   624  		t.Errorf("Version() = %d, want 0", d.Version())
   625  	}
   626  
   627  	if len(d.RequiredExtractors()) != 0 {
   628  		t.Errorf("RequiredExtractors() = %v, want empty slice", d.RequiredExtractors())
   629  	}
   630  
   631  	reqs := d.Requirements()
   632  	if reqs.OS != plugin.OSUnix {
   633  		t.Errorf("Requirements().OS = %q, want %q", reqs.OS, plugin.OSUnix)
   634  	}
   635  
   636  	// Test DetectedFinding
   637  	finding := d.DetectedFinding()
   638  	if len(finding.GenericFindings) != 1 {
   639  		t.Errorf("DetectedFinding() expected 1 finding, got %d", len(finding.GenericFindings))
   640  	}
   641  
   642  	expectedID := &inventory.AdvisoryID{
   643  		Publisher: "SCALIBR",
   644  		Reference: "docker-socket-exposure",
   645  	}
   646  
   647  	if diff := cmp.Diff(expectedID, finding.GenericFindings[0].Adv.ID); diff != "" {
   648  		t.Errorf("DetectedFinding() ID mismatch (-want +got):\n%s", diff)
   649  	}
   650  }