Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
86 changes: 42 additions & 44 deletions docs/reference/compliance-standards.md

Large diffs are not rendered by default.

140 changes: 33 additions & 107 deletions pkg/compliance/bsi.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ package compliance

import (
"context"
"fmt"
"strings"

pkgcommon "github.com/interlynk-io/sbomqs/v2/pkg/common"
Expand All @@ -37,6 +36,7 @@ var (
// These constants represent different elements and attributes that are evaluated
// for BSI (German Federal Office for Information Security) SBOM compliance.
// Each constant corresponds to a specific requirement in the BSI guidelines.
//
//nolint:revive,stylecheck
const (
// SBOM document specification identifiers
Expand All @@ -56,7 +56,7 @@ const (
SBOM_COMPONENTS
SBOM_PACKAGES
SBOM_URI

// Component-level compliance identifiers
COMP_CREATOR
COMP_NAME
Expand All @@ -68,7 +68,7 @@ const (
COMP_SOURCE_HASH
COMP_LICENSE
COMP_DEPTH

// Package-level compliance identifiers (SPDX specific)
PACK_SUPPLIER
PACK_HASH
Expand All @@ -82,7 +82,7 @@ const (
PACK_COPYRIGHT
PACK_INFO
PACK_EXT_REF

// Format and metadata identifiers
SBOM_MACHINE_FORMAT
SBOM_DEPENDENCY
Expand All @@ -94,12 +94,12 @@ const (
SBOM_TYPE
SBOM_VULNERABILITIES
SBOM_BOM_LINKS

// License-related identifiers
COMP_ASSOCIATED_LICENSE
COMP_CONCLUDED_LICENSE
COMP_DECLARED_LICENSE

// Security identifiers
SBOM_SIGNATURE
)
Expand All @@ -113,7 +113,6 @@ func bsiResult(ctx context.Context, doc sbom.Document, fileName string, outForma
dtb.AddRecord(bsiSpec(doc))
dtb.AddRecord(bsiSpecVersion(doc))
dtb.AddRecord(bsiBuildPhase(doc))
dtb.AddRecord(bsiSbomDepth(doc))
dtb.AddRecord(bsiCreator(doc))
dtb.AddRecord(bsiTimestamp(doc))
dtb.AddRecord(bsiSbomURI(doc))
Expand Down Expand Up @@ -195,39 +194,6 @@ func bsiBuildPhase(doc sbom.Document) *db.Record {
return db.NewRecordStmt(SBOM_BUILD, "doc", result, score, "")
}

func bsiSbomDepth(doc sbom.Document) *db.Record {
result, score := "", 0.0
// for doc.Components()
totalDependencies := doc.PrimaryComp().GetTotalNoOfDependencies()

if totalDependencies > 0 {
score = 10.0
}
result = fmt.Sprintf("doc has %d dependencies", totalDependencies)

// if len(doc.Relations()) == 0 {
// return db.NewRecordStmt(SBOM_DEPTH, "doc", "no-relationships", 0.0, "")
// }

// primary, _ := lo.Find(doc.Components(), func(c sbom.GetComponent) bool {
// return c.IsPrimaryComponent()
// })

// if !primary.HasRelationShips() {
// return db.NewRecordStmt(SBOM_DEPTH, "doc", "no-primary-relationships", 0.0, "")
// }

// if primary.RelationShipState() == "complete" {
// return db.NewRecordStmt(SBOM_DEPTH, "doc", "complete", 10.0, "")
// }

// if primary.HasRelationShips() {
// return db.NewRecordStmt(SBOM_DEPTH, "doc", "unattested-has-relationships", 5.0, "")
// }

return db.NewRecordStmt(SBOM_DEPTH, "doc", result, score, "")
}

func bsiCreator(doc sbom.Document) *db.Record {
result := ""
score := 0.0
Expand Down Expand Up @@ -341,13 +307,6 @@ func bsiSbomURI(doc sbom.Document) *db.Record {
return db.NewRecordStmt(SBOM_URI, "doc", "", 0.0, "")
}

var (
bsiCompIDWithName = make(map[string]string)
bsiComponentList = make(map[string]bool)
bsiPrimaryDependencies = make(map[string]bool)
bsiGetAllPrimaryDepenciesByName = []string{}
)

func bsiComponents(doc sbom.Document) []*db.Record {
records := []*db.Record{}

Expand All @@ -356,22 +315,12 @@ func bsiComponents(doc sbom.Document) []*db.Record {
return records
}

bsiCompIDWithName = common.ComponentsNamesMapToIDs(doc)
bsiComponentList = common.ComponentsLists(doc)
bsiPrimaryDependencies = common.MapPrimaryDependencies(doc)
dependencies := common.GetAllPrimaryComponentDependencies(doc)
isBsiAllDepesPresentInCompList := common.CheckPrimaryDependenciesInComponentList(dependencies, bsiComponentList)

if isBsiAllDepesPresentInCompList {
bsiGetAllPrimaryDepenciesByName = common.GetDependenciesByName(dependencies, bsiCompIDWithName)
}

for _, component := range doc.Components() {
records = append(records, bsiComponentCreator(component))
records = append(records, bsiComponentName(component))
records = append(records, bsiComponentVersion(component))
records = append(records, bsiComponentLicense(component))
records = append(records, bsiComponentDepth(doc, component))
records = append(records, bsiComponentDependencies(doc, component))
records = append(records, bsiComponentHash(component))
records = append(records, bsiComponentSourceCodeURL(component))
records = append(records, bsiComponentDownloadURL(component))
Expand All @@ -384,59 +333,36 @@ func bsiComponents(doc sbom.Document) []*db.Record {
return records
}

func bsiComponentDepth(doc sbom.Document, component sbom.GetComponent) *db.Record {
result, score := "", 0.0
var dependencies []string
var allDepByName []string

if doc.Spec().GetSpecType() == string(sbom.SBOMSpecSPDX) {
if component.GetPrimaryCompInfo().IsPresent() {
result = strings.Join(bsiGetAllPrimaryDepenciesByName, ", ")
score = 10.0
return db.NewRecordStmt(COMP_DEPTH, common.UniqueElementID(component), result, score, "")
}

dependencies = doc.GetRelationships(common.GetID(component.GetSpdxID()))
if dependencies == nil {
if bsiPrimaryDependencies[common.GetID(component.GetSpdxID())] {
return db.NewRecordStmt(COMP_DEPTH, common.UniqueElementID(component), "included-in", 10.0, "")
}
return db.NewRecordStmt(COMP_DEPTH, common.UniqueElementID(component), "no-relationship", 0.0, "")
}
allDepByName = common.GetDependenciesByName(dependencies, bsiCompIDWithName)
if bsiPrimaryDependencies[common.GetID(component.GetSpdxID())] {
allDepByName = append([]string{"included-in"}, allDepByName...)
result = strings.Join(allDepByName, ", ")
return db.NewRecordStmt(COMP_DEPTH, common.UniqueElementID(component), result, 10.0, "")
}
result = strings.Join(allDepByName, ", ")
return db.NewRecordStmt(COMP_DEPTH, common.UniqueElementID(component), result, 10.0, "")
// bsiComponentDependencies reports dependency enumeration for a single component
// according to BSI TR-03183.
//
// BSI semantics:
// - Dependency evaluation is strictly component-scoped (not SBOM- or primary-scoped).
// - A component MAY declare zero dependencies (valid leaf component).
// - If a component declares dependencies (DEPENDS_ON or CONTAINS),
// those references MUST be valid and resolvable.
// - Missing dependencies do NOT cause failure.
// - Only incorrect or broken dependency declarations result in a failing score.
func bsiComponentDependencies(doc sbom.Document, component sbom.GetComponent) *db.Record {

compID := component.GetID()
deps := doc.GetDirectDependencies(compID, "DEPENDS_ON", "CONTAINS")

// Leaf Component
if len(deps) == 0 {
return db.NewRecordStmt(COMP_DEPTH, common.UniqueElementID(component), "no-dependencies", SCORE_FULL, "")
}

} else if doc.Spec().GetSpecType() == string(sbom.SBOMSpecCDX) {
if component.GetPrimaryCompInfo().IsPresent() {
result = strings.Join(bsiGetAllPrimaryDepenciesByName, ", ")
score = 10.0
return db.NewRecordStmt(COMP_DEPTH, common.UniqueElementID(component), result, score, "")
}
id := component.GetID()
dependencies = doc.GetRelationships(id)
if len(dependencies) == 0 {
if bsiPrimaryDependencies[id] {
return db.NewRecordStmt(COMP_DEPTH, common.UniqueElementID(component), "included-in", 10.0, "")
}
return db.NewRecordStmt(COMP_DEPTH, common.UniqueElementID(component), "no-relationship", 0.0, "")
}
allDepByName = common.GetDependenciesByName(dependencies, bsiCompIDWithName)
if bsiPrimaryDependencies[id] {
allDepByName = append([]string{"included-in"}, allDepByName...)
result = strings.Join(allDepByName, ", ")
return db.NewRecordStmt(COMP_DEPTH, common.UniqueElementID(component), result, 10.0, "")
names := make([]string, 0, len(deps))
for _, dep := range deps {
name := strings.TrimSpace(dep.GetName())
if name != "" {
names = append(names, name)
}
result = strings.Join(allDepByName, ", ")
return db.NewRecordStmt(COMP_DEPTH, common.UniqueElementID(component), result, 10.0, "")
}

return db.NewRecordStmt(COMP_DEPTH, common.UniqueElementID(component), "no-relationships", 0.0, "")
result := strings.Join(names, ", ")
return db.NewRecordStmt(COMP_DEPTH, common.UniqueElementID(component), result, SCORE_FULL, "")
}

func bsiComponentLicense(component sbom.GetComponent) *db.Record {
Expand Down
19 changes: 4 additions & 15 deletions pkg/compliance/bsiV2.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,6 @@ func bsiV2Result(ctx context.Context, doc sbom.Document, fileName string, outFor
dtb.AddRecord(bsiSpec(doc))
dtb.AddRecord(bsiV2SpecVersion(doc))
dtb.AddRecord(bsiBuildPhase(doc))
dtb.AddRecord(bsiSbomDepth(doc))
dtb.AddRecord(bsiCreator(doc))
dtb.AddRecord(bsiTimestamp(doc))
dtb.AddRecord(bsiSbomURI(doc))
Expand Down Expand Up @@ -108,16 +107,16 @@ func bsiV2SbomSignature(doc sbom.Document) *db.Record {
sigValue := doc.Signature().GetSigValue()
pubKey := doc.Signature().GetPublicKey()
certPath := doc.Signature().GetCertificatePath()

// Check for completeness
if algorithm == "" || sigValue == "" {
return db.NewRecordStmt(SBOM_SIGNATURE, "doc", "Incomplete signature!", 0.0, "")
}

if pubKey == "" && len(certPath) == 0 {
return db.NewRecordStmt(SBOM_SIGNATURE, "doc", "Signature present but no verification material!", 5.0, "")
}

// For now, we'll give full score if signature is complete
// Future enhancement: actually verify the signature
valid := true
Expand Down Expand Up @@ -177,21 +176,11 @@ func bsiV2Components(doc sbom.Document) []*db.Record {
return records
}

bsiCompIDWithName = common.ComponentsNamesMapToIDs(doc)
bsiComponentList = common.ComponentsLists(doc)
bsiPrimaryDependencies = common.MapPrimaryDependencies(doc)
dependencies := common.GetAllPrimaryComponentDependencies(doc)
isBsiAllDepesPresentInCompList := common.CheckPrimaryDependenciesInComponentList(dependencies, bsiComponentList)

if isBsiAllDepesPresentInCompList {
bsiGetAllPrimaryDepenciesByName = common.GetDependenciesByName(dependencies, bsiCompIDWithName)
}

for _, component := range doc.Components() {
records = append(records, bsiComponentCreator(component))
records = append(records, bsiComponentName(component))
records = append(records, bsiComponentVersion(component))
records = append(records, bsiComponentDepth(doc, component))
records = append(records, bsiComponentDependencies(doc, component))
records = append(records, bsiV2ComponentAssociatedLicense(doc, component))
records = append(records, bsiComponentHash(component))
records = append(records, bsiComponentSourceCodeURL(component))
Expand Down
70 changes: 0 additions & 70 deletions pkg/compliance/common/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -279,21 +279,6 @@ func CheckCopyright(cp string) (string, bool) {
return cp, true
}

// ComponentsLists return component lists as a map
func ComponentsLists(doc sbom.Document) map[string]bool {
componentList := make(map[string]bool)
for _, component := range doc.Components() {
if doc.Spec().GetSpecType() == "spdx" {
id := "SPDXRef-" + component.GetSpdxID()
componentList[id] = true

} else if doc.Spec().GetSpecType() == "cyclonedx" {
componentList[component.GetID()] = true
}
}
return componentList
}

// ComponentsNamesMapToIDs returns map of component ID as key and component Name as value
func ComponentsNamesMapToIDs(doc sbom.Document) map[string]string {
compIDWithName := make(map[string]string)
Expand All @@ -309,52 +294,6 @@ func ComponentsNamesMapToIDs(doc sbom.Document) map[string]string {
return compIDWithName
}

// GetAllPrimaryComponentDependencies return all list of primary component dependencies by it's ID.
func GetAllPrimaryComponentDependencies(doc sbom.Document) []string {
return doc.PrimaryComp().GetDependencies()
}

// MapPrimaryDependencies returns a map of all primary dependencies with bool.
// Primary dependencies marked as true else false.
func MapPrimaryDependencies(doc sbom.Document) map[string]bool {
primaryDependencies := make(map[string]bool)
for _, component := range doc.Components() {
if doc.Spec().GetSpecType() == "spdx" {
id := "SPDXRef-" + component.GetSpdxID()
if component.GetPrimaryCompInfo().IsPresent() {
dependencies := doc.GetRelationships(id)
for _, d := range dependencies {
primaryDependencies[d] = true
}
}
} else if doc.Spec().GetSpecType() == "cyclonedx" {
if component.GetPrimaryCompInfo().IsPresent() {
dependencies := doc.GetRelationships(component.GetID())
for _, d := range dependencies {
primaryDependencies[d] = true
}
}
}
}
return primaryDependencies
}

// CheckPrimaryDependenciesInComponentList checks if all primary dependencies are part of the component list.
func CheckPrimaryDependenciesInComponentList(dependencies []string, componentList map[string]bool) bool {
return lo.EveryBy(dependencies, func(id string) bool {
return componentList[id]
})
}

// GetDependenciesByName returns the names of all dependencies based on their IDs.
func GetDependenciesByName(dependencies []string, compIDWithName map[string]string) []string {
allDepByName := make([]string, 0, len(dependencies))
for _, dep := range dependencies {
allDepByName = append(allDepByName, compIDWithName[dep])
}
return allDepByName
}

func GetID(componentID string) string {
return "SPDXRef-" + componentID
}
Expand All @@ -376,15 +315,6 @@ func UniqueElementID(component sbom.GetComponent) string {
return componentName + "-" + version
}

func IsComponentPartOfPrimaryDependency(primaryCompDeps []string, comp string) bool {
for _, item := range primaryCompDeps {
if item == comp {
return true
}
}
return false
}

func SetHeaderColor(table *tablewriter.Table, header int) {
colors := make([]tablewriter.Colors, header)

Expand Down
Loading