Skip to content

Commit 846a248

Browse files
committed
refractor and some feature, category added
1 parent 811d2c6 commit 846a248

File tree

4 files changed

+437
-263
lines changed

4 files changed

+437
-263
lines changed

pkg/scorer/v2/engine.go

Lines changed: 233 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import (
1818
"context"
1919
"fmt"
2020
"os"
21+
"strings"
2122

2223
"github.com/interlynk-io/sbomqs/pkg/logger"
2324
"github.com/interlynk-io/sbomqs/pkg/sbom"
@@ -94,13 +95,13 @@ func ScoreSBOM(ctx context.Context, config Config, paths []string) ([]Result, er
9495
return nil, fmt.Errorf("get signature for %q: %w", path, err)
9596
}
9697

97-
res, err := SBOMEvaluation(ctx, file, signature, config, path)
98+
result, err := SBOMEvaluation(ctx, file, signature, config, path)
9899
if err != nil {
99100
log.Warnf("failed to process SBOM %s: %v", path, err)
100101
return nil, fmt.Errorf("process SBOM %q: %w", path, err)
101102
}
102103

103-
results = append(results, res)
104+
results = append(results, result)
104105
anyProcessed = true
105106
}
106107
}
@@ -155,3 +156,233 @@ func processURLInput(ctx context.Context, url string, config Config) (*os.File,
155156

156157
return tmpFile, sig, nil
157158
}
159+
160+
func SBOMEvaluation(ctx context.Context, file *os.File, sig sbom.Signature, config Config, path string) (Result, error) {
161+
// Parse the SBOM
162+
doc, err := sbom.NewSBOMDocument(ctx, file, sig)
163+
if err != nil {
164+
return Result{}, fmt.Errorf("parse error: %w", err)
165+
}
166+
167+
// Extract metadata for the final report
168+
meta := extractMeta(doc, path)
169+
170+
// Select categories to score
171+
categoriesToScore, err := selectCategoriesToScore(config)
172+
if err != nil {
173+
return Result{}, err
174+
}
175+
176+
// Score doc against (categories + their features)
177+
categoriesResults := ScoreAgainstCategories(doc, categoriesToScore)
178+
179+
// Now update score category weights
180+
var categoryWeight float64
181+
var sumOfScoreWithCategoryWeightage float64
182+
183+
for _, catResult := range categoriesResults {
184+
categoryWeight += catResult.Weight
185+
sumOfScoreWithCategoryWeightage += catResult.Score * catResult.Weight
186+
}
187+
188+
overallScore := 0.0
189+
if categoryWeight > 0 {
190+
overallScore = sumOfScoreWithCategoryWeightage / categoryWeight
191+
}
192+
193+
return Result{
194+
Filename: path,
195+
NumComponents: meta.NumComponents,
196+
CreationTime: meta.CreationTime,
197+
InterlynkScore: overallScore,
198+
Grade: toGrade(overallScore),
199+
Spec: meta.Spec,
200+
SpecVersion: meta.SpecVersion,
201+
FileFormat: meta.FileFormat,
202+
Categories: categoriesResults,
203+
}, nil
204+
}
205+
206+
// Best-effort meta extraction (unchanged)
207+
func extractMeta(doc sbom.Document, fileName string) interlynkMeta {
208+
return interlynkMeta{
209+
Filename: fileName,
210+
NumComponents: len(doc.Components()),
211+
CreationTime: doc.Spec().GetCreationTimestamp(),
212+
Spec: doc.Spec().GetName(),
213+
SpecVersion: doc.Spec().GetVersion(),
214+
FileFormat: doc.Spec().FileFormat(),
215+
}
216+
}
217+
218+
// selectCategoriesToScore returns the exact list of categories we’ll score.
219+
func selectCategoriesToScore(cfg Config) ([]CategorySpec, error) {
220+
cats := baseCategories() // Identification, Provenance (with their feature specs)
221+
222+
// filters (by category name and/or feature key).
223+
cats = filterCategories(cats, cfg)
224+
225+
if len(cats) == 0 {
226+
return nil, fmt.Errorf("no categories to score after applying filters (check config)")
227+
}
228+
229+
// Also prune categories that lost all features due to feature-level filters.
230+
pruned := make([]CategorySpec, 0, len(cats))
231+
for _, c := range cats {
232+
if len(c.Features) > 0 {
233+
pruned = append(pruned, c)
234+
}
235+
}
236+
if len(pruned) == 0 {
237+
return nil, fmt.Errorf("no features to score after applying filters (check config)")
238+
}
239+
return pruned, nil
240+
}
241+
242+
func baseCategories() []CategorySpec {
243+
return []CategorySpec{
244+
Identification,
245+
Provenance,
246+
Integrity,
247+
Completeness,
248+
// LicensingAndCompliance
249+
// VulnerabilityAndTraceability
250+
// Structural
251+
// Component Quality
252+
}
253+
}
254+
255+
// Grade mapping per spec (A: 9–10, B: 8–8.9, C: 7–7.9, D: 5–6.9, F: <5)
256+
func toGrade(v float64) string {
257+
switch {
258+
case v >= 9.0:
259+
return "A"
260+
case v >= 8.0:
261+
return "B"
262+
case v >= 7.0:
263+
return "C"
264+
case v >= 5.0:
265+
return "D"
266+
default:
267+
return "F"
268+
}
269+
}
270+
271+
// Rules (simple and explicit):
272+
// - If no filters are provided, return the input as-is.
273+
// - If Categories are provided: keep only those categories (by name).
274+
// - If Features are provided: within the kept categories, keep only those features (by key).
275+
// - If both are provided: intersection semantics (category must match, and only listed features remain).
276+
// - Categories that end up with zero features after filtering are dropped.
277+
// - Order is preserved.
278+
func filterCategories(cats []CategorySpec, cfg Config) []CategorySpec {
279+
if len(cfg.Categories) == 0 && len(cfg.Features) == 0 {
280+
return cats
281+
}
282+
283+
// Normalize filters once (trim + lowercase) and put them in sets for O(1) lookups.
284+
toSet := func(ss []string) map[string]struct{} {
285+
if len(ss) == 0 {
286+
return nil
287+
}
288+
m := make(map[string]struct{}, len(ss))
289+
for _, s := range ss {
290+
k := strings.ToLower(strings.TrimSpace(s))
291+
if k != "" {
292+
m[k] = struct{}{}
293+
}
294+
}
295+
return m
296+
}
297+
catAllow := toSet(cfg.Categories)
298+
featAllow := toSet(cfg.Features)
299+
300+
wantCats := len(catAllow) > 0
301+
wantFeats := len(featAllow) > 0
302+
303+
out := make([]CategorySpec, 0, len(cats))
304+
305+
for _, cat := range cats {
306+
// Category filter (if any)
307+
if wantCats {
308+
if _, ok := catAllow[strings.ToLower(cat.Name)]; !ok {
309+
continue
310+
}
311+
}
312+
313+
// If no feature filter, keep category as-is.
314+
if !wantFeats {
315+
out = append(out, cat)
316+
continue
317+
}
318+
319+
// Otherwise, keep only requested features inside this category.
320+
filtered := make([]FeatureSpec, 0, len(cat.Features))
321+
for _, feat := range cat.Features {
322+
if _, ok := featAllow[strings.ToLower(feat.Key)]; ok {
323+
filtered = append(filtered, feat)
324+
}
325+
}
326+
// Drop category if nothing remains after feature filtering.
327+
if len(filtered) == 0 {
328+
continue
329+
}
330+
331+
// Append a copy of the category with its filtered feature list.
332+
cat.Features = filtered
333+
out = append(out, cat)
334+
}
335+
336+
return out
337+
}
338+
339+
func EvaluateFeature(doc sbom.Document, feature FeatureSpec) FeatureResult {
340+
featureResult := feature.Evaluate(doc)
341+
342+
return FeatureResult{
343+
Key: feature.Key,
344+
Weight: feature.Weight,
345+
Score: featureResult.Score,
346+
Desc: featureResult.Desc,
347+
Ignored: featureResult.Ignore,
348+
}
349+
}
350+
351+
// ScoreAgainstCategories checks SBOM against all defined categories
352+
func ScoreAgainstCategories(doc sbom.Document, categories []CategorySpec) []CategoryResult {
353+
categoryResults := make([]CategoryResult, 0, len(categories))
354+
for _, cs := range categories {
355+
categoryResults = append(categoryResults, EvaluateCategory(doc, cs))
356+
}
357+
return categoryResults
358+
}
359+
360+
// Evaluate a category (feature-weighted average, ignoring N/A).
361+
func EvaluateCategory(doc sbom.Document, category CategorySpec) CategoryResult {
362+
categoryWiseResult := CategoryResult{
363+
Name: category.Name,
364+
Weight: category.Weight,
365+
}
366+
367+
var featureWeight float64 // feature weights actually used
368+
var scoreWithFeatureWeightage float64 // feature-weighted score sum
369+
370+
for _, feature := range category.Features {
371+
featureResult := EvaluateFeature(doc, feature)
372+
categoryWiseResult.Features = append(categoryWiseResult.Features, featureResult)
373+
374+
if featureResult.Ignored {
375+
continue
376+
}
377+
378+
featureWeight += featureResult.Weight
379+
scoreWithFeatureWeightage += featureResult.Score * featureResult.Weight
380+
}
381+
382+
if featureWeight > 0 {
383+
categoryWiseResult.Score = scoreWithFeatureWeightage / featureWeight
384+
} else {
385+
categoryWiseResult.Score = 0
386+
}
387+
return categoryWiseResult
388+
}

0 commit comments

Comments
 (0)