github.com/lineaje-labs/syft@v0.98.1-0.20231227153149-9e393f60ff1b/syft/pkg/cataloger/ruby/parse_gemspec.go (about) 1 package ruby 2 3 import ( 4 "bufio" 5 "encoding/json" 6 "fmt" 7 "regexp" 8 "strings" 9 10 "github.com/mitchellh/mapstructure" 11 12 "github.com/anchore/syft/syft/artifact" 13 "github.com/anchore/syft/syft/file" 14 "github.com/anchore/syft/syft/pkg" 15 "github.com/anchore/syft/syft/pkg/cataloger/generic" 16 "github.com/lineaje-labs/syft/internal" 17 ) 18 19 var _ generic.Parser = parseGemFileLockEntries 20 21 type postProcessor func(string) []string 22 23 type gemData struct { 24 Licenses []string `mapstructure:"licenses" json:"licenses,omitempty"` 25 pkg.RubyGemspec `mapstructure:",squash" json:",inline"` 26 } 27 28 // match example: Al\u003Ex ---> 003E 29 var unicodePattern = regexp.MustCompile(`\\u(?P<unicode>[0-9A-F]{4})`) 30 31 var patterns = map[string]*regexp.Regexp{ 32 // match example: name = "railties".freeze ---> railties 33 "name": regexp.MustCompile(`.*\.name\s*=\s*["']{1}(?P<name>.*)["']{1} *`), 34 35 // match example: version = "1.0.4".freeze ---> 1.0.4 36 "version": regexp.MustCompile(`.*\.version\s*=\s*["']{1}(?P<version>.*)["']{1} *`), 37 38 // match example: 39 // homepage = "https://github.com/anchore/syft".freeze ---> https://github.com/anchore/syft 40 "homepage": regexp.MustCompile(`.*\.homepage\s*=\s*["']{1}(?P<homepage>.*)["']{1} *`), 41 42 // match example: files = ["exe/bundle".freeze, "exe/bundler".freeze] ---> "exe/bundle".freeze, "exe/bundler".freeze 43 "files": regexp.MustCompile(`.*\.files\s*=\s*\[(?P<files>.*)] *`), 44 45 // match example: authors = ["Andr\u00E9 Arko".freeze, "Samuel Giddins".freeze, "Colby Swandale".freeze, 46 // "Hiroshi Shibata".freeze, "David Rodr\u00EDguez".freeze, "Grey Baker".freeze...] 47 "authors": regexp.MustCompile(`.*\.authors\s*=\s*\[(?P<authors>.*)] *`), 48 49 // match example: licenses = ["MIT".freeze] ----> "MIT".freeze 50 "licenses": regexp.MustCompile(`.*\.licenses\s*=\s*\[(?P<licenses>.*)] *`), 51 } 52 53 var postProcessors = map[string]postProcessor{ 54 "files": processList, 55 "authors": processList, 56 "licenses": processList, 57 } 58 59 func processList(s string) []string { 60 var results []string 61 for _, item := range strings.Split(s, ",") { 62 results = append(results, strings.Trim(item, "\" ")) 63 } 64 return results 65 } 66 67 // parseGemFileLockEntries parses the gemfile.lock file and returns the packages and relationships found. 68 func parseGemSpecEntries( 69 _ file.Resolver, _ *generic.Environment, reader file.LocationReadCloser, 70 ) ([]pkg.Package, []artifact.Relationship, error) { 71 var pkgs []pkg.Package 72 var fields = make(map[string]interface{}) 73 scanner := bufio.NewScanner(reader) 74 75 for scanner.Scan() { 76 line := scanner.Text() 77 78 sanitizedLine := strings.TrimSpace(line) 79 sanitizedLine = strings.ReplaceAll(sanitizedLine, ".freeze", "") 80 sanitizedLine = renderUtf8(sanitizedLine) 81 82 if sanitizedLine == "" { 83 continue 84 } 85 86 for field, pattern := range patterns { 87 matchMap := internal.MatchNamedCaptureGroups(pattern, sanitizedLine) 88 if value := matchMap[field]; value != "" { 89 if pp := postProcessors[field]; pp != nil { 90 fields[field] = pp(value) 91 } else { 92 fields[field] = value 93 } 94 // TODO: know that a line could actually match on multiple patterns, this is unlikely though 95 break 96 } 97 } 98 } 99 100 if fields["name"] != "" && fields["version"] != "" { 101 var metadata gemData 102 if err := mapstructure.Decode(fields, &metadata); err != nil { 103 return nil, nil, fmt.Errorf("unable to decode gem metadata: %w", err) 104 } 105 106 pkgs = append( 107 pkgs, 108 newGemspecPackage( 109 metadata, 110 reader.Location, 111 ), 112 ) 113 } 114 115 return pkgs, nil, nil 116 } 117 118 // renderUtf8 takes any string escaped string sub-sections from the ruby string and replaces those sections with the UTF8 runes. 119 func renderUtf8(s string) string { 120 fullReplacement := unicodePattern.ReplaceAllStringFunc(s, func(unicodeSection string) string { 121 var replacement string 122 // note: the json parser already has support for interpreting hex-representations of unicode escaped strings as unicode runes. 123 // we can do this ourselves with strconv.Atoi, or leverage the existing json package. 124 if err := json.Unmarshal([]byte(`"`+unicodeSection+`"`), &replacement); err != nil { 125 return unicodeSection 126 } 127 return replacement 128 }) 129 return fullReplacement 130 }