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