@@ -192,22 +192,32 @@ func externalRef(bomLink string, bomRef string) (string, error) {
192
192
193
193
func (e * Marshaler ) marshalComponents (r types.Report , bomRef string ) (* []cdx.Component , * []cdx.Dependency , * []cdx.Vulnerability , error ) {
194
194
components := make ([]cdx.Component , 0 ) // To export an empty array in JSON
195
- var dependencies []cdx.Dependency
195
+ // we use map to avoid duplicate components
196
+ dependencies := map [string ]cdx.Dependency {}
196
197
metadataDependencies := make ([]string , 0 ) // To export an empty array in JSON
197
198
libraryUniqMap := map [string ]struct {}{}
198
199
vulnMap := map [string ]cdx.Vulnerability {}
199
200
for _ , result := range r .Results {
200
201
bomRefMap := map [string ]string {}
201
- var componentDependencies []string
202
+ pkgIDToRef := map [string ]string {}
203
+ var directDepRefs []string
204
+
205
+ // Get dependency parents first
206
+ parents := ftypes .Packages (result .Packages ).ParentDeps ()
207
+
202
208
for _ , pkg := range result .Packages {
203
209
pkgComponent , err := pkgToCdxComponent (result .Type , r .Metadata , pkg )
204
210
if err != nil {
205
211
return nil , nil , nil , xerrors .Errorf ("failed to parse pkg: %w" , err )
206
212
}
207
213
pkgID := packageID (result .Target , pkg .Name , utils .FormatVersion (pkg ), pkg .FilePath )
208
- if _ , ok := bomRefMap [pkgID ]; ! ok {
209
- bomRefMap [pkgID ] = pkgComponent .BOMRef
210
- componentDependencies = append (componentDependencies , pkgComponent .BOMRef )
214
+ bomRefMap [pkgID ] = pkgComponent .BOMRef
215
+ if pkg .ID != "" {
216
+ pkgIDToRef [pkg .ID ] = pkgComponent .BOMRef
217
+ }
218
+ // This package is a direct dependency
219
+ if ! pkg .Indirect || len (parents [pkg .ID ]) == 0 {
220
+ directDepRefs = append (directDepRefs , pkgComponent .BOMRef )
211
221
}
212
222
213
223
// When multiple lock files have the same dependency with the same name and version,
@@ -226,12 +236,30 @@ func (e *Marshaler) marshalComponents(r types.Report, bomRef string) (*[]cdx.Com
226
236
227
237
// For components
228
238
// ref. https://cyclonedx.org/use-cases/#inventory
229
- //
230
- // TODO: All packages are flattened at the moment. We should construct dependency tree.
231
239
components = append (components , pkgComponent )
232
240
}
233
241
}
234
242
243
+ // Iterate packages again to build dependency graph
244
+ for _ , pkg := range result .Packages {
245
+ deps := lo .FilterMap (pkg .DependsOn , func (dep string , _ int ) (string , bool ) {
246
+ if ref , ok := pkgIDToRef [dep ]; ok {
247
+ return ref , true
248
+ }
249
+ return "" , false
250
+ })
251
+ if len (deps ) == 0 {
252
+ continue
253
+ }
254
+ sort .Strings (deps )
255
+ ref := pkgIDToRef [pkg .ID ]
256
+ dependencies [ref ] = cdx.Dependency {
257
+ Ref : ref ,
258
+ Dependencies : & deps ,
259
+ }
260
+ }
261
+ sort .Strings (directDepRefs )
262
+
235
263
for _ , vuln := range result .Vulnerabilities {
236
264
// Take a bom-ref
237
265
pkgID := packageID (result .Target , vuln .PkgName , vuln .InstalledVersion , vuln .PkgPath )
@@ -260,7 +288,7 @@ func (e *Marshaler) marshalComponents(r types.Report, bomRef string) (*[]cdx.Com
260
288
// ref. https://cyclonedx.org/use-cases/#inventory
261
289
262
290
// Dependency graph from #1 to #2
263
- metadataDependencies = append (metadataDependencies , componentDependencies ... )
291
+ metadataDependencies = append (metadataDependencies , directDepRefs ... )
264
292
} else if result .Class == types .ClassOSPkg || result .Class == types .ClassLangPkg {
265
293
// If a package is OS package, it will be a dependency of "Operating System" component.
266
294
// e.g.
@@ -283,29 +311,29 @@ func (e *Marshaler) marshalComponents(r types.Report, bomRef string) (*[]cdx.Com
283
311
components = append (components , resultComponent )
284
312
285
313
// Dependency graph from #2 to #3
286
- dependencies = append (dependencies ,
287
- cdx.Dependency {
288
- Ref : resultComponent .BOMRef ,
289
- Dependencies : & componentDependencies ,
290
- },
291
- )
292
-
314
+ dependencies [resultComponent .BOMRef ] = cdx.Dependency {
315
+ Ref : resultComponent .BOMRef ,
316
+ Dependencies : & directDepRefs ,
317
+ }
293
318
// Dependency graph from #1 to #2
294
319
metadataDependencies = append (metadataDependencies , resultComponent .BOMRef )
295
320
}
296
321
}
322
+
297
323
vulns := maps .Values (vulnMap )
298
324
sort .Slice (vulns , func (i , j int ) bool {
299
325
return vulns [i ].ID > vulns [j ].ID
300
326
})
301
327
302
- dependencies = append (dependencies ,
303
- cdx.Dependency {
304
- Ref : bomRef ,
305
- Dependencies : & metadataDependencies ,
306
- },
307
- )
308
- return & components , & dependencies , & vulns , nil
328
+ dependencies [bomRef ] = cdx.Dependency {
329
+ Ref : bomRef ,
330
+ Dependencies : & metadataDependencies ,
331
+ }
332
+ dependencyList := maps .Values (dependencies )
333
+ sort .Slice (dependencyList , func (i , j int ) bool {
334
+ return dependencyList [i ].Ref < dependencyList [j ].Ref
335
+ })
336
+ return & components , & dependencyList , & vulns , nil
309
337
}
310
338
311
339
func packageID (target , pkgName , pkgVersion , pkgFilePath string ) string {
0 commit comments