github.com/quay/claircore@v1.5.28/dpkg/distroless_scanner.go (about) 1 package dpkg 2 3 import ( 4 "bufio" 5 "context" 6 "errors" 7 "fmt" 8 "io" 9 "io/fs" 10 "net/textproto" 11 "path/filepath" 12 "runtime/trace" 13 14 "github.com/quay/zlog" 15 16 "github.com/quay/claircore" 17 "github.com/quay/claircore/indexer" 18 ) 19 20 const ( 21 distrolessName = "dpkg-distroless" 22 distrolessKind = "package" 23 distrolessVersion = "1" 24 ) 25 26 var ( 27 _ indexer.VersionedScanner = (*Scanner)(nil) 28 _ indexer.PackageScanner = (*Scanner)(nil) 29 ) 30 31 // DistrolessScanner implements the scanner.PackageScanner interface. 32 // 33 // This looks for directories that look like dpkg databases and examines the 34 // files it finds there. 35 // 36 // The zero value is ready to use. 37 type DistrolessScanner struct{} 38 39 // Name implements scanner.VersionedScanner. 40 func (ps *DistrolessScanner) Name() string { return distrolessName } 41 42 // Version implements scanner.VersionedScanner. 43 func (ps *DistrolessScanner) Version() string { return distrolessVersion } 44 45 // Kind implements scanner.VersionedScanner. 46 func (ps *DistrolessScanner) Kind() string { return distrolessKind } 47 48 // Scan attempts to find a dpkg database files in the layer and read all 49 // of the installed packages it can find. These files are found in the 50 // dpkg/status.d directory. 51 // 52 // It's expected to return (nil, nil) if there's no dpkg databases in the layer. 53 // 54 // It does not respect any dpkg configuration files. 55 func (ps *DistrolessScanner) Scan(ctx context.Context, layer *claircore.Layer) ([]*claircore.Package, error) { 56 defer trace.StartRegion(ctx, "Scanner.Scan").End() 57 trace.Log(ctx, "layer", layer.Hash.String()) 58 ctx = zlog.ContextWithValues(ctx, 59 "component", "dpkg/DistrolessScanner.Scan", 60 "version", ps.Version(), 61 "layer", layer.Hash.String()) 62 zlog.Debug(ctx).Msg("start") 63 defer zlog.Debug(ctx).Msg("done") 64 65 sys, err := layer.FS() 66 if err != nil { 67 return nil, fmt.Errorf("dpkg-distroless: opening layer failed: %w", err) 68 } 69 70 var pkgs []*claircore.Package 71 walk := func(p string, d fs.DirEntry, err error) error { 72 if err != nil { 73 return err 74 } 75 if d.Name() == "status.d" && d.IsDir() { 76 zlog.Debug(ctx).Str("path", p).Msg("found potential distroless dpkg db directory") 77 dbFiles, err := fs.ReadDir(sys, p) 78 if err != nil { 79 return fmt.Errorf("error reading DB directory: %w", err) 80 } 81 for _, f := range dbFiles { 82 pkgCt := 0 83 fn := filepath.Join(p, f.Name()) 84 ctx = zlog.ContextWithValues(ctx, "database-file", fn) 85 zlog.Debug(ctx).Msg("examining package database") 86 db, err := sys.Open(fn) 87 if err != nil { 88 return fmt.Errorf("reading database files from layer failed: %w", err) 89 } 90 91 // The database is actually an RFC822-like message with "\n\n" 92 // separators, so don't be alarmed by the usage of the "net/textproto" 93 // package here. 94 tp := textproto.NewReader(bufio.NewReader(db)) 95 Restart: 96 hdr, err := tp.ReadMIMEHeader() 97 for ; (err == nil || errors.Is(err, io.EOF)) && len(hdr) > 0; hdr, err = tp.ReadMIMEHeader() { 98 // NB The "Status" header is not considered here. It seems 99 // to not be populated in the "distroless" scheme. 100 name := hdr.Get("Package") 101 v := hdr.Get("Version") 102 p := &claircore.Package{ 103 Name: name, 104 Version: v, 105 Kind: claircore.BINARY, 106 Arch: hdr.Get("Architecture"), 107 PackageDB: fn, 108 } 109 if src := hdr.Get("Source"); src != "" { 110 p.Source = &claircore.Package{ 111 Name: src, 112 Kind: claircore.SOURCE, 113 // Right now, this is an assumption that discovered source 114 // packages relate to their binary versions. We see this in 115 // Debian. 116 Version: v, 117 PackageDB: fn, 118 } 119 } 120 pkgCt++ 121 pkgs = append(pkgs, p) 122 } 123 switch { 124 case errors.Is(err, io.EOF): 125 default: 126 if _, ok := err.(textproto.ProtocolError); ok { 127 zlog.Warn(ctx).Err(err).Msg("unable to read DB entry") 128 goto Restart 129 } 130 zlog.Warn(ctx).Err(err).Msg("error reading DB file") 131 } 132 zlog.Debug(ctx). 133 Int("count", pkgCt). 134 Msg("found packages") 135 } 136 } 137 return nil 138 } 139 140 if err := fs.WalkDir(sys, ".", walk); err != nil { 141 return nil, err 142 } 143 return pkgs, nil 144 }