Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
bin/
dist/
go.work
go.work.sum
9 changes: 6 additions & 3 deletions go.mod
Original file line number Diff line number Diff line change
@@ -1,24 +1,27 @@
module github.com/bomly-dev/bomly-plugin-jsreach-analyzer

go 1.26.3
go 1.27.0

require (
github.com/bomly-dev/bomly-sdk v0.3.0
github.com/bomly-dev/bomly-sdk v0.8.0
github.com/evanw/esbuild v0.28.1
go.uber.org/zap v1.28.0
gopkg.in/yaml.v3 v3.0.1
)

require (
github.com/anchore/packageurl-go v0.2.0 // indirect
github.com/CycloneDX/cyclonedx-go v0.11.0 // indirect
github.com/fatih/color v1.13.0 // indirect
github.com/github/go-spdx/v2 v2.7.0 // indirect
github.com/golang/protobuf v1.5.4 // indirect
github.com/hashicorp/go-hclog v1.6.3 // indirect
github.com/hashicorp/go-plugin v1.8.0 // indirect
github.com/hashicorp/yamux v0.1.2 // indirect
github.com/mattn/go-colorable v0.1.12 // indirect
github.com/mattn/go-isatty v0.0.17 // indirect
github.com/oklog/run v1.1.0 // indirect
github.com/package-url/packageurl-go v0.1.7 // indirect
github.com/spdx/tools-golang v0.6.0-rc4 // indirect
go.uber.org/multierr v1.10.0 // indirect
golang.org/x/net v0.57.0 // indirect
golang.org/x/sys v0.47.0 // indirect
Expand Down
28 changes: 22 additions & 6 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
github.com/anchore/packageurl-go v0.2.0 h1:CkrM4RMUwrEGAiE1OVlxaZNzWj0TuHRey7o4T/EAErk=
github.com/anchore/packageurl-go v0.2.0/go.mod h1:2JCgOQMIsqZ7TmliXG4PnUthPJAKE3mWQbsW2XHjAOE=
github.com/bomly-dev/bomly-sdk v0.3.0 h1:JtC7qZ9yq3r4fUYyq7e/Os4f9wGMa4Qot8U0l9MepFA=
github.com/bomly-dev/bomly-sdk v0.3.0/go.mod h1:yn1LBkoHG9gDBXKyRj0UNJo0BlXl8Bj9Ymb3WKLIh78=
github.com/CycloneDX/cyclonedx-go v0.11.0 h1:GokP8FiRC+foiuwWhSSLpSD5H4hSWtGnR3wo7apkBFI=
github.com/CycloneDX/cyclonedx-go v0.11.0/go.mod h1:vUvbCXQsEm48OI6oOlanxstwNByXjCZ2wuleUlwGEO8=
github.com/bomly-dev/bomly-sdk v0.8.0 h1:XpIr0yS5vg2B2oMt9xPornZ+krgy2AYlm/ts/At4JA0=
github.com/bomly-dev/bomly-sdk v0.8.0/go.mod h1:7RJLUANK8xHMZ5/r45zYxGyKUHC8yQiVem22yM0MyZw=
github.com/bradleyjkemp/cupaloy/v2 v2.8.0 h1:any4BmKE+jGIaMpnU8YgH/I2LPiLBufr6oMMlVBbn9M=
github.com/bradleyjkemp/cupaloy/v2 v2.8.0/go.mod h1:bm7JXdkRd4BHJk9HpwqAI8BoAY1lps46Enkdqw6aRX0=
github.com/bufbuild/protocompile v0.14.1 h1:iA73zAf/fyljNjQKwYzUHD6AD4R8KMasmwa/FBatYVw=
github.com/bufbuild/protocompile v0.14.1/go.mod h1:ppVdAIhbr2H8asPk6k4pY7t9zB1OU5DoEw9xY/FUi1c=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
Expand All @@ -13,6 +15,8 @@ github.com/evanw/esbuild v0.28.1 h1:ds+yuRyUaZGx++GR56CrCeuXh8PVhVM4xq8v7PNELFc=
github.com/evanw/esbuild v0.28.1/go.mod h1:D2vIQZqV/vIf/VRHtViaUtViZmG7o+kKmlBfVQuRi48=
github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w=
github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=
github.com/github/go-spdx/v2 v2.7.0 h1:GzfXx4wFdlilARxmFRXW/mgUy3A4vSqZocCMFV6XFdQ=
github.com/github/go-spdx/v2 v2.7.0/go.mod h1:Ftc45YYG1WzpzwEPKRVm9Jv8vDqOrN4gWoCkK+bHer0=
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
Expand Down Expand Up @@ -40,12 +44,24 @@ github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPn
github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/oklog/run v1.1.0 h1:GEenZ1cK0+q0+wsJew9qUg/DyD8k3JzYsZAi5gYi2mA=
github.com/oklog/run v1.1.0/go.mod h1:sVPdnTZT1zYwAJeCMu2Th4T21pA3FPOQRfWjQlk7DVU=
github.com/package-url/packageurl-go v0.1.7 h1:iFWg6tzAjLA6F/qX3M5nZaiMHJgc+p2zxVyr/fY+sZY=
github.com/package-url/packageurl-go v0.1.7/go.mod h1:nKAWB8E6uk1MHqiS/lQb9pYBGH2+mdJ2PJc2s50dQY0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/spdx/tools-golang v0.6.0-rc4 h1:2GkvNr0DcnJHY9BDm3OYHo229jZS/h4qYDK+tHYXPOo=
github.com/spdx/tools-golang v0.6.0-rc4/go.mod h1:ruCHu3shgy7bVbZ7gtEU4Gq4fI08n2SdXtgV5PoN/OM=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals=
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/terminalstatic/go-xsd-validate v0.1.6 h1:TenYeQ3eY631qNi1/cTmLH/s2slHPRKTTHT+XSHkepo=
github.com/terminalstatic/go-xsd-validate v0.1.6/go.mod h1:18lsvYFofBflqCrvo1umpABZ99+GneNTw2kEEc8UPJw=
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f h1:J9EGpcZtP0E/raorCMxlFGSTBrsSlaDGf3jU/qvAE2c=
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0=
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ=
github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74=
github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y=
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
go.opentelemetry.io/otel v1.44.0 h1:JjwHmHpA4iZ3wBxluu2fbbE7j4kqlE8jXyAyPXH7HqU=
Expand Down
73 changes: 49 additions & 24 deletions plugin/analyzer.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ func (a Analyzer) Applicable(_ context.Context, req model.AnalyzeRequest) (bool,
if req.Graph == nil || req.Registry == nil {
return false, nil
}
for _, pkg := range req.Graph.Nodes() {
for _, pkg := range req.Graph.DependencyNodes() {
if pkg == nil || !isNPMPackage(pkg) {
continue
}
Expand All @@ -79,20 +79,20 @@ func (a Analyzer) Applicable(_ context.Context, req model.AnalyzeRequest) (bool,
}

// dependencyPURL returns the registry key for a dependency node.
func dependencyPURL(dep *model.Dependency) string {
func dependencyPURL(dep *model.DependencyNode) string {
if dep == nil {
return ""
}
if dep.PackageRef != "" {
return dep.PackageRef
}
return dep.PURL
return dep.NodeID()
}

// vulnerabilitiesForDep returns the registry slice for a dependency. The
// caller may mutate the returned slice in place; entries live on the
// shared backing array owned by the registry package.
func vulnerabilitiesForDep(req model.AnalyzeRequest, dep *model.Dependency) []model.Vulnerability {
func vulnerabilitiesForDep(req model.AnalyzeRequest, dep *model.DependencyNode) []model.Vulnerability {
if req.Registry == nil || dep == nil {
return nil
}
Expand Down Expand Up @@ -412,7 +412,7 @@ func applyImportedPackageSeeds(req model.AnalyzeRequest, projectRoot string, imp
}
timestamp := now.UTC().Format(time.RFC3339)
hopsByID := computeReachablePackageHopsFromSeeds(req.Graph, imports)
for _, pkg := range req.Graph.Nodes() {
for _, pkg := range req.Graph.DependencyNodes() {
if pkg == nil || !isNPMPackage(pkg) {
continue
}
Expand All @@ -422,16 +422,22 @@ func applyImportedPackageSeeds(req model.AnalyzeRequest, projectRoot string, imp
vulns := vulnerabilitiesForDep(req, pkg)
for i := range vulns {
vuln := &vulns[i]
if vuln.Reachability != nil && vuln.Reachability.Analyzer == Name {
continue // already annotated by an earlier project pass
}
r := &model.Reachability{
// No skip on an earlier project pass. That skip was the loss
// phase 2.8 removes: a workspace's second project can import a
// package the first does not, and the first answer stood. Each
// project root now contributes evidence and the annotation is
// the derived summary over all of them.
r := &model.ReachabilityEvidence{
ModuleRoot: projectRoot,
// jsreach resolves per project root, and the hop map is keyed
// by node, so the finding is attributable to this occurrence.
DependencyRefs: []string{pkg.NodeID()},
Analyzer: Name,
AnalyzedAt: timestamp,
Tier: model.TierPackage,
DynamicImportsDetected: dynamicImports,
}
if hops, ok := hopsByID[pkg.ID]; ok {
if hops, ok := hopsByID[pkg.NodeID()]; ok {
r.Status = model.ReachabilityReachable
h := hops
r.Hops = &h
Expand All @@ -442,12 +448,31 @@ func applyImportedPackageSeeds(req model.AnalyzeRequest, projectRoot string, imp
r.Reason = "package-not-imported"
outcome.unreachable++
}
vuln.Reachability = r
vuln.Reachability = withEvidence(vuln.Reachability, *r, timestamp)
}
}
return outcome
}

// withEvidence appends one project root's finding to a vulnerability's
// reachability record and recomputes the summary.
//
// The summary is derived, never accumulated by hand: reachable anywhere wins,
// and unreachable requires every root to say so. Writing that rule at each
// call site is how the first-root-wins behaviour got there.
func withEvidence(current *model.Reachability, evidence model.ReachabilityEvidence, timestamp string) *model.Reachability {
var all []model.ReachabilityEvidence
if current != nil && current.Analyzer == Name {
all = current.Evidence
}
all = append(all, evidence)
summary := model.DeriveReachability(all)
summary.Analyzer = Name
summary.AnalyzedAt = timestamp
summary.Evidence = all
return &summary
}

// computeReachablePackageHops returns a map from graph package ID to
// the shortest dep-graph distance from any directly-imported package.
// The seed set (hop 0) is every npm package whose name (or qualified
Expand All @@ -470,18 +495,18 @@ func computeReachablePackageHopsFromSeeds(g *model.Graph, imports map[string]int
}
queue := make([]string, 0)
// Seed: every npm package whose name matches the import set.
for _, pkg := range g.Nodes() {
for _, pkg := range g.DependencyNodes() {
if pkg == nil || !isNPMPackage(pkg) {
continue
}
if !isPackageImported(pkg, imports) {
continue
}
if _, ok := hops[pkg.ID]; ok {
if _, ok := hops[pkg.NodeID()]; ok {
continue
}
hops[pkg.ID] = importedPackageDepth(pkg, imports)
queue = append(queue, pkg.ID)
hops[pkg.NodeID()] = importedPackageDepth(pkg, imports)
queue = append(queue, pkg.NodeID())
}
// BFS: every dep edge from a reachable package adds its target at
// hop+1 if it has not been seen yet (shortest-distance wins).
Expand All @@ -497,17 +522,17 @@ func computeReachablePackageHopsFromSeeds(g *model.Graph, imports map[string]int
if dep == nil {
continue
}
if _, ok := hops[dep.ID]; ok {
if _, ok := hops[dep.NodeID()]; ok {
continue
}
hops[dep.ID] = current + 1
queue = append(queue, dep.ID)
hops[dep.NodeID()] = current + 1
queue = append(queue, dep.NodeID())
}
}
return hops
}

func importedPackageDepth(pkg *model.Dependency, imports map[string]int) int {
func importedPackageDepth(pkg *model.DependencyNode, imports map[string]int) int {
if depth, ok := imports[importSpecifier(pkg)]; ok {
return depth
}
Expand All @@ -517,7 +542,7 @@ func importedPackageDepth(pkg *model.Dependency, imports map[string]int) int {
// isPackageImported reports whether pkg's npm name appears in the runner's
// bare-specifier import set. Used as the seed predicate for the transitive
// walk.
func isPackageImported(pkg *model.Dependency, imports map[string]int) bool {
func isPackageImported(pkg *model.DependencyNode, imports map[string]int) bool {
if pkg == nil || len(imports) == 0 {
return false
}
Expand All @@ -534,7 +559,7 @@ func isPackageImported(pkg *model.Dependency, imports map[string]int) bool {
// "@tailwindcss/postcss" would be seeded as reachable by any `import "postcss"`
// in the project, and QualifiedName's "tailwindcss:postcss" is not a specifier
// any module resolver ever produces.
func importSpecifier(pkg *model.Dependency) string {
func importSpecifier(pkg *model.DependencyNode) string {
if pkg == nil {
return ""
}
Expand All @@ -547,7 +572,7 @@ func annotateProjectUnknown(req model.AnalyzeRequest, projectRoot, reason string
}
timestamp := now.UTC().Format(time.RFC3339)
count := 0
for _, pkg := range req.Graph.Nodes() {
for _, pkg := range req.Graph.DependencyNodes() {
if pkg == nil || !isNPMPackage(pkg) {
continue
}
Expand Down Expand Up @@ -577,7 +602,7 @@ func annotateAllUnknown(req model.AnalyzeRequest, reason string, now time.Time)
return
}
timestamp := now.UTC().Format(time.RFC3339)
for _, pkg := range req.Graph.Nodes() {
for _, pkg := range req.Graph.DependencyNodes() {
if pkg == nil || !isNPMPackage(pkg) {
continue
}
Expand All @@ -603,7 +628,7 @@ func annotateAllUnknown(req model.AnalyzeRequest, reason string, now time.Time)
// to it. In multi-project repos this may over-attribute; the second
// pass through applyRunnerResult skips already-annotated vulns to
// avoid double-counting.
func packageBelongsToProjectRoot(pkg *model.Dependency, projectRoot string) bool {
func packageBelongsToProjectRoot(pkg *model.DependencyNode, projectRoot string) bool {
if pkg == nil {
return false
}
Expand Down
Loading
Loading