github.com/kastenhq/syft@v0.0.0-20230821225854-0710af25cdbe/syft/pkg/cataloger/golang/parse_go_binary.go (about) 1 package golang 2 3 import ( 4 "bytes" 5 "debug/elf" 6 "debug/macho" 7 "debug/pe" 8 "errors" 9 "fmt" 10 "io" 11 "regexp" 12 "runtime/debug" 13 "strings" 14 "time" 15 16 "golang.org/x/mod/module" 17 18 "github.com/kastenhq/syft/internal" 19 "github.com/kastenhq/syft/internal/log" 20 "github.com/kastenhq/syft/syft/artifact" 21 "github.com/kastenhq/syft/syft/file" 22 "github.com/kastenhq/syft/syft/pkg" 23 "github.com/kastenhq/syft/syft/pkg/cataloger/generic" 24 "github.com/kastenhq/syft/syft/pkg/cataloger/golang/internal/xcoff" 25 "github.com/kastenhq/syft/syft/pkg/cataloger/internal/unionreader" 26 ) 27 28 const GOARCH = "GOARCH" 29 30 var ( 31 // errUnrecognizedFormat is returned when a given executable file doesn't 32 // appear to be in a known format, or it breaks the rules of that format, 33 // or when there are I/O errors reading the file. 34 errUnrecognizedFormat = errors.New("unrecognized file format") 35 // devel is used to recognize the current default version when a golang main distribution is built 36 // https://github.com/golang/go/issues/29228 this issue has more details on the progress of being able to 37 // inject the correct version into the main module of the build process 38 39 knownBuildFlagPatterns = []*regexp.Regexp{ 40 regexp.MustCompile(`(?m)\.([gG]it)?([bB]uild)?[vV]ersion=(\S+/)*(?P<version>v?\d+.\d+.\d+[-\w]*)`), 41 regexp.MustCompile(`(?m)\.([tT]ag)=(\S+/)*(?P<version>v?\d+.\d+.\d+[-\w]*)`), 42 } 43 ) 44 45 const devel = "(devel)" 46 47 type goBinaryCataloger struct { 48 licenses goLicenses 49 } 50 51 // Catalog is given an object to resolve file references and content, this function returns any discovered Packages after analyzing rpm db installation. 52 func (c *goBinaryCataloger) parseGoBinary(resolver file.Resolver, _ *generic.Environment, reader file.LocationReadCloser) ([]pkg.Package, []artifact.Relationship, error) { 53 var pkgs []pkg.Package 54 55 unionReader, err := unionreader.GetUnionReader(reader.ReadCloser) 56 if err != nil { 57 return nil, nil, err 58 } 59 60 mods, archs := scanFile(unionReader, reader.RealPath) 61 internal.CloseAndLogError(reader.ReadCloser, reader.RealPath) 62 63 for i, mod := range mods { 64 pkgs = append(pkgs, c.buildGoPkgInfo(resolver, reader.Location, mod, archs[i])...) 65 } 66 return pkgs, nil, nil 67 } 68 69 func (c *goBinaryCataloger) makeGoMainPackage(resolver file.Resolver, mod *ExtendedBuildInfo, arch string, location file.Location) pkg.Package { 70 gbs := getBuildSettings(mod.Settings) 71 main := c.newGoBinaryPackage( 72 resolver, 73 &mod.Main, 74 mod.Main.Path, 75 mod.GoVersion, 76 arch, 77 gbs, 78 mod.cryptoSettings, 79 location.WithAnnotation(pkg.EvidenceAnnotationKey, pkg.PrimaryEvidenceAnnotation), 80 ) 81 82 if main.Version != devel { 83 return main 84 } 85 86 version, hasVersion := gbs["vcs.revision"] 87 timestamp, hasTimestamp := gbs["vcs.time"] 88 89 var ldflags string 90 if metadata, ok := main.Metadata.(pkg.GolangBinMetadata); ok { 91 // we've found a specific version from the ldflags! use it as the version. 92 // why not combine that with the pseudo version (e.g. v1.2.3-0.20210101000000-abcdef123456)? 93 // short answer: we're assuming that if a specific semver was provided in the ldflags that 94 // there is a matching vcs tag to match that could be referenced. This assumption could 95 // be incorrect in terms of the go.mod contents, but is not incorrect in terms of the logical 96 // version of the package. 97 ldflags = metadata.BuildSettings["-ldflags"] 98 } 99 100 majorVersion, fullVersion := extractVersionFromLDFlags(ldflags) 101 if fullVersion != "" { 102 version = fullVersion 103 } else if hasVersion && hasTimestamp { 104 //NOTE: err is ignored, because if parsing fails 105 // we still use the empty Time{} struct to generate an empty date, like 00010101000000 106 // for consistency with the pseudo-version format: https://go.dev/ref/mod#pseudo-versions 107 ts, _ := time.Parse(time.RFC3339, timestamp) 108 if len(version) >= 12 { 109 version = version[:12] 110 } 111 112 version = module.PseudoVersion(majorVersion, fullVersion, ts, version) 113 } 114 if version != "" { 115 main.Version = version 116 main.PURL = packageURL(main.Name, main.Version) 117 118 main.SetID() 119 } 120 121 return main 122 } 123 124 func extractVersionFromLDFlags(ldflags string) (majorVersion string, fullVersion string) { 125 if ldflags == "" { 126 return "", "" 127 } 128 129 for _, pattern := range knownBuildFlagPatterns { 130 groups := internal.MatchNamedCaptureGroups(pattern, ldflags) 131 v, ok := groups["version"] 132 133 if !ok { 134 continue 135 } 136 137 fullVersion = v 138 if !strings.HasPrefix(v, "v") { 139 fullVersion = fmt.Sprintf("v%s", v) 140 } 141 components := strings.Split(v, ".") 142 143 if len(components) == 0 { 144 continue 145 } 146 147 majorVersion = strings.TrimPrefix(components[0], "v") 148 return majorVersion, fullVersion 149 } 150 151 return "", "" 152 } 153 154 // getArchs finds a binary architecture by two ways: 155 // 1) reading build info from binaries compiled by go1.18+ 156 // 2) reading file headers from binaries compiled by < go1.18 157 func getArchs(readers []io.ReaderAt, builds []*ExtendedBuildInfo) []string { 158 if len(readers) != len(builds) { 159 log.Trace("golang cataloger: bin parsing: number of builds and readers doesn't match") 160 return nil 161 } 162 163 if len(readers) == 0 || len(builds) == 0 { 164 log.Tracef("golang cataloger: bin parsing: %d readers and %d build info items", len(readers), len(builds)) 165 return nil 166 } 167 168 archs := make([]string, len(builds)) 169 for i, build := range builds { 170 archs[i] = getGOARCH(build.Settings) 171 } 172 173 // if architecture was found via build settings return 174 if archs[0] != "" { 175 return archs 176 } 177 178 for i, r := range readers { 179 a, err := getGOARCHFromBin(r) 180 if err != nil { 181 log.Tracef("golang cataloger: bin parsing: getting arch from binary: %v", err) 182 continue 183 } 184 185 archs[i] = a 186 } 187 return archs 188 } 189 190 func getGOARCH(settings []debug.BuildSetting) string { 191 for _, s := range settings { 192 if s.Key == GOARCH { 193 return s.Value 194 } 195 } 196 197 return "" 198 } 199 200 func getGOARCHFromBin(r io.ReaderAt) (string, error) { 201 // Read the first bytes of the file to identify the format, then delegate to 202 // a format-specific function to load segment and section headers. 203 ident := make([]byte, 16) 204 if n, err := r.ReadAt(ident, 0); n < len(ident) || err != nil { 205 return "", fmt.Errorf("unrecognized file format: %w", err) 206 } 207 208 var arch string 209 switch { 210 case bytes.HasPrefix(ident, []byte("\x7FELF")): 211 f, err := elf.NewFile(r) 212 if err != nil { 213 return "", fmt.Errorf("unrecognized file format: %w", err) 214 } 215 arch = f.Machine.String() 216 case bytes.HasPrefix(ident, []byte("MZ")): 217 f, err := pe.NewFile(r) 218 if err != nil { 219 return "", fmt.Errorf("unrecognized file format: %w", err) 220 } 221 arch = fmt.Sprintf("%d", f.Machine) 222 case bytes.HasPrefix(ident, []byte("\xFE\xED\xFA")) || bytes.HasPrefix(ident[1:], []byte("\xFA\xED\xFE")): 223 f, err := macho.NewFile(r) 224 if err != nil { 225 return "", fmt.Errorf("unrecognized file format: %w", err) 226 } 227 arch = f.Cpu.String() 228 case bytes.HasPrefix(ident, []byte{0x01, 0xDF}) || bytes.HasPrefix(ident, []byte{0x01, 0xF7}): 229 f, err := xcoff.NewFile(r) 230 if err != nil { 231 return "", fmt.Errorf("unrecognized file format: %w", err) 232 } 233 arch = fmt.Sprintf("%d", f.FileHeader.TargetMachine) 234 default: 235 return "", errUnrecognizedFormat 236 } 237 238 arch = strings.Replace(arch, "EM_", "", 1) 239 arch = strings.Replace(arch, "Cpu", "", 1) 240 arch = strings.ToLower(arch) 241 242 return arch, nil 243 } 244 245 func getBuildSettings(settings []debug.BuildSetting) map[string]string { 246 m := make(map[string]string) 247 for _, s := range settings { 248 m[s.Key] = s.Value 249 } 250 return m 251 } 252 253 func createMainModuleFromPath(path string) (mod debug.Module) { 254 mod.Path = path 255 mod.Version = devel 256 return 257 } 258 259 func (c *goBinaryCataloger) buildGoPkgInfo(resolver file.Resolver, location file.Location, mod *ExtendedBuildInfo, arch string) []pkg.Package { 260 var pkgs []pkg.Package 261 if mod == nil { 262 return pkgs 263 } 264 265 var empty debug.Module 266 if mod.Main == empty && mod.Path != "" { 267 mod.Main = createMainModuleFromPath(mod.Path) 268 } 269 270 for _, dep := range mod.Deps { 271 if dep == nil { 272 continue 273 } 274 p := c.newGoBinaryPackage( 275 resolver, 276 dep, 277 mod.Main.Path, 278 mod.GoVersion, 279 arch, 280 nil, 281 mod.cryptoSettings, 282 location.WithAnnotation(pkg.EvidenceAnnotationKey, pkg.PrimaryEvidenceAnnotation), 283 ) 284 if pkg.IsValid(&p) { 285 pkgs = append(pkgs, p) 286 } 287 } 288 289 if mod.Main == empty { 290 return pkgs 291 } 292 293 main := c.makeGoMainPackage(resolver, mod, arch, location) 294 pkgs = append(pkgs, main) 295 296 return pkgs 297 }