Skip to content

Commit eb5fd86

Browse files
committed
add completeness extractor
1 parent 913b9fc commit eb5fd86

File tree

4 files changed

+157
-48
lines changed

4 files changed

+157
-48
lines changed

pkg/scorer/v2/engine/farmulas.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,13 @@ import (
2020
"github.com/interlynk-io/sbomqs/pkg/scorer/v2/config"
2121
)
2222

23+
func ScoreNA() config.FeatureScore {
24+
return config.FeatureScore{
25+
Score: PerComponentScore(0, 0),
26+
Desc: NoComponentsNA(),
27+
Ignore: true,
28+
}
29+
}
2330
func NoComponentsNA() string { return "N/A (no components)" }
2431
func MissingField(field string) string { return "missing " + field }
2532
func PresentField(field string) string { return "present " + field }

pkg/scorer/v2/extractors/completeness.go

Lines changed: 127 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,14 +14,140 @@
1414

1515
package extractors
1616

17-
// comp_with_dependencies: Valid dependency graph ?
17+
import (
18+
"strings"
19+
20+
"github.com/interlynk-io/sbomqs/pkg/sbom"
21+
"github.com/interlynk-io/sbomqs/pkg/scorer/v2/config"
22+
"github.com/interlynk-io/sbomqs/pkg/scorer/v2/engine"
23+
"github.com/samber/lo"
24+
)
25+
26+
// comp_with_dependencies (component-level coverage)
27+
// SPDX: relationships (DEPENDS_ON); CDX: component.dependencies / bom.dependencies
28+
func CompWithDependencies(doc sbom.Document) config.FeatureScore {
29+
comps := doc.Components()
30+
if len(comps) == 0 {
31+
return engine.ScoreNA()
32+
}
33+
34+
have := lo.CountBy(comps, func(c sbom.GetComponent) bool {
35+
return c.HasRelationShips() || c.CountOfDependencies() > 0
36+
})
37+
38+
return config.FeatureScore{
39+
Score: engine.PerComponentScore(have, len(comps)),
40+
Desc: engine.CompDescription(have, len(comps), "dependencies"),
41+
Ignore: false,
42+
}
43+
}
1844

1945
// comp_with_declared_completeness: Completeness declaration present
46+
func CompWithCompleteness(doc sbom.Document) config.FeatureScore {
47+
comps := doc.Components()
48+
if len(comps) == 0 {
49+
return engine.ScoreNA()
50+
}
51+
52+
spec := doc.Spec().GetSpecType()
53+
54+
switch spec {
55+
case string(sbom.SBOMSpecSPDX):
56+
// N/A for SPDX
57+
return config.FeatureScore{
58+
Score: engine.BooleanScore(false),
59+
Desc: engine.NonSupportedSPDXField(),
60+
Ignore: true,
61+
}
62+
63+
case string(sbom.SBOMSpecCDX):
64+
// TODO: to add this method in our sbom module, then only we can fetch it here
65+
// Compositions/Aggregate
66+
// have := lo.CountBy(doc.Components(), func(c sbom.GetComponent) bool {
67+
// return c.GetComposition() != ""
68+
// })
69+
}
70+
71+
return config.FeatureScore{
72+
Score: engine.BooleanScore(false),
73+
Desc: engine.UnknownSpec(),
74+
Ignore: true,
75+
}
76+
}
2077

2178
// sbom_with_primary_comp: Single primary component defined
79+
func SBOMWithPrimaryComponent(doc sbom.Document) config.FeatureScore {
80+
comps := doc.Components()
81+
isPrimaryPresent := doc.PrimaryComp().IsPresent()
82+
83+
if !isPrimaryPresent {
84+
return config.FeatureScore{
85+
Score: engine.PerComponentScore(0, len(comps)),
86+
Desc: "absent",
87+
Ignore: true,
88+
}
89+
}
90+
91+
return config.FeatureScore{
92+
Score: engine.BooleanScore(isPrimaryPresent),
93+
Desc: "identified",
94+
Ignore: false,
95+
}
96+
}
2297

2398
// comps_with_source_code: Valid VCS URL
99+
func CompWithSourceCode(doc sbom.Document) config.FeatureScore {
100+
comps := doc.Components()
101+
if len(comps) == 0 {
102+
return engine.ScoreNA()
103+
}
104+
105+
have := lo.CountBy(doc.Components(), func(c sbom.GetComponent) bool {
106+
return strings.TrimSpace(c.SourceCodeURL()) != ""
107+
})
108+
109+
return config.FeatureScore{
110+
Score: engine.PerComponentScore(have, len(comps)),
111+
Desc: engine.CompDescription(have, len(comps), "source URIs"),
112+
Ignore: false,
113+
}
114+
}
24115

25116
// comp_with_supplier
117+
func CompWithSupplier(doc sbom.Document) config.FeatureScore {
118+
comps := doc.Components()
119+
if len(comps) == 0 {
120+
return engine.ScoreNA()
121+
}
122+
123+
have := lo.CountBy(comps, func(c sbom.GetComponent) bool {
124+
s := c.Suppliers()
125+
hasName := strings.TrimSpace(s.GetName()) != ""
126+
hasContact := strings.TrimSpace(s.GetEmail()) != "" || strings.TrimSpace(s.GetURL()) != ""
127+
return c.Suppliers().IsPresent() && hasName && hasContact
128+
})
129+
130+
return config.FeatureScore{
131+
Score: engine.PerComponentScore(have, len(comps)),
132+
Desc: engine.CompDescription(have, len(comps), "suppliers"),
133+
Ignore: false,
134+
}
135+
}
26136

27137
// comp_with_primary_purpose
138+
func CompWithPackagePurpose(doc sbom.Document) config.FeatureScore {
139+
comps := doc.Components()
140+
if len(comps) == 0 {
141+
return engine.ScoreNA()
142+
}
143+
144+
have := lo.CountBy(comps, func(c sbom.GetComponent) bool {
145+
return c.PrimaryPurpose() != ""
146+
})
147+
148+
return config.FeatureScore{
149+
Score: engine.PerComponentScore(have, len(comps)),
150+
Desc: engine.CompDescription(have, len(comps), "type"),
151+
Ignore: false,
152+
}
153+
}

pkg/scorer/v2/extractors/identification.go

Lines changed: 15 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -26,62 +26,48 @@ import (
2626

2727
// CompWithName: percentage of components that have a non-empty name.
2828
func CompWithName(doc sbom.Document) config.FeatureScore {
29-
total := len(doc.Components())
30-
if total == 0 {
31-
return config.FeatureScore{
32-
Score: engine.PerComponentScore(0, total),
33-
Desc: engine.NoComponentsNA(),
34-
Ignore: true,
35-
}
29+
comps := doc.Components()
30+
if len(comps) == 0 {
31+
return engine.ScoreNA()
3632
}
3733

3834
have := lo.CountBy(doc.Components(), func(c sbom.GetComponent) bool {
3935
return strings.TrimSpace(c.GetName()) != ""
4036
})
4137

4238
return config.FeatureScore{
43-
Score: engine.PerComponentScore(have, total),
44-
Desc: engine.CompDescription(have, total, "names"),
45-
39+
Score: engine.PerComponentScore(have, len(comps)),
40+
Desc: engine.CompDescription(have, len(comps), "names"),
4641
Ignore: false,
4742
}
4843
}
4944

5045
// CompWithVersion: percentage of components that have a non-empty version.
5146
func CompWithVersion(doc sbom.Document) config.FeatureScore {
52-
total := len(doc.Components())
53-
if total == 0 {
54-
return config.FeatureScore{
55-
Score: engine.PerComponentScore(0, total),
56-
Desc: engine.NoComponentsNA(),
57-
Ignore: true,
58-
}
47+
comps := doc.Components()
48+
if len(comps) == 0 {
49+
return engine.ScoreNA()
5950
}
6051

6152
have := lo.CountBy(doc.Components(), func(c sbom.GetComponent) bool {
6253
return strings.TrimSpace(c.GetVersion()) != ""
6354
})
6455

6556
return config.FeatureScore{
66-
Score: engine.PerComponentScore(have, total),
67-
Desc: engine.CompDescription(have, total, "versions"),
57+
Score: engine.PerComponentScore(have, len(comps)),
58+
Desc: engine.CompDescription(have, len(comps), "versions"),
6859
Ignore: false,
6960
}
7061
}
7162

7263
// CompWithUniqIDs: percentage of components whose ID is present and unique within the SBOM.
7364
func CompWithUniqLocalIDs(doc sbom.Document) config.FeatureScore {
74-
total := len(doc.Components())
75-
if total == 0 {
76-
return config.FeatureScore{
77-
Score: engine.PerComponentScore(0, total),
78-
Desc: engine.NoComponentsNA(),
79-
Ignore: true,
80-
}
65+
comps := doc.Components()
66+
if len(comps) == 0 {
67+
return engine.ScoreNA()
8168
}
8269

8370
have := lo.FilterMap(doc.Components(), func(c sbom.GetComponent, _ int) (string, bool) {
84-
// cross-check: is this local unique id or unique id like purl, cpe ?
8571
if c.GetID() == "" {
8672
fmt.Println("c.GetID(): ", c.GetID())
8773
return "", false
@@ -90,8 +76,8 @@ func CompWithUniqLocalIDs(doc sbom.Document) config.FeatureScore {
9076
})
9177

9278
return config.FeatureScore{
93-
Score: engine.PerComponentScore(len(have), total),
94-
Desc: engine.CompDescription(len(have), total, "unique IDs"),
79+
Score: engine.PerComponentScore(len(have), len(comps)),
80+
Desc: engine.CompDescription(len(have), len(comps), "unique IDs"),
9581
Ignore: false,
9682
}
9783
}

pkg/scorer/v2/extractors/integrity.go

Lines changed: 8 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -29,13 +29,8 @@ import (
2929
// (SHA-1, SHA-256, SHA-384, or SHA-512).
3030
func CompWithSHA1Plus(doc sbom.Document) config.FeatureScore {
3131
comps := doc.Components()
32-
total := len(comps)
33-
if total == 0 {
34-
return config.FeatureScore{
35-
Score: engine.PerComponentScore(0, 0),
36-
Desc: engine.NoComponentsNA(),
37-
Ignore: true,
38-
}
32+
if len(comps) == 0 {
33+
return engine.ScoreNA()
3934
}
4035

4136
have := 0
@@ -46,22 +41,17 @@ func CompWithSHA1Plus(doc sbom.Document) config.FeatureScore {
4641
}
4742

4843
return config.FeatureScore{
49-
Score: engine.PerComponentScore(have, total),
50-
Desc: engine.CompDescription(have, total, "SHA-1+"),
44+
Score: engine.PerComponentScore(have, len(comps)),
45+
Desc: engine.CompDescription(have, len(comps), "SHA-1+"),
5146
Ignore: false,
5247
}
5348
}
5449

5550
// CompWithSHA256Plus returns coverage of components that have SHA-256 or stronger.
5651
func CompWithSHA256Plus(doc sbom.Document) config.FeatureScore {
5752
comps := doc.Components()
58-
total := len(comps)
59-
if total == 0 {
60-
return config.FeatureScore{
61-
Score: engine.PerComponentScore(0, 0),
62-
Desc: engine.NoComponentsNA(),
63-
Ignore: true,
64-
}
53+
if len(comps) == 0 {
54+
return engine.ScoreNA()
6555
}
6656

6757
have := 0
@@ -72,8 +62,8 @@ func CompWithSHA256Plus(doc sbom.Document) config.FeatureScore {
7262
}
7363

7464
return config.FeatureScore{
75-
Score: engine.PerComponentScore(have, total),
76-
Desc: engine.CompDescription(have, total, "SHA-256+"),
65+
Score: engine.PerComponentScore(have, len(comps)),
66+
Desc: engine.CompDescription(have, len(comps), "SHA-256+"),
7767
Ignore: false,
7868
}
7969
}

0 commit comments

Comments
 (0)