github.com/quay/claircore@v1.5.28/gobin/gobin.go (about) 1 // Package gobin implements a package scanner that pulls go runtime and 2 // dependency information out of a compiled executable. 3 // 4 // # Main module versioning 5 // 6 // The go toolchain currently only fills in version information for modules 7 // obtained as a module. Most go executables are built from source checkouts, 8 // meaning they are not in module form. See [issue 50603] for details on why and 9 // what's being explored to provide this information. Accordingly, claircore 10 // cannot report advisories for main modules. 11 // 12 // [issue 50603]: https://golang.org/issues/50603 13 package gobin 14 15 import ( 16 "bytes" 17 "context" 18 "encoding/binary" 19 "errors" 20 "fmt" 21 "io" 22 "io/fs" 23 "os" 24 "runtime/trace" 25 "sync" 26 27 "github.com/quay/zlog" 28 29 "github.com/quay/claircore" 30 "github.com/quay/claircore/indexer" 31 ) 32 33 // Detector detects go binaries and reports the packages used to build them. 34 type Detector struct{} 35 36 const ( 37 detectorName = `gobin` 38 detectorVersion = `5` 39 detectorKind = `package` 40 ) 41 42 var ( 43 _ indexer.PackageScanner = Detector{} 44 _ indexer.DefaultRepoScanner = Detector{} 45 46 Repository = claircore.Repository{ 47 Name: "go", 48 URI: "https://pkg.go.dev/", 49 } 50 ) 51 52 // Name implements [indexer.PackageScanner]. 53 func (Detector) Name() string { return detectorName } 54 55 // Version implements [indexer.PackageScanner]. 56 func (Detector) Version() string { return detectorVersion } 57 58 // Kind implements [indexer.PackageScanner]. 59 func (Detector) Kind() string { return detectorKind } 60 61 // Scan implements [indexer.PackageScanner]. 62 func (Detector) Scan(ctx context.Context, l *claircore.Layer) ([]*claircore.Package, error) { 63 const peekSz = 18 64 if err := ctx.Err(); err != nil { 65 return nil, err 66 } 67 defer trace.StartRegion(ctx, "Scanner.Scan").End() 68 trace.Log(ctx, "layer", l.Hash.String()) 69 ctx = zlog.ContextWithValues(ctx, 70 "component", "gobin/Detector.Scan", 71 "version", detectorVersion, 72 "layer", l.Hash.String()) 73 zlog.Debug(ctx).Msg("start") 74 defer zlog.Debug(ctx).Msg("done") 75 76 sys, err := l.FS() 77 if err != nil { 78 return nil, fmt.Errorf("gobin: unable to open layer: %w", err) 79 } 80 81 var out []*claircore.Package 82 83 peek := make([]byte, peekSz) 84 // Spooling support. 85 // 86 // Only create a single spool file per call, re-use for every binary. 87 var spool spoolfile 88 walk := func(p string, d fs.DirEntry, err error) error { 89 ctx := zlog.ContextWithValues(ctx, "path", d.Name()) 90 switch { 91 case err != nil: 92 return err 93 case d.IsDir(): 94 return nil 95 case ctx.Err() != nil: 96 return ctx.Err() 97 } 98 fi, err := d.Info() 99 if err != nil { 100 return err 101 } 102 m := fi.Mode() 103 switch { 104 case !m.IsRegular(): 105 return nil 106 case m.Perm()&0o555 == 0: 107 // Not executable 108 return nil 109 } 110 f, err := sys.Open(p) 111 if err != nil { 112 // TODO(crozzy): Remove log line once controller is in a 113 // position to log all the context when receiving an error. 114 zlog.Warn(ctx).Msg("unable to open file") 115 return fmt.Errorf("gobin: unable to open %q: %w", p, err) 116 } 117 defer f.Close() 118 119 _, err = io.ReadFull(f, peek) 120 switch { 121 case errors.Is(err, nil): 122 case errors.Is(err, io.EOF), errors.Is(err, io.ErrUnexpectedEOF): 123 // Valid error with empty, or tiny files. 124 return nil 125 default: 126 // TODO(crozzy): Remove log line once controller is in a 127 // position to log all the context when receiving an error. 128 zlog.Warn(ctx).Msg("unable to read file") 129 return fmt.Errorf("gobin: unable to read %q: %w", p, err) 130 } 131 132 isELF := bytes.HasPrefix(peek, []byte("\x7fELF")) 133 isPE := bytes.HasPrefix(peek, []byte("MZ")) 134 if !isELF && !isPE { // Do OSX containers exist? 135 // not an ELF or PE binary 136 return nil 137 } 138 if isELF { 139 // Using hex constants because the nice table on Wikipedia uses 140 // them. 141 var typ uint16 142 switch e := peek[0x05]; e { 143 case 1: // little-endian 144 typ = binary.LittleEndian.Uint16(peek[0x10:]) 145 case 2: // big-endian 146 typ = binary.BigEndian.Uint16(peek[0x10:]) 147 default: 148 zlog.Warn(ctx). 149 Uint8("endianness", e). 150 Msg("martian ELF") 151 } 152 if typ != 0x02 && typ != 0x03 { 153 // AKA [debug/elf.ET_EXEC] and [debug/elf.ET_DYN] -- not imported in this file by convention. 154 // Not an executable or shared object, skip. 155 return nil 156 } 157 } 158 159 rd, ok := f.(io.ReaderAt) 160 if !ok { 161 // Need to spool the exe. 162 if err := spool.Setup(); err != nil { 163 return fmt.Errorf("gobin: unable to setup spool: %w", err) 164 } 165 if _, err := spool.File.Write(peek); err != nil { 166 return fmt.Errorf("gobin: unable to spool %q: %w", p, err) 167 } 168 sz, err := io.Copy(spool.File, f) 169 if err != nil { 170 return fmt.Errorf("gobin: unable to spool %q: %w", p, err) 171 } 172 rd = io.NewSectionReader(spool.File, 0, sz+peekSz) 173 } 174 return toPackages(ctx, &out, p, rd) 175 } 176 if err := fs.WalkDir(sys, ".", walk); err != nil { 177 return nil, err 178 } 179 180 return out, nil 181 } 182 183 // DefaultRepository implements [indexer.DefaultRepoScanner]. 184 func (Detector) DefaultRepository(ctx context.Context) *claircore.Repository { 185 return &Repository 186 } 187 188 type spoolfile struct { 189 sync.Once 190 File *os.File 191 err error 192 } 193 194 func (s *spoolfile) Setup() error { 195 s.Do(s.setup) 196 if s.err != nil { 197 return s.err 198 } 199 if _, err := s.File.Seek(0, io.SeekStart); err != nil { 200 return err 201 } 202 return nil 203 } 204 205 func (s *spoolfile) setup() { 206 f, err := os.CreateTemp("", "gobin.spool.*") 207 if err != nil { 208 s.err = err 209 return 210 } 211 if err := os.Remove(f.Name()); err != nil { 212 s.err = err 213 return 214 } 215 s.File = f 216 }