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