github.com/google/osv-scalibr@v0.4.1/annotator/noexecutable/dpkg/dpkg.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 dpkg implements an annotator for DPKG packages that don't contain any executables. 16 package dpkg 17 18 import ( 19 "bufio" 20 "context" 21 "errors" 22 "fmt" 23 "io/fs" 24 "path/filepath" 25 "strings" 26 27 "github.com/google/osv-scalibr/annotator" 28 "github.com/google/osv-scalibr/extractor/filesystem" 29 dpkgmetadata "github.com/google/osv-scalibr/extractor/filesystem/os/dpkg/metadata" 30 "github.com/google/osv-scalibr/extractor/filesystem/simplefileapi" 31 "github.com/google/osv-scalibr/inventory" 32 "github.com/google/osv-scalibr/inventory/vex" 33 "github.com/google/osv-scalibr/plugin" 34 ) 35 36 const ( 37 // Name of the Annotator. 38 Name = "vex/no-executable/dpkg" 39 dpkgInfoDirPath = "var/lib/dpkg/info" 40 ) 41 42 // Annotator adds annotations for DPKG packages that don't contain any executables. 43 type Annotator struct{} 44 45 // New returns a new Annotator. 46 func New() annotator.Annotator { return &Annotator{} } 47 48 // Name of the annotator. 49 func (Annotator) Name() string { return Name } 50 51 // Version of the annotator. 52 func (Annotator) Version() int { return 0 } 53 54 // Requirements of the annotator. 55 func (Annotator) Requirements() *plugin.Capabilities { 56 return &plugin.Capabilities{OS: plugin.OSLinux} 57 } 58 59 // Annotate adds annotations for DPKG packages that don't contain any executables. 60 func (a *Annotator) Annotate(ctx context.Context, input *annotator.ScanInput, results *inventory.Inventory) error { 61 errs := []error{} 62 63 // early exit if the dpkgInfoDirPath does not exists 64 if _, err := input.ScanRoot.FS.Stat(dpkgInfoDirPath); err != nil { 65 if errors.Is(err, fs.ErrNotExist) { 66 // Nothing to annotate if we're not running on a DPKG based distro. 67 return nil 68 } 69 return fmt.Errorf("folder %q does not exists", dpkgInfoDirPath) 70 } 71 72 for _, pkg := range results.Packages { 73 // Return if canceled or exceeding deadline. 74 if err := ctx.Err(); err != nil { 75 return fmt.Errorf("%s halted at %q because of context error: %w", a.Name(), input.ScanRoot.Path, err) 76 } 77 78 metadata, ok := pkg.Metadata.(dpkgmetadata.Metadata) 79 if !ok { 80 continue 81 } 82 83 // check if the pkg files contains at least one executable file 84 containsExecutable, err := pkgContainsExecutable(ctx, input, pkg.Name, metadata.Architecture) 85 // if the pkg contains an executable or there was an error checking the files, skip the pkg 86 // here, false positives are better then false negatives 87 if containsExecutable || err != nil { 88 errs = append(errs, err) 89 continue 90 } 91 92 // if the pkg does not contain any executable then add an annotation 93 pkg.ExploitabilitySignals = append(pkg.ExploitabilitySignals, &vex.PackageExploitabilitySignal{ 94 Plugin: Name, 95 Justification: vex.ComponentNotPresent, 96 MatchesAllVulns: true, 97 }) 98 } 99 return errors.Join(errs...) 100 } 101 102 // pkgContainsExecutable opens a pkg related .list file and check if at least one of the listed file IsInterestingExecutable 103 func pkgContainsExecutable(ctx context.Context, input *annotator.ScanInput, pkgName, pkgArchitecture string) (bool, error) { 104 listF, err := getListFile(input, pkgName, pkgArchitecture) 105 if err != nil { 106 return false, err 107 } 108 defer listF.Close() 109 110 s := bufio.NewScanner(listF) 111 errs := []error{} 112 for s.Scan() { 113 if err := ctx.Err(); err != nil { 114 return false, fmt.Errorf("%s halted at %q because of context error: %w", pkgName, input.ScanRoot.Path, err) 115 } 116 117 // Remove leading '/' since SCALIBR fs paths don't include that. 118 filePath := strings.TrimPrefix(s.Text(), "/") 119 120 info, err := input.ScanRoot.FS.Stat(filePath) 121 if err != nil { 122 errs = append(errs, err) 123 continue 124 } 125 126 if info.IsDir() { 127 continue 128 } 129 130 api := simplefileapi.New(filePath, info) 131 if filesystem.IsInterestingExecutable(api) { 132 return true, errors.Join(errs...) 133 } 134 } 135 return false, errors.Join(errs...) 136 } 137 138 // getListFile given a pkgName and pkgArchitecture returns a DPKG .list file containing all the installed files 139 func getListFile(input *annotator.ScanInput, pkgName string, pkgArchitecture string) (fs.File, error) { 140 options := []string{ 141 pkgName, 142 pkgName + ":" + pkgArchitecture, 143 } 144 145 for _, opt := range options { 146 listPath := filepath.Join(dpkgInfoDirPath, opt+".list") 147 148 f, err := input.ScanRoot.FS.Open(listPath) 149 if err != nil { 150 continue 151 } 152 return f, nil 153 } 154 155 return nil, fmt.Errorf("no list file detected for %q", pkgName) 156 }