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