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