github.com/anchore/syft@v1.4.2-0.20240516191711-1bec1fc5d397/syft/pkg/cataloger/cpp/parse_conanlock.go (about)

     1  package cpp
     2  
     3  import (
     4  	"context"
     5  	"encoding/json"
     6  	"strings"
     7  
     8  	"github.com/anchore/syft/syft/artifact"
     9  	"github.com/anchore/syft/syft/file"
    10  	"github.com/anchore/syft/syft/pkg"
    11  	"github.com/anchore/syft/syft/pkg/cataloger/generic"
    12  )
    13  
    14  var _ generic.Parser = parseConanLock
    15  
    16  type conanLock struct {
    17  	GraphLock struct {
    18  		Nodes map[string]struct {
    19  			Ref            string   `json:"ref"`
    20  			PackageID      string   `json:"package_id"`
    21  			Context        string   `json:"context"`
    22  			Prev           string   `json:"prev"`
    23  			Requires       []string `json:"requires"`
    24  			PythonRequires string   `json:"py_requires"`
    25  			Options        string   `json:"options"`
    26  			Path           string   `json:"path"`
    27  		} `json:"nodes"`
    28  	} `json:"graph_lock"`
    29  	Version      string `json:"version"`
    30  	ProfileHost  string `json:"profile_host"`
    31  	ProfileBuild string `json:"profile_build,omitempty"`
    32  	// conan v0.5+ lockfiles use "requires", "build_requires" and "python_requires"
    33  	Requires       []string `json:"requires,omitempty"`
    34  	BuildRequires  []string `json:"build_requires,omitempty"`
    35  	PythonRequires []string `json:"python_requires,omitempty"`
    36  }
    37  
    38  // parseConanLock is a parser function for conan.lock (v1 and V2) contents, returning all packages discovered.
    39  func parseConanLock(_ context.Context, _ file.Resolver, _ *generic.Environment, reader file.LocationReadCloser) ([]pkg.Package, []artifact.Relationship, error) {
    40  	var cl conanLock
    41  	if err := json.NewDecoder(reader).Decode(&cl); err != nil {
    42  		return nil, nil, err
    43  	}
    44  
    45  	// requires is a list of package indices. We first need to fill it, and then we can resolve the package
    46  	// in a second iteration
    47  	var indexToPkgMap = map[string]pkg.Package{}
    48  
    49  	v1Pkgs := handleConanLockV2(cl, reader, indexToPkgMap)
    50  
    51  	// we do not want to store the index list requires in the conan metadata, because it is not useful to have it in
    52  	// the SBOM. Instead, we will store it in a map and then use it to build the relationships
    53  	// maps pkg.ID to a list of indices
    54  	var parsedPkgRequires = map[artifact.ID][]string{}
    55  
    56  	v2Pkgs := handleConanLockV1(cl, reader, parsedPkgRequires, indexToPkgMap)
    57  
    58  	var relationships []artifact.Relationship
    59  	var pkgs []pkg.Package
    60  	pkgs = append(pkgs, v1Pkgs...)
    61  	pkgs = append(pkgs, v2Pkgs...)
    62  
    63  	for _, p := range pkgs {
    64  		requires := parsedPkgRequires[p.ID()]
    65  		for _, r := range requires {
    66  			// this is a pkg that package "p" depends on... make a relationship
    67  			relationships = append(relationships, artifact.Relationship{
    68  				From: indexToPkgMap[r],
    69  				To:   p,
    70  				Type: artifact.DependencyOfRelationship,
    71  			})
    72  		}
    73  	}
    74  
    75  	return pkgs, relationships, nil
    76  }
    77  
    78  // handleConanLockV1 handles the parsing of conan lock v1 files (aka v0.4)
    79  func handleConanLockV1(cl conanLock, reader file.LocationReadCloser, parsedPkgRequires map[artifact.ID][]string, indexToPkgMap map[string]pkg.Package) []pkg.Package {
    80  	var pkgs []pkg.Package
    81  	for idx, node := range cl.GraphLock.Nodes {
    82  		metadata := pkg.ConanV1LockEntry{
    83  			Ref:       node.Ref,
    84  			Options:   parseOptions(node.Options),
    85  			Path:      node.Path,
    86  			Context:   node.Context,
    87  			PackageID: node.PackageID,
    88  			Prev:      node.Prev,
    89  		}
    90  
    91  		p := newConanlockPackage(
    92  			metadata,
    93  			reader.Location.WithAnnotation(pkg.EvidenceAnnotationKey, pkg.PrimaryEvidenceAnnotation),
    94  		)
    95  
    96  		if p != nil {
    97  			pk := *p
    98  			pkgs = append(pkgs, pk)
    99  			parsedPkgRequires[pk.ID()] = node.Requires
   100  			indexToPkgMap[idx] = pk
   101  		}
   102  	}
   103  	return pkgs
   104  }
   105  
   106  // handleConanLockV2 handles the parsing of conan lock v2 files (aka v0.5)
   107  func handleConanLockV2(cl conanLock, reader file.LocationReadCloser, indexToPkgMap map[string]pkg.Package) []pkg.Package {
   108  	var pkgs []pkg.Package
   109  	for _, ref := range cl.Requires {
   110  		reference, name := parseConanV2Reference(ref)
   111  		if name == "" {
   112  			continue
   113  		}
   114  
   115  		p := newConanReferencePackage(
   116  			reference,
   117  			reader.Location.WithAnnotation(pkg.EvidenceAnnotationKey, pkg.PrimaryEvidenceAnnotation),
   118  		)
   119  
   120  		if p != nil {
   121  			pk := *p
   122  			pkgs = append(pkgs, pk)
   123  			indexToPkgMap[name] = pk
   124  		}
   125  	}
   126  	return pkgs
   127  }
   128  
   129  func parseOptions(options string) []pkg.KeyValue {
   130  	o := make([]pkg.KeyValue, 0)
   131  	if len(options) == 0 {
   132  		return nil
   133  	}
   134  
   135  	kvps := strings.Split(options, "\n")
   136  	for _, kvp := range kvps {
   137  		kv := strings.Split(kvp, "=")
   138  		if len(kv) == 2 {
   139  			o = append(o, pkg.KeyValue{
   140  				Key:   kv[0],
   141  				Value: kv[1],
   142  			})
   143  		}
   144  	}
   145  
   146  	return o
   147  }
   148  
   149  func parseConanV2Reference(ref string) (pkg.ConanV2LockEntry, string) {
   150  	// very flexible format name/version[@username[/channel]][#rrev][:pkgid[#prev]][%timestamp]
   151  	reference := pkg.ConanV2LockEntry{Ref: ref}
   152  
   153  	parts := strings.SplitN(ref, "%", 2)
   154  	if len(parts) == 2 {
   155  		ref = parts[0]
   156  		reference.TimeStamp = parts[1]
   157  	}
   158  
   159  	parts = strings.SplitN(ref, ":", 2)
   160  	if len(parts) == 2 {
   161  		ref = parts[0]
   162  		parts = strings.SplitN(parts[1], "#", 2)
   163  		reference.PackageID = parts[0]
   164  		if len(parts) == 2 {
   165  			reference.PackageRevision = parts[1]
   166  		}
   167  	}
   168  
   169  	parts = strings.SplitN(ref, "#", 2)
   170  	if len(parts) == 2 {
   171  		ref = parts[0]
   172  		reference.RecipeRevision = parts[1]
   173  	}
   174  
   175  	parts = strings.SplitN(ref, "@", 2)
   176  	if len(parts) == 2 {
   177  		ref = parts[0]
   178  		UsernameChannel := parts[1]
   179  
   180  		parts = strings.SplitN(UsernameChannel, "/", 2)
   181  		reference.Username = parts[0]
   182  		if len(parts) == 2 {
   183  			reference.Channel = parts[1]
   184  		}
   185  	}
   186  
   187  	parts = strings.SplitN(ref, "/", 2)
   188  	var name string
   189  	if len(parts) == 2 {
   190  		name = parts[0]
   191  	} else {
   192  		// consumer conanfile.txt or conanfile.py might not have a name
   193  		name = ""
   194  	}
   195  
   196  	return reference, name
   197  }