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
- func AnnotateGraph(g *dag.DAG, report *Report)
- func EcosystemFromLanguage(language string) string
- func PackageSeverities(r *Report) map[string]Severity
- func StripLicenseData(g *dag.DAG)
- func StripVulnData(g *dag.DAG)
- type Dependency
- type Finding
- type LicenseInfo
- type LicenseReport
- type LicenseRisk
- type OSVScanner
- type Report
- type Scanner
- type Severity
Constants ¶
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.
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.
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.
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 ¶
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 ¶
EcosystemFromLanguage maps stacktower language identifiers to OSV ecosystem names.
func PackageSeverities ¶
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 ¶
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 ¶
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 (*Report) AddFinding ¶
AddFinding adds a finding to the report and updates summary counts.
func (*Report) HasCritical ¶
HasCritical returns true if the report contains critical severity findings.
func (*Report) HasHighOrCritical ¶
HasHighOrCritical returns true if the report contains high or critical findings.
func (*Report) MaxSeverity ¶
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 ¶
SeverityFromString converts a string to a Severity value. Returns SeverityUnknown for unrecognised strings.
func (Severity) Color ¶
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) IsHighOrCritical ¶
IsHighOrCritical returns true for critical or high severity findings.