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 }