Skip to content

Commit 4c901b1

Browse files
committed
feat(sbom): resolve conversations
Signed-off-by: Radmir Khurum <radmir.khurum@flant.com>
1 parent 34fdf77 commit 4c901b1

8 files changed

Lines changed: 153 additions & 70 deletions

File tree

docs/_data/werf_yaml.yml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -593,13 +593,13 @@ sections:
593593
- name: workdir
594594
value: "string"
595595
description:
596-
en: "Directory inside the image that contains the manifest files (required for go-mod and python-*)"
597-
ru: "Директория внутри образа, содержащая файлы манифеста (обязательно для go-mod и python-*)"
596+
en: "Directory inside the image that contains the spec files (required for go-mod and python-*)"
597+
ru: "Директория внутри образа, содержащая файлы спецификации (обязательно для go-mod и python-*)"
598598
- name: spec
599599
value: "string"
600600
description:
601-
en: "Manifest file name (defaults: go.mod for go-mod, pyproject.toml for python-uv/python-poetry, requirements.txt for python-pip)"
602-
ru: "Имя файла манифеста (по умолчанию: go.mod для go-mod, pyproject.toml для python-uv/python-poetry, requirements.txt для python-pip)"
601+
en: "Spec file name (defaults: go.mod for go-mod, pyproject.toml for python-uv/python-poetry, requirements.txt for python-pip)"
602+
ru: "Имя файла спецификации (по умолчанию: go.mod для go-mod, pyproject.toml для python-uv/python-poetry, requirements.txt для python-pip)"
603603
- name: lock
604604
value: "string"
605605
description:

pkg/config/packages_directive.go

Lines changed: 29 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -27,29 +27,32 @@ type FileBasedSpec struct {
2727
}
2828

2929
type PackageEcosystem struct {
30-
Type PackagesDirectiveType
31-
Aliases []string
32-
DefaultSpec string
33-
DefaultLock string
34-
InstallCmd func(workdir, spec string) string
35-
CatalogerName string
30+
Type PackagesDirectiveType
31+
Aliases []string
32+
DefaultSpec string
33+
DefaultLock string
34+
InstallCmd func(workdir, spec string) string
35+
CatalogerName string
36+
UseWorkdirFilter bool
3637
}
3738

3839
var ecosystems = map[PackagesDirectiveType]PackageEcosystem{
3940
PackagesDirectiveTypeGoMod: {
40-
Type: PackagesDirectiveTypeGoMod,
41-
DefaultSpec: "go.mod",
42-
DefaultLock: "go.sum",
43-
InstallCmd: func(workdir, _ string) string { return fmt.Sprintf("cd %q && go mod download", workdir) },
44-
CatalogerName: "go-module-file-cataloger",
41+
Type: PackagesDirectiveTypeGoMod,
42+
DefaultSpec: "go.mod",
43+
DefaultLock: "go.sum",
44+
InstallCmd: func(workdir, _ string) string { return fmt.Sprintf("cd %q && go mod download", workdir) },
45+
CatalogerName: "go-module-file-cataloger",
46+
UseWorkdirFilter: false,
4547
},
4648
PackagesDirectiveTypePythonUV: {
47-
Type: PackagesDirectiveTypePythonUV,
48-
Aliases: []string{"uv"},
49-
DefaultSpec: "pyproject.toml",
50-
DefaultLock: "uv.lock",
51-
InstallCmd: func(workdir, _ string) string { return fmt.Sprintf("cd %q && uv sync --frozen", workdir) },
52-
CatalogerName: "python-package-cataloger",
49+
Type: PackagesDirectiveTypePythonUV,
50+
Aliases: []string{"uv"},
51+
DefaultSpec: "pyproject.toml",
52+
DefaultLock: "uv.lock",
53+
InstallCmd: func(workdir, _ string) string { return fmt.Sprintf("cd %q && uv sync --frozen", workdir) },
54+
CatalogerName: "python-package-cataloger",
55+
UseWorkdirFilter: true,
5356
},
5457
PackagesDirectiveTypePythonPip: {
5558
Type: PackagesDirectiveTypePythonPip,
@@ -59,15 +62,17 @@ var ecosystems = map[PackagesDirectiveType]PackageEcosystem{
5962
InstallCmd: func(workdir, spec string) string {
6063
return fmt.Sprintf("cd %q && pip install --no-cache-dir -r %q", workdir, spec)
6164
},
62-
CatalogerName: "python-package-cataloger",
65+
CatalogerName: "python-package-cataloger",
66+
UseWorkdirFilter: true,
6367
},
6468
PackagesDirectiveTypePythonPoetry: {
65-
Type: PackagesDirectiveTypePythonPoetry,
66-
Aliases: []string{"poetry"},
67-
DefaultSpec: "pyproject.toml",
68-
DefaultLock: "poetry.lock",
69-
InstallCmd: func(workdir, _ string) string { return fmt.Sprintf("cd %q && poetry install --no-root", workdir) },
70-
CatalogerName: "python-package-cataloger",
69+
Type: PackagesDirectiveTypePythonPoetry,
70+
Aliases: []string{"poetry"},
71+
DefaultSpec: "pyproject.toml",
72+
DefaultLock: "poetry.lock",
73+
InstallCmd: func(workdir, _ string) string { return fmt.Sprintf("cd %q && poetry install --no-root", workdir) },
74+
CatalogerName: "python-package-cataloger",
75+
UseWorkdirFilter: true,
7176
},
7277
}
7378

pkg/sbom/managedinput/managedinput.go

Lines changed: 60 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,9 @@ import (
1515
type inputResolver struct {
1616
inputType config.PackagesDirectiveType
1717
catalogerName string
18+
filterMode scanner.CatalogerFilterMode
1819
sourcePaths func(directive *config.PackagesDirective) []string
20+
workdir func(directive *config.PackagesDirective) string
1921
}
2022

2123
var resolvers = buildResolvers()
@@ -31,16 +33,24 @@ func buildResolvers() []inputResolver {
3133
built := make([]inputResolver, 0, len(types))
3234
for _, t := range types {
3335
eco := ecosystems[t]
36+
filterMode := scanner.CatalogerFilterExactPath
37+
if eco.UseWorkdirFilter {
38+
filterMode = scanner.CatalogerFilterWorkdirPrefix
39+
}
3440
built = append(built, inputResolver{
3541
inputType: eco.Type,
3642
catalogerName: eco.CatalogerName,
43+
filterMode: filterMode,
3744
sourcePaths: func(d *config.PackagesDirective) []string {
3845
paths := []string{path.Join(d.FileBased.Workdir, d.FileBased.Spec)}
3946
if d.FileBased.Lock != "" {
4047
paths = append(paths, path.Join(d.FileBased.Workdir, d.FileBased.Lock))
4148
}
4249
return paths
4350
},
51+
workdir: func(d *config.PackagesDirective) string {
52+
return d.FileBased.Workdir
53+
},
4454
})
4555
}
4656
return built
@@ -59,7 +69,9 @@ func ToCatalogers(packages []*config.PackagesDirective) []scanner.Cataloger {
5969

6070
catalogers = append(catalogers, scanner.Cataloger{
6171
Name: res.catalogerName,
72+
FilterMode: res.filterMode,
6273
SourcePaths: res.sourcePaths(directive),
74+
Workdir: res.workdir(directive),
6375
})
6476
}
6577

@@ -71,33 +83,55 @@ func FilterBOMBySourcePaths(bom *cdx.BOM, catalogers []scanner.Cataloger) {
7183
return
7284
}
7385

74-
catalogerNames := make(map[string]struct{})
75-
allowedPaths := make(map[string]struct{})
86+
type catalogerFilter struct {
87+
name string
88+
filterMode scanner.CatalogerFilterMode
89+
paths map[string]struct{}
90+
workdir string
91+
}
92+
93+
filters := make([]catalogerFilter, 0, len(catalogers))
7694
for _, cat := range catalogers {
77-
catalogerNames[cat.Name] = struct{}{}
95+
paths := make(map[string]struct{}, len(cat.SourcePaths))
7896
for _, p := range cat.SourcePaths {
79-
allowedPaths[p] = struct{}{}
97+
paths[p] = struct{}{}
8098
}
99+
filters = append(filters, catalogerFilter{
100+
name: cat.Name,
101+
filterMode: cat.FilterMode,
102+
paths: paths,
103+
workdir: cat.Workdir,
104+
})
81105
}
82106

83107
filtered := lo.Filter(*bom.Components, func(comp cdx.Component, _ int) bool {
84-
if !componentFoundByCatalogers(comp, catalogerNames) {
85-
return false
108+
for _, f := range filters {
109+
if !componentFoundByCataloger(comp, f.name) {
110+
continue
111+
}
112+
if f.filterMode == scanner.CatalogerFilterWorkdirPrefix {
113+
if componentMatchesWorkdirPrefix(comp, f.workdir) {
114+
return true
115+
}
116+
} else {
117+
if componentMatchesAllowedPaths(comp, f.paths) {
118+
return true
119+
}
120+
}
86121
}
87-
return componentMatchesAllowedPaths(comp, allowedPaths)
122+
return false
88123
})
89124

90125
*bom.Components = filtered
91126
}
92127

93-
func componentFoundByCatalogers(comp cdx.Component, catalogerNames map[string]struct{}) bool {
128+
func componentFoundByCataloger(comp cdx.Component, catalogerName string) bool {
94129
if comp.Properties == nil {
95130
return false
96131
}
97132
for _, prop := range *comp.Properties {
98133
if prop.Name == "syft:package:foundBy" {
99-
_, ok := catalogerNames[prop.Value]
100-
return ok
134+
return prop.Value == catalogerName
101135
}
102136
}
103137
return false
@@ -117,3 +151,19 @@ func componentMatchesAllowedPaths(comp cdx.Component, allowedPaths map[string]st
117151
}
118152
return false
119153
}
154+
155+
func componentMatchesWorkdirPrefix(comp cdx.Component, workdir string) bool {
156+
if comp.Properties == nil {
157+
return false
158+
}
159+
prefix := workdir + "/"
160+
for _, prop := range *comp.Properties {
161+
if !strings.HasPrefix(prop.Name, "syft:location:") || !strings.HasSuffix(prop.Name, ":path") {
162+
continue
163+
}
164+
if strings.HasPrefix(prop.Value, prefix) {
165+
return true
166+
}
167+
}
168+
return false
169+
}

pkg/sbom/managedinput/managedinput_test.go

Lines changed: 48 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -61,8 +61,8 @@ var _ = Describe("ToCatalogers", func() {
6161
},
6262
},
6363
[]scanner.Cataloger{
64-
{Name: "go-module-file-cataloger", SourcePaths: []string{"/app/api/go.mod", "/app/api/go.sum"}},
65-
{Name: "go-module-file-cataloger", SourcePaths: []string{"/app/cli/go.mod", "/app/cli/go.sum"}},
64+
{Name: "go-module-file-cataloger", FilterMode: scanner.CatalogerFilterExactPath, SourcePaths: []string{"/app/api/go.mod", "/app/api/go.sum"}, Workdir: "/app/api"},
65+
{Name: "go-module-file-cataloger", FilterMode: scanner.CatalogerFilterExactPath, SourcePaths: []string{"/app/cli/go.mod", "/app/cli/go.sum"}, Workdir: "/app/cli"},
6666
},
6767
),
6868

@@ -120,7 +120,7 @@ var _ = Describe("FilterBOMBySourcePaths", func() {
120120
},
121121
},
122122
[]scanner.Cataloger{
123-
{Name: "go-module-file-cataloger", SourcePaths: []string{"/app/api/go.mod", "/app/api/go.sum"}},
123+
{Name: "go-module-file-cataloger", FilterMode: scanner.CatalogerFilterExactPath, SourcePaths: []string{"/app/api/go.mod", "/app/api/go.sum"}, Workdir: "/app/api"},
124124
},
125125
[]string{"github.com/foo/bar"},
126126
),
@@ -137,7 +137,7 @@ var _ = Describe("FilterBOMBySourcePaths", func() {
137137

138138
Entry("does nothing when BOM is nil",
139139
(*cdx.BOM)(nil),
140-
[]scanner.Cataloger{{Name: "x", SourcePaths: []string{"/app/go.mod"}}},
140+
[]scanner.Cataloger{{Name: "x", FilterMode: scanner.CatalogerFilterExactPath, SourcePaths: []string{"/app/go.mod"}, Workdir: "/app"}},
141141
[]string(nil),
142142
),
143143
)
@@ -157,7 +157,7 @@ var _ = Describe("ToCatalogers python", func() {
157157
},
158158
},
159159
[]scanner.Cataloger{
160-
{Name: "python-package-cataloger", SourcePaths: []string{"/app/requirements.txt"}},
160+
{Name: "python-package-cataloger", FilterMode: scanner.CatalogerFilterWorkdirPrefix, SourcePaths: []string{"/app/requirements.txt"}, Workdir: "/app"},
161161
},
162162
),
163163

@@ -169,7 +169,7 @@ var _ = Describe("ToCatalogers python", func() {
169169
},
170170
},
171171
[]scanner.Cataloger{
172-
{Name: "python-package-cataloger", SourcePaths: []string{"/app/pyproject.toml", "/app/uv.lock"}},
172+
{Name: "python-package-cataloger", FilterMode: scanner.CatalogerFilterWorkdirPrefix, SourcePaths: []string{"/app/pyproject.toml", "/app/uv.lock"}, Workdir: "/app"},
173173
},
174174
),
175175

@@ -181,20 +181,21 @@ var _ = Describe("ToCatalogers python", func() {
181181
},
182182
},
183183
[]scanner.Cataloger{
184-
{Name: "python-package-cataloger", SourcePaths: []string{"/svc/pyproject.toml", "/svc/poetry.lock"}},
184+
{Name: "python-package-cataloger", FilterMode: scanner.CatalogerFilterWorkdirPrefix, SourcePaths: []string{"/svc/pyproject.toml", "/svc/poetry.lock"}, Workdir: "/svc"},
185185
},
186186
),
187187
)
188188
})
189189

190190
var _ = Describe("FilterBOMBySourcePaths python declared", func() {
191-
pythonProps := func(paths ...string) *[]cdx.Property {
191+
distInfoProps := func(workdir, pkgName string, extraPaths ...string) *[]cdx.Property {
192192
props := []cdx.Property{
193193
{Name: "syft:package:foundBy", Value: "python-package-cataloger"},
194+
{Name: "syft:location:0:path", Value: workdir + "/.venv/lib/python3.12/site-packages/" + pkgName + ".dist-info/METADATA"},
194195
}
195-
for i, p := range paths {
196+
for i, p := range extraPaths {
196197
props = append(props, cdx.Property{
197-
Name: fmt.Sprintf("syft:location:%d:path", i),
198+
Name: fmt.Sprintf("syft:location:%d:path", i+1),
198199
Value: p,
199200
})
200201
}
@@ -208,7 +209,7 @@ var _ = Describe("FilterBOMBySourcePaths python declared", func() {
208209
}
209210
}
210211

211-
DescribeTable("exact-match path filtering for python and go-mod",
212+
DescribeTable("workdir-prefix filtering for python, exact-match for go-mod",
212213
func(bom *cdx.BOM, catalogers []scanner.Cataloger, expectedNames []string) {
213214
FilterBOMBySourcePaths(bom, catalogers)
214215
Expect(*bom.Components).To(HaveLen(len(expectedNames)))
@@ -217,31 +218,56 @@ var _ = Describe("FilterBOMBySourcePaths python declared", func() {
217218
}
218219
},
219220

220-
Entry("python pip component with matching requirements.txt path is kept",
221+
Entry("python-uv: keeps dist-info components under workdir, drops components from other workdirs",
221222
&cdx.BOM{
222223
Components: &[]cdx.Component{
223-
{Name: "requests", Properties: pythonProps("/app/requirements.txt")},
224-
{Name: "flask", Properties: pythonProps("/other/requirements.txt")},
224+
{Name: "requests", Properties: distInfoProps("/app", "requests-2.32.3")},
225+
{Name: "flask", Properties: distInfoProps("/other", "flask-3.0.0")},
225226
},
226227
},
227228
[]scanner.Cataloger{
228-
{Name: "python-package-cataloger", SourcePaths: []string{"/app/requirements.txt"}},
229+
{Name: "python-package-cataloger", FilterMode: scanner.CatalogerFilterWorkdirPrefix, SourcePaths: []string{"/app/pyproject.toml", "/app/uv.lock"}, Workdir: "/app"},
229230
},
230231
[]string{"requests"},
231232
),
232233

233-
Entry("python uv component is kept when both spec and lock paths match",
234+
Entry("python-pip: keeps dist-info components under workdir",
234235
&cdx.BOM{
235236
Components: &[]cdx.Component{
236-
{Name: "requests", Properties: pythonProps("/svc/pyproject.toml")},
237-
{Name: "requests", Properties: pythonProps("/svc/uv.lock")},
238-
{Name: "flask", Properties: pythonProps("/other/pyproject.toml")},
237+
{Name: "requests", Properties: distInfoProps("/app", "requests-2.32.3")},
238+
{Name: "flask", Properties: distInfoProps("/other", "flask-3.0.0")},
239239
},
240240
},
241241
[]scanner.Cataloger{
242-
{Name: "python-package-cataloger", SourcePaths: []string{"/svc/pyproject.toml", "/svc/uv.lock"}},
242+
{Name: "python-package-cataloger", FilterMode: scanner.CatalogerFilterWorkdirPrefix, SourcePaths: []string{"/app/requirements.txt"}, Workdir: "/app"},
243243
},
244-
[]string{"requests", "requests"},
244+
[]string{"requests"},
245+
),
246+
247+
Entry("python-poetry: keeps dist-info components under workdir",
248+
&cdx.BOM{
249+
Components: &[]cdx.Component{
250+
{Name: "requests", Properties: distInfoProps("/svc", "requests-2.32.3")},
251+
{Name: "flask", Properties: distInfoProps("/app", "flask-3.0.0")},
252+
},
253+
},
254+
[]scanner.Cataloger{
255+
{Name: "python-package-cataloger", FilterMode: scanner.CatalogerFilterWorkdirPrefix, SourcePaths: []string{"/svc/pyproject.toml", "/svc/poetry.lock"}, Workdir: "/svc"},
256+
},
257+
[]string{"requests"},
258+
),
259+
260+
Entry("workdir prefix does not match exact workdir path (no trailing slash confusion)",
261+
&cdx.BOM{
262+
Components: &[]cdx.Component{
263+
{Name: "requests", Properties: distInfoProps("/app", "requests-2.32.3")},
264+
{Name: "flask", Properties: distInfoProps("/appother", "flask-3.0.0")},
265+
},
266+
},
267+
[]scanner.Cataloger{
268+
{Name: "python-package-cataloger", FilterMode: scanner.CatalogerFilterWorkdirPrefix, SourcePaths: []string{"/app/pyproject.toml"}, Workdir: "/app"},
269+
},
270+
[]string{"requests"},
245271
),
246272

247273
Entry("regression: go-mod exact-match still works alongside python cataloger",
@@ -252,7 +278,7 @@ var _ = Describe("FilterBOMBySourcePaths python declared", func() {
252278
},
253279
},
254280
[]scanner.Cataloger{
255-
{Name: "go-module-file-cataloger", SourcePaths: []string{"/app/go.mod", "/app/go.sum"}},
281+
{Name: "go-module-file-cataloger", FilterMode: scanner.CatalogerFilterExactPath, SourcePaths: []string{"/app/go.mod", "/app/go.sum"}, Workdir: "/app"},
256282
},
257283
[]string{"github.com/foo/bar"},
258284
),

0 commit comments

Comments
 (0)