github.com/anchore/syft@v1.38.2/syft/pkg/cataloger/snap/parse_kernel_changelog.go (about)

     1  package snap
     2  
     3  import (
     4  	"compress/gzip"
     5  	"context"
     6  	"fmt"
     7  	"regexp"
     8  	"strings"
     9  
    10  	"github.com/anchore/syft/syft/artifact"
    11  	"github.com/anchore/syft/syft/file"
    12  	"github.com/anchore/syft/syft/pkg"
    13  	"github.com/anchore/syft/syft/pkg/cataloger/generic"
    14  )
    15  
    16  // kernelVersionInfo holds parsed kernel version information
    17  type kernelVersionInfo struct {
    18  	baseVersion    string // e.g., "5.4.0-195"
    19  	releaseVersion string // e.g., "215"
    20  	fullVersion    string // e.g., "5.4.0-195.215"
    21  	majorVersion   string // e.g., "5.4"
    22  }
    23  
    24  // parseKernelChangelog parses changelog files from kernel snaps to extract kernel version
    25  func parseKernelChangelog(_ context.Context, _ file.Resolver, _ *generic.Environment, reader file.LocationReadCloser) ([]pkg.Package, []artifact.Relationship, error) {
    26  	// The file should be gzipped
    27  	lines, err := readChangelogLines(reader)
    28  	if err != nil {
    29  		return nil, nil, err
    30  	}
    31  
    32  	// pull from first line
    33  	versionInfo, err := extractKernelVersion(lines[0])
    34  	if err != nil {
    35  		return nil, nil, err
    36  	}
    37  
    38  	snapMetadata := pkg.SnapEntry{
    39  		SnapType: pkg.SnapTypeKernel,
    40  	}
    41  
    42  	packages := createMainKernelPackage(versionInfo, snapMetadata, reader.Location)
    43  
    44  	// Check for base kernel package
    45  	basePackage := findBaseKernelPackage(lines, versionInfo, snapMetadata, reader.Location)
    46  	if basePackage != nil {
    47  		packages = append(packages, *basePackage)
    48  	}
    49  
    50  	return packages, nil, nil
    51  }
    52  
    53  // readChangelogLines reads and decompresses the changelog content
    54  func readChangelogLines(reader file.LocationReadCloser) ([]string, error) {
    55  	gzReader, err := gzip.NewReader(reader)
    56  	if err != nil {
    57  		return nil, fmt.Errorf("failed to create gzip reader for changelog: %w", err)
    58  	}
    59  	defer gzReader.Close()
    60  
    61  	content, err := readAll(gzReader)
    62  	if err != nil {
    63  		return nil, fmt.Errorf("failed to read changelog content: %w", err)
    64  	}
    65  
    66  	lines := strings.Split(string(content), "\n")
    67  	if len(lines) == 0 {
    68  		return nil, fmt.Errorf("changelog file is empty")
    69  	}
    70  
    71  	// Parse the first line to extract kernel version information
    72  	// Format: "linux (5.4.0-195.215) focal; urgency=medium"
    73  	return lines, nil
    74  }
    75  
    76  // extractKernelVersion parses version information from the first changelog line
    77  func extractKernelVersion(firstLine string) (*kernelVersionInfo, error) {
    78  	// Format: "linux (5.4.0-195.215) focal; urgency=medium"
    79  	kernelVersionRegex := regexp.MustCompile(`linux \(([0-9]+\.[0-9]+\.[0-9]+-[0-9]+)\.([0-9]+)\)`)
    80  	matches := kernelVersionRegex.FindStringSubmatch(firstLine)
    81  
    82  	if len(matches) < 3 {
    83  		return nil, fmt.Errorf("could not parse kernel version from changelog: %s", firstLine)
    84  	}
    85  
    86  	info := &kernelVersionInfo{
    87  		baseVersion:    matches[1], // e.g., "5.4.0-195"
    88  		releaseVersion: matches[2], // eg., "215"
    89  	}
    90  	// eg "5.4.0-195.215"
    91  	info.fullVersion = fmt.Sprintf("%s.%s", info.baseVersion, info.releaseVersion)
    92  
    93  	// Extract major version; package naming
    94  	majorVersionRegex := regexp.MustCompile(`([0-9]+\.[0-9]+)\.[0-9]+-[0-9]+`)
    95  	majorMatches := majorVersionRegex.FindStringSubmatch(info.baseVersion)
    96  
    97  	if len(majorMatches) >= 2 {
    98  		info.majorVersion = majorMatches[1]
    99  	} else {
   100  		info.majorVersion = info.baseVersion
   101  	}
   102  
   103  	return info, nil
   104  }
   105  
   106  // createMainKernelPackage creates the main kernel package
   107  func createMainKernelPackage(versionInfo *kernelVersionInfo, snapMetadata pkg.SnapEntry, location file.Location) []pkg.Package {
   108  	kernelPackageName := fmt.Sprintf("linux-image-%s-generic", versionInfo.baseVersion)
   109  	kernelPkg := newDebianPackageFromSnap(
   110  		kernelPackageName,
   111  		versionInfo.fullVersion,
   112  		snapMetadata,
   113  		location,
   114  	)
   115  
   116  	return []pkg.Package{kernelPkg}
   117  }
   118  
   119  // findBaseKernelPackage searches for and creates base kernel package if present
   120  func findBaseKernelPackage(lines []string, versionInfo *kernelVersionInfo, snapMetadata pkg.SnapEntry, location file.Location) *pkg.Package {
   121  	baseKernelEntry := fmt.Sprintf("%s/linux:", strings.ReplaceAll(versionInfo.releaseVersion, ";", "/"))
   122  
   123  	for _, line := range lines {
   124  		if strings.Contains(line, baseKernelEntry) {
   125  			return parseBaseKernelLine(line, versionInfo.majorVersion, snapMetadata, location)
   126  		}
   127  	}
   128  
   129  	return nil
   130  }
   131  
   132  // parseBaseKernelLine extracts base kernel version from a changelog line
   133  func parseBaseKernelLine(line string, majorVersion string, snapMetadata pkg.SnapEntry, location file.Location) *pkg.Package {
   134  	baseKernelRegex := regexp.MustCompile(fmt.Sprintf(`(%s-[0-9]+)\.?[0-9]*`, regexp.QuoteMeta(majorVersion)))
   135  	baseMatches := baseKernelRegex.FindStringSubmatch(line)
   136  
   137  	if len(baseMatches) < 2 {
   138  		return nil
   139  	}
   140  
   141  	baseKernelVersion := baseMatches[1]
   142  	baseKernelFullRegex := regexp.MustCompile(fmt.Sprintf(`(%s-[0-9]+\.[0-9]+)`, regexp.QuoteMeta(majorVersion)))
   143  	baseFullMatches := baseKernelFullRegex.FindStringSubmatch(line)
   144  
   145  	var baseFullVersion string
   146  	if len(baseFullMatches) >= 2 {
   147  		baseFullVersion = baseFullMatches[1]
   148  	} else {
   149  		baseFullVersion = baseKernelVersion
   150  	}
   151  
   152  	baseKernelPkg := newDebianPackageFromSnap(
   153  		fmt.Sprintf("linux-image-%s-generic", baseKernelVersion),
   154  		baseFullVersion,
   155  		snapMetadata,
   156  		location,
   157  	)
   158  
   159  	return &baseKernelPkg
   160  }