github.com/google/osv-scalibr@v0.4.1/extractor/filesystem/language/cpp/conanlock/conanlock.go (about) 1 // Copyright 2025 Google LLC 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 // Package conanlock extracts conan.lock files. 16 package conanlock 17 18 import ( 19 "context" 20 "encoding/json" 21 "fmt" 22 "path/filepath" 23 "strings" 24 25 "github.com/google/osv-scalibr/extractor" 26 "github.com/google/osv-scalibr/extractor/filesystem" 27 "github.com/google/osv-scalibr/extractor/filesystem/osv" 28 "github.com/google/osv-scalibr/inventory" 29 "github.com/google/osv-scalibr/plugin" 30 "github.com/google/osv-scalibr/purl" 31 ) 32 33 const ( 34 // Name is the unique name of this extractor. 35 Name = "cpp/conanlock" 36 ) 37 38 type conanReference struct { 39 Name string 40 Version string 41 Username string 42 Channel string 43 RecipeRevision string 44 PackageID string 45 PackageRevision string 46 TimeStamp string 47 } 48 49 // conanGraphNode contains a subset of a graph entry that includes package information 50 type conanGraphNode struct { 51 Pref string `json:"pref"` 52 Ref string `json:"ref"` 53 Path string `json:"path"` 54 } 55 56 type conanGraphLock struct { 57 Nodes map[string]conanGraphNode `json:"nodes"` 58 } 59 60 type conanLockFile struct { 61 Version string `json:"version"` 62 // conan v0.4- lockfiles use "graph_lock", "profile_host" and "profile_build" 63 GraphLock conanGraphLock `json:"graph_lock"` 64 // conan v0.5+ lockfiles use "requires", "build_requires" and "python_requires" 65 Requires []string `json:"requires,omitempty"` 66 BuildRequires []string `json:"build_requires,omitempty"` 67 PythonRequires []string `json:"python_requires,omitempty"` 68 } 69 70 func parseConanReference(ref string) conanReference { 71 // very flexible format name/version[@username[/channel]][#rrev][:pkgid[#prev]][%timestamp] 72 var reference conanReference 73 74 parts := strings.SplitN(ref, "%", 2) 75 if len(parts) == 2 { 76 ref = parts[0] 77 reference.TimeStamp = parts[1] 78 } 79 80 parts = strings.SplitN(ref, ":", 2) 81 if len(parts) == 2 { 82 ref = parts[0] 83 parts = strings.SplitN(parts[1], "#", 2) 84 reference.PackageID = parts[0] 85 if len(parts) == 2 { 86 reference.PackageRevision = parts[1] 87 } 88 } 89 90 parts = strings.SplitN(ref, "#", 2) 91 if len(parts) == 2 { 92 ref = parts[0] 93 reference.RecipeRevision = parts[1] 94 } 95 96 parts = strings.SplitN(ref, "@", 2) 97 if len(parts) == 2 { 98 ref = parts[0] 99 usernameChannel := parts[1] 100 101 parts = strings.SplitN(usernameChannel, "/", 2) 102 reference.Username = parts[0] 103 if len(parts) == 2 { 104 reference.Channel = parts[1] 105 } 106 } 107 108 parts = strings.SplitN(ref, "/", 2) 109 if len(parts) == 2 { 110 reference.Name = parts[0] 111 reference.Version = parts[1] 112 } else { 113 // consumer conanfile.txt or conanfile.py might not have a name 114 reference.Name = "" 115 reference.Version = ref 116 } 117 118 return reference 119 } 120 121 func parseConanV1Lock(lockfile conanLockFile) []*extractor.Package { 122 var reference conanReference 123 packages := make([]*extractor.Package, 0, len(lockfile.GraphLock.Nodes)) 124 125 for _, node := range lockfile.GraphLock.Nodes { 126 if node.Path != "" { 127 // a local "conanfile.txt", skip 128 continue 129 } 130 131 if node.Pref != "" { 132 // old format 0.3 (conan 1.27-) lockfiles use "pref" instead of "ref" 133 reference = parseConanReference(node.Pref) 134 } else if node.Ref != "" { 135 reference = parseConanReference(node.Ref) 136 } else { 137 continue 138 } 139 // skip entries with no name, they are most likely consumer's conanfiles 140 // and not dependencies to be searched in a database anyway 141 if reference.Name == "" { 142 continue 143 } 144 145 packages = append(packages, &extractor.Package{ 146 Name: reference.Name, 147 Version: reference.Version, 148 PURLType: purl.TypeConan, 149 Metadata: osv.DepGroupMetadata{ 150 DepGroupVals: []string{}, 151 }, 152 }) 153 } 154 155 return packages 156 } 157 158 func parseConanRequires(packages *[]*extractor.Package, requires []string, group string) { 159 for _, ref := range requires { 160 reference := parseConanReference(ref) 161 // skip entries with no name, they are most likely consumer's conanfiles 162 // and not dependencies to be searched in a database anyway 163 if reference.Name == "" { 164 continue 165 } 166 167 *packages = append(*packages, &extractor.Package{ 168 Name: reference.Name, 169 Version: reference.Version, 170 PURLType: purl.TypeConan, 171 Metadata: osv.DepGroupMetadata{ 172 DepGroupVals: []string{group}, 173 }, 174 }) 175 } 176 } 177 178 func parseConanV2Lock(lockfile conanLockFile) []*extractor.Package { 179 packages := make( 180 []*extractor.Package, 181 0, 182 uint64(len(lockfile.Requires))+uint64(len(lockfile.BuildRequires))+uint64(len(lockfile.PythonRequires)), 183 ) 184 185 parseConanRequires(&packages, lockfile.Requires, "requires") 186 parseConanRequires(&packages, lockfile.BuildRequires, "build-requires") 187 parseConanRequires(&packages, lockfile.PythonRequires, "python-requires") 188 189 return packages 190 } 191 192 func parseConanLock(lockfile conanLockFile) []*extractor.Package { 193 if lockfile.GraphLock.Nodes != nil { 194 return parseConanV1Lock(lockfile) 195 } 196 197 return parseConanV2Lock(lockfile) 198 } 199 200 // Extractor extracts Conan packages from conan.lock files. 201 type Extractor struct{} 202 203 // New returns a new instance of this Extractor. 204 func New() filesystem.Extractor { return &Extractor{} } 205 206 // Name of the extractor 207 func (e Extractor) Name() string { return Name } 208 209 // Version of the extractor 210 func (e Extractor) Version() int { return 0 } 211 212 // Requirements of the extractor 213 func (e Extractor) Requirements() *plugin.Capabilities { 214 return &plugin.Capabilities{} 215 } 216 217 // FileRequired returns true if the specified file matches Conan lockfile patterns. 218 func (e Extractor) FileRequired(api filesystem.FileAPI) bool { 219 return filepath.Base(api.Path()) == "conan.lock" 220 } 221 222 // Extract extracts packages from conan.lock files passed through the scan input. 223 func (e Extractor) Extract(ctx context.Context, input *filesystem.ScanInput) (inventory.Inventory, error) { 224 var parsedLockfile *conanLockFile 225 226 err := json.NewDecoder(input.Reader).Decode(&parsedLockfile) 227 if err != nil { 228 return inventory.Inventory{}, fmt.Errorf("could not extract: %w", err) 229 } 230 231 pkgs := parseConanLock(*parsedLockfile) 232 233 for i := range pkgs { 234 pkgs[i].Locations = []string{input.Path} 235 } 236 237 return inventory.Inventory{Packages: pkgs}, nil 238 } 239 240 var _ filesystem.Extractor = Extractor{}