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 }