github.com/quay/claircore@v1.5.28/rhel/rhcc/scanner.go (about) 1 package rhcc 2 3 import ( 4 "context" 5 "encoding/json" 6 "errors" 7 "fmt" 8 "io/fs" 9 "net/http" 10 "os" 11 "strings" 12 "time" 13 14 "github.com/quay/zlog" 15 16 "github.com/quay/claircore" 17 "github.com/quay/claircore/indexer" 18 "github.com/quay/claircore/internal/zreader" 19 "github.com/quay/claircore/pkg/rhctag" 20 "github.com/quay/claircore/rhel/dockerfile" 21 "github.com/quay/claircore/rhel/internal/common" 22 ) 23 24 var ( 25 _ indexer.PackageScanner = (*scanner)(nil) 26 _ indexer.RPCScanner = (*scanner)(nil) 27 ) 28 29 type scanner struct { 30 upd *common.Updater 31 client *http.Client 32 cfg ScannerConfig 33 } 34 35 // ScannerConfig is the configuration for the package scanner. 36 // 37 // The interaction between the "URL" and "File" members is the same as described 38 // in the [github.com/quay/claircore/rhel.RepositoryScannerConfig] documentation. 39 // 40 // By convention, it's in a "rhel_containerscanner" key. 41 type ScannerConfig struct { 42 // Name2ReposMappingURL is a URL where a mapping file can be fetched. 43 // 44 // See also [DefaultName2ReposMappingURL] 45 Name2ReposMappingURL string `json:"name2repos_mapping_url" yaml:"name2repos_mapping_url"` 46 // Name2ReposMappingFile is a path to a local mapping file. 47 Name2ReposMappingFile string `json:"name2repos_mapping_file" yaml:"name2repos_mapping_file"` 48 // Timeout is a timeout for all network calls made to update the mapping 49 // file. 50 // 51 // The default is 10 seconds. 52 Timeout time.Duration `json:"timeout" yaml:"timeout"` 53 } 54 55 // DefaultName2ReposMappingURL is the default URL with a mapping file provided by Red 56 // Hat. 57 // 58 //doc:url indexer 59 const DefaultName2ReposMappingURL = "https://access.redhat.com/security/data/metrics/container-name-repos-map.json" 60 61 // Configure implements [indexer.RPCScanner]. 62 func (s *scanner) Configure(ctx context.Context, f indexer.ConfigDeserializer, c *http.Client) error { 63 ctx = zlog.ContextWithValues(ctx, "component", "rhel/rhcc/scanner.Configure") 64 s.client = c 65 if err := f(&s.cfg); err != nil { 66 return err 67 } 68 69 if s.cfg.Timeout == 0 { 70 s.cfg.Timeout = 10 * time.Second 71 } 72 var mf *mappingFile 73 switch { 74 case s.cfg.Name2ReposMappingURL == "" && s.cfg.Name2ReposMappingFile == "": 75 // defaults 76 s.cfg.Name2ReposMappingURL = DefaultName2ReposMappingURL 77 case s.cfg.Name2ReposMappingURL != "" && s.cfg.Name2ReposMappingFile == "": 78 // remote only 79 case s.cfg.Name2ReposMappingFile != "": 80 // local only 81 f, err := os.Open(s.cfg.Name2ReposMappingFile) 82 if err != nil { 83 return err 84 } 85 defer f.Close() 86 z, err := zreader.Reader(f) 87 if err != nil { 88 return err 89 } 90 defer z.Close() 91 mf = &mappingFile{} 92 if err := json.NewDecoder(z).Decode(mf); err != nil { 93 return err 94 } 95 } 96 s.upd = common.NewUpdater(s.cfg.Name2ReposMappingURL, mf) 97 tctx, done := context.WithTimeout(ctx, s.cfg.Timeout) 98 defer done() 99 s.upd.Get(tctx, c) 100 101 return nil 102 } 103 104 // Name implements [indexer.VersionedScanner]. 105 func (s *scanner) Name() string { return "rhel_containerscanner" } 106 107 // Version implements [indexer.VersionedScanner]. 108 func (s *scanner) Version() string { return "1" } 109 110 // Kind implements [indexer.VersionedScanner]. 111 func (s *scanner) Kind() string { return "package" } 112 113 // Scan performs a package scan on the given layer and returns all 114 // the RHEL container identifying metadata 115 116 // Scan implements [indexer.PackageScanner]. 117 func (s *scanner) Scan(ctx context.Context, l *claircore.Layer) ([]*claircore.Package, error) { 118 ctx = zlog.ContextWithValues(ctx, "component", "rhel/rhcc/scanner.Scan") 119 const ( 120 compLabel = `com.redhat.component` 121 nameLabel = `name` 122 archLabel = `architecture` 123 ) 124 if err := ctx.Err(); err != nil { 125 return nil, err 126 } 127 sys, err := l.FS() 128 if err != nil { 129 return nil, fmt.Errorf("rhcc: unable to open layer: %w", err) 130 } 131 132 // add source package from component label 133 labels, p, err := findLabels(ctx, sys) 134 switch { 135 case errors.Is(err, nil): 136 case errors.Is(err, errNotFound): 137 return nil, nil 138 default: 139 return nil, err 140 } 141 142 vr := getVR(p) 143 rhctagVersion, err := rhctag.Parse(vr) 144 if err != nil { 145 // This can happen for containers which don't use semantic versioning, 146 // such as UBI. 147 return nil, nil 148 } 149 var buildName, arch, name string 150 for _, chk := range []struct { 151 Found *string 152 Want string 153 }{ 154 {&buildName, compLabel}, 155 {&arch, archLabel}, 156 {&name, nameLabel}, 157 } { 158 var ok bool 159 (*chk.Found), ok = labels[chk.Want] 160 if !ok { 161 zlog.Info(ctx).Str("label", chk.Want).Msg("expected label not found in dockerfile") 162 return nil, nil 163 } 164 } 165 166 minorRange := rhctagVersion.MinorStart() 167 src := claircore.Package{ 168 Kind: claircore.SOURCE, 169 Name: buildName, 170 Version: vr, 171 NormalizedVersion: minorRange.Version(true), 172 PackageDB: p, 173 Arch: arch, 174 RepositoryHint: `rhcc`, 175 } 176 pkgs := []*claircore.Package{&src} 177 178 tctx, done := context.WithTimeout(ctx, s.cfg.Timeout) 179 defer done() 180 vi, err := s.upd.Get(tctx, s.client) 181 if err != nil && vi == nil { 182 return nil, err 183 } 184 v, ok := vi.(*mappingFile) 185 if !ok || v == nil { 186 return nil, fmt.Errorf("rhcc: unable to create a mappingFile object") 187 } 188 repos, ok := v.Data[name] 189 if ok { 190 zlog.Debug(ctx).Str("name", name). 191 Msg("name present in mapping file") 192 } else { 193 // Didn't find external_repos in mapping, use name label as package 194 // name. 195 repos = []string{name} 196 } 197 for _, name := range repos { 198 // Add each external repo as a binary package. The same container image 199 // can ship to multiple repos eg. `"toolbox-container": 200 // ["rhel8/toolbox", "ubi8/toolbox"]`. Therefore, we want a binary 201 // package entry for each. 202 pkgs = append(pkgs, &claircore.Package{ 203 Kind: claircore.BINARY, 204 Name: name, 205 Version: vr, 206 NormalizedVersion: minorRange.Version(true), 207 Source: &src, 208 PackageDB: p, 209 Arch: arch, 210 RepositoryHint: `rhcc`, 211 }) 212 } 213 return pkgs, nil 214 } 215 216 // MappingFile is a struct for mapping file between container NAME label and 217 // container registry repository location. 218 type mappingFile struct { 219 Data map[string][]string `json:"data"` 220 } 221 222 func findLabels(ctx context.Context, sys fs.FS) (map[string]string, string, error) { 223 ms, err := fs.Glob(sys, "root/buildinfo/Dockerfile-*") 224 if err != nil { // Can only return ErrBadPattern. 225 panic("progammer error: " + err.Error()) 226 } 227 if len(ms) == 0 { 228 return nil, "", errNotFound 229 } 230 zlog.Debug(ctx). 231 Strs("paths", ms). 232 Msg("found possible buildinfo Dockerfile(s)") 233 var p string 234 for _, m := range ms { 235 if strings.Count(m, "-") > 1 { 236 p = m 237 break 238 } 239 } 240 if p == "" { 241 return nil, "", errNotFound 242 } 243 zlog.Info(ctx). 244 Str("path", p). 245 Msg("found buildinfo Dockerfile") 246 f, err := sys.Open(p) 247 if err != nil { 248 return nil, "", err 249 } 250 defer f.Close() 251 labels, err := dockerfile.GetLabels(ctx, f) 252 if err != nil { 253 return nil, "", err 254 } 255 return labels, p, nil 256 } 257 258 var errNotFound = errors.New("not found") 259 260 // GetVR extracts the version-release string from the provided string ending in 261 // an NVR. 262 // 263 // Panics if passed malformed input. 264 func getVR(nvr string) string { 265 if strings.Count(nvr, "-") < 2 { 266 panic("programmer error: not an nvr string: " + nvr) 267 } 268 i := strings.LastIndexByte(nvr, '-') 269 i = strings.LastIndexByte(nvr[:i], '-') 270 return nvr[i+1:] 271 } 272 273 type reposcanner struct{} 274 275 var _ indexer.RepositoryScanner = (*reposcanner)(nil) 276 277 // Name implements [indexer.VersionedScanner]. 278 func (s *reposcanner) Name() string { return "rhel_containerscanner" } 279 280 // Version implements [indexer.VersionedScanner]. 281 func (s *reposcanner) Version() string { return "1" } 282 283 // Kind implements [indexer.VersionedScanner]. 284 func (s *reposcanner) Kind() string { return "repository" } 285 286 // Scan implements [indexer.RepositoryScanner]. 287 func (s *reposcanner) Scan(ctx context.Context, l *claircore.Layer) ([]*claircore.Repository, error) { 288 ctx = zlog.ContextWithValues(ctx, "component", "rhel/rhcc/reposcanner.Scan") 289 sys, err := l.FS() 290 if err != nil { 291 return nil, fmt.Errorf("rhcc: unable to open layer: %w", err) 292 } 293 ms, err := fs.Glob(sys, "root/buildinfo/Dockerfile-*") 294 if err != nil { // Can only return ErrBadPattern. 295 panic("progammer error") 296 } 297 if len(ms) == 0 { 298 return nil, nil 299 } 300 zlog.Debug(ctx). 301 Msg("found buildinfo Dockerfile") 302 return []*claircore.Repository{&goldRepo}, nil 303 }