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  }