sdk

package module
v0.9.4 Latest Latest
Warning

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

Go to latest
Published: Sep 6, 2026 License: Apache-2.0 Imports: 36 Imported by: 0

README

Bomly SDK

CI OpenSSF Scorecard Latest release Go Reference

github.com/bomly-dev/bomly-sdk is the contract module for building Bomly components: detectors, matchers, auditors, and analyzers. It contains the neutral domain types (dependencies, packages, vulnerabilities, findings, the package registry), the component interfaces, and the managed-plugin serving adapters and gRPC protocol used by external plugin binaries.

go get github.com/bomly-dev/bomly-sdk@latest

Building a plugin

A Bomly plugin is a standalone Go binary that imports this module and serves one component over the managed-plugin runtime:

package main

import sdk "github.com/bomly-dev/bomly-sdk"

func main() {
	sdk.ServeDetector(myDetector{})
}

See the Bomly plugin documentation for the full authoring guide, packaging layout (bomly-plugin.json), and installation flow.

Embed the Base* types (sdk.BaseDetector, sdk.BaseMatcher, sdk.BaseAuditor, sdk.BaseAnalyzer) in your implementation so future additions to the component interfaces do not break your build.

Helper packages

The SDK ships shared helper subpackages so component modules and external plugins reuse the same implementations Bomly's built-ins use:

  • system — bounded filesystem reads plus exec, path, and environment wrappers.
  • filecache — TTL-based on-disk JSON cache with typed Get/Set helpers.
  • logkit — secret-safe subprocess logging: argument/URL sanitizers, command fields, stderr counter.
  • detectorkit — detector helpers: manifest metadata, source positions, remediation hints, subgraphs, build-tool readiness and timeouts.
  • matcherkit — matcher helpers: registry package seeding and license normalization.
  • testkit — test helpers: fuzz graph invariants, typed-node constructors, Go binary builders, lockfile position assertions.

Compatibility

Two independent compatibility axes govern this module:

  1. In-process (Go API) — the component interfaces and types consumed by embedders. Signature changes require a recompile. Embedding the Base* defaults insulates implementations from most interface growth.
  2. Wire (managed-plugin protocol bomly.plugin.v1) — JSON payloads exchanged with external plugin binaries. Within protocol v1, changes are strictly additive: new optional (omitempty) fields and new optional RPCs only. Hosts treat unimplemented RPCs as feature fall-backs; unknown JSON fields are ignored by both sides. Fields and RPCs are never removed, renamed, or repurposed within v1. A breaking wire change would ship as a new bomly.plugin.v2 service negotiated alongside v1 — old binaries keep speaking v1.

Plugin binaries built against an older SDK release keep working against newer hosts (and vice versa) as long as both speak protocol v1.

Versioning and releases

Releases are plain semver tags (vX.Y.Z) cut from main. While the module is v0, minor releases may adjust the in-process Go API (the wire contract stays additive regardless); patch releases are always safe. Consumers — Bomly itself and plugin repositories — should pin released versions, never commits or branches.

Release ordering when the contract changes: this module tags first, plugin repositories adopt the new tag, then Bomly updates its pin.

License

Apache-2.0. See LICENSE.

Documentation

Overview

Package sdk is Bomly's public Go contract for dependency graphs, package enrichment, policy findings, and managed external plugins.

Most external developers use this package to build a managed plugin. Managed plugins are native Go binaries that Bomly launches as separate subprocesses over the HashiCorp go-plugin gRPC transport. A plugin implements exactly one externally supported role:

  • detector: reads project evidence and returns dependency graphs
  • matcher: enriches PURL-keyed package records with vulnerability, license, lifecycle, or other package metadata
  • auditor: evaluates graph and registry data and emits findings or risk scores
  • analyzer: runs code analysis (e.g. reachability) over the matched graph and annotates registry vulnerability entries

A plugin binary serves its role from main by calling one of the runtime entrypoints:

func main() {
	sdk.ServeDetector(&detector{})
}

The corresponding plugin-facing interfaces are ServedDetector, ServedMatcher, ServedAuditor, and ServedAnalyzer. They use the same request and response types as Bomly core: DetectionRequest and DetectionResult for detectors, MatchRequest and MatchResult for matchers, AuditRequest and AuditResult for auditors, and AnalyzeRequest and AnalyzeResult for analyzers.

The central data model deliberately separates pipeline stages. Dependency is a detection-time graph node with identity, locations, scopes, and edges. PackageRegistry is a PURL-keyed set of deduplicated Package records that matchers enrich once per package version. Vulnerability records are OSV-aligned package enrichment data, including Bomly fields such as CVSS, EPSS, KEV, fixed versions, affected symbols, and reachability. Finding is a reference-style audit result: it points back to packages by PURL and, for vulnerability findings, to Vulnerability.ID rather than copying the whole package or advisory payload.

Coordinates is the shared embedded identity shape used by Dependency and Package. Plugin authors should prefer canonical PURLs, fill Coordinates where possible, and use typed values such as Ecosystem, PackageManager, PackageType, Scope, and SeverityLevel instead of raw strings. PackageManager is string-backed for compatibility; use PackageManagerOther or a custom PackageManager value when Bomly does not yet have a first-class constant for a package manager.

Node identity is derived, never hand-assembled (ADR-0041): the constructors are the only mint — a dependency node's ID is its canonical package URL (custom purl types are first-class; express any ecosystem as a purl type), and module and manifest nodes carry kind-qualified canonical paths. Never build a node ID by string concatenation.

Plugin identity is split across package metadata and runtime metadata. The bomly-plugin.json manifest describes packaging and install fields such as ID, version, kind, runtime, plugin API version, entrypoint, homepage, and license. The runtime descriptor returned by Descriptor describes the served component: name, display name, aliases, tags, supported ecosystems, supported package managers, and role-specific behavior. Bomly verifies that manifest identity and runtime descriptor identity match when a packaged plugin is installed, and records installed trust state separately.

Attribution is per site, not per package (phase 1.4). A package's scope and directness belong to the location it was found at: in a workspace the same version is a direct development dependency of one module and a transitive runtime dependency of another, so a node's unions answer neither question. PackageLocation carries the module root, scopes, and relationship; reachability is per-module-root evidence with the vulnerability annotation as the derived summary; and SelectUsages joins the two within one module root so a conjunctive question is a statement about a usage that exists. Read scopes through AttributedScopes rather than the node field, and expect both to be empty until the producers migrate.

Merges are classed rather than hand-written. MergeFillGap, MergeUnion, and MergeStrongest name the three rules every field in this model follows, and each field declares which class it is in. A merge written by hand is where this model has repeatedly lost data — a first-wins rule dropping a better value, an early return leaving an ungated claim visible, an unsorted result making a document's bytes depend on read order — so fixing a class is preferred to fixing a field.

Metadata maps carry what the typed fields do not, and the "bomly." prefix is reserved for this project (IsReservedMetadataKey). A value that lives only in a metadata map is invisible to every gate — not normalized, not validated, not merged by a declared rule, not projected to either document format — so anything a typed field can hold belongs in the typed field.

Plugins that need configuration should read only their per-plugin config with DecodePluginConfigFromEnv. Plugins that make HTTP calls should create a process-local provider with NewHTTPClientProviderFromEnv so Bomly's proxy, no-proxy, and CA certificate settings are honored consistently.

The repository documentation contains the workflow-oriented guides for packaging, installing, testing, and distributing plugins. This package documentation is the API-oriented reference for the types those guides use.

Index

Constants

View Source
const (
	// EnvHTTPProxy is Bomly's explicit outbound HTTP proxy environment variable.
	EnvHTTPProxy = "BOMLY_HTTP_PROXY"
	// EnvHTTPNoProxy is Bomly's explicit proxy bypass list environment variable.
	EnvHTTPNoProxy = "BOMLY_HTTP_NO_PROXY"
	// EnvHTTPProxyType is Bomly's explicit outbound proxy type.
	EnvHTTPProxyType = "BOMLY_HTTP_PROXY_TYPE"
	// EnvHTTPProxyHost is Bomly's explicit outbound proxy host.
	EnvHTTPProxyHost = "BOMLY_HTTP_PROXY_HOST"
	// EnvHTTPProxyPort is Bomly's explicit outbound proxy port.
	EnvHTTPProxyPort = "BOMLY_HTTP_PROXY_PORT"
	// EnvHTTPProxyUsername is Bomly's explicit outbound proxy username.
	EnvHTTPProxyUsername = "BOMLY_HTTP_PROXY_USERNAME"
	// EnvHTTPProxyPassword is Bomly's explicit outbound proxy password.
	EnvHTTPProxyPassword = "BOMLY_HTTP_PROXY_PASSWORD"
	// EnvHTTPCACertFile points to an additional PEM certificate chain for outbound HTTPS.
	EnvHTTPCACertFile = "BOMLY_HTTP_CA_CERT_FILE"
	// EnvPluginConfigFile points external plugins at their per-plugin JSON config.
	EnvPluginConfigFile = "BOMLY_PLUGIN_CONFIG_FILE"
	// EnvPluginID identifies the managed plugin currently being executed.
	EnvPluginID = "BOMLY_PLUGIN_ID"
)
View Source
const CapabilityPackageUpdates = "package-updates-v1"

CapabilityPackageUpdates is advertised in a matcher's or analyzer's descriptor Capabilities to signal that it can return MatchResult.PackageUpdates / AnalyzeResult.PackageUpdates deltas when the request sets AcceptPackageUpdates. Hosts and plugins that do not know this capability keep exchanging full registries — the protocol v1 baseline.

View Source
const CycloneDXScopeProperty = "bomly:scopes"

CycloneDXScopeProperty is the property name carrying the full scope set through a CycloneDX document, so the projection below is not a one-way door.

The "bomly:" prefix follows the CycloneDX guidance that property names be namespaced by their producer, which is what keeps this from colliding with another tool's property of the same purpose.

View Source
const EnvVerbosity = "BOMLY_VERBOSE"

EnvVerbosity mirrors the host's verbosity environment variable (0 = normal, 1 = verbose, 2+ = debug). Managed components derive their stderr log level from it when present.

View Source
const (
	ExploitabilityValueExploitable = "exploitable"
)

ExploitabilityValueExploitable constraint values currently supported.

View Source
const GenericFallbackTypeQualifier = "bomly_source_type"

GenericFallbackTypeQualifier names the package URL type that could not express a package whose identity fell back to pkg:generic.

It is prefixed because it is this project's, not the specification's: a consumer reading an exported document should be able to tell a Bomly annotation from a purl-spec qualifier at a glance.

View Source
const InstalledPluginsSchemaVersion = "bomly.installed-plugins.v1"

InstalledPluginsSchemaVersion is the installed plugin database schema version.

View Source
const MetadataKeyDetectionLicenses = "bomly.detection.licenses"

MetadataKeyDetectionLicenses is the Dependency.Metadata key under which detectors that discover license facts at detection time (e.g. SBOM-backed detectors) stash []PackageLicense for consolidation to lift into the package registry.

Deprecated: use SetDetectionLicenses and DetectionLicenses, which write and read the typed field. The stash predates that field and is still read on ingest so a payload written by an older producer is not dropped, but nothing should write it.

The stash is why this deprecation exists rather than a rename. A value here is invisible to every gate -- not normalized, not validated, not merged by a declared rule, not projected to either document format -- so a license that lived only in this key was dropped by any consumer that had not learned to look for it. That is the failure the typed field removes, and keeping both spellings writable would leave one of them able to reintroduce it.

View Source
const MetadataKeyNPM = "npm"

MetadataKeyNPM is the Metadata map key for *NPMPackageMetadata.

View Source
const PackageManifestSchemaVersion = "bomly.plugin.package.v1"

PackageManifestSchemaVersion is the package manifest schema version.

View Source
const PluginAPIVersion = "bomly.plugin.v1"

PluginAPIVersion is the current managed plugin API contract version.

View Source
const (
	ReachabilityValueReachable = "reachable"
)

ReachabilityValueReachable constraint values currently supported.

View Source
const ReservedMetadataPrefix = "bomly."

ReservedMetadataPrefix is the key prefix reserved for Bomly's own use. Component authors namespace their keys by their own component name instead.

View Source
const RuntimeDescriptorSnapshotSchemaVersion = "bomly.plugin.runtime-descriptor.v1"

RuntimeDescriptorSnapshotSchemaVersion is Bomly's internal installed descriptor snapshot schema.

View Source
const RuntimeHashiCorpGRPC = "hashicorp-grpc"

RuntimeHashiCorpGRPC identifies the supported external plugin runtime.

View Source
const (
	SourceChangeValue = "source-change"
)

SourceChangeValue is the supported dependency source-change constraint.

Variables

View Source
var (
	ErrNilNode          = errors.New("graph node is nil")
	ErrEmptyNodeID      = errors.New("graph node id is empty")
	ErrNodeAlreadyExist = errors.New("graph node already exists")
	ErrNodeNotFound     = errors.New("graph node not found")
	ErrSelfDependency   = errors.New("self dependency is not allowed")
	ErrCycleDetected    = errors.New("dependency creates a cycle")
)

Functions

func BuildPackageURL

func BuildPackageURL(purlType, namespace, name, version string) string

BuildPackageURL builds and normalizes a package URL from its parts.

func CanonicalPackageURLFromParts

func CanonicalPackageURLFromParts(existingPURL string, ecosystem Ecosystem, packageManager PackageManager, typ PackageType, org, name, version string) string

CanonicalPackageURLFromParts returns the canonical package URL derived from raw identity fields. existingPURL takes precedence when it canonicalizes.

func CanonicalRepoPath added in v0.6.0

func CanonicalRepoPath(value string) (string, error)

CanonicalRepoPath is the constructor-enforced gate for every path that participates in node identity: the canonical repository-relative, slash-separated form. Backslashes normalize to slashes and the path is cleaned; an empty result, an absolute path, a drive-letter path, a parent escape, or a path carrying '#' or control characters is rejected — the module-ID grammar joins a path and a name with '#', and a raw checkout path would make identities vary across machines.

func CanonicalizePackageURL

func CanonicalizePackageURL(value string) string

CanonicalizePackageURL normalizes a package URL string when possible. It delegates to purlkit, the single home for package-URL behavior.

func ClientPluginMap

func ClientPluginMap() map[string]hplugin.Plugin

ClientPluginMap returns the client-side plugin map used by Bomly core.

func ConfigSchemaFor

func ConfigSchemaFor(prototype any) (json.RawMessage, error)

ConfigSchemaFor derives a JSON Schema (draft 2020-12 subset) for a component's configuration block from a prototype struct. Declare your configuration once as a typed Go struct, decode it at runtime with DecodePluginConfigFromEnv, and advertise its shape in the descriptor:

type Config struct {
	Endpoint string `json:"endpoint" doc:"API endpoint override" default:"https://api.example.com"`
	Timeout  int    `json:"timeoutSeconds" doc:"Request timeout in seconds" default:"30"`
	Strict   bool   `json:"strict" doc:"Fail on partial results"`
}

descriptor.ConfigSchema = sdk.MustConfigSchemaFor(Config{})

Recognized struct tags: `json` (property name and omission), `doc` (property description), and `default` (default value, converted to the field's type). Nested structs, pointers, slices, and string-keyed maps are supported. Unexported fields and fields tagged `json:"-"` are skipped.

func CopyEdgesInto added in v0.7.0

func CopyEdgesInto(dst, src *Graph, rename func(string) string) error

CopyEdgesInto copies every edge of src into dst, keeping each edge's kind and mapping node IDs through rename. A nil rename copies IDs unchanged; a rename that returns "" drops the edge, which is how a filtered graph omits edges to nodes it did not keep.

This is the one primitive for rebuilding a graph's edges. Every site that used to walk edges and call AddEdge -- the container merge, the JSON decoder, the scope filter -- is a place a new edge field would be dropped silently, and there were four of them. Routing them all through here means the next field added to an edge is carried by all four at once.

An edge that becomes a self-edge after renaming is skipped, not an error: folding two nodes into one legitimately collapses the edge between them.

func CycloneDXScope added in v0.7.0

func CycloneDXScope(scopes []Scope) string

CycloneDXScope projects a scope set onto CycloneDX's scalar component scope. It returns "" when the set says nothing, which a caller writes as no scope at all rather than as a guess.

The rule is that a package reachable at runtime is required, and a package reachable only from development roots is excluded -- CycloneDX's word for a component that is present in the source tree but not in what ships. Runtime wins over development in a mixed set for the same reason MergeScope prefers it: a package reachable at runtime ships, whatever else is also true of it.

"optional" is never produced. It means "provides additional functionality", a distinction Bomly's scope vocabulary does not draw, and inventing it here would put a claim in a document that no detector made.

func DecodePluginConfigFromEnv

func DecodePluginConfigFromEnv(target any) error

DecodePluginConfigFromEnv decodes the current plugin's JSON config file into target. Bomly writes this file from the enabled plugin's own plugins.<plugin-id> config block and exposes its path through the plugin environment.

func EncodeScopeSet added in v0.7.0

func EncodeScopeSet(scopes []Scope) string

EncodeScopeSet renders a scope set as a carrier value: the canonical scope tokens, sorted and comma-separated. It returns "" when there is nothing to carry, which a caller writes as no property at all.

Sorted because a document is built from this, and two runs that found the same scopes in a different order must produce the same bytes.

func FindingPolicyStatusRank

func FindingPolicyStatusRank(status FindingPolicyStatus) (int, bool)

FindingPolicyStatusRank returns the enforcement rank for a finding policy status. An omitted status retains the historical fail behavior. The boolean is false for unknown values.

func HandshakeConfig

func HandshakeConfig() hplugin.HandshakeConfig

HandshakeConfig returns the shared HashiCorp go-plugin handshake configuration.

func IsNilNode added in v0.9.0

func IsNilNode(node GraphNode) bool

IsNilNode reports whether a node value carries no node, a typed nil included.

A typed nil is not an untyped one: comparing the interface against nil is false for a (*DependencyNode)(nil), and the next method call dereferences it. Every helper that accepts a GraphNode from a caller's slice needs this, so it is exported rather than repeated.

func IsProjectOwned added in v0.9.0

func IsProjectOwned(node GraphNode) bool

IsProjectOwned reports whether a node stands for the scanned project's own code -- its root package, a workspace member, a reactor module -- rather than a package it consumes.

It reads the node kind. ADR-0041 removed the FirstParty flag because ownership is what the kind means: the project's own artifacts are module nodes, and a dependency node is a consumed package by construction. The application package type is not sufficient on its own -- an application-typed *import* is a consumed package, and treating it as owned is what kept such packages out of diffing and matching.

func IsReservedMetadataKey added in v0.7.0

func IsReservedMetadataKey(key string) bool

IsReservedMetadataKey reports whether a key is in Bomly's reserved namespace. The comparison is case-insensitive, since a key differing only in case is a collision a reader would not see.

func MergeFillGap added in v0.7.0

func MergeFillGap[T comparable](current, next T, publishable func(T) bool) T

MergeFillGap returns the current value when it is stated, and the replacement only when there is a gap to fill.

The gate matters as much as the rule. The destination is checked for publishability before the gap is measured, so an unpublishable non-empty value does not count as "stated": leaving it in place would block a valid replacement and then be dropped at encode, losing both. That defect was found in the M1 review and is the reason this takes a validity test rather than comparing against the zero value.

func MergeGraph

func MergeGraph(dst, src *Graph) error

MergeGraph adds all nodes and relationships from src into dst.

func MergeStrongest added in v0.7.0

func MergeStrongest[T comparable](current, next T, rank func(T) int) T

MergeStrongest returns whichever value ranks higher, treating an unranked value as absent so a stated value always beats an unstated one.

rank must be a total order on the vocabulary. Ties keep the current value, which makes the result independent of the order the two sides arrived in -- a property two graphs merged in either direction depend on.

func MergeUnion added in v0.7.0

func MergeUnion[T any](current, next []T, key func(T) string, publishable func(T) (T, bool)) []T

MergeUnion appends the members of next that are not already in current, keyed by key, and returns the result sorted by that key.

It never returns early. A merge that returns when next is empty leaves the destination exactly as it was -- including any member that would not survive its own gate -- so an ungated claim stays visible in process and disappears only at encode. Running the whole pass unconditionally is what makes the gate total.

Sorted because documents are built from these: two runs that found the same members in a different order must produce the same bytes.

func MustConfigSchemaFor

func MustConfigSchemaFor(prototype any) json.RawMessage

MustConfigSchemaFor is ConfigSchemaFor that panics on error. Use it for static descriptor initialization where the prototype is a compile-time constant shape.

func NewHTTPClient

func NewHTTPClient(config HTTPClientConfig) (*http.Client, error)

NewHTTPClient creates an outbound HTTP client using Go's default transport behavior plus Bomly's proxy configuration.

func NodeDisplayName added in v0.9.0

func NodeDisplayName(node GraphNode) string

NodeDisplayName returns the name a node shows under: the ecosystem-native display name for a node with coordinates, and the path for a manifest, which is the only name a manifest has.

func NodePURL added in v0.9.2

func NodePURL(node GraphNode) string

NodePURL returns the package URL a node publishes, or "" when it has none.

The three kinds answer differently, which is why this lives here: a dependency node's ID is its canonical package URL; a module's ID is the structural "module:<path>#..." grammar and its package URL is a separate field, derived only when its coordinates genuinely allow one; a manifest is a file and has none. Written per consumer, the switch drifts -- the CLI's interactive view rendered a module's NodeID under a column labelled "PURL", handing the viewer a string no consumer can parse as a package URL, while its SBOM and JSON paths had the projection right.

A module whose coordinates could not mint a package URL returns "" rather than a fallback. NewModuleNode already declined to fabricate one for it (a module is the project's own record, and the generic type is a registry lookup fabrication), and this reports what the node publishes, not what it might have.

func NodeVersion added in v0.9.0

func NodeVersion(node GraphNode) string

NodeVersion returns the version a node carries, or "" when it carries none.

func NormalizeCoordinates added in v0.6.0

func NormalizeCoordinates(pkg *Coordinates) []string

NormalizeCoordinates applies ecosystem-aware identity normalization to the coordinate fields in place and returns which rules applied. It is the pre-minting step of node construction — normalize, then mint the canonical package URL, then construct — and records nothing itself: the constructors store the provenance breadcrumbs on the node.

func NormalizeDescription added in v0.7.0

func NormalizeDescription(value string) string

NormalizeDescription is the gate for a component description. Descriptions arrive from untrusted registry records and SBOM documents and are rendered into terminals and written into published documents, so the value is trimmed, bounded, and stripped of control characters that would corrupt the output. Line breaks and tabs survive: both formats carry multi-line descriptions, and removing them would damage a legitimate value.

Over-long input yields "" rather than a truncation, because half a description attributed to a package is a false assertion where no description is merely a missing one.

func NormalizeHomepage added in v0.7.0

func NormalizeHomepage(value string) string

NormalizeHomepage is the gate for a component homepage: URLFormReference, which keeps a bare host and a query -- both normal for a project page -- while rejecting credentials, local paths, and non-http schemes. It returns "" when the value cannot be published.

func NormalizeOriginURL added in v0.4.0

func NormalizeOriginURL(raw string, repository bool) (string, bool)

NormalizeOriginURL is the origin-specific spelling of NormalizeURL, kept as the name detectors and plugins already call. The repository argument selects URLFormRepository; false selects URLFormArtifact.

func NormalizeURL added in v0.7.0

func NormalizeURL(raw string, form URLForm) (string, bool)

NormalizeURL is the single rule every published URL satisfies. Apply it when recording a URL and again when reading one back, so a value that arrives from a plugin or a hand-built graph is held to the same standard as one from a built-in component.

A value passes only when it is an absolute http or https URL with a host and no embedded credentials; the result is re-serialized from the parse rather than returned as given. Everything else -- local paths, file://, git@host:org/repo, ssh://, git+ssh://, "git+" prefixes, and URLs carrying userinfo -- is rejected, so filesystem layout and credentials cannot reach a published document. The form argument selects the remaining rules; see URLForm.

func PackageURLBase

func PackageURLBase(value string) string

PackageURLBase strips version, qualifiers, and subpath from a package URL. It delegates to purlkit.Base, which works on the parsed structure — the previous string surgery mishandled subpath-carrying and version-less package URLs.

func PackageURLTypeForValues

func PackageURLTypeForValues(values ...any) string

PackageURLTypeForValues maps ecosystem/build-system values to a package-url type.

The explicit switch below is the authority: it is consulted for every value before the loose fallback runs, so the most specific mapping wins regardless of the order the caller passes ecosystem / package manager / package type in. The fallback then returns the first non-empty value verbatim, which is only correct where the Bomly identifier happens to be the purl type as well (npm, maven, apk, rpm, ...). Any ecosystem whose purl type differs from its Bomly name needs an explicit case here — without one we emit a type that is not in the purl spec, and consumers keyed on the type (OSV, SBOM ingest) silently fail to match. See issue #317.

Ecosystems that span more than one registry are the exception: erlang covers both Hex (rebar) and OTP (*.app), so it is mapped at the package-manager level only. A bare erlang value with no manager to disambiguate keeps the non-spec pkg:erlang rather than guessing a registry the package may not be published to.

func RawPluginConfigFromEnv

func RawPluginConfigFromEnv() ([]byte, error)

RawPluginConfigFromEnv reads the per-plugin JSON config file named by BOMLY_PLUGIN_CONFIG_FILE. It returns nil when no plugin config file is set.

func ReservedMetadataKeys added in v0.7.0

func ReservedMetadataKeys(metadata map[string]any) []string

ReservedMetadataKeys returns the reserved keys present in a metadata map, sorted. A component runtime uses it to warn an external plugin that is writing into Bomly's namespace, rather than letting the collision be found later as data read as the wrong thing.

func ServeAnalyzer

func ServeAnalyzer(analyzer ServedAnalyzer)

ServeAnalyzer serves one analyzer plugin over Bomly's managed HashiCorp go-plugin gRPC transport. Call it from the plugin binary's main function.

func ServeAuditor

func ServeAuditor(auditor ServedAuditor)

ServeAuditor serves one auditor plugin over Bomly's managed HashiCorp go-plugin gRPC transport. Call it from the plugin binary's main function.

func ServeDetector

func ServeDetector(detector ServedDetector)

ServeDetector serves one detector plugin over Bomly's managed HashiCorp go-plugin gRPC transport. Call it from the plugin binary's main function.

func ServeMatcher

func ServeMatcher(matcher ServedMatcher)

ServeMatcher serves one matcher plugin over Bomly's managed HashiCorp go-plugin gRPC transport. Call it from the plugin binary's main function.

func ServeModule added in v0.2.0

func ServeModule(m Module)

ServeModule serves one Module as a managed plugin over Bomly's HashiCorp go-plugin gRPC transport. Call it from the plugin binary's main function. It validates the module, builds a managed HostContext (stderr logger, HTTP client provider from Bomly environment variables, config decoding from the file named by BOMLY_PLUGIN_CONFIG_FILE), constructs the component lazily on first use, and adapts it to the served plugin protocol.

func SetDetectionLicenses

func SetDetectionLicenses(dep *DependencyNode, licenses []PackageLicense)

SetDetectionLicenses records detection-time license facts on dep, so consolidation can lift them into the package registry. No-op when dep is nil or licenses is empty.

It now writes the typed DependencyNode.Licenses field rather than the metadata stash, which is the migration ADR-0037 calls for: the path for a metadata key is a typed field. Callers of this helper need no change, and payloads written by an older producer are still read — see DetectionLicenses.

func SeverityMeets

func SeverityMeets(candidate SeverityLevel, threshold string) bool

SeverityMeets reports whether candidate's severity is at or above threshold. Threshold "any" matches every candidate, including unknown.

func SeverityRank

func SeverityRank(severity SeverityLevel) int

SeverityRank returns a comparable rank for a severity string. Unknown / empty values rank below "low". The GitHub-aligned levels share the ladder with the CVSS bands: error ≡ high, warning ≡ medium, note ≡ low.

func SortDependencyDetailTransitions

func SortDependencyDetailTransitions(transitions []DependencyDetailTransition)

SortDependencyDetailTransitions orders detail changes deterministically.

func ValidateAnalyzerDescriptor

func ValidateAnalyzerDescriptor(descriptor *AnalyzerDescriptor) error

ValidateAnalyzerDescriptor validates typed analyzer registration data.

func ValidateAuditorDescriptor

func ValidateAuditorDescriptor(descriptor *AuditorDescriptor) error

ValidateAuditorDescriptor validates typed auditor registration data.

func ValidateDetectorDescriptor

func ValidateDetectorDescriptor(descriptor *DetectorDescriptor) error

ValidateDetectorDescriptor validates typed detector registration data.

func ValidateMatcherDescriptor

func ValidateMatcherDescriptor(descriptor *MatcherDescriptor) error

ValidateMatcherDescriptor validates typed matcher registration data.

func ValidateModule added in v0.2.0

func ValidateModule(m Module) error

ValidateModule checks that exactly one role is set, that the role matches the declared Kind, that the role constructor is present, and that the role descriptor validates.

Types

type Affected

type Affected struct {
	Ranges            []VersionRange `json:"ranges,omitempty"`
	Versions          []string       `json:"versions,omitempty"`
	EcosystemSpecific map[string]any `json:"ecosystem_specific,omitempty"`
	DatabaseSpecific  map[string]any `json:"database_specific,omitempty"`
}

Affected describes one OSV affected entry: the version ranges and explicit versions impacted by the vulnerability.

type AffectedSymbol

type AffectedSymbol struct {
	Symbol     string          `json:"symbol,omitempty"`
	Kind       SymbolKind      `json:"kind,omitempty"`
	Package    string          `json:"package,omitempty"`
	Module     string          `json:"module,omitempty"`
	Definition *SourcePosition `json:"definition,omitempty"`
}

AffectedSymbol identifies one vulnerable symbol within a package. Matchers that have symbol-level data populate this on a Vulnerability; reachability analyzers use it to know which symbols to look for in app code.

func (AffectedSymbol) Clone

func (s AffectedSymbol) Clone() AffectedSymbol

Clone returns a deep copy of the affected symbol.

type AnalyzeRequest

type AnalyzeRequest struct {
	ProjectPath     string           `json:"projectPath,omitempty"`
	ExecutionTarget ExecutionTarget  `json:"executionTarget"`
	SubprojectInfo  Subproject       `json:"subprojectInfo"`
	Ecosystem       Ecosystem        `json:"ecosystem,omitempty"`
	PackageManager  PackageManager   `json:"packageManager,omitempty"`
	Language        Language         `json:"language,omitempty"`
	Query           PackageQuery     `json:"query"`
	Graph           *Graph           `json:"graph,omitempty"`
	Registry        *PackageRegistry `json:"registry,omitempty"`
	Target          *DependencyNode  `json:"target,omitempty"`
	AnalyzerFilter  AnalyzerFilter   `json:"analyzerFilter"`
	// AcceptPackageUpdates signals that the host understands
	// AnalyzeResult.PackageUpdates. Analyzers advertising
	// CapabilityPackageUpdates may return updates instead of a full registry
	// only when this is true.
	AcceptPackageUpdates bool      `json:"acceptPackageUpdates,omitempty"`
	Stderr               io.Writer `json:"-"`
}

AnalyzeRequest defines input for an analyzer. Analyzers annotate Vulnerability.Reachability on packages in the Registry.

type AnalyzeResponse

type AnalyzeResponse = AnalyzeResult

AnalyzeResponse is the analyzer response payload exposed to plugins.

type AnalyzeResult

type AnalyzeResult struct {
	Registry       *PackageRegistry             `json:"registry,omitempty"`
	PackageUpdates []*Package                   `json:"packageUpdates,omitempty"`
	AnalyzerRuns   []string                     `json:"analyzerRuns,omitempty"`
	AnalyzerStats  map[string]ReachabilityStats `json:"analyzerStats,omitempty"`
}

AnalyzeResult contains the registry after analyzer enrichment. An analyzer returns either Registry (the full annotated registry — the protocol v1 baseline) or, when the request set AcceptPackageUpdates, PackageUpdates: only the packages it touched. The host merges updates into its registry by PURL. When Registry is non-nil it wins and PackageUpdates is ignored.

type Analyzer

type Analyzer interface {
	Descriptor() AnalyzerDescriptor
	// Ready reports whether the analyzer can run for the given request. It
	// returns nil when ready and a non-nil error describing the reason
	// otherwise. Implementations may perform lightweight, cancellable I/O and
	// should honor ctx.
	Ready(context.Context, AnalyzeRequest) error
	Applicable(context.Context, AnalyzeRequest) (bool, error)
	Analyze(context.Context, AnalyzeRequest) (AnalyzeResult, error)
}

Analyzer enriches Vulnerability entries with reachability data derived from code analysis. Analyzers run after matchers, before auditors, and must never abort the pipeline on failure.

type AnalyzerDescriptor

type AnalyzerDescriptor struct {
	Name                string           `json:"name"`
	DisplayName         string           `json:"displayName,omitempty"`
	Aliases             []string         `json:"aliases,omitempty"`
	Tags                []string         `json:"tags,omitempty"`
	SupportedEcosystems []Ecosystem      `json:"supportedEcosystems,omitempty"`
	SupportedManagers   []PackageManager `json:"supportedManagers,omitempty"`
	// SupportedLanguages is the analyzer's primary dispatch axis.
	SupportedLanguages []Language `json:"supportedLanguages,omitempty"`
	// SupportedTiers communicates the precision the analyzer can deliver.
	SupportedTiers []ReachabilityTier `json:"supportedTiers,omitempty"`
	// Capabilities advertises optional protocol features this analyzer
	// supports, such as CapabilityPackageUpdates.
	Capabilities []string `json:"capabilities,omitempty"`
	// ConfigSchema optionally documents the analyzer's configuration block as
	// a JSON Schema. Build it with ConfigSchemaFor.
	ConfigSchema json.RawMessage `json:"configSchema,omitempty"`
}

AnalyzerDescriptor describes an analyzer registration.

func (AnalyzerDescriptor) Label

func (d AnalyzerDescriptor) Label() string

Label returns the user-facing analyzer label, falling back to Name.

type AnalyzerFilter

type AnalyzerFilter struct {
	Include []string
	Exclude []string
}

AnalyzerFilter narrows analyzer selection for a request.

func (AnalyzerFilter) Excludes

func (f AnalyzerFilter) Excludes(name string) bool

Excludes reports whether an analyzer name is explicitly denied.

func (AnalyzerFilter) Includes

func (f AnalyzerFilter) Includes(name string) bool

Includes reports whether an analyzer name is explicitly allowed.

type AnalyzerModule added in v0.2.0

type AnalyzerModule struct {
	Descriptor AnalyzerDescriptor
	New        func(context.Context, HostContext) (Analyzer, error)
}

AnalyzerModule declares one analyzer component.

type ApplicableResponse

type ApplicableResponse struct {
	Applicable bool `json:"applicable"`
}

ApplicableResponse reports whether a plugin should run for the given request.

type AuditRequest

type AuditRequest struct {
	ProjectPath     string           `json:"projectPath,omitempty"`
	ExecutionTarget ExecutionTarget  `json:"executionTarget"`
	SubprojectInfo  Subproject       `json:"subprojectInfo"`
	Ecosystem       Ecosystem        `json:"ecosystem,omitempty"`
	PackageManager  PackageManager   `json:"packageManager,omitempty"`
	Query           PackageQuery     `json:"query"`
	Graph           *Graph           `json:"graph,omitempty"`
	BaselineGraph   *Graph           `json:"baselineGraph,omitempty"`
	Registry        *PackageRegistry `json:"registry,omitempty"`
	Target          *DependencyNode  `json:"target,omitempty"`
	// DependencyDetailChanges contains canonical head-side transitions for a
	// diff audit. Scan and explain requests leave it empty.
	DependencyDetailChanges []DependencyDetailTransition `json:"dependencyDetailChanges,omitempty"`
	AuditorFilter           AuditorFilter                `json:"auditorFilter"`
	Stderr                  io.Writer                    `json:"-"`
}

AuditRequest defines input for an auditor. Auditors read the dependency Graph and the package Registry and emit reference-style findings.

type AuditResponse

type AuditResponse = AuditResult

AuditResponse is the auditor response payload exposed to plugins.

It aliases AuditResult so plugin code can name payload types by role while sharing the same transport shape Bomly core uses internally.

type AuditResult

type AuditResult struct {
	Findings        []Finding      `json:"findings,omitempty"`
	RiskScores      []RiskScore    `json:"riskScores,omitempty"`
	AuditorRuns     []string       `json:"auditorRuns,omitempty"`
	AuditorFindings map[string]int `json:"auditorFindings,omitempty"`
}

AuditResult contains findings and scores from one auditor.

type Auditor

type Auditor interface {
	Descriptor() AuditorDescriptor
	// Ready reports whether the auditor can run for the given request. It
	// returns nil when ready and a non-nil error describing the reason
	// otherwise. Implementations may perform lightweight, cancellable I/O and
	// should honor ctx.
	Ready(context.Context, AuditRequest) error
	Applicable(context.Context, AuditRequest) (bool, error)
	Audit(context.Context, AuditRequest) (AuditResult, error)
}

Auditor analyzes graphs or components and returns findings.

type AuditorDescriptor

type AuditorDescriptor struct {
	Name                string           `json:"name"`
	DisplayName         string           `json:"displayName,omitempty"`
	Aliases             []string         `json:"aliases,omitempty"`
	Tags                []string         `json:"tags,omitempty"`
	SupportedEcosystems []Ecosystem      `json:"supportedEcosystems,omitempty"`
	SupportedManagers   []PackageManager `json:"supportedManagers,omitempty"`
	// ConfigSchema optionally documents the auditor's configuration block as
	// a JSON Schema. Build it with ConfigSchemaFor.
	ConfigSchema json.RawMessage `json:"configSchema,omitempty"`
}

AuditorDescriptor describes an auditor registration.

func (AuditorDescriptor) Label

func (d AuditorDescriptor) Label() string

Label returns the user-facing auditor label, falling back to Name.

type AuditorFilter

type AuditorFilter struct {
	Include []string
	Exclude []string
}

AuditorFilter narrows auditor selection for a request.

func (AuditorFilter) Excludes

func (f AuditorFilter) Excludes(name string) bool

Excludes reports whether an auditor name is explicitly denied.

func (AuditorFilter) Includes

func (f AuditorFilter) Includes(name string) bool

Includes reports whether an auditor name is explicitly allowed.

type AuditorModule added in v0.2.0

type AuditorModule struct {
	Descriptor AuditorDescriptor
	New        func(context.Context, HostContext) (Auditor, error)
}

AuditorModule declares one auditor component.

type BaseAnalyzer

type BaseAnalyzer struct{}

BaseAnalyzer provides default implementations of Analyzer's optional lifecycle methods: always ready and always applicable.

func (BaseAnalyzer) Applicable

Applicable reports the analyzer as applicable.

func (BaseAnalyzer) Ready

Ready reports the analyzer as ready.

type BaseAuditor

type BaseAuditor struct{}

BaseAuditor provides default implementations of Auditor's optional lifecycle methods: always ready and always applicable.

func (BaseAuditor) Applicable

Applicable reports the auditor as applicable.

func (BaseAuditor) Ready

Ready reports the auditor as ready.

type BaseDetector

type BaseDetector struct{}

BaseDetector provides default implementations of Detector's optional lifecycle methods: always ready and always applicable.

func (BaseDetector) Applicable

Applicable reports the detector as applicable.

func (BaseDetector) Ready

Ready reports the detector as ready.

type BaseMatcher

type BaseMatcher struct{}

BaseMatcher provides default implementations of Matcher's optional lifecycle methods: always ready and always applicable.

func (BaseMatcher) Applicable

Applicable reports the matcher as applicable.

func (BaseMatcher) Ready

Ready reports the matcher as ready.

type CVSSScore

type CVSSScore struct {
	Vector  string       `json:"vector,omitempty"`
	Score   float64      `json:"score,omitempty"`
	Version SeverityType `json:"version,omitempty"`
	Source  string       `json:"source,omitempty"`
}

CVSSScore captures one CVSS vector and score.

type CWE

type CWE struct {
	CVE    string `json:"cve,omitempty"`
	ID     string `json:"id,omitempty"`
	Source string `json:"source,omitempty"`
	Type   string `json:"type,omitempty"`
}

CWE identifies a Common Weakness Enumeration entry for a vulnerability.

type CallFrame

type CallFrame struct {
	Function string         `json:"function,omitempty"`
	Package  string         `json:"package,omitempty"`
	Receiver string         `json:"receiver,omitempty"`
	Position SourcePosition `json:"position,omitempty"`
}

CallFrame represents one stack frame in a reachability call path.

type CallPath

type CallPath struct {
	Sink   AffectedSymbol `json:"sink"`
	Frames []CallFrame    `json:"frames,omitempty"`
}

CallPath is one entry-point → sink path. Frames[0] is the entry point.

func (CallPath) Clone

func (p CallPath) Clone() CallPath

Clone returns a deep copy of the call path.

type Client

Client is the generic runtime client used by Bomly core.

type ComponentDescriptor

type ComponentDescriptor struct {
	Name                string           `json:"name"`
	DisplayName         string           `json:"displayName,omitempty"`
	Aliases             []string         `json:"aliases,omitempty"`
	Tags                []string         `json:"tags,omitempty"`
	SupportedEcosystems []Ecosystem      `json:"supportedEcosystems,omitempty"`
	SupportedManagers   []PackageManager `json:"supportedManagers,omitempty"`
}

ComponentDescriptor describes the common identity and selection fields shared by detectors, matchers, auditors, and analyzers.

Name is required and bounded by maxComponentNameLength; it must be valid UTF-8 with no control characters, because it reaches published documents where a newline would corrupt SPDX's line-oriented tag form. Whitespace inside a name is legal. The checks apply to the value as stored -- validation does not rewrite it -- so padding counts against the bound. The gate is validateComponentDescriptor, through each kind's Validate*Descriptor.

func (ComponentDescriptor) Label

func (d ComponentDescriptor) Label() string

Label returns the user-facing component label, falling back to Name.

type ConsolidatedGraph

type ConsolidatedGraph struct {
	ExecutionTarget ExecutionTarget
	Graphs          *GraphContainer
	Manifests       []ConsolidatedManifest
	Subprojects     []ConsolidatedSubproject
}

ConsolidatedGraph describes a merged view above per-subproject graph results.

type ConsolidatedManifest

type ConsolidatedManifest struct {
	Entry          GraphEntry
	Subproject     Subproject
	DetectorName   string
	Origin         DetectorOrigin
	Technique      DetectorTechnique
	RootManifestID string
}

ConsolidatedManifest describes one selected manifest after detector-level deduplication and precedence rules have been applied.

type ConsolidatedSubproject

type ConsolidatedSubproject struct {
	Subproject      Subproject
	DetectorName    string
	RootManifestIDs []string
}

ConsolidatedSubproject describes one subproject included in a consolidated graph.

type Contact added in v0.7.0

type Contact struct {
	// Kind says whether the party is an organization or a person, or that
	// the document explicitly declined to say. Parsed by ParseContactKind;
	// an unrecognized kind is dropped to unknown, which has no valid SPDX
	// rendering and so omits the field rather than emitting a bad one.
	Kind ContactKind `json:"kind,omitempty"`
	// Name is the party's name as the source stated it, minus any email
	// address. Bounded, and rejected outright if it carries a control
	// character, which would corrupt SPDX's line-oriented tag form.
	Name string `json:"name,omitempty"`
	// URL is the party's own URL, when the source carried one. CycloneDX's
	// organizational entity has a url list; SPDX has no slot for it. Held to
	// URLFormReference and additionally refused when it carries an email
	// address, so the no-email rule above cannot be sidestepped through the
	// query or fragment. An unpublishable URL is cleared on its own; unlike
	// the name, it does not take the contact with it.
	URL string `json:"url,omitempty"`
}

Contact names a party a document makes a claim about: who supplied a package (SPDX PackageSupplier, CycloneDX supplier) or who originally authored it (SPDX PackageOriginator, CycloneDX author/publisher).

No email address

A contact deliberately carries no email address, though both formats have a slot for one. ADR-0037 defers supplier-contact privacy to its own review, and an email is personal data that would flow from an ingested document into Bomly's JSON output, its logs, and every re-export. Storing it now and deciding later is not neutral -- the exposure happens at storage, not at emission -- so the field does not exist yet.

The consequence is stated rather than hidden: an SPDX supplier written as "Organization: Acme Inc (info@acme.com)" round-trips as "Organization: Acme Inc". The name survives, the address does not. When the privacy review lands, an email field is an additive change here. # Gate and merge class

Every field is gated by Contact.Normalized, applied on both wire directions and again wherever a contact is copied onto another record. The gate acts on the contact as a whole, not field by field: a name that cannot be published takes the contact with it and yields nil, rather than leaving a party with no name attached to a package. NOASSERTION is the one kind that stands without a name, because withholding is itself the claim.

A contact is a fill-gaps scalar on its holder: the first publishable supplier wins, and a later witness contributes one only where none was recorded. Both witnesses are gated before the gap is measured, so an unpublishable contact never blocks a valid one.

func ParseSPDXContact added in v0.7.0

func ParseSPDXContact(value string) (Contact, bool)

ParseSPDXContact reads SPDX's PackageSupplier/PackageOriginator form. It accepts "Organization: <name>", "Person: <name>", and "NOASSERTION", and strips the optional "(<email>)" suffix the format allows -- see the type's documentation for why the address is not retained. It returns false when the value carries no publishable claim.

func (*Contact) Clone added in v0.7.0

func (c *Contact) Clone() *Contact

Clone returns a deep copy of the contact.

func (Contact) Empty added in v0.7.0

func (c Contact) Empty() bool

Empty reports whether the contact says nothing.

func (Contact) MarshalJSON added in v0.7.0

func (c Contact) MarshalJSON() ([]byte, error)

MarshalJSON applies the same rule on the way out.

func (Contact) Normalized added in v0.7.0

func (c Contact) Normalized() (Contact, bool)

Normalized returns the contact with its claim re-checked, or false when it says nothing publishable. It is the gate for a contact that arrived from a plugin, an ingested document, or a hand-built value.

func (Contact) SPDXString added in v0.7.0

func (c Contact) SPDXString() string

SPDXString renders the contact in SPDX's PackageSupplier/PackageOriginator form, or "" when the contact has no SPDX projection. SPDX requires the kind prefix, so a contact of unknown kind has no valid rendering -- "" means omit the field rather than emit something that will not validate.

func (*Contact) UnmarshalJSON added in v0.7.0

func (c *Contact) UnmarshalJSON(data []byte) error

UnmarshalJSON applies the contact rule as a value arrives, so a party that would be rejected on read cannot be stored, forwarded, or written back out. A contact that says nothing publishable decodes to the zero value, following DependencyOrigin.

type ContactKind added in v0.7.0

type ContactKind string

ContactKind says what sort of party a Contact names. SPDX writes the kind inline ("Organization: Acme Inc"), and CycloneDX implies it by which field carries the value, so the SDK stores it explicitly and each codec projects it.

const (
	// ContactKindUnknown means the source named a party without saying
	// whether it is a person or an organization.
	ContactKindUnknown ContactKind = ""
	// ContactKindOrganization names a company, foundation, or team.
	ContactKindOrganization ContactKind = "organization"
	// ContactKindPerson names an individual.
	ContactKindPerson ContactKind = "person"
	// ContactKindNoAssertion is SPDX's explicit "the document declines to
	// say". It is not the same as an absent contact: one is a statement that
	// the information was withheld, the other is silence, and SPDX
	// round-trips the difference.
	ContactKindNoAssertion ContactKind = "noassertion"
)

func ParseContactKind added in v0.7.0

func ParseContactKind(value string) (ContactKind, error)

ParseContactKind normalizes a contact kind. An empty value is unknown, which is legal; anything else unrecognized is an error.

type Coordinates

type Coordinates struct {
	PURL           string         `json:"purl,omitempty"`
	Ecosystem      Ecosystem      `json:"ecosystem,omitempty"`
	PackageManager PackageManager `json:"package_manager,omitempty"`
	Type           PackageType    `json:"type,omitempty"`
	Org            string         `json:"org,omitempty"`
	Name           string         `json:"name,omitempty"`
	Version        string         `json:"version,omitempty"`
	Language       Language       `json:"language,omitempty"`
}

Coordinates is the shared identity view embedded by Dependency and Package. It intentionally excludes graph-only fields (scopes, locations, package refs) and enrichment-only fields (licenses, vulnerabilities, scorecard) so detection-time graph nodes and matching-stage package records remain distinct domain models.

func NodeCoordinates added in v0.9.0

func NodeCoordinates(node GraphNode) (Coordinates, bool)

NodeCoordinates returns the package coordinates a node carries, and reports whether it carries any.

A manifest node does not: a file is not a package. A typed nil does not either, which is why the check is here rather than at each call site -- comparing a GraphNode against nil is false for a (*DependencyNode)(nil), and the next field read panics.

func (Coordinates) CanonicalPURL

func (i Coordinates) CanonicalPURL() string

CanonicalPURL returns the canonical package URL for the identity.

func (Coordinates) DisplayName

func (i Coordinates) DisplayName() string

DisplayName returns the package name in its ecosystem-native form: "@org/name" for npm-family packages, "org/name" for path-style ecosystems (Go, Composer), and "org:name" otherwise. Unlike QualifiedName it is a presentation label only and must never be used as an identity key.

func (Coordinates) EcosystemName

func (i Coordinates) EcosystemName() string

EcosystemName returns the package name in the form its ecosystem uses as an identity: "@org/name" for npm, "org:name" for Maven-family coordinates, and "org/name" for the path-style namespaced ecosystems (Go, Composer, Swift, GitHub Actions). This is the name external advisory databases, SBOM documents, and scanners such as Grype and Syft key on, so anything building a lookup for a package must derive it from here rather than from the bare Name — Name alone drops the npm scope and matches the unscoped package's advisories.

Joining is opt-in per ecosystem, and everything else keeps the bare Name, because Org is not always part of the package name. For OS packages Org is the distro that shipped the package (`Org: "alpine"` from `pkg:apk/alpine/libcrypto3`), and Grype's distro-namespace matchers query `libcrypto3`; joining would miss every OS advisory. The same holds for any other ecosystem whose PURL namespace names a vendor or channel rather than part of the package's own identity.

func (Coordinates) GenericPURL added in v0.9.0

func (i Coordinates) GenericPURL() string

GenericPURL returns a pkg:generic package URL for the identity, for the case where the ecosystem's own type profile rejects the coordinates.

It is deliberately separate from CanonicalPURL: that answers "what is this package's canonical identity in its own ecosystem", and answering it with a generic URL would make every caller unable to tell the two apart. Node construction is the only place that reaches for this, and it records a warning when it does.

A qualifier names the type that could not express the package. Two ecosystems whose profiles both reject otherwise identical coordinates would otherwise mint the same identity -- a bare Swift "internal-tools@2.0.0" and a bare Go one both becoming pkg:generic/internal-tools@2.0.0 -- and folding two distinct packages into one node is a worse outcome than the loose type this fallback already accepts. A degraded identity still has to be an identity.

A qualifier rather than the namespace, because coordinates are projected from the identity verbatim once it is minted: a discriminator in the namespace comes back as Coordinates.Org, so a bare Swift package would read as organization "swift" and display as "swift:internal-tools" -- an organization no manifest declared. A qualifier is part of the identity, so it keeps the two records distinct, and it is not projected, so the coordinates still say what the detector found. It also rides the wire, which is what lets the warning below be derived after a decode.

func (Coordinates) QualifiedName

func (i Coordinates) QualifiedName() string

QualifiedName returns the package name prefixed with its organization when present.

type DependencyDetailField

type DependencyDetailField string

DependencyDetailField identifies one dependency property that changed independently of package identity or version.

const (
	// DependencyDetailRelationship is a direct, transitive, or unknown
	// relationship change.
	DependencyDetailRelationship DependencyDetailField = "relationship"
	// DependencyDetailSource is a registry, workspace, file, Git, URL, or
	// project source change.
	DependencyDetailSource DependencyDetailField = "source"
	// DependencyDetailRegistryEligibility indicates that external registry
	// matching eligibility changed.
	DependencyDetailRegistryEligibility DependencyDetailField = "registry_eligibility"
)

type DependencyDetailReviewReason

type DependencyDetailReviewReason string

DependencyDetailReviewReason explains why a dependency detail change should receive extra review.

const (
	// DependencyDetailReviewSourceGit indicates that the dependency now comes
	// from a Git repository.
	DependencyDetailReviewSourceGit DependencyDetailReviewReason = "source-changed-to-git"
	// DependencyDetailReviewSourceURL indicates that the dependency now comes
	// from an arbitrary URL.
	DependencyDetailReviewSourceURL DependencyDetailReviewReason = "source-changed-to-url"
)

type DependencyDetailTransition

type DependencyDetailTransition struct {
	Before                 *DependencyNode         `json:"before"`
	After                  *DependencyNode         `json:"after"`
	ChangedFields          []DependencyDetailField `json:"changedFields"`
	BeforeRelationship     DependencyRelationship  `json:"beforeRelationship,omitempty"`
	AfterRelationship      DependencyRelationship  `json:"afterRelationship,omitempty"`
	BeforeRegistryEligible bool                    `json:"beforeRegistryEligible"`
	AfterRegistryEligible  bool                    `json:"afterRegistryEligible"`
}

DependencyDetailTransition captures same-identity dependency detail changes. Version changes remain represented separately by VersionChange.

func CloneDependencyDetailTransitions

func CloneDependencyDetailTransitions(transitions []DependencyDetailTransition) []DependencyDetailTransition

CloneDependencyDetailTransitions returns a deep copy of dependency detail transitions suitable for crossing component and plugin boundaries.

func CompareDependencyDetails

func CompareDependencyDetails(baseGraph, headGraph *Graph, before, after *DependencyNode) (DependencyDetailTransition, bool)

CompareDependencyDetails returns a transition when relationship, source, or registry-matching eligibility differs between two dependency records. It is exported so trusted fuzzy identity reconciliation can use the same canonical classifier as Compare.

func (DependencyDetailTransition) NeedsReview

func (t DependencyDetailTransition) NeedsReview() bool

NeedsReview reports whether this detail change has at least one review reason.

func (DependencyDetailTransition) ReviewReasons

ReviewReasons returns the reasons this detail change needs extra review. The result is deterministic and does not treat missing evidence, coverage gains, or relationship-only changes as review signals.

type DependencyEdge

type DependencyEdge struct {
	FromID string   `json:"fromId"`
	ToID   string   `json:"toId"`
	Kind   EdgeKind `json:"kind,omitempty"`
}

DependencyEdge captures one directed relationship between node IDs.

Kind is additive and omitted when unknown, so a payload written before the field keeps its exact bytes. On decode an absent kind is derived from the nodes the edge joins, which is why adding the field did not need a wire break: the structure already carried the answer.

type DependencyNode added in v0.6.0

type DependencyNode struct {
	Coordinates
	Relationship DependencyRelationship
	Source       DependencySource
	Scopes       []Scope
	Locations    []PackageLocation
	CPEs         []string
	Digests      []Digest
	Copyright    string
	FoundBy      string
	// ResolvedURL is the manifest's resolution field verbatim — it may be a
	// pseudo-URL, a registry or index root, or a local path, and is never
	// published. It is raw evidence; Origins carry the validated assertions.
	ResolvedURL string
	// Origins is where this dependency was resolved from: metadata, never
	// identity (ADR-0041). Union-merged and deduplicated by normalized
	// value; the ADR-0033 publication gates are the only door in. A list
	// with more than one element is an observable fact — the shape of a
	// dependency-confusion signal — not a reason to split the node.
	Origins []DependencyOrigin
	// Licenses are the license claims the detecting or ingesting source made
	// about this dependency. Detection-time facts belong on the node;
	// consolidation lifts them into the registry package. This is the typed
	// replacement for the MetadataKeyDetectionLicenses stash.
	//
	// Gate: PackageLicense.Normalized. Merge class: set, unioned by
	// MergeLicenses -- a declaration and a conclusion are two claims about
	// one package, and two sources reusing one license reference for
	// different terms are kept apart.
	Licenses []PackageLicense
	// Description, Homepage, Supplier, and Originator are the component-level
	// SBOM assertions a source document made about this dependency
	// (ADR-0037). They live on the node as well as on Package because an
	// ingested document asserts them per component, before matching has
	// produced a registry package to hold them.
	//
	// Gates, applied on both wire directions and again when a node seeds a
	// registry package: NormalizeDescription (trimmed, bounded, control
	// characters dropped), NormalizeHomepage (URLFormReference, so a bare
	// host and a query are fine while credentials and local paths are
	// cleared), and Contact.Normalized for both contacts, which yields nil
	// for an unpublishable party and never retains an email address.
	//
	// Merge class for all four: scalar, fill-gaps. Both witnesses are gated
	// before the gap is measured, so a value that could not be published
	// never blocks one that can.
	Description string
	Homepage    string
	Supplier    *Contact
	Originator  *Contact
	// ExternalReferences are the references the source document attached to
	// this component. Gate: ExternalReference.Normalized, on both wire
	// directions and again when a node seeds a registry package. Merge class:
	// set, unioned by the (category, type, locator) triple.
	ExternalReferences []ExternalReference
	Metadata           map[string]any
	// Matched is true when the referenced package was enriched by a matcher.
	Matched bool
	// PackageRef is the PURL of this dependency's matching artifact. It is
	// derived — seeding sets it to NodeID(), which is the same canonical
	// PURL — and retained for wire compatibility with older readers.
	PackageRef string
	// contains filtered or unexported fields
}

DependencyNode is the union's third-party package record: one resolved dependency, the unit of matching and enrichment (ADR-0041). Its identity — and therefore its published graph ID — is its canonical package URL, minted only by the constructors below; there is no ID override, no occurrence suffix, and no identity outside the PURL. Matching enrichment (licenses, vulnerabilities, scorecard) lives on the referenced Package, not here.

func AsDependencyNode added in v0.9.0

func AsDependencyNode(node GraphNode) (*DependencyNode, bool)

AsDependencyNode narrows a node to a dependency node, reporting whether it is one. A typed nil is not.

func DependencyNodesOf added in v0.9.0

func DependencyNodesOf(list []GraphNode) []*DependencyNode

DependencyNodesOf narrows a slice of graph nodes to the dependency nodes among them.

Graph traversal yields the union -- DirectDependencies, Dependents, Roots, Leaves -- and most logic over the result is about consumed packages specifically: scope propagation, relationship marking, enrichment. Narrowing in one named place keeps every site agreeing about what a structural node means there: it is skipped, not defaulted.

func NewDependencyNode added in v0.6.0

func NewDependencyNode(coords Coordinates) (*DependencyNode, error)

NewDependencyNode constructs a dependency node from coordinates: the fields are normalized (per-ecosystem case, separator, and format rules — the same pass NormalizeCoordinates exposes), the canonical package URL is minted, and the identity is validated against the purl specification. A node that cannot mint a valid package URL is an error, not a silently empty ID; a missing version is a recorded warning, because the specification leaves version optional and first-party-adjacent records legitimately lack one.

func NewDependencyNodeFrom added in v0.9.0

func NewDependencyNodeFrom(proto DependencyNode) (*DependencyNode, error)

NewDependencyNodeFrom constructs a dependency node from a prototype: the identity is minted from the prototype's coordinates, and every other field it states is copied onto the result.

A node's identity is fixed at construction, so a producer that used to describe a package as one struct literal now has to construct first and assign after. Doing that by hand at each site is how a detector silently stops recording what it detected -- four npm-family lockfile parsers lost ResolvedURL and their integrity digests exactly that way, in one release, each for the same reason, and only a fixture assertion noticed.

The field list lives here because this type owns it: a field added to the model is copied by every producer at once, rather than in as many places as remembered.

func NewDependencyNodeFromPURL added in v0.6.0

func NewDependencyNodeFromPURL(rawPURL string) (*DependencyNode, error)

NewDependencyNodeFromPURL constructs a dependency node from a raw package URL — the qualifier-capable path. The URL-valued evidence qualifiers (repository_url, download_url, vcs_url) are relocated through the ADR-0033 origin constructors into Origins — a value the gates reject (a signed or tokenized link) is discarded entirely with a recorded warning, never sanitized into something publishable — and every other qualifier stays on the identity. Coordinates are back-filled from the parsed identity.

func (*DependencyNode) AddScope added in v0.6.0

func (n *DependencyNode) AddScope(scope Scope)

AddScope records a scope on the dependency if not already present.

func (*DependencyNode) AttributedScopes added in v0.7.0

func (n *DependencyNode) AttributedScopes() []Scope

AttributedScopes is the node's scope set, preferring the union over its locations when the sites carry attribution and falling back to the stored node-level set when they do not.

This is the direction the model is moving: the node-level set is a cache of what the sites say, not an independent claim. Reading through this rather than Scopes directly means a caller keeps working both before and after the producers migrate.

func (*DependencyNode) Clone added in v0.6.0

func (n *DependencyNode) Clone() *DependencyNode

Clone returns a deep copy of the dependency node.

func (*DependencyNode) CloneNode added in v0.6.0

func (n *DependencyNode) CloneNode() GraphNode

CloneNode implements GraphNode.

func (*DependencyNode) DisplayName added in v0.6.0

func (n *DependencyNode) DisplayName() string

DisplayName returns the most human-friendly identifier available, using the ecosystem-native name form (e.g. "@org/name" for npm).

func (*DependencyNode) HasScope added in v0.6.0

func (n *DependencyNode) HasScope(scope Scope) bool

HasScope reports whether the dependency carries the given scope.

func (*DependencyNode) Kind added in v0.6.0

func (n *DependencyNode) Kind() NodeKind

Kind returns NodeKindDependency.

func (*DependencyNode) LocationScopes added in v0.7.0

func (n *DependencyNode) LocationScopes() []Scope

LocationScopes returns the union of the scopes recorded across a node's locations, sorted and deduplicated. It returns nil when no location carries attribution, which is what a producer that has not migrated yet leaves.

func (*DependencyNode) MarshalJSON added in v0.6.0

func (n *DependencyNode) MarshalJSON() ([]byte, error)

MarshalJSON encodes a dependency node in its flat wire form.

func (*DependencyNode) NodeID added in v0.6.0

func (n *DependencyNode) NodeID() string

NodeID returns the canonical package URL: the node's identity and its published graph ID are the same string.

func (*DependencyNode) NodeLocations added in v0.6.0

func (n *DependencyNode) NodeLocations() []PackageLocation

NodeLocations returns the dependency's witnessed locations.

func (*DependencyNode) NodeWarnings added in v0.6.0

func (n *DependencyNode) NodeWarnings() []NodeWarning

NodeWarnings returns the constructor-recorded recoverable conditions.

func (*DependencyNode) PURL added in v0.6.0

func (n *DependencyNode) PURL() purlkit.PURL

PURL returns a copy of the parsed canonical identity.

func (*DependencyNode) PrimaryScope added in v0.6.0

func (n *DependencyNode) PrimaryScope() Scope

PrimaryScope returns the merged precedence scope across all recorded scopes.

func (*DependencyNode) QualifiedName added in v0.6.0

func (n *DependencyNode) QualifiedName() string

QualifiedName returns the name prefixed with its organization when present.

func (*DependencyNode) RegistryMatchEligible added in v0.6.0

func (d *DependencyNode) RegistryMatchEligible() bool

RegistryMatchEligible reports whether this dependency may be sent to external package matchers. Ownership and structure are the node kind under the union — module and manifest nodes cannot reach this method — so only the source classification decides. Project, workspace, file, Git, and arbitrary URL records are normally excluded. Swift source-control packages remain eligible because their repository URL is the canonical SwiftURL package identity used by vulnerability sources. An omitted source stays eligible for protocol-v1 and legacy detector compatibility. Graph insertion folds eligibility toward eligible: when records of one identity disagree, the eligible witness's source survives.

func (*DependencyNode) SyncScopesFromLocations added in v0.7.0

func (n *DependencyNode) SyncScopesFromLocations()

SyncScopesFromLocations rewrites the node-level scope set from its locations, so the cache matches what the sites say. It does nothing when no location carries scopes, which keeps it safe to call on a graph whose producers have not migrated -- rather than emptying a set that is the only record there is.

func (*DependencyNode) UnmarshalJSON added in v0.6.0

func (n *DependencyNode) UnmarshalJSON(data []byte) error

UnmarshalJSON decodes a dependency node through the constructor gates. A payload of a different node kind, or one whose identity cannot mint a well-formed package URL, is an error.

type DependencyOrigin added in v0.4.2

type DependencyOrigin struct {
	// ArtifactURL is the exact file the package was downloaded from.
	ArtifactURL string `json:"artifact_url,omitempty"`
	// Repository is the source repository the package was resolved from.
	Repository string `json:"repository,omitempty"`
	// Revision is the revision pinned in Repository, when the lockfile
	// recorded one. Never set without Repository.
	Revision string `json:"revision,omitempty"`
}

DependencyOrigin is where a dependency was resolved from, as asserted by the manifest the detector read. It is distilled at detection time from the manifest's structured source fields -- not derivable later from the raw ResolvedURL, which merges several fields and loses their meaning. The name follows the two standards that record this concept as a structured value: Go modules' Origin (URL, ref, hash) and PEP 610's "Direct URL Origin".

A dependency has one origin: either it was downloaded as an artifact or it was resolved from a repository, never both. An empty origin means the manifest had nothing publishable to say, which is the normal case for a dependency whose lockfile records only a registry or index root. Consumers such as SBOM export should publish nothing rather than guess.

This is detection data. Registry-side enrichment that resolves a source repository from package identity is a different, weaker claim and lives on its own fields (for example PackageScorecard.Repository), never here.

func ArtifactOrigin added in v0.4.0

func ArtifactOrigin(rawURL string) *DependencyOrigin

ArtifactOrigin records the exact artifact a package was resolved from. Callers pass the lockfile field verbatim. It returns nil when the value is not a publishable location, since a missing origin is correct output and a wrong one is not.

func MergeOrigins added in v0.6.0

func MergeOrigins(existing, additions []DependencyOrigin) []DependencyOrigin

MergeOrigins unions two origin lists into one, deduplicating by the normalized value and dropping entries that do not survive validation — the publication gates above remain the only door into a stored list. Merge class: union keyed by the normalized origin; order is deterministic — existing entries first, then new additions in arrival order. Two lists asserting the same location in different spellings collapse to one entry because the key is the normalized form, not the input bytes.

func RepositoryOrigin added in v0.4.0

func RepositoryOrigin(rawURL, revision string) *DependencyOrigin

RepositoryOrigin records the source repository a package was resolved from, plus the revision that was pinned. It returns nil when the URL is not a publishable location; an unusable revision drops only the revision, keeping the repository.

func (*DependencyOrigin) Clone added in v0.4.2

func (o *DependencyOrigin) Clone() *DependencyOrigin

Clone returns a deep copy.

func (*DependencyOrigin) Empty added in v0.4.2

func (o *DependencyOrigin) Empty() bool

Empty reports whether o names no publishable location -- including an origin whose values do not survive validation, so a caller that checks Empty can read Normalized without a second nil check.

func (DependencyOrigin) MarshalJSON added in v0.4.2

func (o DependencyOrigin) MarshalJSON() ([]byte, error)

MarshalJSON applies the same rule on the way out, so a hand-built value that never passed through the constructors cannot leave this process either.

func (*DependencyOrigin) Normalized added in v0.4.2

func (o *DependencyOrigin) Normalized() *DependencyOrigin

Normalized returns o with every value re-validated, or nil when nothing publishable survives. Read origin through this rather than reading the fields directly: it is what keeps a plugin-supplied or hand-built value from reaching a published document unchecked. An artifact wins over a repository in the case -- which the constructors never produce -- where both are set.

func (*DependencyOrigin) UnmarshalJSON added in v0.4.2

func (o *DependencyOrigin) UnmarshalJSON(data []byte) error

UnmarshalJSON applies the origin rule as a value arrives, so a location that would be rejected on read cannot be stored, forwarded to another component, or written back out. A value that fails validation decodes to an empty origin -- including a payload from an older build that still carries the removed "disputed" field, whose remaining values stand on their own.

type DependencyQuery

type DependencyQuery struct {
	Name string `json:"name,omitempty"`
	ID   string `json:"id,omitempty"`
}

DependencyQuery identifies a specific component target.

type DependencyRelationship

type DependencyRelationship string

DependencyRelationship describes how a dependency occurrence relates to the application or manifest root that owns its graph.

const (
	// DependencyRelationshipDirect identifies a dependency declared by a root.
	DependencyRelationshipDirect DependencyRelationship = "direct"
	// DependencyRelationshipTransitive identifies a dependency reached through another dependency.
	DependencyRelationshipTransitive DependencyRelationship = "transitive"
	// DependencyRelationshipUnknown identifies a dependency whose parent could not be recovered.
	DependencyRelationshipUnknown DependencyRelationship = "unknown"
)

func MergeDependencyRelationship

func MergeDependencyRelationship(current, next DependencyRelationship) DependencyRelationship

MergeDependencyRelationship combines occurrence relationships for a merged graph, retaining the strongest known project relationship.

func ParseDependencyRelationship

func ParseDependencyRelationship(value string) DependencyRelationship

ParseDependencyRelationship normalizes a dependency relationship value.

func RelationshipForPath

func RelationshipForPath(path []GraphNode) DependencyRelationship

RelationshipForPath returns the explicit target relationship when present, otherwise derives directness from a root-to-target path.

type DependencySource

type DependencySource string

DependencySource describes how a dependency occurrence is resolved.

const (
	DependencySourceRegistry  DependencySource = "registry"
	DependencySourceProject   DependencySource = "project"
	DependencySourceWorkspace DependencySource = "workspace"
	DependencySourceFile      DependencySource = "file"
	DependencySourceGit       DependencySource = "git"
	DependencySourceURL       DependencySource = "url"
)

type DetectRequest

type DetectRequest = DetectionRequest

DetectRequest is the detector request payload exposed to plugins.

It aliases DetectionRequest so plugin code can name payload types by role while sharing the same transport shape Bomly core uses internally.

type DetectResponse

type DetectResponse = DetectionResult

DetectResponse is the detector response payload exposed to plugins.

It aliases DetectionResult so plugin code can name payload types by role while sharing the same transport shape Bomly core uses internally.

type DetectionRequest

type DetectionRequest struct {
	ProjectPath     string          `json:"projectPath,omitempty"`
	ExecutionTarget ExecutionTarget `json:"executionTarget"`
	Subproject      Subproject      `json:"subproject"`
	Ecosystem       Ecosystem       `json:"ecosystem,omitempty"`
	PackageManager  PackageManager  `json:"packageManager,omitempty"`
	// EnrichmentEnabled allows orchestration to request detector-time metadata
	// enrichment when a downstream command has opted into package enrichment.
	EnrichmentEnabled bool            `json:"enrichmentEnabled,omitempty"`
	DetectorFilter    DetectorFilter  `json:"detectorFilter"`
	ScopeFilter       Scope           `json:"scopeFilter,omitempty"`
	Query             DependencyQuery `json:"query"`
	InstallFirst      bool            `json:"installFirst,omitempty"`
	InstallArgs       []string        `json:"installArgs,omitempty"`
	CoreVersion       string          `json:"coreVersion,omitempty"`
	// AllowStdErrLogging tells a detector that the user enabled debug output
	// and accepts the detector's raw subprocess diagnostics in that output.
	AllowStdErrLogging bool `json:"allowStdErrLogging,omitempty"`
	// Stderr and Verbose are process-local fields used by built-in detectors.
	// Stderr is nil unless debug output is enabled. Verbose mirrors
	// AllowStdErrLogging for compatibility with existing detector code.
	Stderr  io.Writer `json:"-"`
	Verbose bool      `json:"-"`
	// Logger is a request-scoped logger injected by the pipeline, already
	// bound to the subproject and detector this request targets. It lets a
	// detector instance that is shared across concurrently-resolved
	// subprojects emit log lines that identify which subproject they belong
	// to. It is process-local and never serialized. Use DetectorLogger to
	// read it with a safe fallback.
	Logger *zap.Logger `json:"-"`
}

DetectionRequest defines input for dependency graph resolution.

func (DetectionRequest) DetectorLogger

func (r DetectionRequest) DetectorLogger(fallback *zap.Logger) *zap.Logger

DetectorLogger returns the most specific non-nil logger for this request: the request-scoped Logger injected by the pipeline (carrying subproject and detector context) when present, otherwise the supplied fallback (typically the detector's own instance logger), otherwise a no-op logger. It never returns nil, so callers can drop the usual "if logger == nil" guard.

type DetectionResult

type DetectionResult struct {
	SubprojectInfo      Subproject        `json:"subprojectInfo"`
	RootExecutionTarget ExecutionTarget   `json:"rootExecutionTarget"`
	DetectorName        string            `json:"detectorName,omitempty"`
	Origin              DetectorOrigin    `json:"origin,omitempty"`
	Technique           DetectorTechnique `json:"technique,omitempty"`
	// FallbackFrom names the planned primary detector that failed before a
	// fallback detector produced this result. Empty for routine applicability
	// hand-off between chained detectors.
	FallbackFrom string `json:"fallbackFrom,omitempty"`
	// FallbackReason is the human-readable cause of the primary detector's
	// failure, e.g. "not ready: java executable not found on PATH".
	FallbackReason string          `json:"fallbackReason,omitempty"`
	Graphs         *GraphContainer `json:"graphs,omitempty"`
	// Warnings are non-fatal problems the detector found while resolving: the
	// graphs above are usable, but something about the project will break or
	// degrade an install elsewhere. The engine fills in each warning's
	// Subproject and surfaces them alongside the ones it observes itself.
	Warnings []DetectorWarning `json:"warnings,omitempty"`
}

DetectionResult contains one or more manifest-scoped graphs.

func FilterDetectionResultByScope

func FilterDetectionResultByScope(result DetectionResult, scope Scope) (DetectionResult, error)

FilterDetectionResultByScope applies scope filtering to each graph entry in a detector result.

func (DetectionResult) ConsolidatedGraph

func (r DetectionResult) ConsolidatedGraph() (*Graph, error)

ConsolidatedGraph returns a single graph view for the resolve result.

type Detector

type Detector interface {
	Descriptor() DetectorDescriptor
	PackageManagerSupport() []PackageManagerSupport
	// Ready reports whether the detector can run for the given request. It
	// returns nil when ready and a non-nil error describing the reason
	// (e.g. a missing toolchain) otherwise. Implementations may perform
	// lightweight, cancellable I/O (such as probing for a runtime) and should
	// honor ctx.
	Ready(context.Context, DetectionRequest) error
	Applicable(context.Context, DetectionRequest) (bool, error)
	ResolveGraph(context.Context, DetectionRequest) (DetectionResult, error)
}

Detector resolves dependency information.

type DetectorDescriptor

type DetectorDescriptor struct {
	Name                  string                  `json:"name"`
	DisplayName           string                  `json:"displayName,omitempty"`
	Aliases               []string                `json:"aliases,omitempty"`
	Tags                  []string                `json:"tags,omitempty"`
	SupportedEcosystems   []Ecosystem             `json:"supportedEcosystems,omitempty"`
	SupportedManagers     []PackageManager        `json:"supportedManagers,omitempty"`
	Technique             DetectorTechnique       `json:"technique,omitempty"`
	PackageManagerSupport []PackageManagerSupport `json:"packageManagerSupport,omitempty"`
	FallbackDetectors     []string                `json:"fallbackDetectors,omitempty"`
	SupportsInstallFirst  bool                    `json:"supportsInstallFirst,omitempty"`
	// RemediationCapabilities advertises optional, read-only support for
	// package-manager-specific remediation strategies. Core calls the optional
	// provider only when this list is non-empty.
	RemediationCapabilities []RemediationCapability `json:"remediationCapabilities,omitempty"`
	// IgnoredDirectories lists directory basename globs (Go
	// path.Match syntax) that recursive subproject discovery must not descend
	// into because they hold third-party installs, vendored dependencies, or
	// build outputs for this detector's ecosystem (e.g. "node_modules",
	// "target"). Discovery aggregates these across every registered detector,
	// including external plugins. Optional; omitted by older plugins.
	IgnoredDirectories []string `json:"ignoredDirectories,omitempty"`
	// IgnoredDirectoryMarkers lists file names whose presence inside
	// a directory marks that directory as ignored during recursive discovery
	// regardless of its name (e.g. "pyvenv.cfg" identifies a Python
	// virtualenv). Optional; omitted by older plugins.
	IgnoredDirectoryMarkers []string `json:"ignoredDirectoryMarkers,omitempty"`
	// ConfigSchema optionally documents the detector's configuration block as
	// a JSON Schema. Build it with ConfigSchemaFor.
	ConfigSchema json.RawMessage `json:"configSchema,omitempty"`
}

DetectorDescriptor describes a detector registration.

func (DetectorDescriptor) Clone

Clone returns a deep copy of the detector descriptor.

func (DetectorDescriptor) Label

func (d DetectorDescriptor) Label() string

Label returns the user-facing detector label, falling back to Name.

type DetectorFilter

type DetectorFilter struct {
	Include []string
	Exclude []string
}

DetectorFilter narrows detector selection for a request.

func (DetectorFilter) Excludes

func (f DetectorFilter) Excludes(name string) bool

Excludes reports whether a detector name is explicitly denied.

func (DetectorFilter) Includes

func (f DetectorFilter) Includes(name string) bool

Includes reports whether a detector name is explicitly allowed.

type DetectorInstaller

type DetectorInstaller interface {
	Install(context.Context, *DetectRequest) (*InstallResponse, error)
}

DetectorInstaller optionally performs install-first preparation before detection. Implement this only for detectors that need to prepare project dependencies before reading them; Bomly calls it only when install-first execution is requested.

type DetectorModule added in v0.2.0

type DetectorModule struct {
	Descriptor DetectorDescriptor
	Support    []PackageManagerSupport
	// TargetKinds lists the execution target kinds the detector supports.
	// Empty means the host derives the default [filesystem, git-repository],
	// mirroring the managed-plugin derivation.
	TargetKinds []ExecutionTargetKind
	New         func(context.Context, HostContext) (Detector, error)
}

DetectorModule declares one detector component: its static descriptor, package-manager support, and a constructor invoked once per execution.

type DetectorOrigin

type DetectorOrigin string

DetectorOrigin describes where a detector, matcher, or auditor is sourced from.

const (
	// CoreOrigin identifies components implemented directly in Bomly's own codebase.
	CoreOrigin DetectorOrigin = "core"
	// BundledOrigin identifies third-party components that are compiled into the Bomly binary (e.g. Syft, Grype).
	BundledOrigin DetectorOrigin = "bundled"
	// ExternalOrigin identifies components loaded as external plugins at runtime.
	ExternalOrigin DetectorOrigin = "external"
)

type DetectorRemediationProvider

type DetectorRemediationProvider interface {
	RemediationHints(context.Context, RemediationHintRequest) (RemediationHintResponse, error)
}

DetectorRemediationProvider optionally contributes read-only package-manager evidence after vulnerability enrichment.

type DetectorTechnique

type DetectorTechnique string

DetectorTechnique describes the resolution strategy used by a detector. Only meaningful for detectors; matchers and auditors leave this empty.

const (
	// ManifestTechnique reads a declarative dependency manifest file (e.g. package.json, Gemfile).
	ManifestTechnique DetectorTechnique = "manifest"
	// LockfileTechnique parses a deterministic lockfile (e.g. package-lock.json, yarn.lock).
	LockfileTechnique DetectorTechnique = "lockfile"
	// BuildToolTechnique invokes a build tool to resolve the live dependency graph.
	BuildToolTechnique DetectorTechnique = "build-tool"
	// SBOMTechnique ingests an existing SBOM document.
	SBOMTechnique DetectorTechnique = "sbom"
	// BinaryTechnique analyses a compiled binary or installed artifact.
	BinaryTechnique DetectorTechnique = "binary"
	// ContainerTechnique inspects a container image.
	ContainerTechnique DetectorTechnique = "container"
	// MultipleTechnique applies several of the above strategies depending on the target.
	MultipleTechnique DetectorTechnique = "multiple"
)

type DetectorWarning

type DetectorWarning struct {
	Type       DetectorWarningType `json:"type"`
	Code       DetectorWarningCode `json:"code,omitempty"`
	Source     string              `json:"source,omitempty"`
	Subproject string              `json:"subproject,omitempty"`
	Manifest   string              `json:"manifest,omitempty"`
	Message    string              `json:"message"`
}

DetectorWarning is one non-fatal problem found while detecting dependencies. Detectors return warnings alongside the graphs they resolve; the engine adds the ones it observes around them (a failed chain, a fallback). Every warning travels the same way to every surface, so a consumer never has to know which of the two produced it.

Source names the detector or tool the warning is about ("maven-detector", "pnpm"). Subproject and Manifest locate it when known; the engine fills in Subproject, so detectors only set Manifest.

func (DetectorWarning) DegradesCoverage

func (w DetectorWarning) DegradesCoverage() bool

DegradesCoverage reports whether this warning means the graph may be incomplete. It is shorthand for Type.DegradesCoverage.

type DetectorWarningCode

type DetectorWarningCode string

DetectorWarningCode identifies the specific check that produced a warning. It is empty for warnings the engine synthesizes from a detector failure, where Type already carries the full meaning.

const (
	// DetectorWarningCodeLockfileFormat means the committed lockfile's format
	// version disagrees with the package-manager version the project declares.
	DetectorWarningCodeLockfileFormat DetectorWarningCode = "lockfile-format-mismatch"
	// DetectorWarningCodeLockfileUnsupported means the project commits a
	// lockfile the declared package manager does not read.
	DetectorWarningCodeLockfileUnsupported DetectorWarningCode = "lockfile-unsupported"
	// DetectorWarningCodeEnginesConstraint means a declared engines constraint
	// contradicts another declaration in the same project.
	DetectorWarningCodeEnginesConstraint DetectorWarningCode = "engines-constraint-mismatch"
	// DetectorWarningCodeInstallGate means an install policy rejects versions by
	// age or publish date, so a freshly published fix version cannot install.
	DetectorWarningCodeInstallGate DetectorWarningCode = "install-policy-gate"
)

type DetectorWarningType

type DetectorWarningType string

DetectorWarningType classifies a detector warning by what it means for the run. It is the field policy decisions branch on: see DegradesCoverage.

const (
	// DetectorWarningResolutionFailure means a detector chain failed for a
	// subproject and the scan continued without that subproject's dependencies.
	DetectorWarningResolutionFailure DetectorWarningType = "resolution-failure"
	// DetectorWarningFallback means a fallback detector produced the graph after
	// the planned primary detector failed, so transitive dependencies may be
	// missing.
	DetectorWarningFallback DetectorWarningType = "fallback"
	// DetectorWarningPackageManager means the graph is sound, but the project's
	// package-manager configuration will break or degrade an install elsewhere —
	// typically in CI.
	DetectorWarningPackageManager DetectorWarningType = "package-manager"
)

func (DetectorWarningType) DegradesCoverage

func (t DetectorWarningType) DegradesCoverage() bool

DegradesCoverage reports whether the warning means the dependency graph may be incomplete, and therefore that findings may be missing. Consumers that require complete coverage before recording a decision — writing a finding baseline, for example — gate on this rather than on the presence of any warning: a package-manager mismatch says nothing about coverage.

type Diff

type Diff struct {
	Added       []*DependencyNode
	Removed     []*DependencyNode
	Updated     []VersionChange
	Transitions []DependencyDetailTransition
}

Diff summarizes the dependency changes between two graphs. Diffs are dependency-only: manifest and module nodes are structural and do not participate.

func Compare

func Compare(base, head *Graph) Diff

Compare returns added, removed, version-changed, and detail-changed dependencies between base and head. Only dependency nodes participate: manifest and module nodes are structural.

type Digest

type Digest struct {
	Algorithm DigestAlgorithm `json:"algorithm,omitempty"`
	Value     string          `json:"value,omitempty"`
	// Subject says what the digest covers. Empty means the published artifact,
	// which is what most ecosystems record and what a consumer should assume.
	// It exists because some ecosystems record a hash that is not a hash of a
	// file: a Go module's "h1:" value is SHA-256 over a manifest of the source
	// tree's file hashes, not over the module zip, so a consumer that treats it
	// as an artifact digest and compares it against a downloaded file will
	// always find a mismatch.
	//
	// Gate: ParseDigestSubject, through Digest.Normalized. The vocabulary is
	// closed, and an unrecognized subject rejects the whole digest rather
	// than being cleared -- empty is itself a claim ("the published
	// artifact"), so treating an uninterpretable label as absent would
	// publish something the producer never said.
	//
	// Merge class: part of the digest's set identity. Two records with the
	// same algorithm and value but different subjects are distinct claims and
	// both survive a union, because they say different things about what was
	// hashed.
	Subject DigestSubject `json:"subject,omitempty"`
}

Digest captures integrity information for a package artifact.

func (Digest) MarshalJSON added in v0.7.0

func (d Digest) MarshalJSON() ([]byte, error)

MarshalJSON applies the same rule on the way out, so a hand-built value that bypassed the constructors is still held to it at the wire.

func (Digest) Normalized added in v0.7.0

func (d Digest) Normalized() (Digest, bool)

Normalized returns the digest with its algorithm resolved to the canonical token and its value trimmed, or false when the digest cannot be published.

func (*Digest) UnmarshalJSON added in v0.7.0

func (d *Digest) UnmarshalJSON(data []byte) error

UnmarshalJSON resolves the algorithm spelling as a value arrives, so a producer that wrote CycloneDX's "SHA-256" or SPDX's "SHA256" is understood rather than silently carried as an algorithm nothing matches. A digest that cannot be published decodes to the zero value, following DependencyOrigin: both formats close their hash enumeration, so an unpublishable digest has nowhere to go, and dropping it is better than failing the whole payload.

func (Digest) Validate added in v0.7.0

func (d Digest) Validate() error

Validate reports why a digest cannot be published, or nil when it can.

The value itself is checked for shape, not for length-per-algorithm: the encoding is not fixed. Ecosystems record digests in hex, in base64 (npm's "sha512-..." integrity strings), and over subjects that are not files at all (a Go module "h1:" dirhash), so a per-algorithm hex length would reject values that are correct for their ecosystem.

type DigestAlgorithm

type DigestAlgorithm string

DigestAlgorithm identifies an artifact digest algorithm. The vocabulary is the registry in digest.go, aligned with the SPDX 2.3 and CycloneDX 1.5/1.6 hash vocabularies; see DigestAlgorithms.

const (
	DigestAlgorithmMD2         DigestAlgorithm = "md2"
	DigestAlgorithmMD4         DigestAlgorithm = "md4"
	DigestAlgorithmMD5         DigestAlgorithm = "md5"
	DigestAlgorithmMD6         DigestAlgorithm = "md6"
	DigestAlgorithmSHA1        DigestAlgorithm = "sha1"
	DigestAlgorithmSHA224      DigestAlgorithm = "sha224"
	DigestAlgorithmSHA256      DigestAlgorithm = "sha256"
	DigestAlgorithmSHA384      DigestAlgorithm = "sha384"
	DigestAlgorithmSHA512      DigestAlgorithm = "sha512"
	DigestAlgorithmSHA3256     DigestAlgorithm = "sha3-256"
	DigestAlgorithmSHA3384     DigestAlgorithm = "sha3-384"
	DigestAlgorithmSHA3512     DigestAlgorithm = "sha3-512"
	DigestAlgorithmBLAKE2b256  DigestAlgorithm = "blake2b-256"
	DigestAlgorithmBLAKE2b384  DigestAlgorithm = "blake2b-384"
	DigestAlgorithmBLAKE2b512  DigestAlgorithm = "blake2b-512"
	DigestAlgorithmBLAKE3      DigestAlgorithm = "blake3"
	DigestAlgorithmADLER32     DigestAlgorithm = "adler32"
	DigestAlgorithmStreebog256 DigestAlgorithm = "streebog-256"
	DigestAlgorithmStreebog512 DigestAlgorithm = "streebog-512"
)

The digest algorithm registry. Both SBOM formats define a closed hash vocabulary, so an algorithm Bomly cannot name in the target format cannot be published at all -- which makes the registry, not a pair of string constants, the thing that decides whether a digest survives export.

Canonical SDK spellings are lowercase and keep the family separator that the formats use ("sha3-256", not "sha3256"), so a token reads the way the algorithm is named in its own literature.

func DigestAlgorithms added in v0.7.0

func DigestAlgorithms() []DigestAlgorithm

DigestAlgorithms returns every registered algorithm in canonical order.

func ParseDigestAlgorithm added in v0.7.0

func ParseDigestAlgorithm(value string) (DigestAlgorithm, error)

ParseDigestAlgorithm resolves any spelling either format uses -- and the canonical SDK token -- to the canonical token. It errors on an algorithm no format defines, because such a digest has no export projection and a consumer is better told than handed a value it cannot publish.

func (DigestAlgorithm) CycloneDXName added in v0.7.0

func (a DigestAlgorithm) CycloneDXName() string

CycloneDXName returns the algorithm's CycloneDX 1.5/1.6 spelling, or "" when CycloneDX has no such member -- which is the case for the SPDX-only algorithms MD2, MD4, MD6, SHA224, and ADLER32. Treat "" as "omit this digest", as for SPDXName.

func (DigestAlgorithm) SPDXName added in v0.7.0

func (a DigestAlgorithm) SPDXName() string

SPDXName returns the algorithm's SPDX 2.3 spelling, or "" when SPDX has no such member. A caller emitting a checksum must treat "" as "omit this digest": SPDX's algorithm field is a closed enumeration, so there is no spelling that would validate.

func (DigestAlgorithm) String added in v0.7.0

func (a DigestAlgorithm) String() string

String returns the canonical token.

func (DigestAlgorithm) Valid added in v0.7.0

func (a DigestAlgorithm) Valid() bool

Valid reports whether a is a registered algorithm.

type DigestSubject added in v0.4.0

type DigestSubject string

DigestSubject identifies what a digest was computed over.

const (
	// DigestSubjectArtifact is a digest of the published file itself. It is
	// the zero value: a producer that does not say means the artifact.
	DigestSubjectArtifact DigestSubject = ""
	// DigestSubjectSourceTree is a digest over a source tree or over a
	// manifest of its file hashes, such as a Go module "h1:" dirhash.
	DigestSubjectSourceTree DigestSubject = "source-tree"
	// DigestSubjectMetadata is a digest of a package's metadata document
	// rather than of the package itself, such as a manifest or lockfile entry.
	DigestSubjectMetadata DigestSubject = "metadata"
)

func ParseDigestSubject added in v0.7.0

func ParseDigestSubject(value string) (DigestSubject, error)

ParseDigestSubject normalizes a digest subject. The vocabulary is closed: Subject says what a hash covers, and it takes part in a digest's identity, so an unrecognized value is an integrity claim no consumer can interpret rather than a label to carry along.

func (DigestSubject) String added in v0.7.0

func (s DigestSubject) String() string

String returns the subject token.

func (DigestSubject) Valid added in v0.7.0

func (s DigestSubject) Valid() bool

Valid reports whether s is a recognized subject.

type DocumentAssertions added in v0.7.0

type DocumentAssertions struct {
	// Identity is the document's own identifier: SPDX's documentNamespace, or
	// a CycloneDX BOM-Link ("urn:cdx:<serial>/<version>"). It is held to the
	// IRI rule rather than the web-URL rule, because a BOM-Link is a URN and
	// the web gate would reject the identifier a merged export links back to.
	Identity string `json:"identity,omitempty"`
	// Name is the document's stated name.
	Name string `json:"name,omitempty"`
	// DataLicense is the license of the document's own data, which SPDX
	// requires and CycloneDX does not model. Held to the same expression rule
	// as a package license, so an unparseable value is dropped rather than
	// written into a field consumers read as an SPDX expression.
	DataLicense string `json:"data_license,omitempty"`
	// Created is the document's timestamp, as the source stated it. Kept
	// verbatim: re-rendering a timestamp is a change to a claim, and the two
	// formats agree on RFC 3339 anyway.
	Created string `json:"created,omitempty"`
	// Creators are the parties credited with producing the document. Contact
	// carries no email, per ADR-0037's deferred privacy review.
	Creators []Contact `json:"creators,omitempty"`
	// Tools are the tools credited with producing the document.
	Tools []DocumentTool `json:"tools,omitempty"`
	// Comment is the document-level comment.
	Comment string `json:"comment,omitempty"`
}

DocumentAssertions are the claims a source document makes about itself, carried per GraphEntry so a merged export can say which claim came from which document.

func MergeDocumentAssertions added in v0.7.0

func MergeDocumentAssertions(dst, src DocumentAssertions) DocumentAssertions

MergeDocumentAssertions combines two sets of document claims.

Scalars fill gaps only: two documents disagreeing about their own name is not something to resolve by picking one, so the first stated value stands and the second is dropped rather than overwriting it. Creators and tools union, because two documents having produced a merged one is the normal case and both deserve credit.

Both sides are gated on the way in. An unpublishable value must not become visible in-process just because it arrived through a merge rather than through a constructor.

func (DocumentAssertions) Clone added in v0.7.0

Clone returns a deep copy.

func (DocumentAssertions) IsEmpty added in v0.7.0

func (d DocumentAssertions) IsEmpty() bool

IsEmpty reports whether the assertions carry nothing.

func (DocumentAssertions) Normalized added in v0.7.0

func (d DocumentAssertions) Normalized() (DocumentAssertions, bool)

Normalized returns the assertions with every field held to its gate, and reports whether anything publishable remains.

Each field is gated independently: a document with an unusable data license and a good identity keeps the identity. Dropping the whole record because one field failed would lose the link a merged export needs.

type DocumentTool added in v0.7.0

type DocumentTool struct {
	// Vendor is the organization that publishes the tool. Optional.
	Vendor string `json:"vendor,omitempty"`
	// Name is the tool's name. A tool without one is not a tool.
	Name string `json:"name,omitempty"`
	// Version is the tool's version, when the document stated it.
	Version string `json:"version,omitempty"`
}

DocumentTool is one tool that produced a document.

Both formats record this, and neither models it the same way -- SPDX writes a creator string, CycloneDX a structured tool entry -- so the structured form is kept and the flat one rendered from it, never the reverse.

func (DocumentTool) Normalized added in v0.7.0

func (t DocumentTool) Normalized() (DocumentTool, bool)

Normalized returns the tool with its fields bounded and trimmed, and reports whether anything publishable remains. A tool with no name is dropped: a version with nothing to attach it to says nothing.

type EPSSScore

type EPSSScore struct {
	CVE        string  `json:"cve,omitempty"`
	EPSS       float64 `json:"epss"`
	Percentile float64 `json:"percentile,omitempty"`
	Date       string  `json:"date,omitempty"`
}

EPSSScore captures Exploit Prediction Scoring System data for a vulnerability.

type Ecosystem

type Ecosystem string

Ecosystem groups package managers under a registry-specific dependency model.

const (
	EcosystemUnknown   Ecosystem = ""
	EcosystemNPM       Ecosystem = "npm"
	EcosystemMaven     Ecosystem = "maven"
	EcosystemGo        Ecosystem = "go"
	EcosystemPython    Ecosystem = "python"
	EcosystemALPM      Ecosystem = "alpm"
	EcosystemAPK       Ecosystem = "apk"
	EcosystemCPP       Ecosystem = "cpp"
	EcosystemConda     Ecosystem = "conda"
	EcosystemDart      Ecosystem = "dart"
	EcosystemDPKG      Ecosystem = "dpkg"
	EcosystemElixir    Ecosystem = "elixir"
	EcosystemErlang    Ecosystem = "erlang"
	EcosystemGitHub    Ecosystem = "github-actions"
	EcosystemHaskell   Ecosystem = "haskell"
	EcosystemHomebrew  Ecosystem = "homebrew"
	EcosystemLua       Ecosystem = "lua"
	EcosystemDotNet    Ecosystem = "dotnet"
	EcosystemNix       Ecosystem = "nix"
	EcosystemOCaml     Ecosystem = "ocaml"
	EcosystemPHP       Ecosystem = "php"
	EcosystemPortage   Ecosystem = "portage"
	EcosystemProlog    Ecosystem = "prolog"
	EcosystemR         Ecosystem = "r"
	EcosystemRPM       Ecosystem = "rpm"
	EcosystemRuby      Ecosystem = "ruby"
	EcosystemRust      Ecosystem = "rust"
	EcosystemScala     Ecosystem = "scala"
	EcosystemSBOM      Ecosystem = "sbom"
	EcosystemSnap      Ecosystem = "snap"
	EcosystemSwift     Ecosystem = "swift"
	EcosystemTerraform Ecosystem = "terraform"
	EcosystemWordPress Ecosystem = "wordpress"
	EcosystemOther     Ecosystem = "other"
)

Keep this list aligned with the Syft-backed support matrix in docs/SUPPORT_MATRIX.md and the Syft manifest mappings in internal/detectors/syft/detector.go.

func ParseEcosystem

func ParseEcosystem(value string) (Ecosystem, error)

ParseEcosystem normalizes a user-provided ecosystem value.

func (Ecosystem) String

func (e Ecosystem) String() string

String returns the ecosystem value.

type EcosystemFilter

type EcosystemFilter struct {
	Include []Ecosystem
	Exclude []Ecosystem
}

EcosystemFilter specifies inclusion and exclusion rules for filtering ecosystems.

func (EcosystemFilter) Excludes

func (f EcosystemFilter) Excludes(name Ecosystem) bool

Excludes reports whether a detector name is explicitly denied.

func (EcosystemFilter) Includes

func (f EcosystemFilter) Includes(name Ecosystem) bool

Includes reports whether a detector name is explicitly allowed.

type EdgeKind added in v0.7.0

type EdgeKind string

EdgeKind says what an edge between two nodes asserts.

Every edge used to mean the same thing, because every node was a dependency and the only relationship was "depends on". With the typed node union a graph also holds manifests and modules, and the edge from a manifest to the module it declares is not a dependency claim -- exporting it as one puts a relationship in a document that no detector asserted.

const (
	// EdgeKindUnknown is an edge whose kind was never stated. It is what a
	// payload written before this field carried, and it is derived from the
	// nodes it joins rather than published as-is.
	EdgeKindUnknown EdgeKind = ""
	// EdgeKindDependsOn is the dependency claim: the source needs the target.
	EdgeKindDependsOn EdgeKind = "depends-on"
	// EdgeKindDescribes joins a manifest to a module it declares. It is a
	// structural edge, not a dependency: a package.json does not depend on the
	// workspace member it describes.
	EdgeKindDescribes EdgeKind = "describes"
)

func DeriveEdgeKind added in v0.7.0

func DeriveEdgeKind(from, to GraphNode) EdgeKind

DeriveEdgeKind names what an edge between two nodes asserts, from the kinds of the nodes themselves.

This is what makes the field safe to add: a graph built before the field existed, or by a caller that does not set it, still exports correct relationships, because the structure already carries the answer. Only a manifest-to-module edge is structural; everything else is a dependency claim, including a manifest that names a dependency directly, which is what a lockfile with no workspace layer produces.

func MergeEdgeKind added in v0.7.0

func MergeEdgeKind(current, next EdgeKind) EdgeKind

MergeEdgeKind combines two kinds for one edge, which happens when graphs merge or duplicate nodes fold.

A stated kind beats an unstated one, so folding a legacy edge into a typed one keeps the type. Two different stated kinds disagree about what the edge means; the dependency claim wins, because it is the stronger assertion and losing it would drop the edge from a dependency-only export.

func ParseEdgeKind added in v0.7.0

func ParseEdgeKind(value string) (EdgeKind, error)

ParseEdgeKind normalizes an edge kind read from a payload. An empty value is unknown, which is legal and is what every pre-field payload carries; anything else unrecognized is an error, so a kind Bomly cannot read fails a decode rather than silently becoming a dependency claim.

func (EdgeKind) SPDXName added in v0.7.0

func (k EdgeKind) SPDXName() string

SPDXName returns the kind's SPDX relationship spelling, or "" when it has none. A caller emitting SPDX treats "" as "this edge has no projection" and writes no relationship, rather than guessing one.

func (EdgeKind) String added in v0.7.0

func (k EdgeKind) String() string

String returns the canonical token.

type ExecutionMode added in v0.2.0

type ExecutionMode string

ExecutionMode identifies how a component instance is being executed.

const (
	// ExecutionEmbedded marks a component compiled into the host binary and
	// registered in-process.
	ExecutionEmbedded ExecutionMode = "embedded"
	// ExecutionManaged marks a component running in its own plugin process
	// managed by the host over the plugin transport.
	ExecutionManaged ExecutionMode = "managed"
)

type ExecutionTarget

type ExecutionTarget struct {
	Kind          ExecutionTargetKind `json:"kind,omitempty"`
	Location      string              `json:"location,omitempty"`
	RepositoryURL string              `json:"repositoryUrl,omitempty"`
	Ref           string              `json:"ref,omitempty"`
}

type ExecutionTargetKind

type ExecutionTargetKind string

ExecutionTargetKind identifies the top-level source selected by the user for one scan execution.

const (
	// ExecutionTargetFilesystem points at a local filesystem path. The path may be a
	// directory or a single file depending on the selected scan target.
	ExecutionTargetFilesystem ExecutionTargetKind = "filesystem"
	// ExecutionTargetWorkingDirectory is kept as an alias for the existing local-path model.
	ExecutionTargetWorkingDirectory ExecutionTargetKind = ExecutionTargetFilesystem
	ExecutionTargetGitRepository    ExecutionTargetKind = "git-repository"
	ExecutionTargetContainerImage   ExecutionTargetKind = "container-image"
)

type ExternalReference added in v0.7.0

type ExternalReference struct {
	// Category is SPDX's referenceCategory. Empty for a CycloneDX-sourced
	// reference, which has no such axis.
	Category ExternalReferenceCategory `json:"category,omitempty"`
	// Type is the reference type: SPDX's referenceType or CycloneDX's type.
	// The vocabulary is open -- both specifications add types, and a type
	// this build does not know still round-trips -- so it is bounded and
	// checked for shape rather than matched against a closed list.
	Type string `json:"type,omitempty"`
	// Locator is what the reference points at. Its shape is decided by
	// LocatorKindFor, not by inspecting the value: a locator that fails its
	// declared grammar is a rejected reference, not one to reclassify.
	Locator string `json:"locator,omitempty"`
	// Comment is the source document's note about this reference. SPDX has a
	// comment field; CycloneDX 1.6 has one too.
	Comment string `json:"comment,omitempty"`
	// Hashes are the reference's own integrity claims. CycloneDX carries
	// these natively; SPDX 2.3 has no slot for them, so a caller emitting
	// SPDX has nowhere to put them and omits them.
	//
	// Gate: Digest.Normalized, through mergeDigestSet. Merge class: set,
	// unioned by the digest's own identity.
	Hashes []Digest `json:"hashes,omitempty"`
}

ExternalReference is one external reference a source document attached to a component: an advisory, a repository, a package-manager coordinate, a CPE.

Gate and merge class

Every field is gated by ExternalReference.Normalized, applied on both wire directions. A reference whose locator cannot be published is dropped entirely rather than published without it, since a reference with no locator points at nothing.

References are a set. Their identity is the (category, type, locator) triple, so the same locator recorded under two types stays two references -- they say different things about the component. Hashes union within a matching reference, and the comment fills a gap.

func MergeExternalReferences added in v0.7.0

func MergeExternalReferences(existing, additions []ExternalReference) []ExternalReference

MergeExternalReferences unions reference sets, keeping the first record of each distinct triple. A later record with the same triple contributes its hashes and fills a missing comment: it is the same assertion, seen twice.

Both sides are re-gated, and no early return when one is empty -- an existing slice may hold references that never passed the gate, and returning it untouched would leave them visible to in-process consumers.

func (ExternalReference) LocatorKind added in v0.7.0

func (r ExternalReference) LocatorKind() LocatorKind

LocatorKind returns the shape this reference's locator is held to.

func (ExternalReference) MarshalJSON added in v0.7.0

func (r ExternalReference) MarshalJSON() ([]byte, error)

MarshalJSON applies the same rule on the way out.

func (ExternalReference) Normalized added in v0.7.0

func (r ExternalReference) Normalized() (ExternalReference, bool)

Normalized returns the reference with every field re-checked, or false when it says nothing publishable. It is the gate for a reference that arrived from a plugin, an ingested document, or a hand-built value.

func (*ExternalReference) UnmarshalJSON added in v0.7.0

func (r *ExternalReference) UnmarshalJSON(data []byte) error

UnmarshalJSON applies the reference rule as a value arrives, so a locator that would be rejected on read cannot be stored, forwarded, or written back out. A reference that says nothing publishable decodes to the zero value, following DependencyOrigin.

type ExternalReferenceCategory added in v0.7.0

type ExternalReferenceCategory string

ExternalReferenceCategory is SPDX's referenceCategory axis: the grouping it puts an external reference in, alongside the reference type.

It is retained rather than re-derived because SPDX's external reference is a triple -- category, type, locator -- and the same type string can appear under more than one category in principle. Dropping the category on ingest would mean guessing it again on export.

CycloneDX has no equivalent axis. A reference that came from a CycloneDX document carries ExternalReferenceCategoryUnknown, which is a fact about the source, not a gap to fill in.

const (
	// ExternalReferenceCategoryUnknown means the source document has no
	// category axis. Every CycloneDX-sourced reference is this.
	ExternalReferenceCategoryUnknown ExternalReferenceCategory = ""
	// ExternalReferenceCategorySecurity is SPDX's SECURITY.
	ExternalReferenceCategorySecurity ExternalReferenceCategory = "security"
	// ExternalReferenceCategoryPackageManager is SPDX's PACKAGE-MANAGER.
	ExternalReferenceCategoryPackageManager ExternalReferenceCategory = "package-manager"
	// ExternalReferenceCategoryPersistentID is SPDX's PERSISTENT-ID.
	ExternalReferenceCategoryPersistentID ExternalReferenceCategory = "persistent-id"
	// ExternalReferenceCategoryOther is SPDX's OTHER.
	ExternalReferenceCategoryOther ExternalReferenceCategory = "other"
)

func ParseExternalReferenceCategory added in v0.7.0

func ParseExternalReferenceCategory(value string) (ExternalReferenceCategory, error)

ParseExternalReferenceCategory normalizes a category, accepting SPDX's own spelling and the canonical token. An empty value is unknown, which is legal and is what every CycloneDX-sourced reference carries; anything else unrecognized is an error.

func (ExternalReferenceCategory) SPDXName added in v0.7.0

func (c ExternalReferenceCategory) SPDXName() string

SPDXName returns the category's SPDX spelling, or "" when it has none -- which is the case for a CycloneDX-sourced reference. A caller emitting SPDX treats "" as "this reference has no SPDX projection".

func (ExternalReferenceCategory) String added in v0.7.0

func (c ExternalReferenceCategory) String() string

String returns the canonical token.

type FailOnConstraint

type FailOnConstraint struct {
	Kind  FailOnKind
	Value string
}

FailOnConstraint is one parsed --fail-on value. Vulnerability constraints form an AND-set. Other finding types may define independent gates, such as a dependency source change in a diff.

func ParseFailOn

func ParseFailOn(raw string) (FailOnConstraint, error)

ParseFailOn parses one raw --fail-on value into a typed constraint. Severity tokens (any|low|medium|high|critical) yield a SeverityConstraint. "reachable" yields a ReachabilityConstraint. "exploitable" yields an ExploitabilityConstraint. "source-change" yields a SourceChangeConstraint. Empty input returns the zero value with no error so callers can treat empty repeats as no-ops.

func ParseFailOnList

func ParseFailOnList(raws []string) ([]FailOnConstraint, error)

ParseFailOnList parses every raw value, skipping empty entries. It returns an aggregate error if any value is invalid; valid constraints are still returned alongside the error so callers can surface partial diagnostics.

func (FailOnConstraint) String

func (c FailOnConstraint) String() string

String returns a stable string form for the constraint, suitable for debug logs and error messages.

type FailOnKind

type FailOnKind string

FailOnKind classifies one --fail-on constraint.

const (
	// SeverityConstraint matches when a finding's severity is at or above
	// the constraint Value (any|low|medium|high|critical).
	SeverityConstraint FailOnKind = "severity"
	// ReachabilityConstraint matches when a vulnerability's reachability
	// status equals the constraint Value (currently only "reachable").
	ReachabilityConstraint FailOnKind = "reachability"
	// ExploitabilityConstraint matches when a vulnerability has known
	// exploitation metadata.
	ExploitabilityConstraint FailOnKind = "exploitability"
	// SourceChangeConstraint matches package-auditor findings for dependency
	// source changes in a diff.
	SourceChangeConstraint FailOnKind = "source-change"
)

type FallbackDetector deprecated

type FallbackDetector interface {
	FallbackDetector() Detector
}

FallbackDetector optionally provides a fallback detector that should run when the primary detector cannot produce a result.

Deprecated: the host executes planned detector chains; fallback interfaces are no longer consulted. The interface is kept for one release so existing implementations keep compiling.

type Finding

type Finding struct {
	ID           string              `json:"id"`
	Kind         FindingKind         `json:"kind"`
	Title        string              `json:"title,omitempty"`
	Severity     SeverityLevel       `json:"severity,omitempty"`
	PolicyStatus FindingPolicyStatus `json:"policy_status,omitempty"`
	Reasons      []string            `json:"reasons,omitempty"`
	Source       string              `json:"source,omitempty"`
	Auditor      string              `json:"auditor,omitempty"`
	// RuleID is the stable auditor rule that produced the finding. Unlike ID,
	// it must not contain package versions or project-specific occurrence data.
	RuleID           string    `json:"rule_id,omitempty"`
	VexStatus        VEXStatus `json:"vex_status,omitempty"`
	VEXJustification string    `json:"vex_justification,omitempty"`
	// PackageRef is the PURL of the offending package in the registry.
	PackageRef string `json:"package_ref,omitempty"`
	// DependencyRefs are the dependency node IDs that introduced the package.
	DependencyRefs []string `json:"dependency_refs,omitempty"`
	// VulnerabilityID is the advisory id within the referenced package, set
	// for vulnerability-kind findings.
	VulnerabilityID string `json:"vulnerability_id,omitempty"`
}

Finding describes a normalized audit result as a reference into the package registry rather than an inlined copy of vulnerability data. Consumers resolve the underlying enrichment via PackageRef (PURL) and, for vulnerability findings, VulnerabilityID (the OSV id inside the referenced package).

func (Finding) Clone

func (f Finding) Clone() Finding

Clone returns a deep copy of the finding.

func (*Finding) UnmarshalJSON

func (f *Finding) UnmarshalJSON(data []byte) error

UnmarshalJSON accepts the current policy_status field and the protocol-v1 legacy field so existing external auditor plugins continue to interoperate.

type FindingKind

type FindingKind string

FindingKind categorizes audit findings by the underlying concern the auditor is reporting on. Built-in auditors normally emit these kinds:

FindingKindVulnerability — advisory findings from the vulnerability auditor
FindingKindLicense       — emitted by the license auditor
FindingKindPackage       — package policy findings, including dependency
                          source changes

External plugins may introduce new kinds; consumers should treat the list as open.

const (
	FindingKindVulnerability FindingKind = "vulnerability"
	FindingKindLicense       FindingKind = "license"
	FindingKindPackage       FindingKind = "package"
)

type FindingPolicyDecision

type FindingPolicyDecision struct {
	Status FindingPolicyStatus
	Source string
	Reason string
}

FindingPolicyDecision is a resolver's proposed policy status for one finding. Source and Reason provide diagnostic provenance.

type FindingPolicyResolver

type FindingPolicyResolver interface {
	ResolveFindingPolicy(context.Context, Finding, *PackageRegistry) (FindingPolicyDecision, bool)
}

FindingPolicyResolver may refine a finding's policy status during auditing. Resolvers must not remove findings or rewrite their evidence.

type FindingPolicyStatus

type FindingPolicyStatus string

FindingPolicyStatus controls whether a finding fails evaluation, remains a warning, or is accepted by project policy.

const (
	FindingPolicyStatusFail FindingPolicyStatus = "fail"
	FindingPolicyStatusWarn FindingPolicyStatus = "warn"
	// FindingPolicyStatusSuppressed keeps a finding visible while excluding it
	// from policy-failure evaluation.
	FindingPolicyStatusSuppressed FindingPolicyStatus = "suppressed"
)

type FixAvailable

type FixAvailable struct {
	Version string           `json:"version,omitempty"`
	Date    string           `json:"date,omitempty"`
	Kind    FixAvailableKind `json:"kind,omitempty"`
}

FixAvailable captures one version/date/kind tuple for an available fix.

type FixAvailableKind

type FixAvailableKind string

FixAvailableKind identifies why a fix version was selected.

const (
	FixAvailableFirstObserved FixAvailableKind = "first-observed"
)

type FixState

type FixState string

FixState identifies whether a vulnerability has a known fix.

const (
	FixStateUnknown  FixState = "unknown"
	FixStateFixed    FixState = "fixed"
	FixStateNotFixed FixState = "not-fixed"
	FixStateWontFix  FixState = "wont-fix"
)

type Graph

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

Graph stores the typed graph nodes as a directed graph, keyed by NodeID. A node's ID is its identity (ADR-0041), so the ID index doubles as the identity index: two nodes are the same node exactly when their IDs match.

func ConsolidateGraphContainerEntry

func ConsolidateGraphContainerEntry(container *GraphContainer) (*Graph, error)

ConsolidateGraphContainerEntry ensures one entry is present.

func FilterGraphByScope

func FilterGraphByScope(src *Graph, scope Scope) (*Graph, error)

FilterGraphByScope returns a graph view containing roots plus dependencies whose normalized scope matches the requested filter.

func New

func New() *Graph

New creates an empty graph.

func NewWithCapacity

func NewWithCapacity(nodeCount int) *Graph

NewWithCapacity creates an empty graph sized for the expected node count.

func (*Graph) AddEdge

func (g *Graph) AddEdge(fromID, toID string) error

AddEdge adds a dependency relationship fromID -> toID, meaning fromID depends on toID.

The edge's kind is derived from the two nodes, so a caller that knows nothing about EdgeKind still produces correctly typed edges. Use AddTypedEdge to state a kind the structure does not imply.

func (*Graph) AddNode

func (g *Graph) AddNode(node GraphNode) error

AddNode inserts a node, rejecting a duplicate identity. Use InsertNode for fold-by-identity insertion.

func (*Graph) AddTypedEdge added in v0.7.0

func (g *Graph) AddTypedEdge(fromID, toID string, kind EdgeKind) error

AddTypedEdge adds a relationship and states what it asserts. An unknown kind is derived from the nodes, which is what AddEdge passes.

Adding an edge that already exists merges the kinds rather than ignoring the second call, so a stated kind is never lost to an earlier unstated one.

func (*Graph) CollectPathsTo

func (g *Graph) CollectPathsTo(targetID string) ([]Path, error)

CollectPathsTo returns deterministic root-to-target paths.

func (*Graph) DependencyNode added in v0.6.0

func (g *Graph) DependencyNode(id string) (*DependencyNode, bool)

DependencyNode returns the dependency node with the given ID, or false when the ID is absent or names a different kind.

func (*Graph) DependencyNodes added in v0.6.0

func (g *Graph) DependencyNodes() []*DependencyNode

DependencyNodes returns all dependency nodes sorted by ID — the iteration surface for matching, enrichment, and diffing, which are dependency-only.

func (*Graph) Dependents

func (g *Graph) Dependents(id string) ([]GraphNode, error)

Dependents returns direct dependents for a node, sorted by ID.

func (*Graph) DirectDependencies

func (g *Graph) DirectDependencies(id string) ([]GraphNode, error)

DirectDependencies returns direct dependencies for a node, sorted by ID.

func (*Graph) EdgeKindOf added in v0.7.0

func (g *Graph) EdgeKindOf(fromID, toID string) EdgeKind

EdgeKindOf returns the kind recorded for an edge, or EdgeKindUnknown when there is no such edge.

func (*Graph) InsertNode added in v0.6.0

func (g *Graph) InsertNode(node GraphNode) (GraphNode, error)

InsertNode is fold-by-identity insertion (ADR-0041): a node whose identity already exists in the graph unions into the existing record and the survivor is returned. Identity is the node ID, and IDs are disjoint across kinds, so a fold always joins records of one kind. Dependency folds union scopes, locations, and origins, merge the relationship, and fold registry-match eligibility toward eligible (any-witness: when exactly one witness is eligible, its source survives — withholding enrichment from a package a registry release genuinely uses would hide vulnerabilities). Module folds union locations; manifest folds are no-ops beyond the identity match.

func (*Graph) Leaves

func (g *Graph) Leaves() []GraphNode

Leaves returns nodes with no outgoing relationships.

func (*Graph) ManifestNodes added in v0.6.0

func (g *Graph) ManifestNodes() []*ManifestNode

ManifestNodes returns all manifest nodes sorted by ID.

func (*Graph) MarshalJSON

func (g *Graph) MarshalJSON() ([]byte, error)

MarshalJSON encodes a graph as a stable transport-friendly adjacency list.

func (*Graph) ModuleNodes added in v0.6.0

func (g *Graph) ModuleNodes() []*ModuleNode

ModuleNodes returns all module nodes sorted by ID.

func (*Graph) Node

func (g *Graph) Node(id string) (GraphNode, bool)

Node returns a node by ID.

func (*Graph) Nodes

func (g *Graph) Nodes() []GraphNode

Nodes returns all nodes sorted by ID.

func (*Graph) PrettyString

func (g *Graph) PrettyString() string

PrettyString returns a stable, human-readable adjacency list.

func (*Graph) PrettyTree

func (g *Graph) PrettyTree() string

PrettyTree returns an ASCII tree view of dependencies from graph roots.

func (*Graph) RemoveEdge

func (g *Graph) RemoveEdge(fromID, toID string) bool

RemoveEdge removes a dependency relationship and reports whether it existed.

func (*Graph) RemoveNode

func (g *Graph) RemoveNode(id string) bool

RemoveNode removes a node and all incident relationships.

func (*Graph) Roots

func (g *Graph) Roots() []GraphNode

Roots returns nodes with no incoming relationships.

func (*Graph) Size

func (g *Graph) Size() int

Size returns the number of nodes in the graph.

func (*Graph) TopologicalSort

func (g *Graph) TopologicalSort() ([]GraphNode, error)

TopologicalSort returns a topological ordering for the acyclic portion of the graph. If cycles remain, the returned slice contains the ordered prefix and ErrCycleDetected.

func (*Graph) UnmarshalJSON

func (g *Graph) UnmarshalJSON(data []byte) error

UnmarshalJSON decodes a graph from the plugin transport adjacency list. Nodes are reconstructed through the constructor gates (strict: an invalid dependency identity fails the decode) and inserted with fold-by-identity semantics, so a legacy payload whose distinct wire IDs mint one canonical identity folds instead of erroring. Edges follow the wire-ID → identity mapping; an edge that becomes a self-edge after folding is dropped — the fold made it meaningless.

func (*Graph) WalkDependencyNodes added in v0.6.0

func (g *Graph) WalkDependencyNodes(fn func(*DependencyNode) bool)

WalkDependencyNodes iterates all live dependency nodes. Returning false from fn stops iteration.

func (*Graph) WalkEdges

func (g *Graph) WalkEdges(fn func(from, to GraphNode) bool)

WalkEdges iterates all dependency relationships (from -> to). Returning false stops iteration.

func (*Graph) WalkNodes

func (g *Graph) WalkNodes(fn func(GraphNode) bool)

WalkNodes iterates all live nodes. Returning false from fn stops iteration.

func (*Graph) WalkTypedEdges added in v0.7.0

func (g *Graph) WalkTypedEdges(fn func(from, to GraphNode, kind EdgeKind) bool)

WalkTypedEdges iterates every relationship with the kind it asserts. Returning false stops iteration.

Reconstruction sites must use this, or CopyEdgesInto which is built on it, rather than WalkEdges: rebuilding a graph from (from, to) pairs alone drops every kind, and TestGraphReconstructionPreservesEdgeKind fails when it does.

type GraphContainer

type GraphContainer struct {
	Entries []GraphEntry `json:"entries,omitempty"`
}

GraphContainer groups one or more manifest-scoped dependency graphs.

func SingleGraphContainer

func SingleGraphContainer(g *Graph, manifest ManifestMetadata) *GraphContainer

SingleGraphContainer wraps a single graph entry.

func (*GraphContainer) ConsolidatedGraph

func (c *GraphContainer) ConsolidatedGraph() (*Graph, error)

ConsolidatedGraph materializes a single graph view for the container.

func (*GraphContainer) Len

func (c *GraphContainer) Len() int

Len returns the number of graph entries.

type GraphEntry

type GraphEntry struct {
	Graph    *Graph           `json:"graph,omitempty"`
	Manifest ManifestMetadata `json:"manifest"`
	Packages []*Package       `json:"packages,omitempty"`
	// Document carries the claims the source SBOM made about itself, when
	// this entry came from one. It lives here rather than on the scan because
	// an entry is what a document maps to: a scan that read three SBOMs read
	// three sets of these, and collapsing them loses which claim came from
	// where -- which is what a merged export links back to.
	Document *DocumentAssertions `json:"document,omitempty"`
}

GraphEntry describes one manifest-scoped dependency graph. Detection-time package facts discovered alongside the graph (licenses, digests, copyright pulled from lockfiles) are carried in Packages for folding into the global package registry during consolidation.

type GraphNode added in v0.6.0

type GraphNode interface {
	// NodeID returns the node's published graph ID: its identity.
	NodeID() string
	// Kind returns which member of the union this node is.
	Kind() NodeKind
	// NodeLocations returns the file locations that witnessed this node.
	NodeLocations() []PackageLocation
	// NodeWarnings returns the constructor-recorded recoverable conditions.
	NodeWarnings() []NodeWarning
	// CloneNode returns a deep copy of the node.
	CloneNode() GraphNode
	// contains filtered or unexported methods
}

GraphNode is the sealed union of the three graph-node kinds. Only this package's ManifestNode, ModuleNode, and DependencyNode implement it: the unexported method seals the union, so a type switch over the three kinds is exhaustive. A node's published graph ID is its identity itself — canonical package URL for dependency nodes, kind-qualified canonical paths for module and manifest nodes — which makes IDs disjoint across kinds and identity comparison a string comparison on IDs.

type HTTPClientConfig

type HTTPClientConfig struct {
	ProxyURL      string
	NoProxy       string
	ProxyType     string
	ProxyHost     string
	ProxyPort     int
	ProxyUsername string
	ProxyPassword string
	CACertFile    string
	Timeout       time.Duration
}

HTTPClientConfig configures Bomly's shared outbound HTTP client. External plugins normally obtain this from HTTPClientConfigFromEnv instead of building it by hand, so Bomly-managed proxy and CA settings are honored.

func HTTPClientConfigFromEnv

func HTTPClientConfigFromEnv() HTTPClientConfig

HTTPClientConfigFromEnv returns Bomly-specific HTTP client settings from environment variables. Standard HTTP_PROXY, HTTPS_PROXY, and NO_PROXY are still honored by NewHTTPClient when Bomly-specific values are absent.

func (HTTPClientConfig) EffectiveProxyURL

func (config HTTPClientConfig) EffectiveProxyURL() (string, error)

EffectiveProxyURL returns the effective proxy URL after applying Bomly's URL or decomposed proxy settings. It does not inspect standard proxy environment variables.

type HTTPClientProvider

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

HTTPClientProvider owns reusable HTTP transport state for one Bomly execution or plugin process. Reuse one provider for repeated outbound calls so connection pools, proxy settings, and TLS configuration stay consistent.

func NewHTTPClientProvider

func NewHTTPClientProvider(config HTTPClientConfig) (*HTTPClientProvider, error)

NewHTTPClientProvider creates an HTTP client provider with a reusable transport. Call Client to create timeout-specific clients that share connection pools and TLS/proxy settings.

func NewHTTPClientProviderFromEnv

func NewHTTPClientProviderFromEnv() (*HTTPClientProvider, error)

NewHTTPClientProviderFromEnv creates a provider from Bomly HTTP environment variables, with standard proxy environment variables honored as fallback. Use this in external plugins that make outbound HTTP calls.

func (*HTTPClientProvider) Client

func (p *HTTPClientProvider) Client(timeout time.Duration) *http.Client

Client returns an HTTP client with the requested timeout. A zero timeout uses the provider's configured default timeout.

func (*HTTPClientProvider) CloseIdleConnections

func (p *HTTPClientProvider) CloseIdleConnections()

CloseIdleConnections closes idle connections held by the provider transport.

type HostContext added in v0.2.0

type HostContext interface {
	Logger() *zap.Logger
	HTTPClient() *HTTPClientProvider
	Runtime() RuntimeInfo
	// DecodeConfig unmarshals the component's own configuration block into v.
	// Embedded execution sources it from the host config's kind-scoped
	// plugins.<kind>.<name> block; managed execution sources it from the
	// config file the host passes via BOMLY_PLUGIN_CONFIG_FILE. Identical
	// JSON semantics both ways.
	DecodeConfig(v any) error
}

HostContext is the only channel through which a component reaches host services. The same contract is satisfied by the embedded host (in-process registration) and the managed host (plugin subprocess), so a component written against it runs unchanged in both execution modes.

type InstallFirstDetector

type InstallFirstDetector interface {
	Install(context.Context, DetectionRequest) error
}

InstallFirstDetector optionally prepares dependencies before graph resolution.

type InstallResponse

type InstallResponse struct {
	Performed bool `json:"performed,omitempty"`
}

InstallResponse reports install-first execution details.

type KnownExploited

type KnownExploited struct {
	CVE                        string   `json:"cve,omitempty"`
	VendorProject              string   `json:"vendor_project,omitempty"`
	Product                    string   `json:"product,omitempty"`
	DateAdded                  string   `json:"date_added,omitempty"`
	RequiredAction             string   `json:"required_action,omitempty"`
	DueDate                    string   `json:"due_date,omitempty"`
	KnownRansomwareCampaignUse string   `json:"known_ransomware_campaign_use,omitempty"`
	Notes                      string   `json:"notes,omitempty"`
	URLs                       []string `json:"urls,omitempty"`
	CWEs                       []string `json:"cwes,omitempty"`
}

KnownExploited captures CISA KEV-style known exploitation metadata.

type Language

type Language string

Language identifies the programming language used by a package or analyzed by a code analyzer. Languages are coarse-grained and ecosystem-agnostic; one PackageManager can carry multiple languages (e.g. Maven covers Java, Kotlin, Scala, and Groovy).

const (
	LanguageUnknown    Language = ""
	LanguageGo         Language = "go"
	LanguageJavaScript Language = "javascript"
	LanguageTypeScript Language = "typescript"
	LanguagePython     Language = "python"
	LanguageJava       Language = "java"
	LanguageKotlin     Language = "kotlin"
	LanguageScala      Language = "scala"
	LanguageGroovy     Language = "groovy"
	LanguageRuby       Language = "ruby"
	LanguagePHP        Language = "php"
	LanguageRust       Language = "rust"
	LanguageCSharp     Language = "csharp"
	LanguageFSharp     Language = "fsharp"
	LanguageVB         Language = "vb"
	LanguageSwift      Language = "swift"
	LanguageObjC       Language = "objective-c"
	LanguageDart       Language = "dart"
	LanguageElixir     Language = "elixir"
	LanguageErlang     Language = "erlang"
	LanguageHaskell    Language = "haskell"
	LanguageOCaml      Language = "ocaml"
	LanguageLua        Language = "lua"
	LanguageR          Language = "r"
	LanguageC          Language = "c"
	LanguageCPP        Language = "cpp"
)

func LanguageFromPackage

func LanguageFromPackage(p Package) Language

LanguageFromPackage returns the most specific language for a package. It prefers the package's own Language field, then falls back to the primary language declared by the package's PackageManager (if recognizable), and finally returns LanguageUnknown.

func ParseLanguage

func ParseLanguage(value string) Language

ParseLanguage normalizes a string into a Language. Returns LanguageUnknown for unrecognized values; callers that need strict validation should compare the result against LanguageUnknown for non-empty input.

type LicenseType

type LicenseType string

LicenseType identifies license provenance: who is making the claim. Both SBOM formats draw the same distinction -- SPDX as licenseDeclared versus licenseConcluded, CycloneDX 1.6 as the acknowledgement field -- and it matters to a reader, because a declared license is what the package says about itself while a concluded one is what an analysis decided after looking. Collapsing them, as a single-valued vocabulary does, publishes an opinion as if the package had stated it.

const (
	// LicenseTypeDeclared is the license the package declares about itself:
	// a manifest field, a registry record, a lockfile entry.
	LicenseTypeDeclared LicenseType = "declared"
	// LicenseTypeConcluded is the license an analysis concluded, having
	// looked at more than the declaration -- a license file's text, a scan
	// result, a human review. It may contradict the declaration, which is a
	// fact worth publishing rather than a conflict to resolve.
	LicenseTypeConcluded LicenseType = "concluded"
)

func ParseLicenseType added in v0.7.0

func ParseLicenseType(value string) (LicenseType, error)

ParseLicenseType normalizes a license provenance value. An empty value is unknown provenance, which is legal; anything else unrecognized is an error, since a misspelled provenance would silently publish a conclusion as a declaration.

type LocatorKind added in v0.7.0

type LocatorKind string

LocatorKind says what shape an external reference's locator has, and so which grammar validates it.

It exists because a locator is not always a URL. Both formats carry non-URL locators: Bomly itself emits package URLs and CPE values as SPDX external references, SPDX's package-manager references carry bare coordinates such as "org.apache.tomcat:tomcat:9.0.0.M4", and a persistent identifier is a Software Heritage or gitoid string. Validating every locator as a URL would discard most of them.

const (
	// LocatorKindURL is a web location, held to URLFormReference.
	LocatorKindURL LocatorKind = "url"
	// LocatorKindPURL is a package URL, held to purlkit.
	LocatorKindPURL LocatorKind = "purl"
	// LocatorKindCPE22 is a CPE 2.2 URI ("cpe:/a:vendor:product").
	LocatorKindCPE22 LocatorKind = "cpe22"
	// LocatorKindCPE23 is a CPE 2.3 formatted string
	// ("cpe:2.3:a:vendor:product:...").
	//
	// The two bindings are separate kinds because the reference type names
	// one of them. Collapsing them into a single "cpe" kind let a cpe23Type
	// reference carry a 2.2 URI and vice versa, so an exporter would publish
	// a reference whose declared type contradicts its own locator.
	LocatorKindCPE23 LocatorKind = "cpe23"
	// LocatorKindIRI is CycloneDX's externalReference url field, which its
	// schema types as an IRI reference rather than a web URL. A BOM-Link
	// ("urn:cdx:<serial>/<version>") is a valid locator there, and ADR-0037
	// relies on exactly that to link a merged document back to its sources --
	// so holding this field to the web-URL gate would drop the reference the
	// merged-export design depends on.
	LocatorKindIRI LocatorKind = "iri"
	// LocatorKindIdentifier is the bounded free-form fallback: a token with
	// no whitespace or control characters and no grammar of its own.
	LocatorKindIdentifier LocatorKind = "identifier"
)

func LocatorKindFor added in v0.7.0

func LocatorKindFor(category ExternalReferenceCategory, referenceType string) LocatorKind

LocatorKindFor derives the locator shape from a reference's category and type. The pair is the input, never the category alone: an SPDX-only axis cannot decide the shape of a reference that arrived without one.

A reference with no category came from CycloneDX, whose schema types the field as an IRI reference, so its locator is a URL. A known category with an unrecognized type -- SPDX's OTHER, and any type the specification adds after this table was written -- takes the bounded identifier form, which accepts the most and asserts the least.

type ManifestKind

type ManifestKind string

ManifestKind identifies the manifest family represented by one graph entry.

const (
	// ManifestKindPackageLockJSON identifies npm package-lock.json manifests.
	ManifestKindPackageLockJSON ManifestKind = "package-lock.json"
	// ManifestKindNPMLockfile identifies generic npm lockfile manifests.
	ManifestKindNPMLockfile ManifestKind = "npm-lockfile"
	// ManifestKindPackageJSON identifies npm package.json manifests.
	ManifestKindPackageJSON ManifestKind = "package.json"
	// ManifestKindBunLock identifies Bun text lockfiles.
	ManifestKindBunLock ManifestKind = "bun.lock"
	// ManifestKindGoMod identifies Go module manifests.
	ManifestKindGoMod ManifestKind = "go.mod"
	// ManifestKindGoModule identifies normalized Go module manifests.
	ManifestKindGoModule ManifestKind = "go-module"
	// ManifestKindPomXML identifies Maven POM manifests.
	ManifestKindPomXML ManifestKind = "pom.xml"
	// ManifestKindRequirementsTXT identifies Python requirements manifests.
	ManifestKindRequirementsTXT ManifestKind = "requirements.txt"
	// ManifestKindSPDX identifies SPDX SBOM manifests.
	ManifestKindSPDX ManifestKind = "spdx"
	// ManifestKindSBOM identifies generic SBOM manifests.
	ManifestKindSBOM ManifestKind = "sbom"
	// ManifestKindGitHubSPDX identifies GitHub-produced SPDX SBOM manifests.
	ManifestKindGitHubSPDX ManifestKind = "github.spdx"
	// ManifestKindBomlySPDX identifies Bomly-produced SPDX SBOM manifests.
	ManifestKindBomlySPDX ManifestKind = "bomly.spdx"
	// ManifestKindGitHubActions identifies GitHub Actions manifests.
	ManifestKindGitHubActions ManifestKind = "github-actions"
	// ManifestKindGitHubActionsWorkflow identifies GitHub Actions workflow files.
	ManifestKindGitHubActionsWorkflow ManifestKind = "github-actions-workflow"
	// ManifestKindGitHubActionsAction identifies GitHub Actions action metadata files.
	ManifestKindGitHubActionsAction ManifestKind = "github-actions-action"
)

type ManifestMetadata

type ManifestMetadata struct {
	Path       string              `json:"path,omitempty"`
	Kind       ManifestKind        `json:"kind,omitempty"`
	Resolution *ResolutionMetadata `json:"resolution,omitempty"`
}

ManifestMetadata describes the manifest or evidence file associated with one graph.

type ManifestNode added in v0.6.0

type ManifestNode struct {
	// Path is the canonical repository-relative path (identity).
	Path string
	// FileKind classifies the manifest file, reusing the ManifestMetadata
	// vocabulary.
	FileKind ManifestKind
	// Metadata is the free-form escape hatch shared by all node kinds.
	Metadata map[string]any
	// contains filtered or unexported fields
}

ManifestNode is the structural file record of the union: a manifest, lockfile, or build script. Its identity is its canonical repository-relative path, its published ID is "manifest:" + path, and it is never matched or enriched. The per-entry ManifestMetadata on GraphEntry remains the authoritative manifest record; a manifest node is the graph's projection of it (detectorkit.ManifestNodeForEntry derives one), with the public constructor covering nested workspace manifests.

func NewManifestNode added in v0.6.0

func NewManifestNode(manifestPath string, kind ManifestKind) (*ManifestNode, error)

NewManifestNode constructs a manifest node from a repository-relative path. The path passes CanonicalRepoPath; there is no other gate.

func (*ManifestNode) Clone added in v0.6.0

func (n *ManifestNode) Clone() *ManifestNode

Clone returns a deep copy.

func (*ManifestNode) CloneNode added in v0.6.0

func (n *ManifestNode) CloneNode() GraphNode

CloneNode implements GraphNode.

func (*ManifestNode) Kind added in v0.6.0

func (n *ManifestNode) Kind() NodeKind

Kind returns NodeKindManifest.

func (*ManifestNode) NodeID added in v0.6.0

func (n *ManifestNode) NodeID() string

NodeID returns "manifest:" + the canonical path.

func (*ManifestNode) NodeLocations added in v0.6.0

func (n *ManifestNode) NodeLocations() []PackageLocation

NodeLocations returns the manifest's own path as its single location.

func (*ManifestNode) NodeWarnings added in v0.6.0

func (n *ManifestNode) NodeWarnings() []NodeWarning

NodeWarnings returns nil: manifest construction records no recoverable conditions.

type MatchRequest

type MatchRequest struct {
	ProjectPath     string           `json:"projectPath,omitempty"`
	ExecutionTarget ExecutionTarget  `json:"executionTarget"`
	SubprojectInfo  Subproject       `json:"subprojectInfo"`
	Ecosystem       Ecosystem        `json:"ecosystem,omitempty"`
	PackageManager  PackageManager   `json:"packageManager,omitempty"`
	Query           PackageQuery     `json:"query"`
	Graph           *Graph           `json:"graph,omitempty"`
	Registry        *PackageRegistry `json:"registry,omitempty"`
	Target          *DependencyNode  `json:"target,omitempty"`
	MatcherFilter   MatcherFilter    `json:"matcherFilter"`
	// AcceptPackageUpdates signals that the host understands
	// MatchResult.PackageUpdates. Matchers advertising
	// CapabilityPackageUpdates may return updates instead of a full registry
	// only when this is true.
	AcceptPackageUpdates bool      `json:"acceptPackageUpdates,omitempty"`
	Stderr               io.Writer `json:"-"`
}

MatchRequest defines input for a matcher. Matchers enrich the package Registry keyed by PURL; the dependency Graph provides identity and structure.

type MatchResponse

type MatchResponse = MatchResult

MatchResponse is the matcher response payload exposed to plugins.

It aliases MatchResult so plugin code can name payload types by role while sharing the same transport shape Bomly core uses internally.

type MatchResult

type MatchResult struct {
	Registry       *PackageRegistry `json:"registry,omitempty"`
	PackageUpdates []*Package       `json:"packageUpdates,omitempty"`
	MatcherStats   MatcherStats     `json:"matcherStats,omitempty"`
}

MatchResult contains the package registry after matcher enrichment.

A matcher returns either Registry (the full enriched registry — the protocol v1 baseline) or, when the request set AcceptPackageUpdates, PackageUpdates: only the packages it touched. The host merges updates into its registry by PURL. When Registry is non-nil it wins and PackageUpdates is ignored.

type Matcher

type Matcher interface {
	Descriptor() MatcherDescriptor
	// Ready reports whether the matcher can run for the given request. It
	// returns nil when ready and a non-nil error describing the reason
	// otherwise. Implementations may perform lightweight, cancellable I/O and
	// should honor ctx.
	Ready(context.Context, MatchRequest) error
	Applicable(context.Context, MatchRequest) (bool, error)
	Match(context.Context, MatchRequest) (MatchResult, error)
}

Matcher enriches registry packages with license and vulnerability data.

type MatcherDescriptor

type MatcherDescriptor struct {
	Name                string           `json:"name"`
	DisplayName         string           `json:"displayName,omitempty"`
	Aliases             []string         `json:"aliases,omitempty"`
	Tags                []string         `json:"tags,omitempty"`
	SupportedEcosystems []Ecosystem      `json:"supportedEcosystems,omitempty"`
	SupportedManagers   []PackageManager `json:"supportedManagers,omitempty"`
	// Capabilities advertises optional protocol features this matcher
	// supports, such as CapabilityPackageUpdates.
	Capabilities []string `json:"capabilities,omitempty"`
	// ConfigSchema optionally documents the matcher's configuration block as
	// a JSON Schema. Build it with ConfigSchemaFor.
	ConfigSchema json.RawMessage `json:"configSchema,omitempty"`
}

MatcherDescriptor describes a matcher registration.

func (MatcherDescriptor) Label

func (d MatcherDescriptor) Label() string

Label returns the user-facing matcher label, falling back to Name.

type MatcherFilter

type MatcherFilter struct {
	Include []string
	Exclude []string
}

MatcherFilter narrows matcher selection for a request.

func (MatcherFilter) Excludes

func (f MatcherFilter) Excludes(name string) bool

Excludes reports whether a matcher name is explicitly denied.

func (MatcherFilter) Includes

func (f MatcherFilter) Includes(name string) bool

Includes reports whether a matcher name is explicitly allowed.

type MatcherModule added in v0.2.0

type MatcherModule struct {
	Descriptor MatcherDescriptor
	New        func(context.Context, HostContext) (Matcher, error)
}

MatcherModule declares one matcher component.

type MatcherStats

type MatcherStats struct {
	Name              string `json:"name"`
	DisplayName       string `json:"displayName,omitempty"`
	MatchedPackages   int    `json:"matchedPackages,omitempty"`
	UnmatchedPackages int    `json:"unmatchedPackages,omitempty"`
	Licenses          int    `json:"licenses,omitempty"`
	Vulnerabilities   int    `json:"vulnerabilities,omitempty"`
}

MatcherStats describes one completed matcher run and optional summary counts.

type Module added in v0.2.0

type Module struct {
	Kind     PluginKind
	Detector *DetectorModule
	Matcher  *MatcherModule
	Auditor  *AuditorModule
	Analyzer *AnalyzerModule
}

Module is the execution-neutral packaging of one component. Exactly one of the role fields must be set, and it must match Kind. The same Module value can be registered embedded by the host or served managed via ServeModule.

type ModuleNode added in v0.6.0

type ModuleNode struct {
	Coordinates
	// DeclaringManifestPath is the canonical repository-relative path of
	// the manifest that declares this module — always part of the identity.
	DeclaringManifestPath string
	// Locations are the file locations that witnessed the module.
	Locations []PackageLocation
	// Metadata is the free-form escape hatch shared by all node kinds.
	Metadata map[string]any
	// contains filtered or unexported fields
}

ModuleNode is one of the scanned project's own artifacts: the root project and every workspace or reactor module. Ownership is the kind — it cannot be dropped by a fold or asserted by an imported document — and module nodes are never matched or enriched. Identity is the declaring manifest path beside the canonical package URL when one is derivable, or beside the module name otherwise: a recursive scan can discover two unrelated projects with identical coordinates, and the path keeps those roots apart.

func AsModuleNode added in v0.9.0

func AsModuleNode(node GraphNode) (*ModuleNode, bool)

AsModuleNode narrows a node to a module node, reporting whether it is one.

func NewModuleNode added in v0.6.0

func NewModuleNode(declaringManifestPath string, coords Coordinates) (*ModuleNode, error)

NewModuleNode constructs a module node. The declaring manifest path passes CanonicalRepoPath and always participates in identity. The coordinates are normalized, and a canonical package URL is derived when they allow one — under the same missing-version warning policy as dependency nodes — otherwise the module is identified by path and name, and the name is then required.

func (*ModuleNode) Clone added in v0.6.0

func (n *ModuleNode) Clone() *ModuleNode

Clone returns a deep copy.

func (*ModuleNode) CloneNode added in v0.6.0

func (n *ModuleNode) CloneNode() GraphNode

CloneNode implements GraphNode.

func (*ModuleNode) Kind added in v0.6.0

func (n *ModuleNode) Kind() NodeKind

Kind returns NodeKindModule.

func (*ModuleNode) NodeID added in v0.6.0

func (n *ModuleNode) NodeID() string

NodeID returns "module:" + path + "#" + (canonical PURL | name).

func (*ModuleNode) NodeLocations added in v0.6.0

func (n *ModuleNode) NodeLocations() []PackageLocation

NodeLocations returns the module's witnessed locations.

func (*ModuleNode) NodeWarnings added in v0.6.0

func (n *ModuleNode) NodeWarnings() []NodeWarning

NodeWarnings returns the constructor-recorded recoverable conditions.

func (*ModuleNode) PURL added in v0.6.0

func (n *ModuleNode) PURL() string

PURL returns the module's canonical package URL, or "" when none was derivable.

type NPMPackageMetadata

type NPMPackageMetadata struct {
	Bundled                  bool              `json:"bundled,omitempty"`
	Extraneous               bool              `json:"extraneous,omitempty"`
	HasInstallScript         bool              `json:"hasInstallScript,omitempty"`
	PeerDependencies         map[string]string `json:"peerDependencies,omitempty"`
	OptionalPeerDependencies []string          `json:"optionalPeerDependencies,omitempty"`
	Engines                  map[string]string `json:"engines,omitempty"`
}

NPMPackageMetadata holds npm-specific package data extracted from npm/pnpm/yarn lockfiles that does not fit into the cross-ecosystem fields.

type NodeKind added in v0.6.0

type NodeKind string

NodeKind discriminates the sealed graph-node union (ADR-0041 in bomly-cli's dev-docs/adr). Exactly three kinds exist in protocol v1; a future kind means a v2 negotiation, so ParseNodeKind rejects anything else rather than guessing.

const (
	// NodeKindManifest is a structural file record: a package.json, a
	// lockfile, a build script. Identified by its path, never matched or
	// enriched.
	NodeKindManifest NodeKind = "manifest"
	// NodeKindModule is one of the scanned project's own artifacts: the
	// root project itself and every workspace or reactor module.
	// First-party ownership is the kind, not a flag.
	NodeKindModule NodeKind = "module"
	// NodeKindDependency is one resolved third-party package — the unit of
	// matching and enrichment. Its identity is its canonical package URL.
	NodeKindDependency NodeKind = "dependency"
)

func ParseNodeKind added in v0.6.0

func ParseNodeKind(value string) (NodeKind, error)

ParseNodeKind validates a wire-supplied kind value. An unrecognized kind is an error, never a guess: a v1 payload can only carry v1 kinds.

type NodeWarning added in v0.6.0

type NodeWarning struct {
	Code    NodeWarningCode
	Message string
}

NodeWarning is a recoverable, constructor-recorded observation about a node. Warnings are in-process state: they are re-derived wherever the node is reconstructed (the wire decoder runs the same constructor gates), so they never need to travel.

type NodeWarningCode added in v0.6.0

type NodeWarningCode string

NodeWarningCode identifies a recoverable condition a node constructor recorded instead of failing.

const (
	// NodeWarningMissingVersion marks a node whose package URL carries no
	// version. The purl specification leaves version optional, so absence is
	// visible rather than fatal.
	NodeWarningMissingVersion NodeWarningCode = "missing-version"
	// NodeWarningDroppedEvidenceQualifier marks a URL-valued evidence
	// qualifier whose value did not survive the origin gates and was
	// discarded entirely — a signed or tokenized link is never sanitized
	// into something publishable.
	NodeWarningDroppedEvidenceQualifier NodeWarningCode = "dropped-evidence-qualifier"
	// NodeWarningGenericIdentity marks a node whose ecosystem's own package
	// URL type could not express its coordinates, so its identity was minted
	// as pkg:generic instead.
	//
	// The alternative was refusing the node, which is worse: the package was
	// installed, it is in the artifact, and an inventory that omits it is
	// wrong in a way a loosely typed identity is not. The warning is how a
	// consumer can tell the difference -- the ecosystem stays on the
	// coordinates either way, so nothing but the identity's type degrades.
	NodeWarningGenericIdentity NodeWarningCode = "generic-identity"
)

type Package

type Package struct {
	Coordinates
	// ID is the package registry identifier. It may be a database ID, PURL, or
	// another stable key chosen by the package registry.
	ID        string `json:"id,omitempty"`
	Copyright string `json:"copyright,omitempty"`
	// ResolvedURL is detection-time evidence carried onto the registry package
	// for matchers (repository resolution reads it). It is raw and never
	// published; the dependency's validated Origin stays on the graph node.
	ResolvedURL string `json:"resolved_url,omitempty"`
	// DetectedOrigins carries the graph node's vetted ADR-0033 origins onto
	// the seeded registry package, so matchers that resolved repositories
	// from the (now identity-stripped) URL-valued purl qualifiers receive
	// the relocated signal. Additive and optional; every element passes the
	// origin codecs' validation. Merge class: union by normalized value.
	DetectedOrigins []DependencyOrigin `json:"detected_origins,omitempty"`

	// Description is the package's own summary of itself, as the source
	// document or registry stated it. SPDX PackageDescription / CycloneDX
	// component description.
	//
	// Gate: NormalizeDescription -- trimmed and bounded, control characters
	// dropped, an over-long value cleared rather than truncated.
	// Merge class: scalar, fill-gaps. The first publishable witness wins and
	// a later one contributes only what is missing.
	Description string `json:"description,omitempty"`
	// Homepage is the package's project page. SPDX PackageHomePage /
	// CycloneDX an external reference of type website.
	//
	// Gate: NormalizeHomepage -- URLFormReference, so a bare host and a query
	// are legitimate where they would not be for an artifact URL, while
	// credentials, local paths, and non-http schemes are cleared.
	// Merge class: scalar, fill-gaps.
	Homepage string `json:"homepage,omitempty"`
	// Supplier is who distributed the package. SPDX PackageSupplier /
	// CycloneDX supplier.
	//
	// Gate: Contact.Normalized -- an unpublishable contact becomes nil, and
	// no email address is retained (see Contact).
	// Merge class: scalar, fill-gaps.
	Supplier *Contact `json:"supplier,omitempty"`
	// Originator is who originally authored the package, which is often not
	// the supplier -- a redistributor supplies what someone else wrote. SPDX
	// PackageOriginator / CycloneDX author or publisher.
	//
	// Gate and merge class: as Supplier.
	Originator *Contact `json:"originator,omitempty"`
	// ExternalReferences are the references a source document attached to
	// this component: advisories, repositories, package-manager coordinates,
	// CPE values. SPDX externalRefs / CycloneDX externalReferences.
	//
	// Gate: ExternalReference.Normalized. Merge class: set, unioned by the
	// (category, type, locator) triple through MergeExternalReferences.
	ExternalReferences []ExternalReference `json:"external_references,omitempty"`

	// CPEs, Digests, and Licenses are set-valued: every witness's claims
	// survive a merge, because two sources can each know something the other
	// does not. Gates: Digest.Normalized and PackageLicense.Normalized drop
	// what cannot be published; MergeLicenses additionally keeps two sources
	// that reuse one license reference for different terms apart.
	CPEs            []string             `json:"cpes,omitempty"`
	Digests         []Digest             `json:"digests,omitempty"`
	Licenses        []PackageLicense     `json:"licenses,omitempty"`
	Vulnerabilities []Vulnerability      `json:"vulnerabilities,omitempty"`
	Attestations    []PackageAttestation `json:"attestations,omitempty"`
	Scorecard       *PackageScorecard    `json:"scorecard,omitempty"`
	EOL             *PackageEOL          `json:"eol,omitempty"`
	Remediation     *PackageRemediation  `json:"remediation,omitempty"`

	// Matched indicates that this package was successfully matched by one or
	// more external enrichment sources.
	Matched bool `json:"matched,omitempty"`

	// Metadata holds per-ecosystem extensible data.
	Metadata map[string]any `json:"metadata,omitempty"`
}

Package describes one matching artifact: the PURL-keyed, deduplicated record produced by the matching stage. Many Dependency nodes (across manifests and subprojects) reference a single Package by PURL. A Package holds only matching-stage enrichment; detection-time identity and relationships live on Dependency.

func PackageFromDependencyNode added in v0.6.0

func PackageFromDependencyNode(dep *DependencyNode) *Package

PackageFromDependencyNode seeds a registry package from a dependency node's identity. The node's ID is its canonical package URL, so the package is keyed on it directly. The returned package carries no enrichment; matchers fill it in. DetectedOrigins projects the node's vetted origins onto the package, so matchers that used to read the URL-valued purl qualifiers (Scorecard's repository resolution) receive the relocated signal.

func (*Package) Clone

func (p *Package) Clone() *Package

Clone returns a deep copy of the package.

func (*Package) DisplayName

func (p *Package) DisplayName() string

DisplayName returns the most human-friendly identifier available, using the ecosystem-native name form (e.g. "@org/name" for npm).

func (*Package) IdentityKey

func (p *Package) IdentityKey() string

IdentityKey returns a stable package identity without version information.

func (*Package) LicenseValues

func (p *Package) LicenseValues() []string

LicenseValues returns normalized package license labels in stable order.

func (Package) MarshalJSON added in v0.7.0

func (p Package) MarshalJSON() ([]byte, error)

MarshalJSON re-gates on the way out. The receiver is a value, so normalization applies to this copy and never rewrites the record its holder still owns.

func (*Package) MergeFrom

func (p *Package) MergeFrom(src *Package)

MergeFrom folds enrichment from src into p in place. Used by the package registry to deduplicate multiple records for the same PURL. Existing typed data on p wins; src contributes anything p is missing, and vulnerability lists are unioned by (Source, ID).

func (*Package) NormalizeAssertions added in v0.7.0

func (p *Package) NormalizeAssertions()

NormalizeAssertions re-runs the publication gate on every field of p that carries untrusted input, in place. Values that cannot be published are cleared rather than corrected, following the codecs on DependencyOrigin and Contact.

Package has no JSON codec of its own -- it is a large struct decoded by the standard rules, and matcher package updates cross the plugin wire as plain values -- so there is no unmarshal hook to hang these rules on. This method is that hook, and PackageRegistry.Add calls it, which is the one door every package goes through on its way into the registry.

func (*Package) QualifiedName

func (p *Package) QualifiedName() string

QualifiedName returns the package name prefixed with its organization when present.

func (*Package) UnmarshalJSON added in v0.7.0

func (p *Package) UnmarshalJSON(data []byte) error

UnmarshalJSON applies the package rule as a value arrives, and MarshalJSON applies it again on the way out.

The codec lives on the type because the registry is not the only path a package takes. A matcher or analyzer returns PackageUpdates on its result, which the plugin transport serializes directly -- never through PackageRegistry -- so a gate that lived only at the registry let a credential-bearing homepage cross the wire, and let contacts and digests the model had already rejected encode as empty "{}" objects.

type PackageAttestation added in v0.4.0

type PackageAttestation struct {
	// PredicateType identifies what the statement asserts, using the in-toto
	// predicate vocabulary (for example "https://slsa.dev/provenance/v1").
	PredicateType string `json:"predicate_type,omitempty"`
	// Source names the component or service that attached the statement, in
	// the same style as PackageScorecard.Source.
	Source string `json:"source,omitempty"`
	// URL is where the statement can be fetched.
	URL string `json:"url,omitempty"`
	// Digest identifies the statement itself, so two fetches of one URL can be
	// told apart.
	Digest *Digest `json:"digest,omitempty"`
	// Issuer is the identity that signed the statement -- an OIDC identity, a
	// key id, or a registry account -- as reported by whatever verified it.
	Issuer string `json:"issuer,omitempty"`
	// Verified records that the component attaching this statement checked its
	// signature. False means the statement was found but not verified, which is
	// weaker evidence rather than evidence of tampering; consumers must not
	// present an unverified statement as proof of provenance.
	Verified bool `json:"verified,omitempty"`
}

PackageAttestation records a signed statement about how a package was built or published: an in-toto statement such as SLSA provenance, or a publish-time signature.

Bomly does not fetch or verify attestations today. The type exists so a matcher that does can attach what it found without a model change, and so consumers can tell a verified statement from one that was merely present -- a distinction that matters more than the statement itself, and that is easily lost when provenance data is carried in untyped metadata.

func (PackageAttestation) Clone added in v0.4.0

Clone returns a deep copy.

type PackageEOL

type PackageEOL struct {
	Source        string `json:"source,omitempty"`
	Cycle         string `json:"cycle,omitempty"`
	EOL           bool   `json:"eol,omitempty"`
	EOLDate       string `json:"eol_date,omitempty"`
	LatestVersion string `json:"latest_version,omitempty"`
	ReleaseDate   string `json:"release_date,omitempty"`
	Supported     bool   `json:"supported,omitempty"`
}

PackageEOL captures end-of-life enrichment attached by the EOL matcher.

func (*PackageEOL) Clone

func (e *PackageEOL) Clone() *PackageEOL

Clone returns a deep copy of the EOL payload.

type PackageLicense

type PackageLicense struct {
	// Value is the license as the source stated it, unmodified.
	Value string `json:"value,omitempty"`
	// SPDXExpression is the validated SPDX expression form, when the value
	// has one. For a license that is not on the SPDX list, this is a minted
	// LicenseRef-* identifier whose text is carried in ExtractedText.
	SPDXExpression string `json:"spdx_expression,omitempty"`
	// Type is what kind of claim this is: declared by the package, or
	// concluded by an analysis. See LicenseType.
	Type LicenseType `json:"type,omitempty"`
	// Source names the component that supplied the claim -- a matcher name
	// such as "external-depsdev". It answers "who says so", which Type does
	// not: Type is a closed two-member vocabulary about the kind of claim,
	// and the two questions are independent. A deps.dev license is declared
	// *and* sourced from deps.dev.
	//
	// They were briefly the same field, which is how this one came to exist.
	// matcherkit.NormalizeLicenseSet wrote its matcher name into Type, so
	// once Type became a closed vocabulary the gate dropped the value --
	// silently emptying the "licenses[].source" field the CLI documents and
	// publishes. Two independent facts sharing one field is what made that
	// possible.
	//
	// Gate: PackageLicense.Normalized -- held to the component-name rule,
	// not a token rule. A component descriptor requires only a non-blank
	// name, so "My Matcher" and a name over 64 bytes are both valid
	// components; gating this as a single short token would silently erase
	// the source of a legitimately named matcher. What is enforced is what
	// publication actually needs: valid UTF-8, no control characters, and a
	// bound.
	// Merge class: scalar, fill-gaps *within* a claim. Source is deliberately
	// not part of the merge identity -- two matchers reporting one license
	// stay one claim -- so the witness that carries a source supplies it to
	// the one that does not, whichever arrived first.
	Source string `json:"source,omitempty"`
	// Name is the human-readable license name for a LicenseRef-* identifier.
	// SPDX's hasExtractedLicensingInfos carries one, and a reader given only
	// "LicenseRef-bomly-3f2a..." has nothing to go on.
	//
	// Gate: PackageLicense.Normalized (trimmed; cleared with the expression
	// when a reference cannot be published). Merge class: scalar, fill-gaps
	// *within* a claim -- Name is deliberately not part of the merge
	// identity, so two records naming one license stay one claim and the
	// witness that carries a name supplies it to the one that does not.
	Name string `json:"name,omitempty"`
	// ExtractedText is the original license text a LicenseRef-* identifier
	// names. Both formats require the text to accompany the reference -- an
	// SPDX document whose expression cites a LicenseRef without a matching
	// hasExtractedLicensingInfos entry is invalid -- so the pair travels
	// together on one record rather than in a side table that a merge or a
	// projection could separate it from (bomly-cli issue #410).
	//
	// The text is authoritative and the reference is derived from it, per
	// spdxkit.MintLicenseRef. Normalized re-mints rather than trusting a
	// reference that disagrees with its text.
	//
	// Gate: PackageLicense.Normalized -- bounded, and blank text is cleared
	// (it would otherwise mint the reference empty text mints, so every
	// package with a blank license file would share one citation).
	//
	// Merge class: set member, and part of the merge identity -- by what the
	// text mints rather than by its bytes, since whitespace-only differences
	// name one license. Two claims whose texts genuinely differ are both
	// kept, and if they arrived under one reference the later is re-minted so
	// the set never leaves one identifier naming two licenses.
	ExtractedText string `json:"extracted_text,omitempty"`
}

PackageLicense captures normalized license details for a package.

func DetectionLicenses

func DetectionLicenses(dep *DependencyNode) []PackageLicense

DetectionLicenses returns the license facts recorded on dep at detection time: the typed field unioned with anything a producer left under the deprecated MetadataKeyDetectionLicenses stash.

Both are read because the stash outlives this release. A node decoded from an older producer's payload, or built by a component still pinned to an earlier SDK, carries its licenses only in metadata, and a consumer that looked at the typed field alone would silently see a package with no licenses at all.

func MergeLicenses added in v0.7.0

func MergeLicenses(existing, additions []PackageLicense) []PackageLicense

MergeLicenses unions license claims, keeping the first record of each distinct claim. Licenses are a set: a package whose declaration and whose concluded analysis disagree carries both, and two sources that saw the same declaration contribute one entry.

func (PackageLicense) MarshalJSON added in v0.7.0

func (l PackageLicense) MarshalJSON() ([]byte, error)

MarshalJSON applies the same rule on the way out.

func (PackageLicense) Normalized added in v0.7.0

func (l PackageLicense) Normalized() (PackageLicense, bool)

Normalized returns the license with its claim re-checked, or false when the record says nothing publishable. It is the gate for a license that arrived from a plugin, an ingested document, or a hand-built value.

Three rules are enforced. An unrecognized provenance is dropped to unknown rather than published as a claim nobody made. A LicenseRef-* expression is re-minted from its text, since spdxkit makes the text authoritative: a reference that disagrees with the text it names would produce a document whose citation resolves to the wrong license. And every other expression is put to spdxkit -- the single home for SPDX expression semantics (ADR-0038) -- rather than trusted because it merely lacks a reference prefix.

The expression that survives is the canonical one. spdxkit rewrites deprecated identifiers, so "GPL-2.0" is stored as "GPL-2.0-only": Value keeps what the source said, while SPDXExpression is the form Bomly is willing to publish. Anything the parser rejects is dropped rather than exported into a document that would then fail its own validator.

func (*PackageLicense) UnmarshalJSON added in v0.7.0

func (l *PackageLicense) UnmarshalJSON(data []byte) error

UnmarshalJSON applies the license rule as a value arrives, so a claim that would be rejected on read cannot be stored, forwarded, or written back out. A record that says nothing publishable decodes to the zero value, following DependencyOrigin.

type PackageLocation

type PackageLocation struct {
	RealPath   string `json:"real_path,omitempty"`
	AccessPath string `json:"access_path,omitempty"`
	// Position optionally points at the exact line / column in RealPath where
	// the package is declared. nil when unknown.
	Position *SourcePosition `json:"position,omitempty"`

	// ModuleRoot is the module whose resolution produced this site: a
	// workspace member's directory, a Go main module, a Maven reactor
	// project. Empty when the producer did not attribute it.
	//
	// It is what makes the fields below answerable. "Is this package a direct
	// runtime dependency?" has no single answer for a workspace -- it can be
	// direct-in-development in one module and transitive-at-runtime in
	// another -- and a question with two answers can only be asked per module
	// root.
	ModuleRoot string `json:"module_root,omitempty"`
	// Scopes are the scopes this particular site was reached under, as
	// opposed to the union across every site, which is what the node carries.
	Scopes []Scope `json:"scopes,omitempty"`
	// Relationship is whether this site was declared directly by its module
	// root or reached through another dependency. Same reasoning as Scopes:
	// the node-level value is a union and cannot distinguish the sites.
	Relationship DependencyRelationship `json:"relationship,omitempty"`
}

PackageLocation captures where a package was discovered.

type PackageManager

type PackageManager string

PackageManager identifies the concrete package manager or manifest family for a target. The type is string-backed so plugins can pass custom values while Bomly grows first-class constants; use PackageManagerOther when no specific manager value is appropriate.

const (
	PackageManagerUnknown       PackageManager = ""
	PackageManagerNPM           PackageManager = "npm"
	PackageManagerPNPM          PackageManager = "pnpm"
	PackageManagerYarn          PackageManager = "yarn"
	PackageManagerBun           PackageManager = "bun"
	PackageManagerGradle        PackageManager = "gradle"
	PackageManagerMaven         PackageManager = "maven"
	PackageManagerGoMod         PackageManager = "gomod"
	PackageManagerPip           PackageManager = "pip"
	PackageManagerPipenv        PackageManager = "pipenv"
	PackageManagerPoetry        PackageManager = "poetry"
	PackageManagerUV            PackageManager = "uv"
	PackageManagerALPM          PackageManager = "alpm"
	PackageManagerAPK           PackageManager = "apk"
	PackageManagerConan         PackageManager = "conan"
	PackageManagerConda         PackageManager = "conda"
	PackageManagerPub           PackageManager = "pub"
	PackageManagerDPKG          PackageManager = "dpkg"
	PackageManagerMix           PackageManager = "mix"
	PackageManagerRebar         PackageManager = "rebar"
	PackageManagerOTP           PackageManager = "otp"
	PackageManagerGitHubActions PackageManager = "github-actions"
	PackageManagerCabal         PackageManager = "cabal"
	PackageManagerStack         PackageManager = "stack"
	PackageManagerHomebrew      PackageManager = "homebrew"
	PackageManagerLuaRocks      PackageManager = "luarocks"
	PackageManagerNuGet         PackageManager = "nuget"
	PackageManagerNix           PackageManager = "nix"
	PackageManagerOpam          PackageManager = "opam"
	PackageManagerComposer      PackageManager = "composer"
	PackageManagerPear          PackageManager = "pear"
	PackageManagerPDM           PackageManager = "pdm"
	PackageManagerPortage       PackageManager = "portage"
	PackageManagerSWIPLPack     PackageManager = "swipl-pack"
	PackageManagerRPackage      PackageManager = "r-package"
	PackageManagerRPM           PackageManager = "rpm"
	PackageManagerBundler       PackageManager = "bundler"
	PackageManagerGemspec       PackageManager = "gemspec"
	PackageManagerCargo         PackageManager = "cargo"
	PackageManagerSBOM          PackageManager = "sbom"
	PackageManagerSnap          PackageManager = "snap"
	PackageManagerCocoaPods     PackageManager = "cocoapods"
	PackageManagerSwiftPM       PackageManager = "swiftpm"
	PackageManagerTerraform     PackageManager = "terraform"
	PackageManagerWordPress     PackageManager = "wordpress"
	PackageManagerSetupPy       PackageManager = "setuppy"
	PackageManagerOther         PackageManager = "other"
	PackageManagerSBT           PackageManager = "sbt"
	PackageManagerMultiple      PackageManager = "multiple"
)

func AllPackageManagers

func AllPackageManagers() []PackageManager

AllPackageManagers returns the canonical package-manager list in SDK order.

func ParsePackageManager

func ParsePackageManager(value string) (PackageManager, error)

ParsePackageManager normalizes a package-manager value.

func (PackageManager) Ecosystem

func (p PackageManager) Ecosystem() Ecosystem

Ecosystem returns the higher-level grouping for a package manager.

func (PackageManager) Languages

func (p PackageManager) Languages() []Language

Languages returns the programming languages typically built with this package manager. The first entry is the most common / canonical language; callers that need a single value should take Languages()[0]. Returns nil for OS-level managers and any manager that does not have a meaningful language association.

func (PackageManager) MarshalJSON

func (p PackageManager) MarshalJSON() ([]byte, error)

MarshalJSON encodes a package manager by its canonical name.

func (PackageManager) Name

func (p PackageManager) Name() string

Name returns the canonical package-manager name.

func (PackageManager) String

func (p PackageManager) String() string

String returns the canonical package-manager name.

func (*PackageManager) UnmarshalJSON

func (p *PackageManager) UnmarshalJSON(data []byte) error

UnmarshalJSON decodes a package manager from its canonical name.

type PackageManagerSupport

type PackageManagerSupport struct {
	PackageManager   PackageManager `json:"packageManager"`
	EvidencePatterns []string       `json:"evidencePatterns,omitempty"`
	// MultiModule marks that the detector natively expands nested
	// workspace/reactor modules for this package manager from a root manifest
	// (Maven reactors, npm/pnpm/yarn workspaces, cargo workspace members,
	// ...). Recursive discovery prunes nested subprojects for the same
	// package manager below a directory where a native multi-module manager
	// was detected, so the same modules are not scanned twice. Optional;
	// omitted by older plugins.
	MultiModule bool `json:"multiModule,omitempty"`
}

PackageManagerSupport records package-manager discovery metadata for a detector. External detector plugins return this so Bomly can include them in subproject discovery and scan planning before the detector runs.

func Support

func Support(manager PackageManager, evidencePatterns ...string) PackageManagerSupport

Support returns package-manager discovery metadata for a detector.

func (PackageManagerSupport) WithMultiModule

func (s PackageManagerSupport) WithMultiModule() PackageManagerSupport

WithMultiModule returns a copy of the support entry marked as natively expanding nested workspace/reactor modules from a root manifest, opting the package manager into recursive-discovery ancestor pruning.

type PackageManagerSupporter

type PackageManagerSupporter interface {
	PackageManagerSupport() []PackageManagerSupport
}

PackageManagerSupporter reports detector package-manager discovery metadata.

type PackageNodeIndex added in v0.7.0

type PackageNodeIndex map[string][]*DependencyNode

PackageNodeIndex maps a package reference to the dependency nodes that resolved to it.

It is derived, never stored. The stored truth stays DependencyNode's PackageRef -- one pointer per node to the package it matched -- and the registry stays position-free, holding packages keyed by PURL and nothing about where they were found. Storing the reverse direction as well would give the same fact two homes that can disagree, and the one that goes stale is always the derived-looking one.

Build it when a question needs it and discard it: it is a view over a graph that is being mutated, not a record.

func IndexNodesByPackage added in v0.7.0

func IndexNodesByPackage(g *Graph) PackageNodeIndex

IndexNodesByPackage builds the reverse index for a graph. Nodes with no package reference are omitted, and each entry is ordered by node ID so a caller iterating it gets a stable answer.

func (PackageNodeIndex) Nodes added in v0.7.0

func (i PackageNodeIndex) Nodes(packageRef string) []*DependencyNode

Nodes returns the dependency nodes that resolved to a package reference, or nil when none did.

func (PackageNodeIndex) Usages added in v0.7.0

func (i PackageNodeIndex) Usages(packageRef string, evidence []ReachabilityEvidence, filter UsageFilter) []Usage

Usages returns every usage of a package across the graph the index was built from, joined to reachability evidence and filtered.

This is the reverse index earning its place: a vulnerability names a package, and the question "is it reachable at runtime anywhere" is about the nodes that package resolved to. Without the index a caller walks the whole graph per vulnerability.

type PackageQuery

type PackageQuery struct {
	Name string `json:"name,omitempty"`
	ID   string `json:"id,omitempty"`
}

PackageQuery identifies a specific package target.

type PackageRegistry

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

PackageRegistry is the PURL-keyed, deduplicated set of matching artifacts produced by the matching stage. Detection produces Dependency nodes that reference packages here by PURL; matchers enrich the packages once per PURL regardless of how many dependency instances point at them.

func ApplyPackageUpdates

func ApplyPackageUpdates(registry *PackageRegistry, updates []*Package) *PackageRegistry

ApplyPackageUpdates merges package-update deltas returned by a matcher or analyzer (MatchResult.PackageUpdates / AnalyzeResult.PackageUpdates) into the registry. Each update is merged into any existing record with the same PURL via the registry's standard merge semantics; updates without a PURL are ignored. It returns the registry for convenience.

func NewPackageRegistry

func NewPackageRegistry() *PackageRegistry

NewPackageRegistry creates an empty registry.

func (*PackageRegistry) Add

func (r *PackageRegistry) Add(pkg *Package) *Package

Add inserts pkg, merging into any existing record with the same PURL, and returns the canonical stored package. Packages without a PURL are ignored.

func (*PackageRegistry) All

func (r *PackageRegistry) All() []*Package

All returns every package sorted by PURL.

func (*PackageRegistry) Ensure

func (r *PackageRegistry) Ensure(purl string) *Package

Ensure returns the registry package for purl, creating an empty one when absent. Returns nil for an empty purl.

func (*PackageRegistry) Get

func (r *PackageRegistry) Get(purl string) (*Package, bool)

Get returns the package for purl, if present.

func (*PackageRegistry) Len

func (r *PackageRegistry) Len() int

Len returns the number of packages in the registry.

func (*PackageRegistry) MarshalJSON

func (r *PackageRegistry) MarshalJSON() ([]byte, error)

MarshalJSON encodes a package registry as a stable PURL-keyed object for plugin transport.

func (*PackageRegistry) Merge

func (r *PackageRegistry) Merge(other *PackageRegistry)

Merge folds every package from other into r.

func (*PackageRegistry) UnmarshalJSON

func (r *PackageRegistry) UnmarshalJSON(data []byte) error

UnmarshalJSON decodes a PURL-keyed package registry from plugin transport.

type PackageRemediation

type PackageRemediation struct {
	Status             PackageRemediationStatus       `json:"status"`
	RecommendedVersion string                         `json:"recommended_version,omitempty"`
	Suggestions        []PackageRemediationSuggestion `json:"suggestions,omitempty"`
}

PackageRemediation summarizes the fix evidence already present on a package's enriched vulnerabilities.

func (*PackageRemediation) Clone

Clone returns a copy of the package remediation summary.

type PackageRemediationStatus

type PackageRemediationStatus string

PackageRemediationStatus describes how completely vulnerability enrichment identifies a safe package version.

const (
	// PackageRemediationComplete means every vulnerability has usable fix
	// evidence and one recommended package version can address all of them.
	PackageRemediationComplete PackageRemediationStatus = "complete"
	// PackageRemediationPartial means fix evidence exists, but it cannot produce
	// one complete package recommendation.
	PackageRemediationPartial PackageRemediationStatus = "partial"
	// PackageRemediationUnavailable means every vulnerability explicitly reports
	// that no fix is available.
	PackageRemediationUnavailable PackageRemediationStatus = "unavailable"
	// PackageRemediationUnknown means fix evidence is missing or contradictory.
	PackageRemediationUnknown PackageRemediationStatus = "unknown"
)

type PackageRemediationSuggestion

type PackageRemediationSuggestion struct {
	AffectedDependencyRefs       []string          `json:"affected_dependency_refs"`
	SuggestedActionDependencyRef string            `json:"suggested_action_dependency_ref,omitempty"`
	ManifestPath                 string            `json:"manifest_path,omitempty"`
	Action                       RemediationAction `json:"action"`
	OverrideAdvice               string            `json:"override_advice,omitempty"`
}

PackageRemediationSuggestion describes one occurrence-scoped action for the containing package. AffectedDependencyRefs identify occurrences of the vulnerable package. SuggestedActionDependencyRef identifies the direct dependency or manifest anchor the suggested action targets.

type PackageScorecard

type PackageScorecard struct {
	// Source identifies where the data came from (e.g. "api.scorecard.dev").
	Source string `json:"source,omitempty"`
	// Repository is the canonical repo identifier scored, e.g.
	// "github.com/kubernetes/kubernetes".
	Repository string `json:"repository,omitempty"`
	// CommitSHA is the repo commit the run scored.
	CommitSHA string `json:"commitSha,omitempty"`
	// ScorecardVersion is the version of the Scorecard tool that produced
	// the run.
	ScorecardVersion string `json:"scorecardVersion,omitempty"`
	// RunDate is when the run was performed.
	RunDate time.Time `json:"runDate,omitempty"`
	// AggregateScore is the overall Scorecard aggregate, 0.0–10.0.
	// A negative value (typically -1) indicates "unscored".
	AggregateScore float64 `json:"aggregateScore"`
	// Checks holds per-check results in the order returned by Scorecard.
	Checks []PackageScorecardCheck `json:"checks,omitempty"`
}

PackageScorecard holds the latest OpenSSF Scorecard run attached to a package by the scorecard matcher. A nil value means no run was attached (no resolvable source repo, the OSSF has not scored the project, or the matcher was not selected).

func (*PackageScorecard) Clone

func (s *PackageScorecard) Clone() *PackageScorecard

Clone returns a deep copy of the scorecard payload, including its checks.

type PackageScorecardCheck

type PackageScorecardCheck struct {
	// Name is the Scorecard check name, e.g. "Branch-Protection".
	Name string `json:"name"`
	// Score is 0–10, or -1 when the check is inconclusive.
	Score int `json:"score"`
	// Reason is the short summary Scorecard emits for the check.
	Reason string `json:"reason,omitempty"`
	// Documentation links to the canonical documentation page for the check.
	Documentation string `json:"documentation,omitempty"`
}

PackageScorecardCheck describes a single Scorecard check result.

type PackageType

type PackageType string

PackageType describes the broad role or artifact kind of a package node.

const (
	PackageTypeUnknown     PackageType = ""
	PackageTypeApplication PackageType = "application"
	PackageTypePackage     PackageType = "package"
	PackageTypeManifest    PackageType = "manifest"
	PackageTypeWorkflow    PackageType = "workflow"
	PackageTypeAction      PackageType = "action"
	PackageTypeTransitive  PackageType = "transitive"
	PackageTypeProject     PackageType = "project"
	PackageTypeFile        PackageType = "file"
)

func ParsePackageType

func ParsePackageType(value string) PackageType

ParsePackageType normalizes a package role string.

func (PackageType) String

func (t PackageType) String() string

String returns the package type value.

type Path

type Path struct {
	Nodes   []GraphNode
	Cyclic  bool
	CycleTo string
}

Path describes one path through the graph. Paths are heterogeneous: they traverse manifest and module nodes on their way to dependencies.

type PluginKind

type PluginKind string

PluginKind identifies the runtime role implemented by a plugin.

const (
	// PluginKindDetector resolves dependency graphs.
	PluginKindDetector PluginKind = "detector"
	// PluginKindMatcher enriches resolved packages.
	PluginKindMatcher PluginKind = "matcher"
	// PluginKindAuditor evaluates findings and risk.
	PluginKindAuditor PluginKind = "auditor"
	// PluginKindAnalyzer runs code analysis (e.g. reachability) over the
	// matched graph and annotates registry vulnerability entries.
	PluginKindAnalyzer PluginKind = "analyzer"
)

type PluginTargetType

type PluginTargetType string

PluginTargetType identifies the discovery target families a plugin supports.

type RangeEvent

type RangeEvent struct {
	Introduced   string `json:"introduced,omitempty"`
	Fixed        string `json:"fixed,omitempty"`
	LastAffected string `json:"last_affected,omitempty"`
	Limit        string `json:"limit,omitempty"`
}

RangeEvent is one OSV range event marker.

type Reachability

type Reachability struct {
	Status                 ReachabilityStatus     `json:"status"`
	Tier                   ReachabilityTier       `json:"tier,omitempty"`
	Analyzer               string                 `json:"analyzer,omitempty"`
	Reason                 string                 `json:"reason,omitempty"`
	Symbols                []AffectedSymbol       `json:"symbols,omitempty"`
	CallPaths              []CallPath             `json:"call_paths,omitempty"`
	Hops                   *int                   `json:"hops,omitempty"`
	Confidence             ReachabilityConfidence `json:"confidence,omitempty"`
	DynamicImportsDetected bool                   `json:"dynamic_imports_detected,omitempty"`
	AnalyzedAt             string                 `json:"analyzed_at,omitempty"`

	// Evidence is the per-module-root findings this annotation summarizes.
	// Empty when the analyzer made a single whole-scan claim, which is what
	// every analyzer emitted before the field existed -- so the annotation
	// stays the record when there is no evidence, and becomes derived from it
	// when there is. DeriveReachability builds the summary.
	Evidence []ReachabilityEvidence `json:"evidence,omitempty"`
}

Reachability is the analyzer-supplied reachability annotation for one vulnerability. Stored on Vulnerability.

func DeriveReachability added in v0.7.0

func DeriveReachability(evidence []ReachabilityEvidence) Reachability

DeriveReachability summarizes per-module-root evidence into the single annotation a vulnerability carries.

The rule is asymmetric on purpose, because the two answers are not equally safe to be wrong about. One module root reaching the symbol makes the finding reachable, whatever the others found: a real call path is not cancelled by an absence elsewhere. Unreachable requires every piece of evidence to say so, and at least one to exist -- anything less is unknown, not safe. This is the same caution the tier-3 caveat states in prose: "unreachable" is a claim about what was analyzed, not about the world.

The summary keeps the strongest reachable evidence's detail (tier, symbols, call paths, hops), since that is the finding a reader needs to act on.

func (*Reachability) Clone

func (r *Reachability) Clone() *Reachability

Clone returns a deep copy of the reachability annotation.

type ReachabilityConfidence

type ReachabilityConfidence string

ReachabilityConfidence is a coarse triage signal derived from Hops and DynamicImportsDetected.

const (
	ConfidenceUnknown ReachabilityConfidence = ""
	ConfidenceHigh    ReachabilityConfidence = "high"
	ConfidenceMedium  ReachabilityConfidence = "medium"
	ConfidenceLow     ReachabilityConfidence = "low"
)

func DeriveConfidence

func DeriveConfidence(hops *int, dynamicImports bool) ReachabilityConfidence

DeriveConfidence computes a confidence label from a hop count and a dynamic-imports flag. Returns ConfidenceUnknown when hops is nil.

type ReachabilityEvidence added in v0.7.0

type ReachabilityEvidence struct {
	// ModuleRoot is the module this finding is about. Empty means the
	// analyzer did not attribute it, which is a whole-scan claim.
	ModuleRoot string `json:"module_root,omitempty"`
	// DependencyRefs optionally names the exact occurrence nodes the analyzer
	// could attribute the finding to, by node ID. Empty means the analyzer
	// could not narrow it below the module root, which is the common case --
	// so a consumer must treat it as "not stated", never as "no occurrence".
	DependencyRefs []string `json:"dependency_refs,omitempty"`

	Status                 ReachabilityStatus     `json:"status"`
	Tier                   ReachabilityTier       `json:"tier,omitempty"`
	Analyzer               string                 `json:"analyzer,omitempty"`
	Reason                 string                 `json:"reason,omitempty"`
	Symbols                []AffectedSymbol       `json:"symbols,omitempty"`
	CallPaths              []CallPath             `json:"call_paths,omitempty"`
	Hops                   *int                   `json:"hops,omitempty"`
	Confidence             ReachabilityConfidence `json:"confidence,omitempty"`
	DynamicImportsDetected bool                   `json:"dynamic_imports_detected,omitempty"`
	AnalyzedAt             string                 `json:"analyzed_at,omitempty"`
}

ReachabilityEvidence is one analyzer's finding for one vulnerability within one module root.

Reachability is a per-module-root question for the same reason scope is: a vulnerable symbol can be called from one workspace member and unused by another, and one status for the whole scan cannot say so. Vulnerability's Reachability annotation becomes the derived summary over these.

func (ReachabilityEvidence) Clone added in v0.7.0

Clone returns a deep copy of the evidence.

type ReachabilityStats

type ReachabilityStats struct {
	Reachable     int `json:"reachable,omitempty"`
	Unreachable   int `json:"unreachable,omitempty"`
	Unknown       int `json:"unknown,omitempty"`
	NotApplicable int `json:"not_applicable,omitempty"`
}

ReachabilityStats tallies the per-analyzer outcome distribution.

type ReachabilityStatus

type ReachabilityStatus string

ReachabilityStatus is the outcome of a reachability analysis for one vulnerability.

const (
	ReachabilityUnknown     ReachabilityStatus = "unknown"
	ReachabilityReachable   ReachabilityStatus = "reachable"
	ReachabilityUnreachable ReachabilityStatus = "unreachable"
)

type ReachabilityTier

type ReachabilityTier string

ReachabilityTier communicates the precision of a reachability result.

const (
	TierSymbol  ReachabilityTier = "symbol"
	TierPackage ReachabilityTier = "package"
	TierNone    ReachabilityTier = "none"
)

type ReadyResponse

type ReadyResponse struct {
	Ready bool `json:"ready"`
	// Reason explains why the plugin is not ready. It is ignored when Ready is
	// true and surfaced to users (and resolution errors) when Ready is false.
	Reason string `json:"reason,omitempty"`
}

ReadyResponse reports whether a plugin is ready to run.

type Reference

type Reference struct {
	URL  string        `json:"url,omitempty"`
	Type ReferenceType `json:"type,omitempty"`
}

Reference is a URL and type pair.

type ReferenceType

type ReferenceType string

ReferenceType identifies the role of a vulnerability reference URL.

const (
	ReferenceTypeAdvisory   ReferenceType = "advisory"
	ReferenceTypeDataSource ReferenceType = "data_source"
)

type RemediationAction

type RemediationAction string

RemediationAction identifies the user action suggested for one or more occurrences of an enriched vulnerable package.

const (
	// RemediationActionDirectBump suggests updating a directly declared package.
	RemediationActionDirectBump RemediationAction = "direct-bump"
	// RemediationActionTransitiveOverride suggests using a package-manager
	// override for a transitive package.
	RemediationActionTransitiveOverride RemediationAction = "transitive-override"
	// RemediationActionLockfileRefresh suggests asking the package manager to
	// resolve a newer transitive package version.
	RemediationActionLockfileRefresh RemediationAction = "lockfile-refresh"
	// RemediationActionNoFixUpstream reports that every vulnerability explicitly
	// lacks an upstream fix.
	RemediationActionNoFixUpstream RemediationAction = "no-fix-upstream"
	// RemediationActionManualReview reports that available evidence cannot
	// support a safe, concrete automated suggestion.
	RemediationActionManualReview RemediationAction = "manual-review"
)

type RemediationCapability

type RemediationCapability struct {
	SupportedManagers []PackageManager    `json:"supportedManagers,omitempty"`
	Actions           []RemediationAction `json:"actions,omitempty"`
}

RemediationCapability advertises the occurrence-scoped strategies for which a detector can provide package-manager-specific evidence. Capabilities do not grant authority to choose final remediation or modify a project.

type RemediationHint

type RemediationHint struct {
	DependencyRef string                    `json:"dependencyRef"`
	ManifestPath  string                    `json:"manifestPath,omitempty"`
	Strategies    []RemediationStrategyHint `json:"strategies,omitempty"`
}

RemediationHint contributes package-manager evidence for one detected dependency occurrence.

type RemediationHintRequest

type RemediationHintRequest struct {
	ProjectPath string           `json:"projectPath,omitempty"`
	Detection   DetectionResult  `json:"detection"`
	Registry    *PackageRegistry `json:"registry,omitempty"`
}

RemediationHintRequest supplies completed detection and enrichment evidence to an optional detector remediation provider.

type RemediationHintResponse

type RemediationHintResponse struct {
	Hints       []RemediationHint `json:"hints,omitempty"`
	Diagnostics []string          `json:"diagnostics,omitempty"`
}

RemediationHintResponse contains optional read-only detector evidence.

type RemediationStrategyHint

type RemediationStrategyHint struct {
	Action RemediationAction `json:"action"`
	Advice string            `json:"advice,omitempty"`
}

RemediationStrategyHint is read-only detector evidence that a strategy is available for one occurrence. Advice is detector-owned, action-specific package-manager guidance. For example, a transitive-override hint can explain the manager's override syntax, while a lockfile-refresh hint can provide the normal refresh command. Core validates and bounds this text, then retains authority over the final action.

type ResolutionFallback

type ResolutionFallback struct {
	From   string `json:"from"`
	Reason string `json:"reason,omitempty"`
}

ResolutionFallback identifies the primary detector that failed before a fallback detector produced the graph, and why it failed.

type ResolutionMetadata

type ResolutionMetadata struct {
	Method            ResolutionMethod `json:"method,omitempty"`
	InstallExecuted   bool             `json:"install_executed"`
	InstallCommand    []string         `json:"install_command,omitempty"`
	InstallWorkingDir string           `json:"install_working_dir,omitempty"`
	// Fallback records that a fallback detector produced this graph after the
	// planned primary detector failed (not routine applicability hand-off).
	Fallback *ResolutionFallback `json:"fallback,omitempty"`
}

ResolutionMetadata describes how a detector resolved a manifest graph.

type ResolutionMethod

type ResolutionMethod string

ResolutionMethod identifies how a detector produced a manifest graph.

const (
	// ResolutionMethodLockfile means the graph came from a deterministic lockfile parser.
	ResolutionMethodLockfile ResolutionMethod = "lockfile"
	// ResolutionMethodIsolatedInstall means Bomly installed dependencies into its own isolated environment.
	ResolutionMethodIsolatedInstall ResolutionMethod = "isolated-install"
	// ResolutionMethodProjectEnvironment means Bomly inspected an existing project-managed environment.
	ResolutionMethodProjectEnvironment ResolutionMethod = "project-environment"
	// ResolutionMethodManifestOnly means Bomly parsed a manifest without transitive install metadata.
	ResolutionMethodManifestOnly ResolutionMethod = "manifest-only"
)

type RiskBand

type RiskBand string

RiskBand is the normalized label for a risk score range.

const (
	RiskBandUnknown  RiskBand = "unknown"
	RiskBandLow      RiskBand = "low"
	RiskBandMedium   RiskBand = "medium"
	RiskBandHigh     RiskBand = "high"
	RiskBandCritical RiskBand = "critical"
)

type RiskScore

type RiskScore struct {
	PackageRef string         `json:"package_ref,omitempty"`
	Score      int            `json:"score"`
	Band       RiskBand       `json:"band,omitempty"`
	Signals    map[string]any `json:"signals,omitempty"`
}

RiskScore describes a normalized risk result for one package, referenced by its PURL in the package registry.

type RuntimeInfo added in v0.2.0

type RuntimeInfo struct {
	// CoreVersion is the host core version when known; empty otherwise.
	CoreVersion string
	// Execution reports whether the component runs embedded or managed.
	Execution ExecutionMode
}

RuntimeInfo describes the host runtime a component executes under.

type Scope

type Scope string

Scope describes the normalized dependency scope surfaced to users.

const (
	// ScopeUnknown indicates that a detector could not determine dependency scope.
	ScopeUnknown Scope = ""
	// ScopeRuntime indicates a dependency required at runtime.
	ScopeRuntime Scope = "runtime"
	// ScopeDevelopment indicates a dependency used only for development workflows.
	ScopeDevelopment Scope = "development"
)

func DecodeScopeSet added in v0.7.0

func DecodeScopeSet(value string) ([]Scope, error)

DecodeScopeSet parses a carrier value written by EncodeScopeSet. It is strict: an unrecognized token is an error rather than a silently dropped scope, because this value is Bomly's own and a token it cannot read means the value did not come from where the caller thinks it did.

The result is deduplicated and sorted, so decoding and re-encoding gives the same bytes.

func MergeScope

func MergeScope(current, next Scope) Scope

MergeScope combines two normalized scopes, preferring runtime when a package is reachable from both runtime and development roots.

func ParseScope

func ParseScope(value string) (Scope, error)

ParseScope normalizes a user-provided dependency scope value.

func ScopesFromCycloneDX added in v0.7.0

func ScopesFromCycloneDX(value string) []Scope

ScopesFromCycloneDX derives a scope set from CycloneDX's scalar scope, for a document Bomly did not write. It returns nil when the value says nothing, which is also what an unrecognized value gives: a scope Bomly cannot read is not a scope it should guess at.

"optional" reads as runtime. An optional component provides additional functionality at runtime -- it is not a development-only dependency -- so required and optional both land on runtime. That is lossy in the direction that matters least, and it is why CycloneDXScopeProperty exists.

func ScopesFromCycloneDXComponent added in v0.7.0

func ScopesFromCycloneDXComponent(scope, carrier string) []Scope

ScopesFromCycloneDXComponent reads a component's scopes, preferring the carrier property over the scalar scope.

The precedence is the point of the pair. The carrier holds what Bomly recorded; the scalar holds a projection of it that cannot express a set. On a document Bomly wrote, both are present and only the carrier is exact. A carrier that fails to parse is treated as absent -- the scalar is still a true statement about the component, and dropping the scope entirely because the richer field was malformed would lose more than it protects.

func ScopesOf

func ScopesOf(scopes ...Scope) []Scope

ScopesOf returns a deduplicated scope slice without unknown entries, or nil. Convenience for detectors building node scopes from parsed groups.

type ServedAnalyzer

ServedAnalyzer is the analyzer interface implemented by external analyzer plugins. Analyzers read the dependency graph and PURL-keyed package registry and annotate Vulnerability.Reachability on registry packages.

type ServedAuditor

ServedAuditor is the auditor interface implemented by external auditor plugins. Auditors read graph and registry data and return reference-style findings, risk scores, and run metadata.

type ServedDetector

type ServedDetector interface {
	Descriptor(context.Context) (*DetectorDescriptor, error)
	PackageManagerSupport(context.Context) ([]PackageManagerSupport, error)
	Ready(context.Context, *DetectRequest) (*ReadyResponse, error)
	Applicable(context.Context, *DetectRequest) (*ApplicableResponse, error)
	Detect(context.Context, *DetectRequest) (*DetectResponse, error)
}

ServedDetector is the detector interface implemented by external detector plugins. A detector describes its identity and package-manager support, reports readiness/applicability for a planned scan target, and returns one or more manifest-scoped dependency graphs from Detect.

type ServedDetectorRemediationProvider

type ServedDetectorRemediationProvider interface {
	RemediationHints(context.Context, *RemediationHintRequest) (*RemediationHintResponse, error)
}

ServedDetectorRemediationProvider optionally supplies read-only, package-manager-specific remediation evidence.

type ServedMatcher

ServedMatcher is the matcher interface implemented by external matcher plugins. Matchers read the dependency graph and PURL-keyed package registry, then return the registry with package enrichment such as licenses, vulnerabilities, lifecycle data, or other metadata.

type Severity

type Severity struct {
	// Type is the OSV severity type, e.g. "CVSS_V3", "CVSS_V4".
	Type SeverityType `json:"type,omitempty"`
	// Score is the vector string or numeric score for Type.
	Score string `json:"score,omitempty"`
}

Severity is one OSV-format severity entry (a CVSS type + vector/score).

type SeverityLevel

type SeverityLevel string

SeverityLevel is Bomly's normalized severity band.

It carries two parallel vocabularies over one ordered scale, mirroring how GitHub presents code-scanning results:

  • CVSS bands (critical/high/medium/low) are used for vulnerabilities, which also drive SARIF `security-severity`; GitHub renders them as Critical/High/Medium/Low.
  • GitHub levels (error/warning/note) are used for findings that have no CVSS score (license, package); GitHub renders the SARIF `level` directly as Error/Warning/Note.

The two vocabularies share a single rank ladder (see SeverityRank): error ≡ high tier, warning ≡ medium tier, note ≡ low tier.

const (
	// SeverityUnknown indicates that no severity could be determined.
	SeverityUnknown SeverityLevel = "unknown"
	// SeverityNA indicates that severity does not apply to the finding kind.
	//
	// Deprecated: kept for backward-compatible parsing only. New findings that
	// lack a CVSS score should use SeverityError/SeverityWarning/SeverityNote.
	SeverityNA SeverityLevel = "n/a"
	// SeverityLow indicates a low-severity issue.
	SeverityLow SeverityLevel = "low"
	// SeverityMedium indicates a medium-severity issue.
	SeverityMedium SeverityLevel = "medium"
	// SeverityHigh indicates a high-severity issue.
	SeverityHigh SeverityLevel = "high"
	// SeverityCritical indicates a critical-severity issue.
	SeverityCritical SeverityLevel = "critical"
	// SeverityAny is a policy threshold that matches every severity.
	SeverityAny SeverityLevel = "any"

	// SeverityNote is the GitHub-aligned level for low-impact findings without a
	// CVSS score. Ranks alongside SeverityLow.
	SeverityNote SeverityLevel = "note"
	// SeverityWarning is the GitHub-aligned level for findings without a CVSS
	// score that warrant attention. Ranks alongside SeverityMedium.
	SeverityWarning SeverityLevel = "warning"
	// SeverityError is the GitHub-aligned level for high-impact findings without
	// a CVSS score. Ranks alongside SeverityHigh.
	SeverityError SeverityLevel = "error"
)

func ParseSeverityLevel

func ParseSeverityLevel(value string) SeverityLevel

ParseSeverityLevel normalizes a severity string into a SeverityLevel.

type SeverityType

type SeverityType string

SeverityType identifies the OSV severity vector family.

const (
	SeverityTypeCVSSV2  SeverityType = "CVSS_V2"
	SeverityTypeCVSSV3  SeverityType = "CVSS_V3"
	SeverityTypeCVSSV31 SeverityType = "CVSS_V31"
	SeverityTypeCVSSV4  SeverityType = "CVSS_V4"
)

type SourcePosition

type SourcePosition struct {
	File    string `json:"file,omitempty"`
	Line    int    `json:"line,omitempty"`
	Column  int    `json:"column,omitempty"`
	EndLine int    `json:"end_line,omitempty"`
}

SourcePosition is the canonical (file, line, column) tuple used wherever the SDK needs to point at a source location. Used by call frames, affected symbols, and (additively) by PackageLocation for declaration sites.

All fields are optional; consumers should treat zero/empty values as "unknown" rather than as positions at line 0 / column 0.

func (SourcePosition) IsZero

func (p SourcePosition) IsZero() bool

IsZero reports whether the position carries no useful location data.

type Subproject

type Subproject struct {
	ExecutionTarget         ExecutionTarget  `json:"executionTarget"`
	RelativePath            string           `json:"relativePath,omitempty"`
	PrimaryDetector         string           `json:"primaryDetector,omitempty"`
	DetectedPackageManagers []PackageManager `json:"detectedPackageManagers,omitempty"`
	PlannedDetectors        []string         `json:"plannedDetectors,omitempty"`
	Ecosystem               Ecosystem        `json:"ecosystem,omitempty"`
}

Subproject identifies one package-manager root discovered beneath the execution target.

func (Subproject) PrimaryPackageManager

func (s Subproject) PrimaryPackageManager() PackageManager

PrimaryPackageManager returns the first entry in DetectedPackageManagers, or PackageManagerUnknown if the list is empty.

type SymbolKind

type SymbolKind string

SymbolKind identifies a vulnerable or reachable code symbol kind.

const (
	SymbolKindFunction SymbolKind = "function"
	SymbolKindMethod   SymbolKind = "method"
)

type URLForm added in v0.7.0

type URLForm int

URLForm selects which published-URL rule a value is held to. Every form shares the same safety floor -- absolute http or https, a real host, no embedded credentials -- and differs only in what the location half of the URL is allowed to look like, because the three kinds of value answer different questions.

const (
	// URLFormArtifact is the exact file a package was downloaded from. It
	// requires a non-empty path and rejects a query, which marks a signed or
	// tokenized link rather than a stable location.
	URLFormArtifact URLForm = iota
	// URLFormRepository is the source repository a package was resolved from.
	// It requires a non-empty path and drops the query, which carries the ref
	// that was requested rather than the one that was resolved.
	URLFormRepository
	// URLFormReference is a citation: a homepage, an advisory page, a
	// documentation link -- a URL published so a reader can follow it, never
	// one Bomly fetches to establish a fact. It keeps the query and the
	// fragment, and it permits a host root, because "https://example.com/" is
	// a legitimate homepage while it is never a package artifact. The safety
	// floor is unchanged: credentials, local paths, and non-http schemes are
	// rejected exactly as they are for the other two forms.
	URLFormReference
)

type Usage added in v0.7.0

type Usage struct {
	// ModuleRoot is the module this usage belongs to.
	ModuleRoot string
	// Location is the site itself.
	Location PackageLocation
	// Evidence is the reachability finding for this module root, or nil when
	// there is none.
	Evidence *ReachabilityEvidence
}

Usage is one site of a package, with the reachability evidence that applies to it. It is what a conjunctive question is actually about.

func SelectUsages added in v0.7.0

func SelectUsages(node *DependencyNode, evidence []ReachabilityEvidence, filter UsageFilter) []Usage

SelectUsages joins a node's locations to reachability evidence within each module root and returns the usages matching every condition in the filter.

This is the whole point of per-site attribution. Asking "reachable and runtime and direct" of a node's unions can answer yes when no single usage satisfies all three -- reachable in one module, runtime in another, direct in a third. Joining first and filtering after makes the answer a statement about a usage that exists.

Evidence with no module root applies to every location, since a whole-scan claim is a claim about all of them. A location with no module root is matched only by such evidence: an unattributed site cannot be joined to one module's finding without inventing the attribution the producer omitted.

type UsageFilter added in v0.7.0

type UsageFilter struct {
	// Scope, when set, requires the usage's site to carry it.
	Scope Scope
	// Relationship, when set, requires the site to have it.
	Relationship DependencyRelationship
	// Reachable, when true, requires reachability evidence for the usage's
	// module root that says reachable.
	Reachable bool
}

UsageFilter is a conjunction of conditions about one usage of a package.

The zero value matches every usage. A condition left empty is not asked.

type VEXStatus

type VEXStatus string

VEXStatus identifies a finding's VEX status.

const (
	VEXStatusAffected           VEXStatus = "affected"
	VEXStatusNotAffected        VEXStatus = "not_affected"
	VEXStatusFixed              VEXStatus = "fixed"
	VEXStatusUnderInvestigation VEXStatus = "under_investigation"
)

type VersionChange

type VersionChange struct {
	Before *DependencyNode
	After  *DependencyNode
}

VersionChange captures a dependency identity that changed versions.

type VersionRange

type VersionRange struct {
	// Type is the OSV range type: "SEMVER", "ECOSYSTEM", or "GIT".
	Type   VersionRangeType `json:"type,omitempty"`
	Repo   string           `json:"repo,omitempty"`
	Events []RangeEvent     `json:"events,omitempty"`
}

VersionRange is one OSV affected version range.

type VersionRangeType

type VersionRangeType string

VersionRangeType identifies the OSV affected range scheme.

const (
	VersionRangeTypeSemver    VersionRangeType = "SEMVER"
	VersionRangeTypeEcosystem VersionRangeType = "ECOSYSTEM"
	VersionRangeTypeGit       VersionRangeType = "GIT"
)

type Vulnerability

type Vulnerability struct {
	// --- OSV-aligned core ---
	ID               string         `json:"id"`
	Aliases          []string       `json:"aliases,omitempty"`
	Related          []string       `json:"related,omitempty"`
	Summary          string         `json:"summary,omitempty"`
	Details          string         `json:"details,omitempty"`
	Severity         []Severity     `json:"severity,omitempty"`
	Affected         []Affected     `json:"affected,omitempty"`
	References       []Reference    `json:"references,omitempty"`
	Published        string         `json:"published,omitempty"`
	Modified         string         `json:"modified,omitempty"`
	Withdrawn        string         `json:"withdrawn,omitempty"`
	DatabaseSpecific map[string]any `json:"database_specific,omitempty"`

	// --- Bomly enrichment extensions ---
	Source               string           `json:"source,omitempty"`
	DataSource           string           `json:"data_source,omitempty"`
	Namespace            string           `json:"namespace,omitempty"`
	Title                string           `json:"title,omitempty"`
	Reasons              []string         `json:"reasons,omitempty"`
	ParsedSeverity       SeverityLevel    `json:"parsed_severity,omitempty"`
	SeveritySource       string           `json:"severity_source,omitempty"`
	CVSS                 []CVSSScore      `json:"cvss,omitempty"`
	EPSS                 []EPSSScore      `json:"epss,omitempty"`
	CWEs                 []CWE            `json:"cwes,omitempty"`
	KEVExploited         bool             `json:"kev_exploited,omitempty"`
	KnownExploited       []KnownExploited `json:"known_exploited,omitempty"`
	RiskScore            float64          `json:"risk_score,omitempty"`
	FixState             FixState         `json:"fix_state,omitempty"`
	FixedIn              string           `json:"fixed_in,omitempty"`
	FixedVersions        []string         `json:"fixed_versions,omitempty"`
	FixAvailable         []FixAvailable   `json:"fix_available,omitempty"`
	AffectedVersionRange string           `json:"affected_version_range,omitempty"`
	CPEs                 []string         `json:"cpes,omitempty"`
	AffectedSymbols      []AffectedSymbol `json:"affected_symbols,omitempty"`
	Reachability         *Reachability    `json:"reachability,omitempty"`
}

Vulnerability describes a single advisory in an OSV-aligned shape, extended with Bomly-specific enrichment that the OSV schema does not model directly.

The leading block mirrors the OSV (Open Source Vulnerabilities) schema so the records can be exported as OSV with minimal translation. The trailing block carries Bomly enrichment (parsed severity, EPSS, KEV, CWE, risk, fix-state, reachability) attached by matchers and analyzers.

func (Vulnerability) Clone

func (v Vulnerability) Clone() Vulnerability

Clone returns a deep copy of the vulnerability.

func (Vulnerability) IsExploitable

func (v Vulnerability) IsExploitable() bool

IsExploitable reports whether advisory metadata says this vulnerability is known exploitable.

func (Vulnerability) MatchesConstraints

func (v Vulnerability) MatchesConstraints(constraints []FailOnConstraint) bool

MatchesConstraints evaluates one vulnerability against the vulnerability constraints in an AND-set. Source-change constraints apply only to package findings and are ignored here. When constraints is empty, every vulnerability matches (the historical behavior of `--audit` without `--fail-on`). A list containing only non-vulnerability constraints also leaves vulnerability matching unchanged.

Directories

Path Synopsis
Package conformance provides a reusable test suite that plugin authors run against their sdk.Module to verify it satisfies the Bomly plugin contract before shipping: module and descriptor validity, JSON round-trip stability, construction through a HostContext, the Ready/Applicable lifecycle contract, role-specific capabilities such as the package-updates delta protocol, and (optionally) manifest identity and a real managed-transport probe of the built plugin binary.
Package conformance provides a reusable test suite that plugin authors run against their sdk.Module to verify it satisfies the Bomly plugin contract before shipping: module and descriptor validity, JSON round-trip stability, construction through a HostContext, the Ready/Applicable lifecycle contract, role-specific capabilities such as the package-updates delta protocol, and (optionally) manifest identity and a real managed-transport probe of the built plugin binary.
Package detectorkit provides shared helper functions for detector implementations: manifest metadata inference, source-position wiring, remediation hint assembly, subgraph partitioning, and build-tool readiness and timeout helpers.
Package detectorkit provides shared helper functions for detector implementations: manifest metadata inference, source-position wiring, remediation hint assembly, subgraph partitioning, and build-tool readiness and timeout helpers.
Package filecache provides shared on-disk caching helpers for matcher, analyzer, and detector implementations.
Package filecache provides shared on-disk caching helpers for matcher, analyzer, and detector implementations.
Package logkit provides secret-safe subprocess logging helpers shared by Bomly components: argument and URL sanitizers, standard DEBUG command fields, and a counting stderr writer.
Package logkit provides secret-safe subprocess logging helpers shared by Bomly components: argument and URL sanitizers, standard DEBUG command fields, and a counting stderr writer.
Package matcherkit contains shared helper functions for matcher implementations.
Package matcherkit contains shared helper functions for matcher implementations.
Package purlkit is the single home for package-URL behavior in the Bomly SDK (ADR-0038 in bomly-cli's dev-docs/adr).
Package purlkit is the single home for package-URL behavior in the Bomly SDK (ADR-0038 in bomly-cli's dev-docs/adr).
Package spdxkit is the single home for SPDX license behavior in the Bomly SDK (ADR-0038 in bomly-cli's dev-docs/adr): expression validation, classification, deprecated-identifier canonicalization, and deterministic LicenseRef minting.
Package spdxkit is the single home for SPDX license behavior in the Bomly SDK (ADR-0038 in bomly-cli's dev-docs/adr): expression validation, classification, deprecated-identifier canonicalization, and deterministic LicenseRef minting.
Package system provides bounded filesystem reads and small OS helpers (exec, path, and environment wrappers) shared by Bomly components.
Package system provides bounded filesystem reads and small OS helpers (exec, path, and environment wrappers) shared by Bomly components.
Package testkit provides test helpers for component modules and external plugins: fuzz-target invariants, Go binary builders for fake tools, and lockfile position assertions.
Package testkit provides test helpers for component modules and external plugins: fuzz-target invariants, Go binary builders for fake tools, and lockfile position assertions.

Jump to

Keyboard shortcuts

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