Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(npm): Introduce installation for cyclone-node-npm in another folder and fallback to cyclonedx/bom to help users generate BOM #4390

Merged
merged 13 commits into from
Jul 11, 2023
66 changes: 50 additions & 16 deletions pkg/npm/npm.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,11 @@ import (
)

const (
npmBomFilename = "bom-npm.xml"
cycloneDxPackageVersion = "@cyclonedx/[email protected]"
cycloneDxSchemaVersion = "1.4"
npmBomFilename = "bom-npm.xml"
cycloneDxNpmPackageVersion = "@cyclonedx/[email protected]"
cycloneDxBomPackageVersion = "@cyclonedx/bom@^3.10.6"
cycloneDxNpmInstallationFolder = "./tmp" // This folder is also added to npmignore in publish.go.Any changes to this folder needs a change in publish.go publish()
cycloneDxSchemaVersion = "1.4"
)

// Execute struct holds utils to enable mocking and common parameters
Expand Down Expand Up @@ -355,30 +357,62 @@ func (exec *Execute) checkIfLockFilesExist() (bool, bool, error) {

// CreateBOM generates BOM file using CycloneDX from all package.json files
func (exec *Execute) CreateBOM(packageJSONFiles []string) error {
// Install cyclonedx-npm in a new folder (to avoid extraneous errors) and generate BOM
cycloneDxNpmInstallParams := []string{"install", "--no-save", cycloneDxNpmPackageVersion, "--prefix", cycloneDxNpmInstallationFolder}
cycloneDxNpmRunParams := []string{"--output-format", "XML", "--spec-version", cycloneDxSchemaVersion, "--output-file"}

// Install cyclonedx/bom with --nosave and generate BOM.
cycloneDxBomInstallParams := []string{"install", cycloneDxBomPackageVersion, "--no-save"}
cycloneDxBomRunParams := []string{"cyclonedx-bom", "--output"}

// Attempt#1, generate BOM via cyclonedx-npm
err := exec.createBOMWithParams(cycloneDxNpmInstallParams, cycloneDxNpmRunParams, packageJSONFiles, false)
if err != nil {

log.Entry().Infof("Failed to generate BOM CycloneDX BOM with cyclonedx-npm ,fallback to cyclonedx/bom")

// Attempt #2, generate BOM via cyclonedx/bom@^3.10.6
err = exec.createBOMWithParams(cycloneDxBomInstallParams, cycloneDxBomRunParams, packageJSONFiles, true)
if err != nil {
log.Entry().Infof("Failed to generate BOM CycloneDX BOM with fallback package cyclonedx/bom ")
return err
}
}
return nil
}

// Facilitates BOM generation with different packages
func (exec *Execute) createBOMWithParams(packageInstallParams []string, packageRunParams []string, packageJSONFiles []string, fallback bool) error {
execRunner := exec.Utils.GetExecRunner()
// Install CycloneDX Node.js module locally without saving in package.json
err := execRunner.RunExecutable("npm", "install", cycloneDxPackageVersion, "--no-save")

// Install package
err := execRunner.RunExecutable("npm", packageInstallParams...)

if err != nil {
return fmt.Errorf("failed to install CycloneDX package: %w", err)
return fmt.Errorf("failed to install CycloneDX BOM %w", err)
}

// Run package for all package JSON files
if len(packageJSONFiles) > 0 {
for _, packageJSONFile := range packageJSONFiles {
path := filepath.Dir(packageJSONFile)
params := []string{
cycloneDxPackageVersion,
"--output-format",
"XML",
"--spec-version",
cycloneDxSchemaVersion,
"--output-file", filepath.Join(path, npmBomFilename),
packageJSONFile,
executable := "npx"
params := append(packageRunParams, filepath.Join(path, npmBomFilename))

//Below code needed as to adjust according to needs of cyclonedx-npm and fallback cyclonedx/bom@^3.10.6
if !fallback {
params = append(params, packageJSONFile)
executable = cycloneDxNpmInstallationFolder + "/node_modules/.bin/cyclonedx-npm"
} else {
params = append(params, path)
}
err := execRunner.RunExecutable("npx", params...)

err := execRunner.RunExecutable(executable, params...)
if err != nil {
return fmt.Errorf("failed to generate CycloneDX BOM: %w", err)
return fmt.Errorf("failed to generate CycloneDX BOM :%w", err)
}
}
}

return nil
}
60 changes: 48 additions & 12 deletions pkg/npm/npm_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
package npm

import (
"fmt"
"path/filepath"
"testing"

Expand Down Expand Up @@ -342,7 +343,7 @@ func TestNpm(t *testing.T) {
}
})

t.Run("Create BOM", func(t *testing.T) {
t.Run("Create BOM with cyclonedx-npm", func(t *testing.T) {
utils := newNpmMockUtilsBundle()
utils.AddFile("package.json", []byte("{\"scripts\": { \"ci-lint\": \"exit 0\" } }"))
utils.AddFile("package-lock.json", []byte("{}"))
Expand All @@ -357,20 +358,55 @@ func TestNpm(t *testing.T) {
Options: options,
}
err := exec.CreateBOM([]string{"package.json", filepath.Join("src", "package.json")})
cycloneDxNpmInstallParams := []string{"install", "--no-save", "@cyclonedx/[email protected]", "--prefix", "./tmp"}
cycloneDxNpmRunParams := []string{
"--output-format",
"XML",
"--spec-version",
cycloneDxSchemaVersion,
"--output-file",
}

if assert.NoError(t, err) {
if assert.Equal(t, 3, len(utils.execRunner.Calls)) {
assert.Equal(t, mock.ExecCall{Exec: "npm", Params: []string{"install", "@cyclonedx/[email protected]", "--no-save"}}, utils.execRunner.Calls[0])
assert.Equal(t, mock.ExecCall{Exec: "npx", Params: []string{"@cyclonedx/[email protected]", "--output-format",
"XML",
"--spec-version",
"1.4",
"--output-file", "bom-npm.xml", "package.json"}}, utils.execRunner.Calls[1])
assert.Equal(t, mock.ExecCall{Exec: "npx", Params: []string{"@cyclonedx/[email protected]", "--output-format",
"XML",
"--spec-version",
"1.4",
"--output-file", filepath.Join("src", "bom-npm.xml"), filepath.Join("src", "package.json")}}, utils.execRunner.Calls[2])
assert.Equal(t, mock.ExecCall{Exec: "npm", Params: cycloneDxNpmInstallParams}, utils.execRunner.Calls[0])
assert.Equal(t, mock.ExecCall{Exec: "./tmp/node_modules/.bin/cyclonedx-npm", Params: append(cycloneDxNpmRunParams, "bom-npm.xml", "package.json")}, utils.execRunner.Calls[1])
assert.Equal(t, mock.ExecCall{Exec: "./tmp/node_modules/.bin/cyclonedx-npm", Params: append(cycloneDxNpmRunParams, filepath.Join("src", "bom-npm.xml"), filepath.Join("src", "package.json"))}, utils.execRunner.Calls[2])
}

}
})

t.Run("Create BOM with fallback cyclonedx/bom", func(t *testing.T) {
utils := newNpmMockUtilsBundle()
utils.AddFile("package.json", []byte("{\"scripts\": { \"ci-lint\": \"exit 0\" } }"))
utils.AddFile("package-lock.json", []byte("{}"))
utils.AddFile(filepath.Join("src", "package.json"), []byte("{\"scripts\": { \"ci-lint\": \"exit 0\" } }"))
utils.AddFile(filepath.Join("src", "package-lock.json"), []byte("{}"))
utils.execRunner.ShouldFailOnCommand = map[string]error{"npm install --no-save @cyclonedx/[email protected] --prefix ./tmp": fmt.Errorf("failed to install CycloneDX BOM")}

options := ExecutorOptions{}
options.DefaultNpmRegistry = "foo.bar"

exec := &Execute{
Utils: &utils,
Options: options,
}
err := exec.CreateBOM([]string{"package.json", filepath.Join("src", "package.json")})
cycloneDxNpmInstallParams := []string{"install", "--no-save", "@cyclonedx/[email protected]", "--prefix", "./tmp"}

cycloneDxBomInstallParams := []string{"install", cycloneDxBomPackageVersion, "--no-save"}
cycloneDxBomRunParams := []string{
"cyclonedx-bom",
"--output",
}

if assert.NoError(t, err) {
if assert.Equal(t, 4, len(utils.execRunner.Calls)) {
assert.Equal(t, mock.ExecCall{Exec: "npm", Params: cycloneDxNpmInstallParams}, utils.execRunner.Calls[0])
assert.Equal(t, mock.ExecCall{Exec: "npm", Params: cycloneDxBomInstallParams}, utils.execRunner.Calls[1])
assert.Equal(t, mock.ExecCall{Exec: "npx", Params: append(cycloneDxBomRunParams, "bom-npm.xml", ".")}, utils.execRunner.Calls[2])
assert.Equal(t, mock.ExecCall{Exec: "npx", Params: append(cycloneDxBomRunParams, filepath.Join("src", "bom-npm.xml"), filepath.Join("src"))}, utils.execRunner.Calls[3])
}

}
Expand Down
3 changes: 3 additions & 0 deletions pkg/npm/publish.go
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,9 @@ func (exec *Execute) publish(packageJSON, registry, username, password string, p
npmignore.Add("**/piper")
log.Entry().Debug("adding **/sap-piper")
npmignore.Add("**/sap-piper")
// temporary installation folder used to install BOM to be ignored
log.Entry().Debug("adding tmp to npmignore")
npmignore.Add("tmp/")

npmrc := NewNPMRC(filepath.Dir(packageJSON))

Expand Down
Loading