diff --git a/cmd/mavenBuild.go b/cmd/mavenBuild.go
index af6461a8af..552d7b4e65 100644
--- a/cmd/mavenBuild.go
+++ b/cmd/mavenBuild.go
@@ -27,7 +27,6 @@ const (
func mavenBuild(config mavenBuildOptions, telemetryData *telemetry.CustomData, commonPipelineEnvironment *mavenBuildCommonPipelineEnvironment) {
utils := maven.NewUtilsBundle()
-
// enables url-log.json creation
cmd := reflect.ValueOf(utils).Elem().FieldByName("Command")
if cmd.IsValid() {
@@ -62,7 +61,7 @@ func runMavenBuild(config *mavenBuildOptions, telemetryData *telemetry.CustomDat
}
if config.CreateBOM {
- goals = append(goals, "org.cyclonedx:cyclonedx-maven-plugin:2.7.8:makeAggregateBom")
+ goals = append(goals, "org.cyclonedx:cyclonedx-maven-plugin:2.7.8:makeBom", "org.cyclonedx:cyclonedx-maven-plugin:2.7.8:makeAggregateBom")
createBOMConfig := []string{
"-DschemaVersion=1.4",
"-DincludeBomSerialNumber=true",
@@ -183,6 +182,7 @@ func runMavenBuild(config *mavenBuildOptions, telemetryData *telemetry.CustomDat
} else {
coordinate.BuildPath = filepath.Dir(match)
coordinate.URL = config.AltDeploymentRepositoryURL
+ coordinate.PURL = getPurlForThePomAndDeleteIndividualBom(match)
buildCoordinates = append(buildCoordinates, coordinate)
}
}
@@ -209,6 +209,42 @@ func runMavenBuild(config *mavenBuildOptions, telemetryData *telemetry.CustomDat
return err
}
+func getPurlForThePomAndDeleteIndividualBom(pomFilePath string) string {
+ bomPath := filepath.Join(filepath.Dir(pomFilePath) + "/target/" + mvnBomFilename + ".xml")
+ exists, _ := piperutils.FileExists(bomPath)
+ if !exists {
+ log.Entry().Debugf("bom file doesn't exist and hence no pURL info: %v", bomPath)
+ return ""
+ }
+ bom, err := piperutils.GetBom(bomPath)
+ if err != nil {
+ log.Entry().Warnf("failed to get bom file %s: %v", bomPath, err)
+ return ""
+ }
+
+ log.Entry().Debugf("Found purl: %s for the bomPath: %s", bom.Metadata.Component.Purl, bomPath)
+ purl := bom.Metadata.Component.Purl
+
+ // Check if the BOM is an aggregated BOM
+ if !isAggregatedBOM(bom) {
+ // Delete the individual BOM file
+ err = os.Remove(bomPath)
+ if err != nil {
+ log.Entry().Warnf("failed to delete bom file %s: %v", bomPath, err)
+ }
+ }
+ return purl
+}
+
+func isAggregatedBOM(bom piperutils.Bom) bool {
+ for _, property := range bom.Metadata.Properties {
+ if property.Name == "maven.goal" && property.Value == "makeAggregateBom" {
+ return true
+ }
+ }
+ return false
+}
+
func createOrUpdateProjectSettingsXML(projectSettingsFile string, altDeploymentRepositoryID string, altDeploymentRepositoryUser string, altDeploymentRepositoryPassword string, utils maven.Utils) (string, error) {
if len(projectSettingsFile) > 0 {
projectSettingsFilePath, err := maven.UpdateProjectSettingsXML(projectSettingsFile, altDeploymentRepositoryID, altDeploymentRepositoryUser, altDeploymentRepositoryPassword, utils)
diff --git a/cmd/mavenBuild_test.go b/cmd/mavenBuild_test.go
index c7f6bb3d59..ca48d89f95 100644
--- a/cmd/mavenBuild_test.go
+++ b/cmd/mavenBuild_test.go
@@ -4,8 +4,11 @@
package cmd
import (
+ "os"
+ "path/filepath"
"testing"
+ "github.com/SAP/jenkins-library/pkg/piperutils"
"github.com/stretchr/testify/assert"
)
@@ -157,3 +160,105 @@ func TestMavenBuild(t *testing.T) {
})
}
+
+func TestIsAggregatedBOM(t *testing.T) {
+ t.Run("is aggregated BOM", func(t *testing.T) {
+ bom := piperutils.Bom{
+ Metadata: piperutils.Metadata{
+ Properties: []piperutils.BomProperty{
+ {Name: "maven.goal", Value: "makeAggregateBom"},
+ },
+ },
+ }
+ assert.True(t, isAggregatedBOM(bom))
+ })
+
+ t.Run("is not aggregated BOM", func(t *testing.T) {
+ bom := piperutils.Bom{
+ Metadata: piperutils.Metadata{
+ Properties: []piperutils.BomProperty{
+ {Name: "some.property", Value: "someValue"},
+ },
+ },
+ }
+ assert.False(t, isAggregatedBOM(bom))
+ })
+}
+
+func createTempFile(t *testing.T, dir string, filename string, content string) string {
+ filePath := filepath.Join(dir, filename)
+ err := os.WriteFile(filePath, []byte(content), 0666)
+ if err != nil {
+ t.Fatalf("Failed to create temp file: %s", err)
+ }
+ return filePath
+}
+
+func TestGetPurlForThePomAndDeleteIndividualBom(t *testing.T) {
+ t.Run("valid BOM file, non-aggregated", func(t *testing.T) {
+ tempDir, err := piperutils.Files{}.TempDir("", "test")
+ if err != nil {
+ t.Fatalf("Failed to create temp directory: %s", err)
+ }
+
+ bomContent := `
+
+
+ pkg:maven/com.example/mycomponent@1.0.0
+
+
+
+
+
+ `
+ pomFilePath := createTempFile(t, tempDir, "pom.xml", "")
+ bomDir := filepath.Join(tempDir, "target")
+ if err := os.MkdirAll(bomDir, 0777); err != nil {
+ t.Fatalf("Failed to create temp directory: %s", err)
+ }
+ bomFilePath := createTempFile(t, bomDir, mvnBomFilename+".xml", bomContent)
+ defer os.Remove(bomFilePath)
+
+ purl := getPurlForThePomAndDeleteIndividualBom(pomFilePath)
+ assert.Equal(t, "pkg:maven/com.example/mycomponent@1.0.0", purl)
+ _, err = os.Stat(bomFilePath)
+ assert.True(t, os.IsNotExist(err))
+ })
+
+ t.Run("valid BOM file, aggregated BOM", func(t *testing.T) {
+ tempDir, err := piperutils.Files{}.TempDir("", "test")
+ if err != nil {
+ t.Fatalf("Failed to create temp directory: %s", err)
+ }
+
+ bomContent := `
+
+
+ pkg:maven/com.example/aggregatecomponent@1.0.0
+
+
+
+
+
+ `
+ pomFilePath := createTempFile(t, tempDir, "pom.xml", "")
+ bomDir := filepath.Join(tempDir, "target")
+ if err := os.MkdirAll(bomDir, 0777); err != nil {
+ t.Fatalf("Failed to create temp directory: %s", err)
+ }
+ bomFilePath := createTempFile(t, bomDir, mvnBomFilename+".xml", bomContent)
+
+ purl := getPurlForThePomAndDeleteIndividualBom(pomFilePath)
+ assert.Equal(t, "pkg:maven/com.example/aggregatecomponent@1.0.0", purl)
+ _, err = os.Stat(bomFilePath)
+ assert.False(t, os.IsNotExist(err)) // File should not be deleted
+ })
+
+ t.Run("BOM file does not exist", func(t *testing.T) {
+ tempDir := t.TempDir()
+ pomFilePath := createTempFile(t, tempDir, "pom.xml", "") // Create a temp pom file
+
+ purl := getPurlForThePomAndDeleteIndividualBom(pomFilePath)
+ assert.Equal(t, "", purl)
+ })
+}
diff --git a/pkg/npm/publish.go b/pkg/npm/publish.go
index 660e1ae114..3466e89aea 100644
--- a/pkg/npm/publish.go
+++ b/pkg/npm/publish.go
@@ -217,6 +217,7 @@ func (exec *Execute) publish(packageJSON, registry, username, password string, p
coordinate.BuildPath = filepath.Dir(packageJSON)
coordinate.URL = registry
coordinate.Packaging = "tgz"
+ coordinate.PURL = getPurl(packageJSON)
*buildCoordinates = append(*buildCoordinates, coordinate)
}
@@ -225,6 +226,21 @@ func (exec *Execute) publish(packageJSON, registry, username, password string, p
return nil
}
+func getPurl(packageJSON string) string {
+ expectedBomFilePath := filepath.Join(filepath.Dir(packageJSON) + "/" + npmBomFilename)
+ exists, _ := CredentialUtils.FileExists(expectedBomFilePath)
+ if !exists {
+ log.Entry().Debugf("bom file doesn't exist and hence no pURL info: %v", expectedBomFilePath)
+ return ""
+ }
+ bom, err := CredentialUtils.GetBom(expectedBomFilePath)
+ if err != nil {
+ log.Entry().Warnf("unable to get bom metdata : %v", err)
+ return ""
+ }
+ return bom.Metadata.Component.Purl
+}
+
func (exec *Execute) readPackageScope(packageJSON string) (string, error) {
b, err := exec.Utils.FileRead(packageJSON)
if err != nil {
diff --git a/pkg/npm/publish_test.go b/pkg/npm/publish_test.go
index 782e1b7c5c..4c7b24f95d 100644
--- a/pkg/npm/publish_test.go
+++ b/pkg/npm/publish_test.go
@@ -12,6 +12,7 @@ import (
"github.com/SAP/jenkins-library/pkg/piperutils"
"github.com/SAP/jenkins-library/pkg/versioning"
"github.com/stretchr/testify/assert"
+ "os"
)
type npmMockUtilsBundleRelativeGlob struct {
@@ -573,3 +574,46 @@ func TestNpmPublish(t *testing.T) {
})
}
}
+
+func createTempFile(t *testing.T, dir string, filename string, content string) string {
+ filePath := filepath.Join(dir, filename)
+ err := os.WriteFile(filePath, []byte(content), 0666)
+ if err != nil {
+ t.Fatalf("Failed to create temp file: %s", err)
+ }
+ return filePath
+}
+
+func TestGetPurl(t *testing.T) {
+ t.Run("valid BOM file", func(t *testing.T) {
+ tempDir, err := piperutils.Files{}.TempDir("", "test")
+ if err != nil {
+ t.Fatalf("Failed to create temp directory: %s", err)
+ }
+
+ bomContent := `
+
+
+ pkg:npm/com.example/mycomponent@1.0.0
+
+
+
+
+
+ `
+ packageJsonFilePath := createTempFile(t, tempDir, "package.json", "")
+ bomFilePath := createTempFile(t, tempDir, npmBomFilename, bomContent)
+ defer os.Remove(bomFilePath)
+
+ purl := getPurl(packageJsonFilePath)
+ assert.Equal(t, "pkg:npm/com.example/mycomponent@1.0.0", purl)
+ })
+
+ t.Run("BOM file does not exist", func(t *testing.T) {
+ tempDir := t.TempDir()
+ packageJsonFilePath := createTempFile(t, tempDir, "pom.xml", "") // Create a temp pom file
+
+ purl := getPurl(packageJsonFilePath)
+ assert.Equal(t, "", purl)
+ })
+}
diff --git a/pkg/piperutils/cyclonedxBom.go b/pkg/piperutils/cyclonedxBom.go
new file mode 100644
index 0000000000..fddebae8a6
--- /dev/null
+++ b/pkg/piperutils/cyclonedxBom.go
@@ -0,0 +1,48 @@
+package piperutils
+
+import (
+ "encoding/xml"
+ "github.com/SAP/jenkins-library/pkg/log"
+ "io"
+ "os"
+)
+
+// To serialize the cyclonedx BOM file
+type Bom struct {
+ Metadata Metadata `xml:"metadata"`
+}
+
+type Metadata struct {
+ Component BomComponent `xml:"component"`
+ Properties []BomProperty `xml:"properties>property"`
+}
+
+type BomProperty struct {
+ Name string `xml:"name,attr"`
+ Value string `xml:"value,attr"`
+}
+
+type BomComponent struct {
+ Purl string `xml:"purl"`
+}
+
+func GetBom(absoluteBomPath string) (Bom, error) {
+ xmlFile, err := os.Open(absoluteBomPath)
+ if err != nil {
+ log.Entry().Debugf("failed to open bom file %s", absoluteBomPath)
+ return Bom{}, err
+ }
+ defer xmlFile.Close()
+ byteValue, err := io.ReadAll(xmlFile)
+ if err != nil {
+ log.Entry().Debugf("failed to read bom file %s", absoluteBomPath)
+ return Bom{}, err
+ }
+ var bom Bom
+ err = xml.Unmarshal(byteValue, &bom)
+ if err != nil {
+ log.Entry().Debugf("failed to unmarshal bom file %s", absoluteBomPath)
+ return Bom{}, err
+ }
+ return bom, nil
+}
diff --git a/pkg/piperutils/cyclonedxbom_test.go b/pkg/piperutils/cyclonedxbom_test.go
new file mode 100644
index 0000000000..d9d25e6256
--- /dev/null
+++ b/pkg/piperutils/cyclonedxbom_test.go
@@ -0,0 +1,126 @@
+package piperutils
+
+import (
+ "os"
+ "path/filepath"
+ "testing"
+)
+
+func createTempFile(t *testing.T, content string) (string, func()) {
+ dir := t.TempDir()
+ fileName := filepath.Join(dir, "test.xml")
+ err := os.WriteFile(fileName, []byte(content), 0666)
+ if err != nil {
+ t.Fatalf("Failed to create temp file: %s", err)
+ }
+ return fileName, func() {
+ os.Remove(fileName)
+ }
+}
+
+func TestGetBom(t *testing.T) {
+ tests := []struct {
+ name string
+ xmlContent string
+ expectedBom Bom
+ expectError bool
+ expectedError string
+ }{
+ {
+ name: "valid file",
+ xmlContent: `
+
+
+ pkg:maven/com.example/mycomponent@1.0.0
+
+
+
+
+
+
+ `,
+ expectedBom: Bom{
+ Metadata: Metadata{
+ Component: BomComponent{
+ Purl: "pkg:maven/com.example/mycomponent@1.0.0",
+ },
+ Properties: []BomProperty{
+ {Name: "name1", Value: "value1"},
+ {Name: "name2", Value: "value2"},
+ },
+ },
+ },
+ expectError: false,
+ },
+ {
+ name: "file not found",
+ xmlContent: "",
+ expectedBom: Bom{},
+ expectError: true,
+ expectedError: "no such file or directory",
+ },
+ {
+ name: "invalid XML file",
+ xmlContent: "invalid xml",
+ expectedBom: Bom{},
+ expectError: true,
+ expectedError: "XML syntax error",
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ var fileName string
+ var cleanup func()
+ if tt.xmlContent != "" {
+ var err error
+ fileName, cleanup = createTempFile(t, tt.xmlContent)
+ defer cleanup()
+ if err != nil {
+ t.Fatalf("Failed to create temp file: %s", err)
+ }
+ } else {
+ // Use a non-existent file path
+ fileName = "nonexistent.xml"
+ }
+
+ bom, err := GetBom(fileName)
+ if (err != nil) != tt.expectError {
+ t.Errorf("Expected error: %v, got: %v", tt.expectError, err)
+ }
+
+ if err != nil && !tt.expectError {
+ if !tt.expectError && !containsSubstring(err.Error(), tt.expectedError) {
+ t.Errorf("Expected error message: %v, got: %v", tt.expectedError, err.Error())
+ }
+ }
+
+ if !tt.expectError && !bomEquals(bom, tt.expectedBom) {
+ t.Errorf("Expected BOM: %+v, got: %+v", tt.expectedBom, bom)
+ }
+ })
+ }
+}
+
+func bomEquals(a, b Bom) bool {
+ // compare a and b manually since reflect.DeepEqual can be problematic with slices and nil values
+ return a.Metadata.Component.Purl == b.Metadata.Component.Purl &&
+ len(a.Metadata.Properties) == len(b.Metadata.Properties) &&
+ propertiesMatch(a.Metadata.Properties, b.Metadata.Properties)
+}
+
+func propertiesMatch(a, b []BomProperty) bool {
+ for i := range a {
+ if a[i] != b[i] {
+ return false
+ }
+ }
+ return true
+}
+
+func containsSubstring(str, substr string) bool {
+ if len(substr) == 0 {
+ return true
+ }
+ return len(str) >= len(substr) && str[:len(substr)] == substr
+}
diff --git a/pkg/versioning/versioning.go b/pkg/versioning/versioning.go
index 1ea42b48f8..a716cb192f 100644
--- a/pkg/versioning/versioning.go
+++ b/pkg/versioning/versioning.go
@@ -19,6 +19,7 @@ type Coordinates struct {
Packaging string
BuildPath string
URL string
+ PURL string
}
// Artifact defines the versioning operations for various build tools