Skip to content

Commit d47fb89

Browse files
committed
fix: add empty dep-graph when there are no dependencies
1 parent eb1c225 commit d47fb89

File tree

7 files changed

+373
-53
lines changed

7 files changed

+373
-53
lines changed

internal/depgraph/builder.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ const (
2222

2323
func NewBuilder(pkgManager *PkgManager, rootPkg *PkgInfo) (*Builder, error) {
2424
if pkgManager == nil {
25-
return nil, errors.New("cannot create builder without a package manager")
25+
return nil, errors.New("cannot create builder without a package manager") //nolint:wrapcheck // Code is copied from `dep-graph-go`
2626
}
2727

2828
if rootPkg == nil {
@@ -118,12 +118,12 @@ func (b *Builder) addNode(nodeID string, pkgInfo *PkgInfo) *Node {
118118
func (b *Builder) ConnectNodes(parentNodeID, childNodeID string) error {
119119
parentNode, ok := b.nodes[parentNodeID]
120120
if !ok {
121-
return fmt.Errorf("cound not find parent node %s", parentNodeID)
121+
return fmt.Errorf("could not find parent node %s", parentNodeID)
122122
}
123123

124124
childNode, ok := b.nodes[childNodeID]
125125
if !ok {
126-
return fmt.Errorf("cound not find child node %s", childNodeID)
126+
return fmt.Errorf("could not find child node %s", childNodeID)
127127
}
128128

129129
parentNode.Deps = append(parentNode.Deps, Dependency{

internal/mocks/uvclient.go

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,12 @@ func (m *MockUVClient) ExportSBOM(inputDir string) (*scaplugin.Finding, error) {
1818
return m.ExportSBOMFunc(inputDir)
1919
}
2020
return &scaplugin.Finding{
21-
Sbom: []byte(`{"mock":"sbom"}`),
21+
Sbom: []byte(`{"mock":"sbom"}`),
22+
Metadata: scaplugin.Metadata{
23+
PackageManager: "pip",
24+
Name: "mock-project",
25+
Version: "0.0.0",
26+
},
2227
FileExclusions: []string{},
2328
NormalisedTargetFile: uv.UvLockFileName,
2429
}, nil

internal/uv/uvclient.go

Lines changed: 22 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -50,12 +50,14 @@ func (c client) ExportSBOM(inputDir string) (*scaplugin.Finding, error) {
5050
return nil, fmt.Errorf("failed to execute uv export: %w", err)
5151
}
5252

53-
if err := validateSBOM(output); err != nil {
53+
metadata, err := validateSBOM(output)
54+
if err != nil {
5455
return nil, err
5556
}
5657

5758
return &scaplugin.Finding{
5859
Sbom: output,
60+
Metadata: *metadata,
5961
FileExclusions: []string{
6062
// TODO(uv): uncomment when we are able to pass these to the CLI correctly. Currently the
6163
// `--exclude` flag does not accept paths, it only accepts file or dir names, which does not
@@ -68,27 +70,40 @@ func (c client) ExportSBOM(inputDir string) (*scaplugin.Finding, error) {
6870
}
6971

7072
// Verifies that the SBOM is valid JSON and has a root component.
71-
func validateSBOM(sbomData []byte) error {
73+
func validateSBOM(sbomData []byte) (*scaplugin.Metadata, error) {
7274
var sbom struct {
7375
Metadata struct {
74-
Component json.RawMessage `json:"component"`
76+
Component *struct {
77+
Name string `json:"name"`
78+
Version string `json:"version"`
79+
} `json:"component"`
7580
} `json:"metadata"`
7681
}
7782

7883
if err := json.Unmarshal(sbomData, &sbom); err != nil {
79-
return ecosystems.NewUnprocessableFileError(
84+
return nil, ecosystems.NewUnprocessableFileError(
8085
fmt.Sprintf("Failed to parse SBOM JSON: %v", err),
8186
snyk_errors.WithCause(err),
8287
)
8388
}
8489

85-
if len(sbom.Metadata.Component) == 0 {
86-
return ecosystems.NewUnprocessableFileError(
90+
if sbom.Metadata.Component == nil {
91+
return nil, ecosystems.NewUnprocessableFileError(
8792
"SBOM missing root component at metadata.component - uv project may be missing a root package",
8893
)
8994
}
9095

91-
return nil
96+
if sbom.Metadata.Component.Name == "" {
97+
return nil, ecosystems.NewUnprocessableFileError(
98+
"SBOM root component missing name - invalid SBOM structure",
99+
)
100+
}
101+
102+
return &scaplugin.Metadata{
103+
PackageManager: "pip",
104+
Name: sbom.Metadata.Component.Name,
105+
Version: sbom.Metadata.Component.Version,
106+
}, nil
92107
}
93108

94109
func (c *client) ShouldExportSBOM(inputDir string, logger *zerolog.Logger) bool {

internal/uv/uvclient_test.go

Lines changed: 93 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,8 @@ func TestUVClient_ExportSBOM_Success(t *testing.T) {
2828
"component": {
2929
"type": "library",
3030
"bom-ref": "test-project-1",
31-
"name": "test-project"
31+
"name": "test-project",
32+
"version": "1.2.3"
3233
}
3334
}
3435
}`
@@ -46,7 +47,11 @@ func TestUVClient_ExportSBOM_Success(t *testing.T) {
4647
result, err := client.ExportSBOM("/test/dir")
4748

4849
assert.NoError(t, err)
50+
require.NotNil(t, result)
4951
assert.JSONEq(t, validSBOM, string(result.Sbom))
52+
assert.Equal(t, "pip", result.Metadata.PackageManager)
53+
assert.Equal(t, "test-project", result.Metadata.Name)
54+
assert.Equal(t, "1.2.3", result.Metadata.Version)
5055
}
5156

5257
func TestUVClient_ExportSBOM_Error(t *testing.T) {
@@ -159,47 +164,106 @@ func TestParseAndValidateVersion_UnparseableOutput(t *testing.T) {
159164

160165
func TestValidateSBOM_Success(t *testing.T) {
161166
tests := []struct {
162-
name string
163-
sbom string
167+
name string
168+
sbom string
169+
expectedName string
170+
expectedVersion string
164171
}{
165172
{
166-
name: "valid SBOM with component",
173+
name: "valid SBOM with full component",
167174
sbom: `{
168175
"bomFormat": "CycloneDX",
169176
"specVersion": "1.5",
170177
"metadata": {
171178
"component": {
172179
"type": "library",
173180
"name": "test-project",
181+
"version": "1.0.0",
174182
"bom-ref": "test-project-1"
175183
}
176184
}
177185
}`,
186+
expectedName: "test-project",
187+
expectedVersion: "1.0.0",
178188
},
179189
{
180190
name: "valid SBOM with minimal component",
181191
sbom: `{
182192
"metadata": {
183193
"component": {
184-
"name": "my-project"
194+
"name": "my-project",
195+
"version": "0.1.0"
185196
}
186197
}
187198
}`,
199+
expectedName: "my-project",
200+
expectedVersion: "0.1.0",
201+
},
202+
{
203+
name: "component without version",
204+
sbom: `{
205+
"bomFormat": "CycloneDX",
206+
"specVersion": "1.5",
207+
"metadata": {
208+
"component": {
209+
"name": "project-no-version"
210+
}
211+
}
212+
}`,
213+
expectedName: "project-no-version",
214+
expectedVersion: "",
215+
},
216+
{
217+
name: "component with empty version string",
218+
sbom: `{
219+
"metadata": {
220+
"component": {
221+
"name": "project-empty-version",
222+
"version": ""
223+
}
224+
}
225+
}`,
226+
expectedName: "project-empty-version",
227+
expectedVersion: "",
228+
},
229+
{
230+
name: "component with additional fields",
231+
sbom: `{
232+
"bomFormat": "CycloneDX",
233+
"specVersion": "1.5",
234+
"metadata": {
235+
"component": {
236+
"type": "application",
237+
"name": "complex-project",
238+
"version": "3.0.0",
239+
"bom-ref": "pkg:pypi/[email protected]",
240+
"description": "A complex project",
241+
"licenses": []
242+
}
243+
}
244+
}`,
245+
expectedName: "complex-project",
246+
expectedVersion: "3.0.0",
188247
},
189248
}
190249

191250
for _, tt := range tests {
192251
t.Run(tt.name, func(t *testing.T) {
193-
err := validateSBOM([]byte(tt.sbom))
252+
metadata, err := validateSBOM([]byte(tt.sbom))
194253
assert.NoError(t, err)
254+
require.NotNil(t, metadata)
255+
assert.Equal(t, "pip", metadata.PackageManager)
256+
assert.Equal(t, tt.expectedName, metadata.Name)
257+
assert.Equal(t, tt.expectedVersion, metadata.Version)
195258
})
196259
}
197260
}
198261

199262
func TestValidateSBOM_MissingComponent(t *testing.T) {
200263
tests := []struct {
201-
name string
202-
sbom string
264+
name string
265+
sbom string
266+
expectedErrMessage string
203267
}{
204268
{
205269
name: "missing component field",
@@ -210,29 +274,47 @@ func TestValidateSBOM_MissingComponent(t *testing.T) {
210274
"timestamp": "2025-11-17T16:20:47.525804000Z"
211275
}
212276
}`,
277+
expectedErrMessage: "SBOM missing root component at metadata.component",
213278
},
214279
{
215280
name: "missing metadata",
216281
sbom: `{
217282
"bomFormat": "CycloneDX",
218283
"specVersion": "1.5"
219284
}`,
285+
expectedErrMessage: "SBOM missing root component at metadata.component",
286+
},
287+
{
288+
name: "component with empty name",
289+
sbom: `{
290+
"bomFormat": "CycloneDX",
291+
"specVersion": "1.5",
292+
"metadata": {
293+
"component": {
294+
"name": "",
295+
"version": "1.0.0"
296+
}
297+
}
298+
}`,
299+
expectedErrMessage: "SBOM root component missing name",
220300
},
221301
}
222302

223303
for _, tt := range tests {
224304
t.Run(tt.name, func(t *testing.T) {
225-
err := validateSBOM([]byte(tt.sbom))
305+
metadata, err := validateSBOM([]byte(tt.sbom))
306+
assert.Nil(t, metadata)
226307
require.Error(t, err)
227308
var catalogErr snyk_errors.Error
228309
assert.True(t, errors.As(err, &catalogErr), "error should be a catalog error")
229-
assert.Contains(t, catalogErr.Detail, "SBOM missing root component at metadata.component")
310+
assert.Contains(t, catalogErr.Detail, tt.expectedErrMessage)
230311
})
231312
}
232313
}
233314

234315
func TestValidateSBOM_InvalidJSON(t *testing.T) {
235-
err := validateSBOM([]byte("invalid json"))
316+
metadata, err := validateSBOM([]byte("invalid json"))
317+
assert.Nil(t, metadata)
236318
require.Error(t, err)
237319
var catalogErr snyk_errors.Error
238320
assert.True(t, errors.As(err, &catalogErr), "error should be a catalog error")

0 commit comments

Comments
 (0)