mirror of
https://github.com/SAP/jenkins-library.git
synced 2026-06-19 22:58:55 +02:00
109 lines
3.8 KiB
Go
109 lines
3.8 KiB
Go
package python
|
|
|
|
import (
|
|
"fmt"
|
|
"regexp"
|
|
|
|
"github.com/SAP/jenkins-library/pkg/log"
|
|
"github.com/SAP/jenkins-library/pkg/piperutils"
|
|
)
|
|
|
|
const (
|
|
BOMFilename = "bom-pip.xml"
|
|
)
|
|
|
|
func CreateBOM(
|
|
executeFn func(executable string, params ...string) error,
|
|
existsFn func(path string) (bool, error),
|
|
readFileFn func(path string) ([]byte, error),
|
|
virtualEnv string,
|
|
requirementsFile string,
|
|
cycloneDxVersion string,
|
|
cycloneDxSchemaVersion string,
|
|
) error {
|
|
if exists, _ := existsFn(requirementsFile); exists {
|
|
if err := InstallRequirements(executeFn, virtualEnv, requirementsFile); err != nil {
|
|
return fmt.Errorf("failed to install requirements.txt: %w", err)
|
|
}
|
|
} else {
|
|
log.Entry().Warnf("unable to find requirements.txt file at %s , continuing SBOM generation without requirements.txt", requirementsFile)
|
|
}
|
|
|
|
// Install current package to ensure it's in the venv for cyclonedx-py to detect
|
|
// For pyproject.toml builds, this is already done in BuildWithPyProjectToml, but doing it again is safe
|
|
// For setup.py builds, this ensures the package is installed
|
|
if err := InstallProjectDependencies(executeFn, virtualEnv); err != nil {
|
|
return fmt.Errorf("failed to install project dependencies: %w", err)
|
|
}
|
|
|
|
if err := InstallCycloneDX(executeFn, virtualEnv, cycloneDxVersion); err != nil {
|
|
return fmt.Errorf("failed to install cyclonedx module: %w", err)
|
|
}
|
|
|
|
log.Entry().Debug("creating BOM")
|
|
args := append([]string{"env"},
|
|
"--output-file", BOMFilename,
|
|
"--output-format", "XML",
|
|
"--spec-version", cycloneDxSchemaVersion,
|
|
)
|
|
|
|
// Add pyproject.toml only if it exists AND contains [project] metadata
|
|
// Without [project] metadata, cyclonedx-py will fail when using --pyproject flag
|
|
if hasMetadata := pyprojectHasMetadata(readFileFn, "pyproject.toml"); hasMetadata {
|
|
args = append(args, "--pyproject", "pyproject.toml")
|
|
}
|
|
|
|
if err := executeFn(getBinary(virtualEnv, "cyclonedx-py"), args...); err != nil {
|
|
return fmt.Errorf("failed to create BOM: %w", err)
|
|
}
|
|
|
|
// Post-process BOM to add purl to root component
|
|
// cyclonedx-py with --pyproject doesn't generate purl for the root component
|
|
// We use the official CycloneDX library via piperutils.UpdatePurl for robust handling
|
|
if err := addPurlToRootComponent(BOMFilename); err != nil {
|
|
log.Entry().Warnf("failed to add purl to root component: %v", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// addPurlToRootComponent adds a purl element to the root component in the BOM
|
|
// This is needed because cyclonedx-py doesn't generate purl when using --pyproject flag
|
|
// Uses piperutils.UpdatePurl which leverages the official CycloneDX Go library
|
|
func addPurlToRootComponent(bomFile string) error {
|
|
component := piperutils.GetComponent(bomFile)
|
|
|
|
if component.Name == "" || component.Version == "" {
|
|
return fmt.Errorf("could not extract name and version from BOM metadata")
|
|
}
|
|
|
|
// Generate PURL for PyPI package
|
|
purl := fmt.Sprintf("pkg:pypi/%s@%s", component.Name, component.Version)
|
|
|
|
log.Entry().Debugf("Adding purl to root component: %s", purl)
|
|
|
|
// Update PURL using official CycloneDX library
|
|
if err := piperutils.UpdatePurl(bomFile, purl); err != nil {
|
|
return fmt.Errorf("failed to update purl in BOM: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// pyprojectHasMetadata checks if pyproject.toml exists and contains [project] metadata section
|
|
// Returns true only if the file exists and contains a [project] section
|
|
func pyprojectHasMetadata(readFileFn func(path string) ([]byte, error), path string) bool {
|
|
data, err := readFileFn(path)
|
|
if err != nil {
|
|
// File doesn't exist or can't be read
|
|
return false
|
|
}
|
|
|
|
content := string(data)
|
|
|
|
// Check if the file contains a [project] section
|
|
// We use a regex to match [project] at the start of a line (possibly with whitespace)
|
|
projectSectionRegex := regexp.MustCompile(`(?m)^\s*\[project\]`)
|
|
return projectSectionRegex.MatchString(content)
|
|
}
|