@@ -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