github.com/quay/claircore@v1.5.28/ruby/packagescanner.go (about) 1 // Package ruby contains components for interrogating ruby packages in 2 // container layers. 3 package ruby 4 5 import ( 6 "bufio" 7 "context" 8 "fmt" 9 "io/fs" 10 "path/filepath" 11 "regexp" 12 "runtime/trace" 13 "strings" 14 15 "github.com/quay/zlog" 16 17 "github.com/quay/claircore" 18 "github.com/quay/claircore/indexer" 19 ) 20 21 var ( 22 gemspecPath = regexp.MustCompile(`.*/specifications/.+\.gemspec`) 23 24 // Example gemspec: 25 // 26 // Gem::Specification.new do |s| 27 // s.name = 'example' 28 // s.version = '0.1.0' 29 // s.licenses = ['MIT'] 30 // s.summary = "This is an example!" 31 // s.description = "Much longer explanation of the example!" 32 // s.authors = ["Ruby Coder"] 33 // s.email = 'rubycoder@example.com' 34 // s.files = ["lib/example.rb"] 35 // s.homepage = 'https://rubygems.org/gems/example' 36 // s.metadata = { "source_code_uri" => "https://github.com/example/example" } 37 // end 38 nameLine = regexp.MustCompile(`^\S+\.\s*name\s*=\s*(?P<name>\S+)$`) 39 versionLine = regexp.MustCompile(`^\S+\.\s*version\s*=\s*(?P<version>\S+)$`) 40 ) 41 42 const ( 43 nameIdx = 1 44 versionIdx = 1 45 46 repository = "rubygems" 47 ) 48 49 var ( 50 _ indexer.VersionedScanner = (*Scanner)(nil) 51 _ indexer.PackageScanner = (*Scanner)(nil) 52 _ indexer.DefaultRepoScanner = (*Scanner)(nil) 53 54 Repository = claircore.Repository{ 55 Name: repository, 56 URI: "https://rubygems.org/gems/", 57 } 58 ) 59 60 // Scanner implements the scanner.PackageScanner interface. 61 // 62 // It looks for files that seem like gems, and looks at the 63 // metadata recorded there. This type attempts to follow the specs documented 64 // here: https://guides.rubygems.org/specification-reference/. 65 // 66 // The zero value is ready to use. 67 type Scanner struct{} 68 69 // Name implements scanner.VersionedScanner. 70 func (*Scanner) Name() string { return "ruby" } 71 72 // Version implements scanner.VersionedScanner. 73 func (*Scanner) Version() string { return "2" } 74 75 // Kind implements scanner.VersionedScanner. 76 func (*Scanner) Kind() string { return "package" } 77 78 // Scan attempts to find gems and record the package information there. 79 // 80 // A return of (nil, nil) is expected if there's nothing found. 81 func (ps *Scanner) Scan(ctx context.Context, layer *claircore.Layer) ([]*claircore.Package, error) { 82 defer trace.StartRegion(ctx, "Scanner.Scan").End() 83 trace.Log(ctx, "layer", layer.Hash.String()) 84 ctx = zlog.ContextWithValues(ctx, 85 "component", "ruby/Scanner.Scan", 86 "version", ps.Version(), 87 "layer", layer.Hash.String()) 88 zlog.Debug(ctx).Msg("start") 89 defer zlog.Debug(ctx).Msg("done") 90 if err := ctx.Err(); err != nil { 91 return nil, err 92 } 93 94 sys, err := layer.FS() 95 if err != nil { 96 return nil, fmt.Errorf("ruby: unable to open layer: %w", err) 97 } 98 99 gs, err := gems(ctx, sys) 100 if err != nil { 101 return nil, fmt.Errorf("ruby: failed to find packages: %w", err) 102 } 103 104 var ret []*claircore.Package 105 for _, g := range gs { 106 f, err := sys.Open(g) 107 if err != nil { 108 return nil, fmt.Errorf("ruby: unable to open file: %w", err) 109 } 110 111 var name, version string 112 113 scanner := bufio.NewScanner(f) 114 for scanner.Scan() { 115 line := strings.TrimSpace(scanner.Text()) 116 if matches := nameLine.FindStringSubmatch(line); matches != nil { 117 name = trim(matches[nameIdx]) 118 } 119 if matches := versionLine.FindStringSubmatch(line); matches != nil { 120 version = trim(matches[versionIdx]) 121 } 122 } 123 if err := scanner.Err(); err != nil { 124 zlog.Warn(ctx). 125 Err(err). 126 Str("path", g). 127 Msg("unable to read metadata, skipping") 128 continue 129 } 130 131 if name == "" || version == "" { 132 zlog.Warn(ctx). 133 Str("path", g). 134 Msg("couldn't parse name or version, skipping") 135 continue 136 } 137 138 ret = append(ret, &claircore.Package{ 139 Name: name, 140 Version: version, 141 Kind: claircore.BINARY, 142 PackageDB: "ruby:" + g, 143 Filepath: g, 144 RepositoryHint: repository, 145 }) 146 } 147 148 return ret, nil 149 } 150 151 // DefaultRepository implements [indexer.DefaultRepoScanner]. 152 func (Scanner) DefaultRepository(ctx context.Context) *claircore.Repository { 153 return &Repository 154 } 155 156 func trim(s string) string { 157 s = strings.TrimSpace(s) 158 s = strings.TrimSuffix(s, `.freeze`) 159 return strings.Trim(s, `'"`) 160 } 161 162 func gems(ctx context.Context, sys fs.FS) (out []string, err error) { 163 return out, fs.WalkDir(sys, ".", func(p string, d fs.DirEntry, err error) error { 164 ev := zlog.Debug(ctx). 165 Str("file", p) 166 var success bool 167 defer func() { 168 if !success { 169 ev.Discard().Send() 170 } 171 }() 172 switch { 173 case err != nil: 174 return err 175 case !d.Type().IsRegular(): 176 // Should we chase symlinks with the correct name? 177 return nil 178 case strings.HasPrefix(filepath.Base(p), ".wh."): 179 return nil 180 case gemspecPath.MatchString(p): 181 ev = ev.Str("kind", "gem") 182 default: 183 return nil 184 } 185 ev.Msg("found package") 186 success = true 187 out = append(out, p) 188 return nil 189 }) 190 }