security

package
v1.4.0 Latest Latest
Warning

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

Go to latest
Published: Apr 19, 2026 License: Apache-2.0 Imports: 9 Imported by: 0

Documentation

Overview

Package security provides vulnerability scanning and license compliance checking for dependency graphs.

This package defines the Scanner interface and data types for analyzing dependency trees for known vulnerabilities, as well as license risk classification for compliance checking. It provides a common vocabulary that CLI tools, API servers, and CI/CD integrations can use.

Architecture

The package follows the same plugin pattern as [deps.MetadataProvider]:

  • Scanner defines the vulnerability scanning contract
  • Implementations wrap specific vulnerability databases (e.g., OSV.dev)
  • Results are returned as a Report containing Finding entries
  • AnalyzeLicenses classifies all dependencies by license risk
  • LicenseReport summarises compliance status across the graph

Vulnerability Scanning

Create a scanner and scan dependencies:

scanner := security.NewOSVScanner(nil)
deps := security.DependenciesFromGraph(graph, "python")
report, err := scanner.Scan(ctx, deps)
if err != nil {
    log.Fatal(err)
}
fmt.Printf("Found %d vulnerabilities\n", len(report.Findings))

License Compliance

Analyse licenses from the dependency graph metadata:

report := security.AnalyzeLicenses(g) // reads "license" from node metadata
if !report.Compliant {
    fmt.Printf("Copyleft deps: %v\n", report.Copyleft)
    fmt.Printf("Unknown licenses: %v\n", report.Unknown)
}

Integration with Stacktower

The security package is designed to work alongside the existing pipeline:

  • After parsing a dependency graph, extract dependencies with DependenciesFromGraph
  • Run the scanner to produce a Report
  • Run AnalyzeLicenses to classify license risk and annotate nodes
  • Both reports can be displayed in CLI output, rendered as visual indicators, or stored alongside render documents

[deps.MetadataProvider]: github.com/stacktower-io/stacktower/pkg/core/deps.MetadataProvider

Index

Constants

View Source
const MetaLicense = "license"

MetaLicense is the dag.Node.Meta key for the license identifier/SPDX. This is typically a short identifier like "MIT", "Apache-2.0", etc. Populated by registry fetchers during dependency resolution.

View Source
const MetaLicenseRisk = "license_risk"

MetaLicenseRisk is the dag.Node.Meta key used to store the license risk classification for a given package. The value is a LicenseRisk string.

View Source
const MetaLicenseText = "license_text"

MetaLicenseText is the dag.Node.Meta key for the full raw license text. Used when the license is a custom/non-standard license (e.g., full legal text). Can be fed to an LLM for downstream analysis.

View Source
const MetaVulnSeverity = "vuln_severity"

MetaVulnSeverity is the dag.Node.Meta key used to store the maximum vulnerability severity for a given package. The value is a Severity string.

Variables

This section is empty.

Functions

func AnnotateGraph

func AnnotateGraph(g *dag.DAG, report *Report)

AnnotateGraph writes vulnerability severity data into DAG node metadata. After this call, affected nodes will have a MetaVulnSeverity key in their Meta map. Nodes without findings are left untouched. Safe to call with a nil report (no-op).

func EcosystemFromLanguage

func EcosystemFromLanguage(language string) string

EcosystemFromLanguage maps stacktower language identifiers to OSV ecosystem names.

func PackageSeverities

func PackageSeverities(r *Report) map[string]Severity

PackageSeverities returns the maximum severity per package from a report. The map keys are package names matching node IDs in the dependency graph.

func StripLicenseData

func StripLicenseData(g *dag.DAG)

StripLicenseData removes license risk metadata from all nodes in a DAG. This is used when ShowLicenses is false — the renderers will not see license data.

func StripVulnData

func StripVulnData(g *dag.DAG)

StripVulnData removes vulnerability severity metadata from all nodes in a DAG. This is used when ShowVulns is false — the renderers will not see vuln data.

Types

type Dependency

type Dependency struct {
	// Name is the package name as it appears in the registry.
	Name string `json:"name"`

	// Version is the specific version to check. Empty means "latest".
	Version string `json:"version"`

	// Ecosystem identifies the package ecosystem for vulnerability matching.
	// Values follow the OSV ecosystem convention: "npm", "PyPI", "Go",
	// "crates.io", "Maven", "RubyGems", "Packagist".
	Ecosystem string `json:"ecosystem"`
}

Dependency identifies a package to scan for vulnerabilities.

func DependenciesFromDAG

func DependenciesFromDAG(g *dag.DAG, language string) []Dependency

DependenciesFromDAG extracts scannable dependencies from an in-memory DAG. This is the pipeline-friendly counterpart to DependenciesFromGraph. The root package (InDegree == 0, non-synthetic) is excluded for the same reason as DependenciesFromGraph.

func DependenciesFromGraph

func DependenciesFromGraph(g graph.Graph, language string) []Dependency

DependenciesFromGraph extracts scannable dependencies from a serialized graph. It reads package name from node IDs and version from node metadata. The root package (the investigated package itself) is excluded — its own vulnerabilities are out of scope for the investigation.

type Finding

type Finding struct {
	// ID is the primary vulnerability identifier (e.g., "GHSA-xxxx" or "PYSEC-2024-123").
	ID string `json:"id"`

	// Aliases are alternative identifiers (e.g., CVE numbers).
	Aliases []string `json:"aliases,omitempty"`

	// Package is the affected package name.
	Package string `json:"package"`

	// Version is the affected version.
	Version string `json:"version"`

	// Ecosystem is the package ecosystem.
	Ecosystem string `json:"ecosystem"`

	// Summary is a short human-readable description.
	Summary string `json:"summary"`

	// Details is the full vulnerability description (may be empty).
	Details string `json:"details,omitempty"`

	// Severity is the assessed severity level.
	Severity Severity `json:"severity"`

	// FixVersions lists versions that fix this vulnerability (may be empty).
	FixVersions []string `json:"fix_versions,omitempty"`

	// References contains URLs to advisories, patches, etc.
	References []string `json:"references,omitempty"`
}

Finding represents a single vulnerability associated with a dependency.

type LicenseInfo

type LicenseInfo struct {
	// Package is the package/dependency identifier (e.g., "lodash", "requests").
	Package string `json:"package" bson:"package"`

	// License is the license identifier or title (e.g., "MIT", "Apache-2.0", or custom name).
	License string `json:"license" bson:"license"`

	// LicenseText is the full raw license text, if available.
	// Populated for custom/proprietary licenses to enable LLM analysis.
	// May be empty for standard SPDX licenses where the text is well-known.
	LicenseText string `json:"license_text,omitempty" bson:"licenseText,omitempty"`

	// Risk is the classified risk level for this license.
	Risk LicenseRisk `json:"risk" bson:"risk"`
}

LicenseInfo contains detailed license information for a single package. This structure captures both the identifier and full text for LLM analysis.

type LicenseReport

type LicenseReport struct {
	// Licenses maps license names to the list of packages using that license.
	// Example: "MIT": ["express", "lodash"]
	Licenses map[string][]string `json:"licenses" bson:"licenses"`

	// Details contains full license information for each dependency.
	// Keyed by package ID for easy lookup. Includes license text when available.
	Details map[string]*LicenseInfo `json:"details,omitempty" bson:"details,omitempty"`

	// Copyleft lists packages with copyleft licenses that may require attention.
	Copyleft []string `json:"copyleft" bson:"copyleft"`

	// WeakCopyleft lists packages with weak copyleft licenses (LGPL, MPL, etc.).
	// BSON tag uses camelCase to match default Go MongoDB driver behavior for existing data.
	WeakCopyleft []string `json:"weak_copyleft" bson:"weakCopyleft"`

	// Proprietary lists packages with commercial/source-available licenses
	// (SSPL, BUSL, Commons-Clause, or detected custom commercial licenses).
	Proprietary []string `json:"proprietary" bson:"proprietary"`

	// Unknown lists packages where the license could not be determined.
	Unknown []string `json:"unknown" bson:"unknown"`

	// Compliant is true when no copyleft, proprietary, or unknown licenses are present.
	// Note: weak-copyleft licenses do NOT break compliance by default since
	// they typically allow linking from proprietary code.
	Compliant bool `json:"compliant" bson:"compliant"`

	// TotalDeps is the number of dependencies that were analysed.
	TotalDeps int `json:"total_deps" bson:"totalDeps"`
}

LicenseReport summarises license compliance across all dependencies.

func AnalyzeLicenses

func AnalyzeLicenses(g *dag.DAG) *LicenseReport

AnalyzeLicenses scans all nodes in a DAG for license metadata and produces a LicenseReport. It also annotates each node with a MetaLicenseRisk value.

License data is read from the node's "license" metadata key (populated by registry fetchers during dependency resolution).

This function is safe to call on a nil DAG (returns an empty report).

type LicenseRisk

type LicenseRisk string

LicenseRisk classifies how restrictive a license is for downstream consumers.

const (
	// LicenseRiskPermissive means the license imposes minimal restrictions
	// (e.g., MIT, Apache-2.0, BSD, ISC). No action required.
	LicenseRiskPermissive LicenseRisk = "permissive"

	// LicenseRiskWeakCopyleft means the license requires derivative *library*
	// changes to be shared but allows proprietary linking (e.g., LGPL, MPL).
	LicenseRiskWeakCopyleft LicenseRisk = "weak-copyleft"

	// LicenseRiskCopyleft means the license requires all derivative works to
	// be distributed under the same terms (e.g., GPL, AGPL). This is the
	// most restrictive category and is typically flagged in proprietary projects.
	LicenseRiskCopyleft LicenseRisk = "copyleft"

	// LicenseRiskProprietary means the license is a known commercial/source-available
	// license that restricts usage (e.g., SSPL, BUSL, Commons-Clause, custom commercial).
	// These are NOT open source and typically require paid licenses or have usage restrictions.
	LicenseRiskProprietary LicenseRisk = "proprietary"

	// LicenseRiskUnknown means the license could not be determined. This is
	// flagged because unlicensed code defaults to "all rights reserved".
	LicenseRiskUnknown LicenseRisk = "unknown"
)

func ClassifyLicense

func ClassifyLicense(license string) LicenseRisk

ClassifyLicense determines the risk category for a license string. It handles SPDX identifiers, common names, and compound expressions (e.g., "MIT OR Apache-2.0"). For compound OR expressions, the least restrictive alternative is used (the user can choose).

func LicenseRiskFromString

func LicenseRiskFromString(s string) LicenseRisk

LicenseRiskFromString converts a string to a LicenseRisk value. Returns empty string for unrecognised values.

func (LicenseRisk) IconColor

func (r LicenseRisk) IconColor() string

IconColor returns the fill colour for the license-risk flag icon. Only called when IsFlagged() is true.

func (LicenseRisk) IsFlagged

func (r LicenseRisk) IsFlagged() bool

IsFlagged returns true if this risk level should be visually indicated. Permissive licenses are not flagged.

func (LicenseRisk) String

func (r LicenseRisk) String() string

String returns the string representation of the license risk.

func (LicenseRisk) Weight

func (r LicenseRisk) Weight() int

Weight returns a numeric weight for sorting (higher = more restrictive).

type OSVScanner

type OSVScanner struct {
	// contains filtered or unexported fields
}

OSVScanner implements Scanner using the OSV.dev vulnerability database.

OSVScanner uses batch queries for efficiency — a single API call can check hundreds of packages. Results are mapped to the generic Finding type.

OSVScanner is safe for concurrent use.

func NewOSVScanner

func NewOSVScanner(client *osv.Client) *OSVScanner

NewOSVScanner creates a scanner backed by OSV.dev. If client is nil, a default client is created.

func (*OSVScanner) Scan

func (s *OSVScanner) Scan(ctx context.Context, deps []Dependency) (*Report, error)

Scan queries OSV.dev for vulnerabilities in the given dependencies.

Dependencies are batched into a single API call. The returned report contains one Finding per (package, vulnerability) pair.

Returns an error only for network/API failures. An empty dependency list returns an empty report.

type Report

type Report struct {
	// Findings is the list of individual vulnerability findings.
	Findings []Finding `json:"findings"`

	// SeveritySummary counts findings by severity level.
	SeveritySummary map[Severity]int `json:"severity_summary"`

	// TotalDeps is the number of dependencies that were scanned.
	TotalDeps int `json:"total_deps"`

	// VulnerableDeps is the number of dependencies with at least one finding.
	VulnerableDeps int `json:"vulnerable_deps"`

	// ScannedAt is the timestamp when the scan was performed.
	ScannedAt time.Time `json:"scanned_at"`
}

Report contains the results of a vulnerability scan.

func NewReport

func NewReport(totalDeps int) *Report

NewReport creates an empty report with the current timestamp.

func (*Report) AddFinding

func (r *Report) AddFinding(f Finding)

AddFinding adds a finding to the report and updates summary counts.

func (*Report) HasCritical

func (r *Report) HasCritical() bool

HasCritical returns true if the report contains critical severity findings.

func (*Report) HasHighOrCritical

func (r *Report) HasHighOrCritical() bool

HasHighOrCritical returns true if the report contains high or critical findings.

func (*Report) MaxSeverity

func (r *Report) MaxSeverity() Severity

MaxSeverity returns the highest severity level found in the report.

type Scanner

type Scanner interface {
	// Scan analyzes the given dependencies and returns a vulnerability report.
	// An empty dependency list returns an empty report (not an error).
	// Network errors or API failures are returned as errors.
	Scan(ctx context.Context, deps []Dependency) (*Report, error)
}

Scanner analyzes dependencies for known vulnerabilities.

Implementations query vulnerability databases (e.g., OSV.dev) and return structured reports. Scanners should support batch queries for efficiency.

Scanner implementations must be safe for concurrent use.

type Severity

type Severity string

Severity represents the severity level of a vulnerability finding.

const (
	// SeverityCritical indicates a critical vulnerability requiring immediate attention.
	SeverityCritical Severity = "critical"

	// SeverityHigh indicates a high-severity vulnerability.
	SeverityHigh Severity = "high"

	// SeverityMedium indicates a medium-severity vulnerability.
	SeverityMedium Severity = "medium"

	// SeverityLow indicates a low-severity vulnerability.
	SeverityLow Severity = "low"

	// SeverityUnknown is used when severity cannot be determined.
	SeverityUnknown Severity = "unknown"
)

func SeverityFromString

func SeverityFromString(s string) Severity

SeverityFromString converts a string to a Severity value. Returns SeverityUnknown for unrecognised strings.

func (Severity) Color

func (s Severity) Color() string

Color returns the hex fill color for this severity level. Healthy/unaffected nodes should keep their default style (grey or white); this method is only called when a vulnerability is present.

func (Severity) DOTColor

func (s Severity) DOTColor() string

DOTColor returns the Graphviz fill color name or hex for DOT rendering.

func (Severity) IsHighOrCritical

func (s Severity) IsHighOrCritical() bool

IsHighOrCritical returns true for critical or high severity findings.

func (Severity) String

func (s Severity) String() string

String returns the string representation of the severity.

func (Severity) TextColor

func (s Severity) TextColor() string

TextColor returns a contrasting text color for this severity's background.

func (Severity) Weight

func (s Severity) Weight() int

Weight returns a numeric weight for sorting (higher = more severe).

Jump to

Keyboard shortcuts

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