Skip to content

Commit c55e0a7

Browse files
authored
Merge pull request #112 from safedep/jc-staging
feat: CycloneDX SBOM Scanning Introduced by #111
2 parents 311b831 + 7279b34 commit c55e0a7

21 files changed

Lines changed: 44673 additions & 26 deletions

README.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,23 @@ vet scan --lockfiles /path/to/package-lock.json
9999

100100
> [Example Security Gate](https://github.com/safedep/demo-client-java/pull/2) using `vet` to prevent introducing new OSS dependency risk in an application.
101101
102+
#### Scanning SBOM
103+
104+
- To scan an SBOM in [CycloneDX](https://cyclonedx.org/) format
105+
106+
```bash
107+
vet scan --lockfiles /path/to/cyclonedx-sbom.json --lockfile-as bom-cyclonedx
108+
```
109+
110+
> **Note:** SBOM scanning feature is currently in experimental stage
111+
112+
#### Available Parsers
113+
114+
- To list supported package manifest parsers including experimental modules
115+
116+
```bash
117+
vet scan parsers --experimental
118+
```
102119

103120
## 📖 Documentation
104121

docs/docs/quick-start.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,4 +61,11 @@ vet scan --lockfiles /path/to/requirements.txt
6161
vet scan --lockfiles /path/to/package-lock.json
6262
```
6363

64+
:::info
65+
66+
To list all available package manifest parsers run
67+
`vet scan parsers --experimental`
68+
69+
:::
70+
6471
![vet scan files](/img/vet/vet-scan-files.png)

gen/exceptionsapi/exceptions_spec.pb.go

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

gen/filterinput/filter_input_spec.pb.go

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

gen/filtersuite/filter_suite_spec.pb.go

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ module github.com/safedep/vet
33
go 1.18
44

55
require (
6+
github.com/CycloneDX/cyclonedx-go v0.7.1
67
github.com/deepmap/oapi-codegen v1.13.3
78
github.com/golang/protobuf v1.5.3
89
github.com/google/cel-go v0.17.1

go.sum

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
github.com/BurntSushi/toml v1.3.2 h1:o7IhLm0Msx3BaB+n3Ag7L8EVlByGnpq14C4YWiu/gL8=
22
github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
3+
github.com/CycloneDX/cyclonedx-go v0.7.1 h1:5w1SxjGm9MTMNTuRbEPyw21ObdbaagTWF/KfF0qHTRE=
4+
github.com/CycloneDX/cyclonedx-go v0.7.1/go.mod h1:N/nrdWQI2SIjaACyyDs/u7+ddCkyl/zkNs8xFsHF2Ps=
35
github.com/Masterminds/semver/v3 v3.2.0 h1:3MEsd0SM6jqZojhjLWWeBY+Kcjy9i6MQAeY7YgDP83g=
46
github.com/Masterminds/semver/v3 v3.2.0/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ=
57
github.com/RaveNoX/go-jsoncommentstrip v1.0.0/go.mod h1:78ihd09MekBnJnxpICcwzCMzGrKSKYe4AqU6PDYYpjk=
@@ -8,6 +10,7 @@ github.com/antlr/antlr4/runtime/Go/antlr/v4 v4.0.0-20230305170008-8188dc5388df/g
810
github.com/apapsch/go-jsonmerge/v2 v2.0.0 h1:axGnT1gRIfimI7gJifB699GoE/oq+F2MU7Dml6nw9rQ=
911
github.com/apapsch/go-jsonmerge/v2 v2.0.0/go.mod h1:lvDnEdqiQrp0O42VQGgmlKpxL1AP2+08jFMw88y4klk=
1012
github.com/bmatcuk/doublestar v1.1.1/go.mod h1:UD6OnuiIn0yFxxA2le/rnRU1G4RaI4UvFv1sNto9p6w=
13+
github.com/bradleyjkemp/cupaloy/v2 v2.8.0 h1:any4BmKE+jGIaMpnU8YgH/I2LPiLBufr6oMMlVBbn9M=
1114
github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM=
1215
github.com/bytedance/sonic v1.10.0-rc/go.mod h1:ElCzW+ufi8qKqNW0FY314xriJhyJhuoJ3gFZdAHF7NM=
1316
github.com/bytedance/sonic v1.10.0-rc3 h1:uNSnscRapXTwUgTyOF0GVljYD08p9X/Lbr9MweSV3V0=

pkg/models/models.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ const (
2222
EcosystemPackagist = "Packagist"
2323
EcosystemHex = "Hex"
2424
EcosystemPub = "Pub"
25+
EcosystemCyDxSBOM = "CycloneDxSbom"
2526
)
2627

2728
// Represents a package manifest that contains a list

pkg/parser/cyclonedx_sbom.go

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
package parser
2+
3+
import (
4+
"bufio"
5+
"fmt"
6+
"os"
7+
"strings"
8+
9+
cdx "github.com/CycloneDX/cyclonedx-go"
10+
"github.com/google/osv-scanner/pkg/lockfile"
11+
"github.com/safedep/vet/pkg/common/logger"
12+
)
13+
14+
func parseCyclonedxSBOM(pathToLockfile string) ([]lockfile.PackageDetails, error) {
15+
details := []lockfile.PackageDetails{}
16+
17+
bom := new(cdx.BOM)
18+
if file, err := os.Open(pathToLockfile); err != nil {
19+
return nil, err
20+
} else {
21+
sbom_content := bufio.NewReader(file)
22+
decoder := cdx.NewBOMDecoder(sbom_content, cdx.BOMFileFormatJSON)
23+
if err = decoder.Decode(bom); err != nil {
24+
return nil, err
25+
}
26+
}
27+
28+
for _, comp := range *bom.Components {
29+
if d, err := convertSbomComponent2LPD(&comp); err != nil {
30+
logger.Warnf("Failed converting sbom to lockfile component: %v", err)
31+
} else {
32+
details = append(details, *d)
33+
}
34+
}
35+
36+
return details, nil
37+
}
38+
39+
func convertSbomComponent2LPD(comp *cdx.Component) (*lockfile.PackageDetails, error) {
40+
var name string
41+
if comp.Group != "" {
42+
name = fmt.Sprintf("%s:%s", comp.Group, comp.Name)
43+
} else {
44+
name = comp.Name
45+
}
46+
47+
var ecosysystem lockfile.Ecosystem
48+
if eco, err := convertBomRefAsEcosystem(comp.BOMRef); err != nil {
49+
return nil, err
50+
} else {
51+
ecosysystem = eco
52+
}
53+
54+
d := lockfile.PackageDetails{
55+
Name: name,
56+
Version: comp.Version,
57+
Ecosystem: ecosysystem,
58+
CompareAs: ecosysystem,
59+
}
60+
61+
return &d, nil
62+
}
63+
64+
func convertBomRefAsEcosystem(bomref string) (lockfile.Ecosystem, error) {
65+
if strings.Contains(bomref, "pkg:pypi") {
66+
return lockfile.PipEcosystem, nil
67+
} else if strings.Contains(bomref, "pkg:npm") {
68+
return lockfile.NpmEcosystem, nil
69+
} else {
70+
// Return an error, the ecosystem here does not matter
71+
return lockfile.NpmEcosystem, fmt.Errorf("failed parsing bomref %s to ecosystem", bomref)
72+
}
73+
}

pkg/parser/cyclonedx_sbom_test.go

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
package parser
2+
3+
import (
4+
"os"
5+
"io/ioutil"
6+
"testing"
7+
"github.com/stretchr/testify/assert"
8+
"github.com/google/osv-scanner/pkg/lockfile"
9+
cdx "github.com/CycloneDX/cyclonedx-go"
10+
)
11+
12+
func TestParseCyclonedxSBOM(t *testing.T) {
13+
// Create a sample SBOM JSON file
14+
tempFile, _ := ioutil.TempFile("", "sbom_*.json")
15+
defer os.Remove(tempFile.Name())
16+
sbomContent := `{
17+
"Components": [
18+
{
19+
"group": "testGroup",
20+
"name": "testName",
21+
"version": "1.0",
22+
"bom-ref": "pkg:pypi"
23+
},
24+
{
25+
"group": "testGroup2",
26+
"name": "testName2",
27+
"version": "2.0",
28+
"bom-ref": "pkg:npm"
29+
}
30+
]
31+
}`
32+
err := ioutil.WriteFile(tempFile.Name(), []byte(sbomContent), 0644)
33+
assert.Nil(t, err)
34+
35+
packages, err := parseCyclonedxSBOM(tempFile.Name())
36+
37+
assert.Nil(t, err)
38+
assert.Len(t, packages, 2)
39+
assert.Equal(t, "testGroup:testName", packages[0].Name)
40+
assert.Equal(t, "testGroup2:testName2", packages[1].Name)
41+
}
42+
43+
func TestConvertSbomComponent2LPD(t *testing.T) {
44+
component := cdx.Component{
45+
Group: "testGroup",
46+
Name: "testName",
47+
Version: "1.0",
48+
BOMRef: "pkg:pypi",
49+
}
50+
51+
pd, err := convertSbomComponent2LPD(&component)
52+
53+
assert.Nil(t, err)
54+
assert.Equal(t, "testGroup:testName", pd.Name)
55+
assert.Equal(t, "1.0", pd.Version)
56+
assert.Equal(t, lockfile.PipEcosystem, pd.Ecosystem)
57+
}
58+
59+
func TestConvertBomRefAsEcosystem(t *testing.T) {
60+
ecosystem, err := convertBomRefAsEcosystem("pkg:pypi")
61+
62+
assert.Nil(t, err)
63+
assert.Equal(t, lockfile.PipEcosystem, ecosystem)
64+
65+
ecosystem, err = convertBomRefAsEcosystem("pkg:npm")
66+
67+
assert.Nil(t, err)
68+
assert.Equal(t, lockfile.NpmEcosystem, ecosystem)
69+
70+
ecosystem, err = convertBomRefAsEcosystem("pkg:unknown")
71+
72+
assert.NotNil(t, err)
73+
assert.Equal(t, lockfile.NpmEcosystem, ecosystem) // As per your code, it defaults to lockfile.NpmEcosystem
74+
}

0 commit comments

Comments
 (0)