2222import org .apache .commons .collections4 .CollectionUtils ;
2323import org .cyclonedx .model .Bom ;
2424import org .cyclonedx .util .BomLink ;
25- import org .cyclonedx .util .ObjectLocator ;
2625import org .dependencytrack .model .Analysis ;
2726import org .dependencytrack .model .AnalysisJustification ;
2827import org .dependencytrack .model .AnalysisResponse ;
3332import org .dependencytrack .model .Vulnerability ;
3433import org .dependencytrack .parser .cyclonedx .util .ModelConverter ;
3534import org .dependencytrack .persistence .QueryManager ;
36- import org .dependencytrack .util .AnalysisCommentUtil ;
3735
3836import java .util .ArrayList ;
37+ import java .util .Collections ;
38+ import java .util .HashMap ;
3939import java .util .List ;
40+ import java .util .Map ;
41+ import java .util .Objects ;
4042
43+ import static java .util .Objects .requireNonNullElse ;
4144import static org .apache .commons .lang3 .StringUtils .isBlank ;
4245import static org .apache .commons .lang3 .StringUtils .trimToNull ;
4346
@@ -52,17 +55,21 @@ public void applyVex(final QueryManager qm, final Bom bom, final Project project
5255 LOGGER .info ("The uploaded VEX does not contain any vulnerabilities; Skipping VEX import" );
5356 return ;
5457 }
55- if (qm .getVulnerabilityCount (project , true ) == 0 ) {
56- LOGGER .info ("The project %s does not have any vulnerabilities; Skipping VEX import" .formatted (project ));
57- return ;
58- }
5958
6059 final List <org .cyclonedx .model .vulnerability .Vulnerability > vexVulns = getApplicableVexVulnerabilities (bom .getVulnerabilities ());
6160 if (vexVulns .isEmpty ()) {
6261 LOGGER .info ("The uploaded VEX does not contain any applicable vulnerabilities; Skipping VEX import" );
6362 return ;
6463 }
6564
65+ if (!qm .hasVulnerabilities (project )) {
66+ LOGGER .info ("The project %s does not have any vulnerabilities; Skipping VEX import" .formatted (project ));
67+ return ;
68+ }
69+
70+ final Map <String , BomRefTarget > targetByBomRef = indexComponents (bom );
71+ final Map <String , List <Component >> componentsByBomRef = new HashMap <>();
72+
6673 for (final org .cyclonedx .model .vulnerability .Vulnerability vexVuln : vexVulns ) {
6774 final Vulnerability dtVuln = qm .getVulnerabilityByVulnId (vexVuln .getSource ().getName (), vexVuln .getId ());
6875 if (dtVuln == null ) {
@@ -73,31 +80,39 @@ public void applyVex(final QueryManager qm, final Bom bom, final Project project
7380 continue ;
7481 }
7582
83+ List <Component > vulnerableComponents = null ;
84+
7685 for (org .cyclonedx .model .vulnerability .Vulnerability .Affect affect : vexVuln .getAffects ()) {
77- final ObjectLocator ol = new ObjectLocator (bom , affect .getRef ()).locate ();
78- if ((ol .found () && ol .isMetadataComponent ()) || (!ol .found () && BomLink .isBomLink (affect .getRef ()))) {
79- // Affects the project itself
80- List <Component > components = qm .getAllVulnerableComponents (project , dtVuln , true );
81- for (final Component component : components ) {
86+ final String affectedBomRef = affect .getRef ();
87+ final BomRefTarget affectedBomRefTarget = affectedBomRef != null
88+ ? targetByBomRef .get (affectedBomRef )
89+ : null ;
90+
91+ final boolean isProjectScoped =
92+ (affectedBomRefTarget != null && affectedBomRefTarget .isMetadataComponent ())
93+ || (affectedBomRefTarget == null && affectedBomRef != null && BomLink .isBomLink (affectedBomRef ));
94+
95+ if (isProjectScoped ) {
96+ if (vulnerableComponents == null ) {
97+ vulnerableComponents = qm .getAllVulnerableComponents (project , dtVuln );
98+ }
99+ for (final Component component : vulnerableComponents ) {
82100 updateAnalysis (qm , component , dtVuln , vexVuln );
83101 }
84- } else if (ol . found () && ol . isComponent () ) {
85- // Affects an individual component
86- final org . cyclonedx . model . Component cdxComponent = ( org . cyclonedx . model . Component ) ol . getObject ( );
87- final ComponentIdentity cid = new ComponentIdentity ( cdxComponent );
88- List < Component > components = qm . matchIdentity ( project , cid );
102+ } else if (affectedBomRefTarget != null ) {
103+ final List < Component > components = componentsByBomRef . computeIfAbsent ( affectedBomRef , ignored -> {
104+ final var cid = new ComponentIdentity ( affectedBomRefTarget . component () );
105+ return qm . matchIdentity ( project , cid );
106+ } );
89107 for (final Component component : components ) {
90108 updateAnalysis (qm , component , dtVuln , vexVuln );
91109 }
92- } else if (ol .found () && ol .isService ()) {
93- // Affects an individual service
94- // TODO add VEX support for services
95110 } else {
96111 LOGGER .warn ("""
97- Unable to locate affected element (metadata.component, components[].component, \
98- or services[].service) based on the BOM reference %s. The vulnerability.affects[].ref \
112+ Unable to locate affected element (metadata.component or components[].component) \
113+ based on the BOM reference %s. The vulnerability.affects[].ref \
99114 node of %s/%s is not resolvable; Skipping it\
100- """ .formatted (affect . getRef () , vexVuln .getSource ().getName (), vexVuln .getId ()));
115+ """ .formatted (affectedBomRef , vexVuln .getSource ().getName (), vexVuln .getId ()));
101116 }
102117 }
103118 }
@@ -106,16 +121,16 @@ Unable to locate affected element (metadata.component, components[].component, \
106121 private static List <org .cyclonedx .model .vulnerability .Vulnerability > getApplicableVexVulnerabilities (
107122 final List <org .cyclonedx .model .vulnerability .Vulnerability > vexVulns ) {
108123 final var applicableVulns = new ArrayList <org .cyclonedx .model .vulnerability .Vulnerability >();
109- for (final var vexVuln : vexVulns ) {
110- final int vexVulnPos = vexVulns .indexOf ( vexVuln );
124+ for (int vexVulnPos = 0 ; vexVulnPos < vexVulns . size (); vexVulnPos ++ ) {
125+ final var vexVuln = vexVulns .get ( vexVulnPos );
111126 if (isBlank (vexVuln .getId ()) || vexVuln .getSource () == null || isBlank (vexVuln .getSource ().getName ())) {
112127 LOGGER .warn ("VEX vulnerability at position #%d does not have an ID and / or source; Skipping it" .formatted (vexVulnPos ));
113128 continue ;
114129 }
115130
116131 final String vexVulnId = vexVuln .getId ();
117132 final String vexVulnSource = vexVuln .getSource ().getName ();
118- if (!Vulnerability .Source .isKnownSource (vexVuln . getSource (). getName () )) {
133+ if (!Vulnerability .Source .isKnownSource (vexVulnSource )) {
119134 LOGGER .warn ("VEX vulnerability %s/%s at position #%d is from an unsupported source; Skipping it"
120135 .formatted (vexVulnSource , vexVulnId , vexVulnPos ));
121136 continue ;
@@ -137,38 +152,115 @@ private static List<org.cyclonedx.model.vulnerability.Vulnerability> getApplicab
137152 return applicableVulns ;
138153 }
139154
140- private static void updateAnalysis (final QueryManager qm , final Component component , final Vulnerability vuln ,
155+ private record BomRefTarget (org .cyclonedx .model .Component component , boolean isMetadataComponent ) {
156+ }
157+
158+ private static Map <String , BomRefTarget > indexComponents (Bom bom ) {
159+ final Map <String , BomRefTarget > targetByBomRef = new HashMap <>();
160+ if (bom == null ) {
161+ return targetByBomRef ;
162+ }
163+
164+ if (bom .getMetadata () != null && bom .getMetadata ().getComponent () != null ) {
165+ indexComponents (List .of (bom .getMetadata ().getComponent ()), targetByBomRef , true );
166+ }
167+
168+ indexComponents (bom .getComponents (), targetByBomRef , false );
169+ return targetByBomRef ;
170+ }
171+
172+ private static void indexComponents (
173+ List <org .cyclonedx .model .Component > components ,
174+ Map <String , BomRefTarget > targetByBomRef ,
175+ boolean metadataComponent ) {
176+ if (components == null ) {
177+ return ;
178+ }
179+
180+ for (final var component : components ) {
181+ if (component .getBomRef () != null ) {
182+ targetByBomRef .putIfAbsent (
183+ component .getBomRef (),
184+ new BomRefTarget (component , metadataComponent ));
185+ }
186+
187+ if (component .getComponents () != null && !component .getComponents ().isEmpty ()) {
188+ indexComponents (component .getComponents (), targetByBomRef , false );
189+ }
190+ }
191+ }
192+
193+ private static void updateAnalysis (final QueryManager qm , final Component component , final Vulnerability dtVuln ,
141194 final org .cyclonedx .model .vulnerability .Vulnerability cdxVuln ) {
142- // The vulnerability object is detached, so refresh it.
143- final Vulnerability refreshedVuln = qm .getObjectByUuid (Vulnerability .class , vuln .getUuid ());
144- Analysis analysis = qm .getAnalysis (component , refreshedVuln );
145- AnalysisState analysisState = null ;
146- AnalysisJustification analysisJustification = null ;
147- String analysisDetails = null ;
148- AnalysisResponse analysisResponse = null ;
195+ final org .cyclonedx .model .vulnerability .Vulnerability .Analysis cdxAnalysis = cdxVuln .getAnalysis ();
196+
197+ final Analysis existing = qm .getAnalysis (component , dtVuln );
198+ final AnalysisState oldState = existing != null
199+ ? requireNonNullElse (existing .getAnalysisState (), AnalysisState .NOT_SET )
200+ : AnalysisState .NOT_SET ;
201+ final AnalysisJustification oldJustification = existing != null
202+ ? requireNonNullElse (existing .getAnalysisJustification (), AnalysisJustification .NOT_SET )
203+ : AnalysisJustification .NOT_SET ;
204+ final AnalysisResponse oldResponse = existing != null
205+ ? requireNonNullElse (existing .getAnalysisResponse (), AnalysisResponse .NOT_SET )
206+ : AnalysisResponse .NOT_SET ;
207+ final String oldDetails = existing != null
208+ ? requireNonNullElse (existing .getAnalysisDetails (), "" )
209+ : "" ;
210+
211+ AnalysisState newState = null ;
149212 boolean suppress = false ;
150- if (analysis == null ) {
151- analysis = qm .makeAnalysis (component , refreshedVuln , AnalysisState .NOT_SET , null , null , null , null );
152- }
153- if (cdxVuln .getAnalysis ().getState () != null ) {
154- analysisState = ModelConverter .convertCdxVulnAnalysisStateToDtAnalysisState (cdxVuln .getAnalysis ().getState ());
155- suppress = (AnalysisState .FALSE_POSITIVE == analysisState || AnalysisState .NOT_AFFECTED == analysisState || AnalysisState .RESOLVED == analysisState );
156- AnalysisCommentUtil .makeStateComment (qm , analysis , analysisState , COMMENTER );
157- }
158- if (cdxVuln .getAnalysis ().getJustification () != null ) {
159- analysisJustification = ModelConverter .convertCdxVulnAnalysisJustificationToDtAnalysisJustification (cdxVuln .getAnalysis ().getJustification ());
160- AnalysisCommentUtil .makeJustificationComment (qm , analysis , analysisJustification , COMMENTER );
161- }
162- if (trimToNull (cdxVuln .getAnalysis ().getDetail ()) != null ) {
163- analysisDetails = cdxVuln .getAnalysis ().getDetail ().trim ();
164- AnalysisCommentUtil .makeAnalysisDetailsComment (qm , analysis , cdxVuln .getAnalysis ().getDetail ().trim (), COMMENTER );
165- }
166- if (cdxVuln .getAnalysis ().getResponses () != null ) {
167- for (org .cyclonedx .model .vulnerability .Vulnerability .Analysis .Response cdxRes : cdxVuln .getAnalysis ().getResponses ()) {
168- analysisResponse = ModelConverter .convertCdxVulnAnalysisResponseToDtAnalysisResponse (cdxRes );
169- AnalysisCommentUtil .makeAnalysisResponseComment (qm , analysis , analysisResponse , COMMENTER );
213+ if (cdxAnalysis .getState () != null ) {
214+ newState = ModelConverter .convertCdxVulnAnalysisStateToDtAnalysisState (cdxAnalysis .getState ());
215+ suppress = AnalysisState .FALSE_POSITIVE == newState
216+ || AnalysisState .NOT_AFFECTED == newState
217+ || AnalysisState .RESOLVED == newState ;
218+ }
219+
220+ AnalysisJustification newJustification = null ;
221+ if (cdxAnalysis .getJustification () != null ) {
222+ newJustification = ModelConverter .convertCdxVulnAnalysisJustificationToDtAnalysisJustification (cdxAnalysis .getJustification ());
223+ }
224+
225+ String newDetails = null ;
226+ if (trimToNull (cdxAnalysis .getDetail ()) != null ) {
227+ newDetails = cdxAnalysis .getDetail ().trim ();
228+ }
229+
230+ AnalysisResponse newResponse = null ;
231+ final List <AnalysisResponse > responseTrail ;
232+ if (cdxAnalysis .getResponses () != null && !cdxAnalysis .getResponses ().isEmpty ()) {
233+ responseTrail = new ArrayList <>(cdxAnalysis .getResponses ().size ());
234+ for (var cdxRes : cdxAnalysis .getResponses ()) {
235+ final AnalysisResponse response = ModelConverter .convertCdxVulnAnalysisResponseToDtAnalysisResponse (cdxRes );
236+ responseTrail .add (response );
237+ newResponse = response ;
238+ }
239+ } else {
240+ responseTrail = Collections .emptyList ();
241+ }
242+
243+ final Analysis updated ;
244+ if (existing != null ) {
245+ updated = qm .updateAnalysis (existing , newState , newJustification , newResponse , newDetails , suppress );
246+ } else {
247+ final AnalysisState createState = newState != null ? newState : AnalysisState .NOT_SET ;
248+ updated = qm .makeAnalysis (component , dtVuln , createState , newJustification , newResponse , newDetails , suppress );
249+ }
250+
251+ if (newState != null && !Objects .equals (newState , oldState )) {
252+ qm .makeAnalysisComment (updated , "Analysis: %s → %s" .formatted (oldState , newState ), COMMENTER );
253+ }
254+ if (newJustification != null && !Objects .equals (newJustification , oldJustification )) {
255+ qm .makeAnalysisComment (updated , "Justification: %s → %s" .formatted (oldJustification , newJustification ), COMMENTER );
256+ }
257+ if (newDetails != null && !Objects .equals (newDetails , oldDetails )) {
258+ qm .makeAnalysisComment (updated , "Details: %s" .formatted (newDetails ), COMMENTER );
259+ }
260+ for (final AnalysisResponse response : responseTrail ) {
261+ if (response != null && !Objects .equals (response , oldResponse )) {
262+ qm .makeAnalysisComment (updated , "Vendor Response: %s → %s" .formatted (oldResponse , response ), COMMENTER );
170263 }
171264 }
172- qm .makeAnalysis (component , refreshedVuln , analysisState , analysisJustification , analysisResponse , analysisDetails , suppress );
173265 }
174266}
0 commit comments