ecosystems

package
v2.2.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jun 5, 2026 License: Apache-2.0 Imports: 8 Imported by: 0

README

Ecosystems Package

The ecosystems package provides a unified plugin interface for discovering and building dependency graphs across multiple package managers and programming language ecosystems.

Purpose

Modern software projects use diverse package managers (pip, npm, maven, gradle, etc.) to manage dependencies. Each ecosystem has its own format for declaring and resolving dependencies. This package standardizes how dependency graphs are discovered, built, and represented, regardless of the underlying package manager.

Key Goals
  1. Unified Interface: Provide a consistent API for dependency graph generation across all package managers
  2. Extensibility: Make it easy to add support for new ecosystems without modifying core code
  3. Type Safety: Leverage Go's type system to catch errors at compile time
  4. Testability: Enable dependency injection and mocking for robust testing
  5. Ecosystem Isolation: Keep ecosystem-specific logic contained within dedicated plugins

Architecture

Core Interface

Every plugin implements the SCAPlugin interface to:

  • Discover dependency manifests in a directory (e.g., requirements.txt, package.json)
  • Resolve dependencies using the ecosystem's native tooling
  • Build a standardized dependency graph representation
  • Return results with metadata about the analysis

Data Structures

DepGraph: Snyk Dependency Graph Format

The SCAResult.DepGraph field uses the standard Snyk dependency graph format from github.com/snyk/dep-graph/go/pkg/depgraph:

import "github.com/snyk/dep-graph/go/pkg/depgraph"

type SCAResult struct {
    DepGraph          *depgraph.DepGraph         `json:"depGraph,omitempty"`
    ProjectDescriptor identity.ProjectDescriptor `json:"projectDescriptor"`
    ResolverMetadata  *ResolverMetadata          `json:"meta,omitempty"`
    Error             error                      `json:"error,omitempty"`
}
DepGraph Structure

The depgraph.DepGraph type provides a standardized format for representing dependency graphs:

{
  "schemaVersion": "1.3.0",
  "pkgManager": {
    "name": "pip"
  },
  "pkgs": [
    {
      "id": "root@0.0.0",
      "info": {
        "name": "root",
        "version": "0.0.0"
      }
    },
    {
      "id": "requests@2.31.0",
      "info": {
        "name": "requests",
        "version": "2.31.0"
      }
    }
  ],
  "graph": {
    "rootNodeId": "root-node",
    "nodes": [
      {
        "nodeId": "root-node",
        "pkgId": "root@0.0.0",
        "deps": [
          { "nodeId": "requests@2.31.0" }
        ]
      },
      {
        "nodeId": "requests@2.31.0",
        "pkgId": "requests@2.31.0",
        "deps": []
      }
    ]
  }
}
Key Components
Field Description
schemaVersion Version of the dep-graph schema (e.g., "1.3.0")
pkgManager Package manager info with name (e.g., "pip", "npm")
pkgs Array of all packages with id and info (name, version)
graph.rootNodeId ID of the root node in the graph
graph.nodes Array of nodes, each with nodeId, pkgId, and deps
Building a DepGraph

Use the depgraph.Builder to construct dependency graphs:

import "github.com/snyk/dep-graph/go/pkg/depgraph"

// Create a builder with package manager and root package info
builder, err := depgraph.NewBuilder(
    &depgraph.PkgManager{Name: "pip"},
    &depgraph.PkgInfo{Name: "root", Version: "0.0.0"},
)
if err != nil {
    return nil, err
}

// Add package nodes
builder.AddNode("requests@2.31.0", &depgraph.PkgInfo{
    Name:    "requests",
    Version: "2.31.0",
})

// Connect dependencies
rootNode := builder.GetRootNode()
err = builder.ConnectNodes(rootNode.NodeID, "requests@2.31.0")
if err != nil {
    return nil, err
}

// Build the final graph
depGraph := builder.Build()
SCAResult: Analysis Output
type SCAResult struct {
    DepGraph          *depgraph.DepGraph         `json:"depGraph,omitempty"`
    ProjectDescriptor identity.ProjectDescriptor `json:"projectDescriptor"`
    ResolverMetadata  *ResolverMetadata          `json:"meta,omitempty"`
    Error             error                      `json:"error,omitempty"`
}

Each result contains:

  • DepGraph: The complete dependency graph using Snyk's standard format
  • ProjectDescriptor: Project identity information (type, target file, runtime)
  • ResolverMetadata: Information about the resolver/plugin that performed the analysis
  • Error: Any error encountered during analysis (optional)
ProjectDescriptor
type ProjectDescriptor struct {
    Identity ProjectIdentity `json:"identity"`
}

type ProjectIdentity struct {
    ProjectType       string  `json:"type,omitempty"`           // Project type (e.g., "npm", "maven", "pip")
    TargetFile        *string `json:"targetFile,omitempty"`     // Manifest/build file path
    TargetRuntime     *string `json:"targetRuntime,omitempty"`  // Runtime environment
    RootComponentName string  `json:"rootComponentName,omitempty"` // Root component name
}

The ProjectDescriptor contains project identity information that uniquely identifies what was analyzed. This includes the project type (ecosystem), the specific manifest file, and runtime details.

ResolverMetadata
type ResolverMetadata struct {
    PluginName       string            `json:"pluginName"`       // Name of the plugin/resolver
    VersionBuildInfo map[string]string `json:"versionBuildInfo"` // Version and build information
}

Standard keys are available in the metadata package:

import "github.com/snyk/cli-extension-dep-graph/v2/pkg/ecosystems/metadata"

// Available keys:
const (
    // Gradle ecosystem
    metadata.GradleVersion  = "gradleVersion"
    metadata.JavaVersion    = "javaVersion" 
    
    // Python ecosystem
    metadata.PythonVersion  = "pythonVersion"
    
    // General build info
    metadata.BuildTimestamp = "buildTimestamp"
)

Example usage:

import "github.com/snyk/cli-extension-dep-graph/v2/pkg/ecosystems/metadata"

resolverMetadata := ResolverMetadata{
    PluginName: "gradle",
    VersionBuildInfo: map[string]string{
        metadata.GradleVersion:  "8.5",
        metadata.JavaVersion:    "17.0.1", 
        metadata.BuildTimestamp: "2024-01-01T12:00:00Z",
    },
}

Plugins return PluginResult, where:

  • Results contains depgraphs and other data. Multiple findings are permitted to allow for workspaces and other scenarios where more than one project are found within an ecosytem.
  • ProcessedFiles contains files that other plugins should not handle (either because the plugin has processed them directly, e.g. uv.lock for uv, or because it is associated with the handled project, e.g. pyproject.toml or requirements.txt associated with a uv project).

Usage Examples

Basic Usage
import (
    "context"
    "fmt"
    "github.com/snyk/cli-extension-dep-graph/v2/pkg/ecosystems"
    "github.com/snyk/cli-extension-dep-graph/v2/pkg/ecosystems/python/pip"
)

func main() {
    plugin := &pip.Plugin{}
    options := ecosystems.NewPluginOptions().
        WithTargetFile("requirements.txt")
    
    result, err := plugin.BuildDepGraphsFromDir(
        context.Background(),
        logger.Nop(),
        "/path/to/python/project",
        options,
    )
    if err != nil {
        // Handle error
    }
    
    for _, scaResult := range result.Results {
        if scaResult.Error != nil {
            fmt.Printf("Error analyzing with %s: %v\n", scaResult.ResolverMetadata.PluginName, scaResult.Error)
            continue
        }

        fmt.Printf("Package Manager: %s\n", scaResult.DepGraph.PkgManager.Name)
        fmt.Printf("Total Packages: %d\n", len(scaResult.DepGraph.Pkgs))
    }
}
Analyzing All Projects
options := ecosystems.NewPluginOptions().
    WithAllProjects(true)

result, err := plugin.BuildDepGraphsFromDir(ctx, logger.Nop(), "/path/to/monorepo", options)
// Returns results for all requirements.txt files found
Handling Errors in Results
for _, result := range result.Results {
    if result.Error != nil {
        // Individual file failed, but others may have succeeded
        log.Printf("Failed to analyze with %s: %v", result.ResolverMetadata.PluginName, result.Error)
        continue
    }
    
    // Process successful result
    processDepGraph(result.DepGraph)
}

Adding a New Plugin

To add support for a new ecosystem:

  1. Create package directory:

    pkg/ecosystems/<ecosystem>/<tool>/
    
  2. Implement the interface:

    package tool
    
    import (
        "context"
        "github.com/snyk/cli-extension-dep-graph/v2/pkg/ecosystems"
        "github.com/snyk/cli-extension-dep-graph/v2/pkg/ecosystems/logger"
    )
    
    type Plugin struct {
        // Plugin-specific fields
    }
    
    var _ ecosystems.SCAPlugin = (*Plugin)(nil)
    
    func (p *Plugin) BuildDepGraphsFromDir(
        ctx context.Context,
        log logger.Logger,
        dir string,
        options *ecosystems.SCAPluginOptions,
    ) (*ecosystems.PluginResult, error) {
        // 1. Discover manifest files in dir
        // 2. Resolve dependencies using ecosystem tooling
        // 3. Build depgraph.DepGraph using the builder
        // 4. Return PluginResult with results and processed files
    }
    
  3. Add ecosystem-specific options (if needed):

    // In options.go
    type MyEcosystemOptions struct {
        SpecificOption string
    }
    
    type SCAPluginOptions struct {
        Global      GlobalOptions
        Python      *PythonOptions
        MyEcosystem *MyEcosystemOptions  // Add here
    }
    
  4. Write tests:

    func TestPlugin_BuildDepGraphsFromDir(t *testing.T) {
        plugin := &Plugin{}
        options := ecosystems.NewPluginOptions()
    
        result, err := plugin.BuildDepGraphsFromDir(context.Background(), logger.Nop(), "./testdata", options)
        assert.NoError(t, err)
        assert.Len(t, result.Results, 1)
        assert.NotNil(t, result.Results[0].DepGraph)
        assert.Equal(t, "mymanager", result.Results[0].DepGraph.PkgManager.Name)
    }
    

Design Principles

  1. Single Responsibility: Each plugin focuses only on its ecosystem
  2. Dependency Inversion: Depend on abstractions (interfaces), not concrete implementations
  3. Open/Closed: Open for extension (new plugins), closed for modification (core interface)
  4. Interface Segregation: Keep interfaces minimal and focused
  5. Don't Repeat Yourself: Common functionality should be extracted to shared utilities

References

Documentation

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type BazelOptions

type BazelOptions struct {
	TargetQuery string `arg:"--bazel-target-query"`
	MaxTargets  *int   `arg:"--bazel-max-targets"`
	Jvm         bool   `arg:"--bazel-jvm"`
	Go          bool   `arg:"--bazel-go"`
}

BazelOptions contains Bazel-specific options for dependency graph generation.

type CommaSeparatedString

type CommaSeparatedString []string

CommaSeparatedString is a custom type that parses comma-separated values.

func (*CommaSeparatedString) UnmarshalText

func (c *CommaSeparatedString) UnmarshalText(text []byte) error

UnmarshalText implements encoding.TextUnmarshaler.

type GlobalOptions

type GlobalOptions struct {
	TargetFile                    *string              `arg:"--target-file"`
	AllProjects                   bool                 `arg:"--all-projects"`
	IncludeDev                    bool                 `arg:"--dev,-d"`
	Exclude                       CommaSeparatedString `arg:"--exclude"`
	ExcludePaths                  CommaSeparatedString `arg:"--exclude-paths"`
	FailFast                      bool                 `arg:"--fail-fast"`
	AllowOutOfSync                bool                 // Derived from --strict-out-of-sync (inverted); parsed in NewPluginOptionsFromRawFlags.
	ForceSingleGraph              bool                 `arg:"--force-single-graph"`
	ForceIncludeWorkspacePackages bool                 `arg:"--internal-uv-workspace-packages"`
	ProjectName                   *string              `arg:"--project-name"`
	IncludeProvenance             bool                 `arg:"--include-provenance"`
	WorkspacePackage              *string              `arg:"--workspace-package"`
	RawFlags                      []string
}

GlobalOptions contains options that apply globally across all SCA plugins.

type GradleOptions

type GradleOptions struct {
	// ConfigurationMatching is a regex to select only matching Gradle configurations.
	ConfigurationMatching string `arg:"--configuration-matching"`
	// ConfigurationAttributes filters configurations by attribute values (key:value,key:value).
	ConfigurationAttributes string `arg:"--configuration-attributes"`
	// SubProject restricts scanning to a single named Gradle sub-project.
	// Accepts both --gradle-sub-project and --sub-project (legacy alias).
	SubProject string `arg:"--gradle-sub-project,--sub-project"`
	// AllSubProjects scans all sub-projects in a multi-project build.
	AllSubProjects bool `arg:"--all-sub-projects"`
	// InitScript overrides the built-in init script with a user-supplied path.
	InitScript string `arg:"--init-script"`
	// SkipWrapper bypasses gradlew discovery and forces use of the gradle command.
	SkipWrapper bool `arg:"--gradle-skip-wrapper"`
	// NormalizeDeps uses the SHAs of the dependencies provided by the IncludeProvenance flag
	// to lookup the canonical GAV coordinates of the dependency and rewrite the produced DepGraphs.
	NormalizeDeps bool `arg:"--gradle-normalize-deps"`
}

GradleOptions contains Gradle-specific options for dependency graph generation.

type OnGraphFunc

type OnGraphFunc func(SCAResult) error

OnGraphFunc is the per-graph callback BuildDepGraphsFromDir invokes for each emitted SCAResult. See SCAPlugin for the contract.

type PythonOptions

type PythonOptions struct {
	NoBuildIsolation bool `arg:"--no-build-isolation"`
}

PythonOptions contains Python-specific options for dependency graph generation.

type ResolverMetadata

type ResolverMetadata struct {
	PluginName           string            `json:"pluginName,omitempty"`
	VersionBuildInfo     map[string]string `json:"versionBuildInfo,omitempty"`
	NormalisedTargetFile string            `json:"normalisedTargetFile,omitempty"`
}

type SCAPlugin

type SCAPlugin interface {
	BuildDepGraphsFromDir(
		ctx context.Context,
		log logger.Logger,
		dir string,
		options *SCAPluginOptions,
		onGraph OnGraphFunc,
	) error
	GetName() string
}

SCAPlugin builds dependency graphs from a directory containing project files. Results are emitted one at a time via onGraph as the plugin produces them — there is no aggregated return value. This lets consumers stream graphs to disk / network without holding the full set in memory.

onGraph is invoked exactly once per produced SCAResult. Calls are serialized — onGraph need not be goroutine-safe. A non-nil onGraph return aborts the run and BuildDepGraphsFromDir returns that error to the caller.

Setup-time failures (cannot access dir, options invalid, etc.) are returned directly from BuildDepGraphsFromDir without ever invoking onGraph. Per-graph build failures are emitted as SCAResult{Descriptor: ..., Error: err} via onGraph — the run continues so the caller sees every project the plugin attempted.

type SCAPluginOptions

type SCAPluginOptions struct {
	Global GlobalOptions
	Python PythonOptions
	Gradle GradleOptions
	Bazel  BazelOptions
}

SCAPluginOptions contains configuration options for SCA plugins, including global settings and language-specific options.

func NewPluginOptions

func NewPluginOptions() *SCAPluginOptions

func NewPluginOptionsFromRawFlags

func NewPluginOptionsFromRawFlags(rawFlags []string) (*SCAPluginOptions, error)

func (*SCAPluginOptions) WithAllProjects

func (o *SCAPluginOptions) WithAllProjects(allProjects bool) *SCAPluginOptions

func (*SCAPluginOptions) WithAllowOutOfSync

func (o *SCAPluginOptions) WithAllowOutOfSync(allowOutOfSync bool) *SCAPluginOptions

func (*SCAPluginOptions) WithBazelGo

func (o *SCAPluginOptions) WithBazelGo(b bool) *SCAPluginOptions

WithBazelGo sets whether the Bazel Go dep-graph scanner should run.

func (*SCAPluginOptions) WithBazelJvm

func (o *SCAPluginOptions) WithBazelJvm(b bool) *SCAPluginOptions

WithBazelJvm sets whether the Bazel JVM dep-graph scanner should run.

func (*SCAPluginOptions) WithBazelMaxTargets

func (o *SCAPluginOptions) WithBazelMaxTargets(n int) *SCAPluginOptions

WithBazelMaxTargets caps the number of Bazel targets the resolver will process. 0 disables the ceiling. Not calling this leaves the plugin's safe default in place.

func (*SCAPluginOptions) WithBazelTargetQuery

func (o *SCAPluginOptions) WithBazelTargetQuery(query string) *SCAPluginOptions

WithBazelTargetQuery sets the Bazel query used for target discovery (empty = plugin default).

func (*SCAPluginOptions) WithExclude

func (o *SCAPluginOptions) WithExclude(exclude []string) *SCAPluginOptions

func (*SCAPluginOptions) WithExcludePaths

func (o *SCAPluginOptions) WithExcludePaths(excludePaths []string) *SCAPluginOptions

func (*SCAPluginOptions) WithFailFast

func (o *SCAPluginOptions) WithFailFast(failFast bool) *SCAPluginOptions

func (*SCAPluginOptions) WithForceIncludeWorkspacePackages

func (o *SCAPluginOptions) WithForceIncludeWorkspacePackages(forceIncludeWorkspacePackages bool) *SCAPluginOptions

func (*SCAPluginOptions) WithForceSingleGraph

func (o *SCAPluginOptions) WithForceSingleGraph(forceSingleGraph bool) *SCAPluginOptions

func (*SCAPluginOptions) WithGradleAllSubProjects

func (o *SCAPluginOptions) WithGradleAllSubProjects(all bool) *SCAPluginOptions

func (*SCAPluginOptions) WithGradleConfigurationAttributes

func (o *SCAPluginOptions) WithGradleConfigurationAttributes(attributes string) *SCAPluginOptions

func (*SCAPluginOptions) WithGradleConfigurationMatching

func (o *SCAPluginOptions) WithGradleConfigurationMatching(pattern string) *SCAPluginOptions

func (*SCAPluginOptions) WithGradleInitScript

func (o *SCAPluginOptions) WithGradleInitScript(initScript string) *SCAPluginOptions

func (*SCAPluginOptions) WithGradleNormalizeDeps

func (o *SCAPluginOptions) WithGradleNormalizeDeps(normalizeDeps bool) *SCAPluginOptions

func (*SCAPluginOptions) WithGradleSkipWrapper

func (o *SCAPluginOptions) WithGradleSkipWrapper(skipWrapper bool) *SCAPluginOptions

func (*SCAPluginOptions) WithGradleSubProject

func (o *SCAPluginOptions) WithGradleSubProject(subProject string) *SCAPluginOptions

func (*SCAPluginOptions) WithIncludeDev

func (o *SCAPluginOptions) WithIncludeDev(includeDev bool) *SCAPluginOptions

func (*SCAPluginOptions) WithIncludeProvenance

func (o *SCAPluginOptions) WithIncludeProvenance(includeProvenance bool) *SCAPluginOptions

func (*SCAPluginOptions) WithNoBuildIsolation

func (o *SCAPluginOptions) WithNoBuildIsolation(noBuildIsolation bool) *SCAPluginOptions

func (*SCAPluginOptions) WithProjectName

func (o *SCAPluginOptions) WithProjectName(projectName string) *SCAPluginOptions

func (*SCAPluginOptions) WithRawFlags

func (o *SCAPluginOptions) WithRawFlags(rawflags string) *SCAPluginOptions

func (*SCAPluginOptions) WithTargetFile

func (o *SCAPluginOptions) WithTargetFile(targetFile string) *SCAPluginOptions

func (*SCAPluginOptions) WithWorkspacePackage added in v2.2.0

func (o *SCAPluginOptions) WithWorkspacePackage(pkg string) *SCAPluginOptions

type SCAResult

type SCAResult struct {
	DepGraph          *depgraph.DepGraph         `json:"depGraph,omitempty"`
	ProjectDescriptor identity.ProjectDescriptor `json:"projectDescriptor"`
	ResolverMetadata  *ResolverMetadata          `json:"meta,omitempty"`
	ProcessedFiles    []string                   `json:"processedFiles,omitempty"`
	Error             error                      `json:"error,omitempty"`
}

SCAResult represents one Software Composition Analysis result — either a successfully-built dep-graph for one project, or an error surfaced against the project's descriptor.

ProcessedFiles lists the files this result was derived from (lockfile + any manifests consulted). Per-graph attribution; if a consumer wants a deduped union across all results, it computes it itself.

Directories

Path Synopsis
javascript
bun
python
pip
uv
Package scatest provides shared helpers for SCAPlugin tests across pkg/ecosystems/* — chiefly Run, which drives a plugin's BuildDepGraphsFromDir and returns every emitted SCAResult as a slice for the test body to inspect.
Package scatest provides shared helpers for SCAPlugin tests across pkg/ecosystems/* — chiefly Run, which drives a plugin's BuildDepGraphsFromDir and returns every emitted SCAResult as a slice for the test body to inspect.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL