github.com/google/osv-scalibr@v0.4.1/plugin/plugin.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 plugin collects the common code used by extractor and detector plugins. 16 package plugin 17 18 import ( 19 "fmt" 20 "slices" 21 "strings" 22 23 cpb "github.com/google/osv-scalibr/binary/proto/config_go_proto" 24 ) 25 26 // OS is the OS the scanner is running on, or a specific OS type a Plugin needs to be run on. 27 type OS int 28 29 // OS values 30 const ( 31 // OSAny is used only when specifying Plugin requirements. 32 // Specifies that the plugin expects to be compatible with any OS, 33 // and so should be fine to run even if OS is unknown. 34 OSAny OS = iota 35 OSLinux OS = iota 36 OSWindows OS = iota 37 OSMac OS = iota 38 // OSUnix is used only when specifying Plugin requirements. 39 // Specifies that the plugin needs to be run either on Linux or Mac. 40 OSUnix OS = iota 41 ) 42 43 // OSUnknown is only used when specifying Capabilities. 44 // Specifies that the OS is not known and so only 45 // plugins that require OSAny should be run. 46 const OSUnknown = OSAny 47 48 // Network is the network access of the scanner or the network 49 // requirements of a plugin. 50 type Network int 51 52 // Network values 53 const ( 54 // NetworkAny is used only when specifying Plugin requirements. Specifies 55 // that the plugin doesn't care whether the scanner has network access or not. 56 NetworkAny Network = iota 57 NetworkOffline Network = iota 58 NetworkOnline Network = iota 59 ) 60 61 // Capabilities lists capabilities that the scanning environment provides for the plugins. 62 // A plugin can't be enabled if it has more requirements than what the scanning environment provides. 63 type Capabilities struct { 64 // A specific OS type a Plugin needs to be run on. 65 OS OS 66 // Whether network access is provided. 67 Network Network 68 // Whether the scanned artifacts can be access through direct filesystem calls. 69 // True on hosts where the scan target is mounted onto the host's filesystem directly. 70 // In these cases the plugin can open direct file paths with e.g. os.Open(path). 71 // False if the artifact is not on the host but accessed through an abstract FS interface 72 // (e.g. scanning a remote container image). In these cases the plugin must use the FS interface 73 // to access the filesystem. 74 DirectFS bool 75 // Whether the scanner is scanning the real running system it's on. Examples where this is not the case: 76 // * We're scanning a virtual filesystem unrelated to the host where SCALIBR is running. 77 // * We're scanning a real filesystem of e.g. a container image that's mounted somewhere on disk. 78 RunningSystem bool 79 // Whether the filesystem extractor plugin requires scanning directories in addition to files. 80 // TODO(b/400910349): This doesn't quite fit into Capabilities so this should be moved into a 81 // separate Filesystem Extractor specific function. 82 ExtractFromDirs bool 83 } 84 85 // Plugin is the part of the plugin interface that's shared between extractors and detectors. 86 type Plugin interface { 87 // A unique name used to identify this plugin. 88 Name() string 89 // Plugin version, should get bumped whenever major changes are made. 90 Version() int 91 // Requirements about the scanning environment, e.g. "needs to have network access". 92 Requirements() *Capabilities 93 } 94 95 // LINT.IfChange 96 97 // Status contains the status and version of the plugins that ran. 98 type Status struct { 99 Name string 100 Version int 101 Status *ScanStatus 102 } 103 104 // ScanStatus is the status of a scan run. In case the scan fails, FailureReason contains details. 105 type ScanStatus struct { 106 Status ScanStatusEnum 107 FailureReason string 108 FileErrors []*FileError 109 } 110 111 // FileError contains the errors that occurred while scanning a specific file. 112 type FileError struct { 113 FilePath string 114 ErrorMessage string 115 } 116 117 // ScanStatusEnum is the enum for the scan status. 118 type ScanStatusEnum int 119 120 // ScanStatusEnum values. 121 const ( 122 ScanStatusUnspecified ScanStatusEnum = iota 123 ScanStatusSucceeded 124 ScanStatusPartiallySucceeded 125 ScanStatusFailed 126 ) 127 128 // LINT.ThenChange(/binary/proto/scan_result.proto) 129 130 // ValidateRequirements checks that the specified scanning capabilities satisfy 131 // the requirements of a given plugin. 132 func ValidateRequirements(p Plugin, capabs *Capabilities) error { 133 if capabs == nil { 134 return nil 135 } 136 errs := []string{} 137 if p.Requirements().OS == OSUnix { 138 if capabs.OS != OSLinux && capabs.OS != OSMac { 139 errs = append(errs, "needs to run on Unix system but scan environment is non-Unix") 140 } 141 } else if p.Requirements().OS != OSAny && p.Requirements().OS != capabs.OS { 142 errs = append(errs, "needs to run on a different OS than that of the scan environment") 143 } 144 if p.Requirements().Network != NetworkAny && p.Requirements().Network != capabs.Network { 145 if capabs.Network == NetworkOffline { 146 errs = append(errs, "needs network access but scan environment doesn't provide it") 147 } else { 148 errs = append(errs, "should only run offline but the scan environment provides network access") 149 } 150 } 151 if p.Requirements().DirectFS && !capabs.DirectFS { 152 errs = append(errs, "needs direct filesystem access but scan environment doesn't provide it") 153 } 154 if p.Requirements().RunningSystem && !capabs.RunningSystem { 155 errs = append(errs, "scanner isn't scanning the host it's run from directly") 156 } 157 if len(errs) == 0 { 158 return nil 159 } 160 return fmt.Errorf("plugin %s can't be enabled: %s", p.Name(), strings.Join(errs, ", ")) 161 } 162 163 // FilterByCapabilities returns all plugins from the given list that can run 164 // under the specified capabilities (OS, direct filesystem access, network 165 // access, etc.) of the scanning environment. 166 func FilterByCapabilities(pls []Plugin, capabs *Capabilities) []Plugin { 167 result := []Plugin{} 168 for _, pl := range pls { 169 if err := ValidateRequirements(pl, capabs); err == nil { 170 result = append(result, pl) 171 } 172 } 173 return result 174 } 175 176 // FindConfig finds a plugin-specific config in the oveall config proto 177 // using the specified getter function. 178 func FindConfig[T any](cfg *cpb.PluginConfig, getter func(c *cpb.PluginSpecificConfig) *T) *T { 179 for _, specific := range cfg.PluginSpecific { 180 got := getter(specific) 181 if got != nil { 182 return got 183 } 184 } 185 return nil 186 } 187 188 // StatusFromErr returns a successful or failed plugin scan status for a given plugin based on an error. 189 func StatusFromErr(p Plugin, partial bool, overallErr error, fileErrors []*FileError) *Status { 190 status := &ScanStatus{} 191 if overallErr == nil { 192 status.Status = ScanStatusSucceeded 193 } else { 194 if partial { 195 status.Status = ScanStatusPartiallySucceeded 196 } else { 197 status.Status = ScanStatusFailed 198 } 199 status.FileErrors = fileErrors 200 status.FailureReason = overallErr.Error() 201 } 202 return &Status{ 203 Name: p.Name(), 204 Version: p.Version(), 205 Status: status, 206 } 207 } 208 209 // OverallErrFromFileErrs returns an error to set as the scan status overall failure 210 // reason based on the plugin's per-file errors. 211 func OverallErrFromFileErrs(fileErrors []*FileError) error { 212 if len(fileErrors) == 0 { 213 return nil 214 } 215 return fmt.Errorf("encountered %d error(s) while running plugin; check file-specific errors for details", len(fileErrors)) 216 } 217 218 // DedupeStatuses combines the status of multiple instances of the same plugins 219 // in a list, making sure there's only one entry per plugin. 220 func DedupeStatuses(statuses []*Status) []*Status { 221 // Plugin name to status map 222 resultMap := map[string]*Status{} 223 224 for _, s := range statuses { 225 if old, ok := resultMap[s.Name]; ok { 226 resultMap[s.Name] = mergeStatus(old, s) 227 } else { 228 resultMap[s.Name] = s 229 } 230 } 231 232 result := make([]*Status, 0, len(resultMap)) 233 for _, v := range resultMap { 234 result = append(result, v) 235 } 236 return result 237 } 238 239 func mergeStatus(s1 *Status, s2 *Status) *Status { 240 result := &Status{ 241 Name: s1.Name, 242 Version: s1.Version, 243 Status: &ScanStatus{ 244 Status: mergeScanStatus(s1.Status.Status, s2.Status.Status), 245 }, 246 } 247 248 if len(s1.Status.FailureReason) > 0 && len(s2.Status.FailureReason) > 0 { 249 result.Status.FailureReason = s1.Status.FailureReason + "\n" + s2.Status.FailureReason 250 } else if len(s1.Status.FailureReason) > 0 { 251 result.Status.FailureReason = s1.Status.FailureReason 252 } else { 253 result.Status.FailureReason = s2.Status.FailureReason 254 } 255 256 result.Status.FileErrors = slices.Concat(s1.Status.FileErrors, s2.Status.FileErrors) 257 if len(result.Status.FileErrors) > 0 { 258 // Instead of concating two generic "check file errors" message we create a new one. 259 result.Status.FailureReason = OverallErrFromFileErrs(result.Status.FileErrors).Error() 260 } 261 262 return result 263 } 264 265 func mergeScanStatus(e1 ScanStatusEnum, e2 ScanStatusEnum) ScanStatusEnum { 266 // Failures take precedence over successes. 267 if e1 == ScanStatusFailed || e2 == ScanStatusFailed { 268 return ScanStatusFailed 269 } 270 if e1 == ScanStatusPartiallySucceeded || e2 == ScanStatusPartiallySucceeded { 271 return ScanStatusPartiallySucceeded 272 } 273 if e1 == ScanStatusSucceeded || e2 == ScanStatusSucceeded { 274 return ScanStatusSucceeded 275 } 276 return ScanStatusUnspecified 277 } 278 279 // String returns a string representation of the scan status. 280 func (s *ScanStatus) String() string { 281 switch s.Status { 282 case ScanStatusSucceeded: 283 return "SUCCEEDED" 284 case ScanStatusPartiallySucceeded: 285 return "PARTIALLY_SUCCEEDED" 286 case ScanStatusFailed: 287 return "FAILED: " + s.FailureReason 288 case ScanStatusUnspecified: 289 fallthrough 290 default: 291 return "UNSPECIFIED" 292 } 293 }