diff --git a/pkg/npm/npm.go b/pkg/npm/npm.go index 8a729e69e0..ce1bae46f2 100644 --- a/pkg/npm/npm.go +++ b/pkg/npm/npm.go @@ -14,9 +14,11 @@ import ( ) const ( - npmBomFilename = "bom-npm.xml" - cycloneDxPackageVersion = "@cyclonedx/cyclonedx-npm@1.11.0" - cycloneDxSchemaVersion = "1.4" + npmBomFilename = "bom-npm.xml" + cycloneDxNpmPackageVersion = "@cyclonedx/cyclonedx-npm@1.11.0" + 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 @@ -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 } diff --git a/pkg/npm/npm_test.go b/pkg/npm/npm_test.go index 4decdcf2a6..319fb6c1f0 100644 --- a/pkg/npm/npm_test.go +++ b/pkg/npm/npm_test.go @@ -4,6 +4,7 @@ package npm import ( + "fmt" "path/filepath" "testing" @@ -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("{}")) @@ -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/cyclonedx-npm@1.11.0", "--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/cyclonedx-npm@1.11.0", "--no-save"}}, utils.execRunner.Calls[0]) - assert.Equal(t, mock.ExecCall{Exec: "npx", Params: []string{"@cyclonedx/cyclonedx-npm@1.11.0", "--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/cyclonedx-npm@1.11.0", "--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/cyclonedx-npm@1.11.0 --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/cyclonedx-npm@1.11.0", "--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]) } } diff --git a/pkg/npm/publish.go b/pkg/npm/publish.go index 333c8c5405..76cd4a62b0 100644 --- a/pkg/npm/publish.go +++ b/pkg/npm/publish.go @@ -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))