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  }