From 959d88abfb8a24aa9ec12aa4ae3683ecc6c179ab Mon Sep 17 00:00:00 2001 From: Andrew Nesbitt Date: Tue, 12 May 2026 12:35:06 +0100 Subject: [PATCH] sbom: SBOM.FilterProperties for namespace-scoped property pruning MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Walks every package's Properties slice and removes entries where the keep predicate returns false. Useful when handing a document to a downstream consumer that doesn't recognise a particular property namespace — strip the tool-specific prefix before sharing outside the team that produced it. The filter mutates the SBOM in place. Document- and Component-level metadata is untouched; only the per-package Properties slice is affected (which mirrors where Property entries actually live in the parsed model). Usage: doc.FilterProperties(func(name string) bool { return !strings.HasPrefix(name, "mytool:") }) --- filter_test.go | 55 ++++++++++++++++++++++++++++++++++++++++++++++++++ sbom.go | 26 ++++++++++++++++++++++++ 2 files changed, 81 insertions(+) create mode 100644 filter_test.go diff --git a/filter_test.go b/filter_test.go new file mode 100644 index 0000000..95750e0 --- /dev/null +++ b/filter_test.go @@ -0,0 +1,55 @@ +package sbom + +import ( + "strings" + "testing" +) + +func TestFilterProperties(t *testing.T) { + s := New(TypeCycloneDX) + s.AddPackage(Package{ + Name: "a", + Version: "1.0", + Properties: []Property{ + {Name: "tool:scratch", Value: "x"}, + {Name: "cdx:type", Value: "library"}, + {Name: "tool:size", Value: "100"}, + }, + }) + s.AddPackage(Package{ + Name: "b", + Version: "2.0", + Properties: []Property{ + {Name: "tool:scratch", Value: "y"}, + }, + }) + + s.FilterProperties(func(name string) bool { + return !strings.HasPrefix(name, "tool:") + }) + + if got := len(s.Packages[0].Properties); got != 1 { + t.Errorf("package a: properties = %d, want 1", got) + } + if s.Packages[0].Properties[0].Name != "cdx:type" { + t.Errorf("package a kept the wrong property: %+v", s.Packages[0].Properties) + } + if got := len(s.Packages[1].Properties); got != 0 { + t.Errorf("package b: properties = %d, want 0", got) + } +} + +func TestFilterProperties_NilPredicate(t *testing.T) { + s := New(TypeCycloneDX) + s.AddPackage(Package{Name: "a", Version: "1.0", Properties: []Property{{Name: "x"}}}) + s.FilterProperties(nil) + if len(s.Packages[0].Properties) != 1 { + t.Error("nil predicate should be a no-op") + } +} + +func TestFilterProperties_EmptyPackages(t *testing.T) { + s := New(TypeCycloneDX) + s.FilterProperties(func(string) bool { return true }) + // No-op; just shouldn't panic. +} diff --git a/sbom.go b/sbom.go index 83f9623..e17fa0e 100644 --- a/sbom.go +++ b/sbom.go @@ -169,6 +169,32 @@ func newSBOM(t Type) *SBOM { return New(t) } // (Name, Version) pair. func (s *SBOM) AddPackage(p Package) { s.addPackage(p) } +// FilterProperties walks every package and removes properties whose +// names keep returns false for. Useful when handing a document to a +// downstream consumer that doesn't recognise a particular property +// namespace — for example, strip a tool-specific "mytool:" prefix +// before sharing outside the team that produced it. The filter +// mutates the SBOM in place; callers wanting a copy should Encode +// + Parse around the call. +// +// Document- and Component-level metadata is untouched; only the +// per-package Properties slice is filtered. +func (s *SBOM) FilterProperties(keep func(name string) bool) { + if keep == nil { + return + } + for i := range s.Packages { + props := s.Packages[i].Properties + filtered := props[:0] + for _, p := range props { + if keep(p.Name) { + filtered = append(filtered, p) + } + } + s.Packages[i].Properties = filtered + } +} + func (s *SBOM) addPackage(p Package) { if s.pkgIndex == nil { s.pkgIndex = map[[2]string]int{}