github.com/anchore/syft@v1.4.2-0.20240516191711-1bec1fc5d397/syft/linux/identify_release.go (about) 1 package linux 2 3 import ( 4 "fmt" 5 "io" 6 "regexp" 7 "strings" 8 9 "github.com/acobaugh/osrelease" 10 "github.com/google/go-cmp/cmp" 11 12 "github.com/anchore/go-logger" 13 "github.com/anchore/syft/internal" 14 "github.com/anchore/syft/internal/log" 15 "github.com/anchore/syft/syft/file" 16 ) 17 18 // returns a distro or nil 19 type parseFunc func(string) (*Release, error) 20 21 type parseEntry struct { 22 path string 23 fn parseFunc 24 } 25 26 var identityFiles = []parseEntry{ 27 { 28 // most distros provide a link at this location 29 path: "/etc/os-release", 30 fn: parseOsRelease, 31 }, 32 { 33 // standard location for rhel & debian distros 34 path: "/usr/lib/os-release", 35 fn: parseOsRelease, 36 }, 37 { 38 // check for centos:6 39 path: "/etc/system-release-cpe", 40 fn: parseSystemReleaseCPE, 41 }, 42 { 43 // last ditch effort for determining older centos version distro information 44 path: "/etc/redhat-release", 45 fn: parseRedhatRelease, 46 }, 47 // ///////////////////////////////////////////////////////////////////////////////////////////////////// 48 // IMPORTANT! checking busybox must be last since other distros contain the busybox binary 49 { 50 // check for busybox 51 path: "/bin/busybox", 52 fn: parseBusyBox, 53 }, 54 // ///////////////////////////////////////////////////////////////////////////////////////////////////// 55 } 56 57 // IdentifyRelease parses distro-specific files to discover and raise linux distribution release details. 58 func IdentifyRelease(resolver file.Resolver) *Release { 59 logger := log.Nested("operation", "identify-release") 60 for _, entry := range identityFiles { 61 locations, err := resolver.FilesByPath(entry.path) 62 if err != nil { 63 logger.WithFields("error", err, "path", entry.path).Trace("unable to get path") 64 continue 65 } 66 67 for _, location := range locations { 68 release := tryParseReleaseInfo(resolver, location, logger, entry) 69 if release != nil { 70 return release 71 } 72 } 73 } 74 75 return nil 76 } 77 78 func tryParseReleaseInfo(resolver file.Resolver, location file.Location, logger logger.Logger, entry parseEntry) *Release { 79 contentReader, err := resolver.FileContentsByLocation(location) 80 if err != nil { 81 logger.WithFields("error", err, "path", location.RealPath).Trace("unable to get contents") 82 return nil 83 } 84 defer internal.CloseAndLogError(contentReader, location.AccessPath) 85 86 content, err := io.ReadAll(contentReader) 87 if err != nil { 88 logger.WithFields("error", err, "path", location.RealPath).Trace("unable to read contents") 89 return nil 90 } 91 92 release, err := entry.fn(string(content)) 93 if err != nil { 94 logger.WithFields("error", err, "path", location.RealPath).Trace("unable to parse contents") 95 return nil 96 } 97 98 return release 99 } 100 101 func parseOsRelease(contents string) (*Release, error) { 102 values, err := osrelease.ReadString(contents) 103 if err != nil { 104 return nil, fmt.Errorf("unable to read os-release file: %w", err) 105 } 106 107 var idLike []string 108 for _, s := range strings.Split(values["ID_LIKE"], " ") { 109 s = strings.TrimSpace(s) 110 if s == "" { 111 continue 112 } 113 idLike = append(idLike, s) 114 } 115 116 r := Release{ 117 PrettyName: values["PRETTY_NAME"], 118 Name: values["NAME"], 119 ID: values["ID"], 120 IDLike: idLike, 121 Version: values["VERSION"], 122 VersionID: values["VERSION_ID"], 123 VersionCodename: values["VERSION_CODENAME"], 124 BuildID: values["BUILD_ID"], 125 ImageID: values["IMAGE_ID"], 126 ImageVersion: values["IMAGE_VERSION"], 127 Variant: values["VARIANT"], 128 VariantID: values["VARIANT_ID"], 129 HomeURL: values["HOME_URL"], 130 SupportURL: values["SUPPORT_URL"], 131 BugReportURL: values["BUG_REPORT_URL"], 132 PrivacyPolicyURL: values["PRIVACY_POLICY_URL"], 133 CPEName: values["CPE_NAME"], 134 SupportEnd: values["SUPPORT_END"], 135 } 136 137 // don't allow for empty contents to result in a Release object being created 138 if cmp.Equal(r, Release{}) { 139 return nil, nil 140 } 141 142 return &r, nil 143 } 144 145 var busyboxVersionMatcher = regexp.MustCompile(`BusyBox v[\d.]+`) 146 147 func parseBusyBox(contents string) (*Release, error) { 148 matches := busyboxVersionMatcher.FindAllString(contents, -1) 149 for _, match := range matches { 150 parts := strings.Split(match, " ") 151 version := strings.ReplaceAll(parts[1], "v", "") 152 153 return simpleRelease(match, "busybox", version, ""), nil 154 } 155 return nil, nil 156 } 157 158 // example CPE: cpe:/o:centos:linux:6:GA 159 var systemReleaseCpeMatcher = regexp.MustCompile(`cpe:\/o:(.*?):.*?:(.*?):.*?$`) 160 161 // parseSystemReleaseCPE parses the older centos (6) file to determine distro metadata 162 func parseSystemReleaseCPE(contents string) (*Release, error) { 163 matches := systemReleaseCpeMatcher.FindAllStringSubmatch(contents, -1) 164 for _, match := range matches { 165 if len(match) < 3 { 166 continue 167 } 168 return simpleRelease(match[1], strings.ToLower(match[1]), match[2], match[0]), nil 169 } 170 return nil, nil 171 } 172 173 // example: "CentOS release 6.10 (Final)" 174 var redhatReleaseMatcher = regexp.MustCompile(`(.*?)\srelease\s(\d\.\d+)`) 175 176 // parseRedhatRelease is a fallback parsing method for determining distro information in older redhat versions 177 func parseRedhatRelease(contents string) (*Release, error) { 178 matches := redhatReleaseMatcher.FindAllStringSubmatch(contents, -1) 179 for _, match := range matches { 180 if len(match) < 3 { 181 continue 182 } 183 return simpleRelease(match[1], strings.ToLower(match[1]), match[2], ""), nil 184 } 185 return nil, nil 186 } 187 188 func simpleRelease(prettyName, name, version, cpe string) *Release { 189 return &Release{ 190 PrettyName: prettyName, 191 Name: name, 192 ID: name, 193 IDLike: []string{name}, 194 Version: version, 195 VersionID: version, 196 CPEName: cpe, 197 } 198 }