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