diff --git a/buildpack.toml b/buildpack.toml index 6c75e448..31940018 100644 --- a/buildpack.toml +++ b/buildpack.toml @@ -87,7 +87,7 @@ api = "0.7" name = "BP_LIBERTY_FEATURES" [[metadata.dependencies]] - cpes = ["cpe:2.3:a:ibm:websphere_application_server:22.0.0.12:*:*:*:liberty:*:*:*"] + cpes = ["cpe:2.3:a:ibm:open_liberty:22.0.0.12:*:*:*:*:*:*:*"] id = "open-liberty-runtime-full" name = "Open Liberty (All Features)" purl = "pkg:maven/io.openliberty/openliberty-runtime@22.0.0.12" @@ -101,7 +101,7 @@ api = "0.7" uri = "https://raw.githubusercontent.com/OpenLiberty/open-liberty/integration/LICENSE" [[metadata.dependencies]] - cpes = ["cpe:2.3:a:ibm:websphere_application_server:22.0.0.12:*:*:*:liberty:*:*:*"] + cpes = ["cpe:2.3:a:ibm:open_liberty:22.0.0.12:*:*:*:*:*:*:*"] id = "open-liberty-runtime-jakartaee9" name = "Open Liberty (Jakarta EE9)" purl = "pkg:maven/io.openliberty/openliberty-jakartaee9@22.0.0.12" @@ -115,7 +115,7 @@ api = "0.7" uri = "https://raw.githubusercontent.com/OpenLiberty/open-liberty/integration/LICENSE" [[metadata.dependencies]] - cpes = ["cpe:2.3:a:ibm:websphere_application_server:22.0.0.12:*:*:*:liberty:*:*:*"] + cpes = ["cpe:2.3:a:ibm:open_liberty:22.0.0.12:*:*:*:*:*:*:*"] id = "open-liberty-runtime-javaee8" name = "Open Liberty (Java EE8)" purl = "pkg:maven/io.openliberty/openliberty-javaee8@22.0.0.12" @@ -129,7 +129,7 @@ api = "0.7" uri = "https://raw.githubusercontent.com/OpenLiberty/open-liberty/integration/LICENSE" [[metadata.dependencies]] - cpes = ["cpe:2.3:a:ibm:websphere_application_server:22.0.0.12:*:*:*:liberty:*:*:*"] + cpes = ["cpe:2.3:a:ibm:open_liberty:22.0.0.12:*:*:*:*:*:*:*"] id = "open-liberty-runtime-webProfile9" name = "Open Liberty (Web Profile 9)" purl = "pkg:maven/io.openliberty/openliberty-webProfile9@22.0.0.12" @@ -143,7 +143,7 @@ api = "0.7" uri = "https://raw.githubusercontent.com/OpenLiberty/open-liberty/integration/LICENSE" [[metadata.dependencies]] - cpes = ["cpe:2.3:a:ibm:websphere_application_server:22.0.0.12:*:*:*:liberty:*:*:*"] + cpes = ["cpe:2.3:a:ibm:open_liberty:22.0.0.12:*:*:*:*:*:*:*"] id = "open-liberty-runtime-webProfile8" name = "Open Liberty (Web Profile 8)" purl = "pkg:maven/io.openliberty/openliberty-webProfile8@22.0.0.12" @@ -157,7 +157,7 @@ api = "0.7" uri = "https://raw.githubusercontent.com/OpenLiberty/open-liberty/integration/LICENSE" [[metadata.dependencies]] - cpes = ["cpe:2.3:a:ibm:websphere_application_server:22.0.0.12:*:*:*:liberty:*:*:*"] + cpes = ["cpe:2.3:a:ibm:open_liberty:22.0.0.12:*:*:*:*:*:*:*"] id = "open-liberty-runtime-microProfile5" name = "Open Liberty (Micro Profile 5)" purl = "pkg:maven/io.openliberty/openliberty-microProfile5@22.0.0.12" @@ -171,7 +171,7 @@ api = "0.7" uri = "https://raw.githubusercontent.com/OpenLiberty/open-liberty/integration/LICENSE" [[metadata.dependencies]] - cpes = ["cpe:2.3:a:ibm:websphere_application_server:22.0.0.12:*:*:*:liberty:*:*:*"] + cpes = ["cpe:2.3:a:ibm:open_liberty:22.0.0.12:*:*:*:*:*:*:*"] id = "open-liberty-runtime-microProfile4" name = "Open Liberty (Micro Profile 4)" purl = "pkg:maven/io.openliberty/openliberty-microProfile4@22.0.0.12" @@ -185,7 +185,7 @@ api = "0.7" uri = "https://raw.githubusercontent.com/OpenLiberty/open-liberty/integration/LICENSE" [[metadata.dependencies]] - cpes = ["cpe:2.3:a:ibm:websphere_application_server:22.0.0.12:*:*:*:liberty:*:*:*"] + cpes = ["cpe:2.3:a:ibm:open_liberty:22.0.0.12:*:*:*:*:*:*:*"] id = "open-liberty-runtime-kernel" name = "Open Liberty (Kernel)" purl = "pkg:maven/io.openliberty/openliberty-kernel@22.0.0.12" diff --git a/internal/server/server.go b/internal/server/server.go index 8feb4974..d01da6cc 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -17,6 +17,8 @@ package server import ( + "bufio" + "bytes" "encoding/xml" "fmt" "github.com/paketo-buildpacks/liberty/internal/util" @@ -26,6 +28,7 @@ import ( "io/ioutil" "os" "path/filepath" + "regexp" "sort" "strings" ) @@ -242,6 +245,66 @@ func InstallFeatures(runtimePath string, serverName string, executor effect.Exec return nil } +type InstalledIFix struct { + APAR string + IFix string +} + +func GetInstalledIFixes(runtimePath string, executor effect.Executor) ([]InstalledIFix, error) { + buf := &bytes.Buffer{} + + if err := executor.Execute(effect.Execution{ + Command: filepath.Join(runtimePath, "bin", "productInfo"), + Args: []string{"version", "--ifixes"}, + Stdout: buf, + }); err != nil { + return []InstalledIFix{}, fmt.Errorf("unable to get installed iFixes\n%w", err) + } + + re, err := regexp.Compile(`^(.*) in the iFix\(es\): \[(.*)\]$`) + if err != nil { + return []InstalledIFix{}, fmt.Errorf("unable to create iFix regex\n%w", err) + } + + installed := make([]InstalledIFix, 0) + scanner := bufio.NewScanner(buf) + for scanner.Scan() { + line := scanner.Text() + if match := re.FindStringSubmatch(line); len(match) >= 2 { + installed = append(installed, InstalledIFix{ + APAR: strings.TrimSpace(match[1]), + IFix: strings.TrimSpace(match[2]), + }) + } + } + + return installed, nil +} + +func GetInstalledFeatures(runtimePath string, executor effect.Executor) ([]string, error) { + buf := &bytes.Buffer{} + + if err := executor.Execute(effect.Execution{ + Command: filepath.Join(runtimePath, "bin", "productInfo"), + Args: []string{"featureInfo"}, + Stdout: buf, + }); err != nil { + return []string{}, fmt.Errorf("unable to get list of installed features\n%w", err) + } + + installedFeatures := make([]string, 0) + scanner := bufio.NewScanner(buf) + for scanner.Scan() { + feature := strings.TrimSpace(scanner.Text()) + if len(feature) <= 0 { + continue + } + installedFeatures = append(installedFeatures, feature) + } + + return installedFeatures, nil +} + type Config struct { XMLName xml.Name `xml:"server"` FeatureManager struct { diff --git a/internal/server/server_test.go b/internal/server/server_test.go index d86a098b..efa04a5c 100644 --- a/internal/server/server_test.go +++ b/internal/server/server_test.go @@ -232,6 +232,34 @@ func testServer(t *testing.T, when spec.G, it spec.S) { Expect(execution.Command).To(Equal("java")) Expect(execution.Args).To(Equal([]string{"-jar", ifixes[1], "--installLocation", wlpPath})) }) + + it("lists installed iFixes", func() { + executor := &mocks.Executor{} + executor.On("Execute", mock.Anything).Run(func(args mock.Arguments) { + arg := args.Get(0).(effect.Execution) + _, err := arg.Stdout.Write([]byte(` + Product name: Open Liberty + Product version: 22.0.0.11 + Product edition: Open + + PH49719 in the iFix(es): [220011-wlp-archive-IFPH49719]`), + ) + Expect(err).ToNot(HaveOccurred()) + }).Return(nil) + installedFixes, err := server.GetInstalledIFixes(wlpPath, executor) + Expect(err).ToNot(HaveOccurred()) + + execution := executor.Calls[0].Arguments[0].(effect.Execution) + Expect(execution.Command).To(Equal(filepath.Join(wlpPath, "bin", "productInfo"))) + Expect(execution.Args).To(Equal([]string{"version", "--ifixes"})) + + Expect(installedFixes).To(Equal([]server.InstalledIFix{ + { + APAR: "PH49719", + IFix: "220011-wlp-archive-IFPH49719", + }, + })) + }) }) when("installing features", func() { @@ -244,5 +272,25 @@ func testServer(t *testing.T, when spec.G, it spec.S) { Expect(execution.Command).To(Equal(filepath.Join(wlpPath, "bin", "featureUtility"))) Expect(execution.Args).To(Equal([]string{"installServerFeatures", "--acceptLicense", "--noCache", "testServer"})) }) + + it("lists installed features", func() { + executor := &mocks.Executor{} + executor.On("Execute", mock.Anything).Run(func(args mock.Arguments) { + arg := args.Get(0).(effect.Execution) + _, err := arg.Stdout.Write([]byte(` + microProfile-5.0 + webProfile-9.1`), + ) + Expect(err).ToNot(HaveOccurred()) + }).Return(nil) + installedFeatures, err := server.GetInstalledFeatures(wlpPath, executor) + Expect(err).ToNot(HaveOccurred()) + + execution := executor.Calls[0].Arguments[0].(effect.Execution) + Expect(execution.Command).To(Equal(filepath.Join(wlpPath, "bin", "productInfo"))) + Expect(execution.Args).To(Equal([]string{"featureInfo"})) + + Expect(installedFeatures).To(Equal([]string{"microProfile-5.0", "webProfile-9.1"})) + }) }) } diff --git a/liberty/build.go b/liberty/build.go index 212383a4..a2c75bd6 100644 --- a/liberty/build.go +++ b/liberty/build.go @@ -20,7 +20,7 @@ import ( "fmt" "github.com/paketo-buildpacks/liberty/internal/util" "github.com/paketo-buildpacks/libpak/bindings" - sherpa "github.com/paketo-buildpacks/libpak/sherpa" + "github.com/paketo-buildpacks/libpak/sherpa" "strings" "github.com/buildpacks/libcnb" @@ -206,7 +206,17 @@ func (b Build) Build(context libcnb.BuildContext) (libcnb.BuildResult, error) { result.Layers = append(result.Layers, base) if installType == openLibertyInstall || installType == websphereLibertyInstall { - if err := b.buildDistributionRuntime(profile, version, installType, serverName, context.Application.Path, featureList, detectedBuildSrc, dr, dc, &result); err != nil { + if err := b.buildDistributionRuntime( + profile, + version, + installType, + serverName, + context.Application.Path, + featureList, + detectedBuildSrc, + dr, + dc, + &result); err != nil { return libcnb.BuildResult{}, err } } else if installType == noneInstall { @@ -251,7 +261,7 @@ func (b Build) buildDistributionRuntime( return fmt.Errorf("unable to load iFixes\n%w", err) } - distro := NewDistribution(dep, cache, serverName, appPath, features, iFixes, b.Executor) + distro := NewDistribution(dep, cache, installType, serverName, appPath, features, iFixes, b.Executor) distro.Logger = b.Logger result.Layers = append(result.Layers, distro) diff --git a/liberty/distribution.go b/liberty/distribution.go index 4452938a..28ebfb05 100644 --- a/liberty/distribution.go +++ b/liberty/distribution.go @@ -18,11 +18,12 @@ package liberty import ( "fmt" + "github.com/paketo-buildpacks/liberty/internal/server" + "github.com/paketo-buildpacks/libpak/sbom" "os" "path/filepath" "strconv" - - "github.com/paketo-buildpacks/liberty/internal/server" + "strings" "github.com/buildpacks/libcnb" "github.com/paketo-buildpacks/libjvm/count" @@ -33,7 +34,9 @@ import ( ) type Distribution struct { + Dependency libpak.BuildpackDependency ApplicationPath string + InstallType string ServerName string Executor effect.Executor Features []string @@ -45,6 +48,7 @@ type Distribution struct { func NewDistribution( dependency libpak.BuildpackDependency, cache libpak.DependencyCache, + installType string, serverName string, applicationPath string, features []string, @@ -64,6 +68,8 @@ func NewDistribution( } return Distribution{ + Dependency: dependency, + InstallType: installType, ApplicationPath: applicationPath, ServerName: serverName, Executor: executor, @@ -118,10 +124,73 @@ func (d Distribution) Contribute(layer libcnb.Layer) (libcnb.Layer, error) { layer.LaunchEnvironment.Default("WLP_LOGGING_APPS_WRITE_JSON", "true") layer.LaunchEnvironment.Default("WLP_LOGGING_JSON_ACCESS_LOG_FIELDS", "default") + if err := d.ContributeSBOM(layer); err != nil { + return libcnb.Layer{}, fmt.Errorf("unable to contribute SBOM\n%w", err) + } + return layer, nil }) } +func (d Distribution) ContributeSBOM(layer libcnb.Layer) error { + sbomArtifact, err := d.Dependency.AsSyftArtifact() + if err != nil { + return fmt.Errorf("unable to get SBOM artifact %s\n%w", d.Dependency.ID, err) + } + artifacts := []sbom.SyftArtifact{sbomArtifact} + + installedIFixes, err := server.GetInstalledIFixes(layer.Path, d.Executor) + if err != nil { + return fmt.Errorf("unable to get installed iFixes\n%w", err) + } + for _, ifix := range installedIFixes { + d.Logger.Debugf("Found installed iFix for APAR %s: %s\n", ifix.APAR, ifix.IFix) + artifacts = append(artifacts, sbom.SyftArtifact{ + ID: ifix.APAR, + Name: ifix.APAR, + Version: sbomArtifact.Version, + Type: "jar", + Locations: []sbom.SyftLocation{{Path: ifix.IFix + ".jar"}}, + }) + } + installedFeatures, err := server.GetInstalledFeatures(layer.Path, d.Executor) + if err != nil { + return fmt.Errorf("unable to get installed features\n%w", err) + } + var groupId string + if d.InstallType == openLibertyInstall { + groupId = "io.openliberty.features" + } else { + groupId = "com.ibm.websphere.appserver.features" + } + + var version string + if parts := strings.Split(sbomArtifact.PURL, "@"); len(parts) == 2 { + version = parts[1] + } + for _, feature := range installedFeatures { + d.Logger.Debugf("Found installed feature: %s", feature) + artifacts = append(artifacts, sbom.SyftArtifact{ + ID: feature, + Name: feature, + Version: sbomArtifact.Version, + Type: "esa", + PURL: fmt.Sprintf("pkg:maven/%s/%s@%s", groupId, feature, version), + }) + + } + + sbomPath := layer.SBOMPath(libcnb.SyftJSON) + dep := sbom.NewSyftDependency(layer.Path, artifacts) + + d.Logger.Debugf("Writing Syft SBOM at %s: %+v", sbomPath, dep) + if err := dep.WriteTo(sbomPath); err != nil { + return fmt.Errorf("unable to write SBOM\n%w", err) + } + + return nil +} + func (d Distribution) Name() string { return d.LayerContributor.LayerName() } diff --git a/liberty/distribution_test.go b/liberty/distribution_test.go index edfa2001..9d31f243 100644 --- a/liberty/distribution_test.go +++ b/liberty/distribution_test.go @@ -64,12 +64,12 @@ func testDistribution(t *testing.T, when spec.G, it spec.S) { } dc := libpak.DependencyCache{CachePath: "testdata"} - distro := liberty.NewDistribution(dep, dc, "defaultServer", ctx.Application.Path, []string{}, []string{}, executor) - distro.Logger = bard.NewLogger(io.Discard) - layer, err := ctx.Layers.Layer("test-layer") Expect(err).NotTo(HaveOccurred()) + distro := liberty.NewDistribution(dep, dc, "ol", "defaultServer", ctx.Application.Path, []string{}, []string{}, executor) + distro.Logger = bard.NewLogger(io.Discard) + Expect(distro.LayerContributor.ExpectedMetadata.(map[string]interface{})).To(HaveKeyWithValue("dependency", dep)) Expect(distro.LayerContributor.ExpectedMetadata.(map[string]interface{})).To(HaveKeyWithValue("server-name", "defaultServer")) Expect(distro.LayerContributor.ExpectedMetadata.(map[string]interface{})).To(HaveKeyWithValue("features", []string{})) @@ -98,12 +98,12 @@ func testDistribution(t *testing.T, when spec.G, it spec.S) { iFixPath := filepath.Join(iFixesPath, "210012-wlp-archive-ifph12345.jar") Expect(os.WriteFile(iFixPath, []byte{}, 0644)).To(Succeed()) - distro := liberty.NewDistribution(dep, dc, "defaultServer", ctx.Application.Path, []string{}, []string{iFixPath}, executor) - distro.Logger = bard.NewLogger(io.Discard) - layer, err := ctx.Layers.Layer("test-layer") Expect(err).NotTo(HaveOccurred()) + distro := liberty.NewDistribution(dep, dc, "ol", "defaultServer", ctx.Application.Path, []string{}, []string{iFixPath}, executor) + distro.Logger = bard.NewLogger(io.Discard) + Expect(distro.LayerContributor.ExpectedMetadata.(map[string]interface{})).To(HaveKeyWithValue("dependency", dep)) Expect(distro.LayerContributor.ExpectedMetadata.(map[string]interface{})).To(HaveKeyWithValue("server-name", "defaultServer")) Expect(distro.LayerContributor.ExpectedMetadata.(map[string]interface{})).To(HaveKeyWithValue("features", []string{})) @@ -129,16 +129,16 @@ func testDistribution(t *testing.T, when spec.G, it spec.S) { } dc := libpak.DependencyCache{CachePath: "testdata"} + layer, err := ctx.Layers.Layer("test-layer") + Expect(err).NotTo(HaveOccurred()) + executor := &mocks.Executor{} executor.On("Execute", mock.Anything).Return(nil) features := []string{"foo", "bar", "baz"} - distro := liberty.NewDistribution(dep, dc, "defaultServer", ctx.Application.Path, features, []string{}, executor) + distro := liberty.NewDistribution(dep, dc, "ol", "defaultServer", ctx.Application.Path, features, []string{}, executor) distro.Logger = bard.NewLogger(io.Discard) - layer, err := ctx.Layers.Layer("test-layer") - Expect(err).NotTo(HaveOccurred()) - Expect(distro.LayerContributor.ExpectedMetadata.(map[string]interface{})).To(HaveKeyWithValue("dependency", dep)) Expect(distro.LayerContributor.ExpectedMetadata.(map[string]interface{})).To(HaveKeyWithValue("server-name", "defaultServer")) Expect(distro.LayerContributor.ExpectedMetadata.(map[string]interface{})).To(HaveKeyWithValue("features", features))