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{}