@@ -196,6 +196,9 @@ func (t codacyTrivy) getVulnerabilities(ctx context.Context, report ptypes.Repor
196196 lineNumberByPurl [pkg .Identifier .PURL .ToString ()] = lineNumber
197197 }
198198
199+ // Precompute the package graph once per result; reused for every vulnerability's chain lookup.
200+ graph := buildPackageGraph (result .Packages )
201+
199202 // Ensure Trivy only produces results with severities matching the specified patterns.
200203 // Due to the way we invoke Trivy, this won't happen by simply setting it in the config.
201204 if err := tresult .FilterResult (ctx , & result , tresult.IgnoreConfig {}, tresult.FilterOptions {Severities : trivySeverities }); err != nil {
@@ -233,7 +236,11 @@ func (t codacyTrivy) getVulnerabilities(ctx context.Context, report ptypes.Repor
233236 return nil , err
234237 }
235238
236- chains := buildDependencyChains (purl , result .Packages )
239+ targetUID := vuln .PkgIdentifier .UID
240+ if targetUID == "" {
241+ targetUID = graph .uidByPURL [purl ]
242+ }
243+ chains := buildDependencyChains (targetUID , graph )
237244 extraFields , err := json .Marshal (map [string ]any {
238245 "dependenciesChains" : chains ,
239246 "CVE" : vuln .VulnerabilityID ,
@@ -515,75 +522,98 @@ func unencodeComponents(bom *cdx.BOM) {
515522 }
516523}
517524
518- // buildDependencyChains enumerates root-to-vulnerable chains for a vulnerable PURL via DFS.
519- // Each chain is ordered root-most first, vulnerable package last .
520- // DependsOn in Trivy lists a package's children (what it depends on). To find parents
521- // (what depends on the vulnerable package), we build an inverted map keyed by UID .
522- // Limits: max maxDependencyChains chains; per-chain length capped at maxDependencyChainLen (tail kept).
523- func buildDependencyChains ( targetPURL string , packages []ftypes. Package ) [][] string {
524- // Map from UID to package for name resolution .
525- pkgByUID := make ( map [string ]ftypes. Package , len ( packages ))
526- // Inverted map: child UID -> parent UIDs (packages that depend on this child) .
527- parentsByUID := make ( map [string ][] string )
528- targetUID := ""
525+ // packageGraph holds precomputed lookups over a result's packages. Building it once
526+ // per result avoids O(N*M) reconstruction inside the vulnerability loop .
527+ type packageGraph struct {
528+ // pkgByUID resolves a UID to its package (for display-name lookup) .
529+ pkgByUID map [ string ]ftypes. Package
530+ // parentsByUID is the inverted dependency map: child UID -> UIDs that depend on it.
531+ // DependsOn in Trivy lists a package's children; we invert it to walk upward .
532+ parentsByUID map [string ][] string
533+ // uidByPURL is a fallback for vulnerabilities that carry a PURL but no UID .
534+ uidByPURL map [string ]string
535+ }
529536
537+ func buildPackageGraph (packages []ftypes.Package ) packageGraph {
538+ g := packageGraph {
539+ pkgByUID : make (map [string ]ftypes.Package , len (packages )),
540+ parentsByUID : make (map [string ][]string ),
541+ uidByPURL : make (map [string ]string , len (packages )),
542+ }
530543 for _ , pkg := range packages {
531544 uid := pkg .Identifier .UID
532- pkgByUID [uid ] = pkg
533- if pkg .Identifier .PURL != nil && pkg . Identifier . PURL . ToString () == targetPURL {
534- targetUID = uid
545+ g . pkgByUID [uid ] = pkg
546+ if pkg .Identifier .PURL != nil {
547+ g . uidByPURL [ pkg . Identifier . PURL . ToString ()] = uid
535548 }
536549 for _ , depUID := range pkg .DependsOn {
537- parentsByUID [depUID ] = append (parentsByUID [depUID ], uid )
550+ g . parentsByUID [depUID ] = append (g . parentsByUID [depUID ], uid )
538551 }
539552 }
553+ return g
554+ }
540555
541- if targetUID == "" {
556+ // buildDependencyChains enumerates root-to-vulnerable chains for the package identified
557+ // by targetUID via DFS. Each chain is ordered root-most first, vulnerable package last.
558+ // Limits: max maxDependencyChains chains; per-chain length capped at maxDependencyChainLen (tail kept).
559+ func buildDependencyChains (targetUID string , graph packageGraph ) [][]string {
560+ if _ , ok := graph .pkgByUID [targetUID ]; ! ok {
542561 return nil
543562 }
563+ b := chainBuilder {graph : graph }
564+ b .dfs (targetUID , nil , map [string ]bool {})
565+ return b .out
566+ }
544567
545- var out [][]string
568+ // chainBuilder isolates DFS state and helpers so the traversal stays modular and
569+ // each method has a low cyclomatic complexity.
570+ type chainBuilder struct {
571+ graph packageGraph
572+ out [][]string
573+ }
546574
547- var dfs func (currentUID string , path []string , visited map [string ]bool )
548- dfs = func (currentUID string , path []string , visited map [string ]bool ) {
549- if len (out ) >= maxDependencyChains {
550- return
551- }
552- if visited [currentUID ] {
553- return
554- }
555- visited [currentUID ] = true
556- defer delete (visited , currentUID )
557-
558- pkg , ok := pkgByUID [currentUID ]
559- var name string
560- if ok && pkg .Identifier .PURL != nil {
561- name = purlPrettyPrint (* pkg .Identifier .PURL )
562- } else {
563- name = currentUID
564- }
565- path = append ([]string {name }, path ... )
575+ func (b * chainBuilder ) nodeName (uid string ) string {
576+ if pkg , ok := b .graph .pkgByUID [uid ]; ok && pkg .Identifier .PURL != nil {
577+ return purlPrettyPrint (* pkg .Identifier .PURL )
578+ }
579+ return uid
580+ }
581+
582+ // tryRecord appends a chain unless the per-vulnerability chain cap is reached.
583+ // Returns true when a chain was recorded.
584+ func (b * chainBuilder ) tryRecord (chain []string ) bool {
585+ if len (b .out ) >= maxDependencyChains {
586+ return false
587+ }
588+ b .out = append (b .out , trimChainTail (chain , maxDependencyChainLen ))
589+ return true
590+ }
591+
592+ func (b * chainBuilder ) dfs (currentUID string , path []string , visited map [string ]bool ) {
593+ if visited [currentUID ] {
594+ return
595+ }
596+ visited [currentUID ] = true
597+ defer delete (visited , currentUID )
566598
567- parents := parentsByUID [currentUID ]
568- if len (parents ) == 0 {
569- out = append (out , trimChainTail (path , maxDependencyChainLen ))
599+ path = append ([]string {b .nodeName (currentUID )}, path ... )
600+
601+ parents := b .graph .parentsByUID [currentUID ]
602+ if len (parents ) == 0 {
603+ b .tryRecord (path )
604+ return
605+ }
606+ before := len (b .out )
607+ for _ , parentUID := range parents {
608+ if len (b .out ) >= maxDependencyChains {
570609 return
571610 }
572- before := len (out )
573- for _ , parentUID := range parents {
574- if len (out ) >= maxDependencyChains {
575- return
576- }
577- dfs (parentUID , path , visited )
578- }
579- // All parent branches were blocked by cycle detection — treat current node as root.
580- if len (out ) == before {
581- out = append (out , trimChainTail (path , maxDependencyChainLen ))
582- }
611+ b .dfs (parentUID , path , visited )
612+ }
613+ // All parent branches were blocked by cycle detection — treat current node as root.
614+ if len (b .out ) == before {
615+ b .tryRecord (path )
583616 }
584-
585- dfs (targetUID , nil , map [string ]bool {})
586- return out
587617}
588618
589619func trimChainTail (chain []string , max int ) []string {
0 commit comments