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  }