1
0
mirror of https://github.com/SAP/jenkins-library.git synced 2024-12-12 10:55:20 +02:00

SBOM creation for Mend (#3934)

* Fix docs and format

* Assessment format added

* Added sample file

* Added parsing

* Added packageurl implementation

* Slight refinement

* Refactored assessment options

* Adapted sample file

* First attempt of ws sbom gen

* Reworked SBOM generation

* Fix test code

* Add assessment handling

* Update dependencies

* Added golden test

* Small fix

Co-authored-by: Oliver Nocon <33484802+OliverNocon@users.noreply.github.com>
This commit is contained in:
Sven Merk 2022-08-09 13:56:01 +02:00 committed by GitHub
parent a46f796bcd
commit b3f37650a2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 1005 additions and 13 deletions

View File

@ -385,6 +385,7 @@ func checkmarxExecuteScanMetadata() config.StepData {
Inputs: config.StepInputs{
Secrets: []config.StepSecrets{
{Name: "checkmarxCredentialsId", Description: "Jenkins 'Username with password' credentials ID containing username and password to communicate with the Checkmarx backend.", Type: "jenkins"},
{Name: "githubTokenCredentialsId", Description: "Jenkins 'Secret text' credentials ID containing token to authenticate to GitHub.", Type: "jenkins"},
},
Resources: []config.StepResources{
{Name: "checkmarx", Type: "stash"},

View File

@ -298,6 +298,7 @@ func detectExecuteScanMetadata() config.StepData {
Inputs: config.StepInputs{
Secrets: []config.StepSecrets{
{Name: "detectTokenCredentialsId", Description: "Jenkins 'Secret text' credentials ID containing the API token used to authenticate with the Synopsis Detect (formerly BlackDuck) Server.", Type: "jenkins", Aliases: []config.Alias{{Name: "apiTokenCredentialsId", Deprecated: false}}},
{Name: "githubTokenCredentialsId", Description: "Jenkins 'Secret text' credentials ID containing token to authenticate to GitHub.", Type: "jenkins"},
},
Resources: []config.StepResources{
{Name: "buildDescriptor", Type: "stash"},

View File

@ -16,6 +16,7 @@ import (
ws "github.com/SAP/jenkins-library/pkg/whitesource"
"github.com/SAP/jenkins-library/pkg/command"
"github.com/SAP/jenkins-library/pkg/format"
"github.com/SAP/jenkins-library/pkg/log"
"github.com/SAP/jenkins-library/pkg/npm"
"github.com/SAP/jenkins-library/pkg/piperutils"
@ -46,6 +47,7 @@ type whitesource interface {
GetProjectAlerts(projectToken string) ([]ws.Alert, error)
GetProjectAlertsByType(projectToken, alertType string) ([]ws.Alert, error)
GetProjectLibraryLocations(projectToken string) ([]ws.Library, error)
GetProjectHierarchy(projectToken string, includeInHouse bool) ([]ws.Library, error)
}
type whitesourceUtils interface {
@ -323,6 +325,7 @@ func resolveProjectIdentifiers(config *ScanOptions, scan *ws.Scan, utils whiteso
if err != nil {
return errors.Wrap(err, "failed to get build artifact description")
}
scan.Coordinates = coordinates
if len(config.Version) > 0 {
log.Entry().Infof("Resolving product version from default provided '%s' with versioning '%s'", config.Version, config.VersioningModel)
@ -562,24 +565,33 @@ func checkSecurityViolations(ctx context.Context, config *ScanOptions, scan *ws.
"as floating point number: %w", config.CvssSeverityLimit, err)
}
// inhale assessments from file system
assessments := readAssessmentsFromFile(config.AssessmentFile, utils)
if config.ProjectToken != "" {
project := ws.Project{Name: config.ProjectName, Token: config.ProjectToken}
// ToDo: see if HTML report generation is really required here
// we anyway need to do some refactoring here since config.ProjectToken != "" essentially indicates an aggregated project
if _, _, err := checkProjectSecurityViolations(config, cvssSeverityLimit, project, sys, influx); err != nil {
if _, _, err := checkProjectSecurityViolations(config, cvssSeverityLimit, project, sys, assessments, influx); err != nil {
return reportPaths, err
}
} else {
vulnerabilitiesCount := 0
var errorsOccured []string
allAlerts := []ws.Alert{}
allLibraries := []ws.Library{}
for _, project := range scan.ScannedProjects() {
// collect errors and aggregate vulnerabilities from all projects
if vulCount, alerts, err := checkProjectSecurityViolations(config, cvssSeverityLimit, project, sys, influx); err != nil {
if vulCount, alerts, err := checkProjectSecurityViolations(config, cvssSeverityLimit, project, sys, assessments, influx); err != nil {
allAlerts = append(allAlerts, alerts...)
vulnerabilitiesCount += vulCount
errorsOccured = append(errorsOccured, fmt.Sprint(err))
}
// collect all libraries detected in all related projects and errors
if libraries, err := sys.GetProjectHierarchy(project.Token, true); err != nil {
allLibraries = append(allLibraries, libraries...)
errorsOccured = append(errorsOccured, fmt.Sprint(err))
}
}
log.Entry().Debugf("Aggregated %v alerts for scanned projects", len(allAlerts))
@ -613,6 +625,16 @@ func checkSecurityViolations(ctx context.Context, config *ScanOptions, scan *ws.
}
reportPaths = append(reportPaths, paths...)
sbom, err := ws.CreateCycloneSBOM(scan, &allLibraries, &allAlerts)
if err != nil {
errorsOccured = append(errorsOccured, fmt.Sprint(err))
}
paths, err = ws.WriteCycloneSBOM(sbom, utils)
if err != nil {
errorsOccured = append(errorsOccured, fmt.Sprint(err))
}
reportPaths = append(reportPaths, paths...)
if len(errorsOccured) > 0 {
if vulnerabilitiesCount > 0 {
log.SetErrorCategory(log.ErrorCompliance)
@ -623,14 +645,49 @@ func checkSecurityViolations(ctx context.Context, config *ScanOptions, scan *ws.
return reportPaths, nil
}
// read assessments from file and expose them to match alerts and filter them before processing
func readAssessmentsFromFile(assessmentFilePath string, utils whitesourceUtils) *[]format.Assessment {
exists, err := utils.FileExists(assessmentFilePath)
if err != nil {
log.SetErrorCategory(log.ErrorConfiguration)
log.Entry().Errorf("unable to check existence of assessment file at '%s'", assessmentFilePath)
}
assessmentFile, err := utils.Open(assessmentFilePath)
if exists && err != nil {
log.SetErrorCategory(log.ErrorConfiguration)
log.Entry().Errorf("unable to open assessment file at '%s'", assessmentFilePath)
}
assessments := &[]format.Assessment{}
if exists {
defer assessmentFile.Close()
assessments, err = format.ReadAssessments(assessmentFile)
if err != nil {
log.SetErrorCategory(log.ErrorConfiguration)
log.Entry().Errorf("unable to parse assessment file at '%s'", assessmentFilePath)
}
}
return assessments
}
// checkSecurityViolations checks security violations and returns an error if the configured severity limit is crossed.
func checkProjectSecurityViolations(config *ScanOptions, cvssSeverityLimit float64, project ws.Project, sys whitesource, influx *whitesourceExecuteScanInflux) (int, []ws.Alert, error) {
func checkProjectSecurityViolations(config *ScanOptions, cvssSeverityLimit float64, project ws.Project, sys whitesource, assessments *[]format.Assessment, influx *whitesourceExecuteScanInflux) (int, []ws.Alert, error) {
// get project alerts (vulnerabilities)
alerts, err := sys.GetProjectAlertsByType(project.Token, "SECURITY_VULNERABILITY")
if err != nil {
return 0, alerts, fmt.Errorf("failed to retrieve project alerts from WhiteSource: %w", err)
}
// filter alerts related to existing assessments
filteredAlerts := []ws.Alert{}
if len(*assessments) > 0 {
for _, alert := range alerts {
if result, err := alert.ContainedIn(assessments); err == nil && result == false {
filteredAlerts = append(filteredAlerts, alert)
}
}
alerts = filteredAlerts
}
severeVulnerabilities, nonSevereVulnerabilities := ws.CountSecurityVulnerabilities(&alerts, cvssSeverityLimit)
influx.whitesource_data.fields.minor_vulnerabilities = nonSevereVulnerabilities
influx.whitesource_data.fields.major_vulnerabilities = severeVulnerabilities

View File

@ -27,6 +27,7 @@ type whitesourceExecuteScanOptions struct {
AgentParameters []string `json:"agentParameters,omitempty"`
AgentURL string `json:"agentUrl,omitempty"`
AggregateVersionWideReport bool `json:"aggregateVersionWideReport,omitempty"`
AssessmentFile string `json:"assessmentFile,omitempty"`
BuildDescriptorExcludeList []string `json:"buildDescriptorExcludeList,omitempty"`
BuildDescriptorFile string `json:"buildDescriptorFile,omitempty"`
BuildTool string `json:"buildTool,omitempty"`
@ -310,6 +311,7 @@ func addWhitesourceExecuteScanFlags(cmd *cobra.Command, stepConfig *whitesourceE
cmd.Flags().StringSliceVar(&stepConfig.AgentParameters, "agentParameters", []string{}, "[NOT IMPLEMENTED] List of additional parameters passed to the Unified Agent command line.")
cmd.Flags().StringVar(&stepConfig.AgentURL, "agentUrl", `https://saas.whitesourcesoftware.com/agent`, "URL to the WhiteSource agent endpoint.")
cmd.Flags().BoolVar(&stepConfig.AggregateVersionWideReport, "aggregateVersionWideReport", false, "This does not run a scan, instead just generated a report for all projects with projectVersion = config.ProductVersion")
cmd.Flags().StringVar(&stepConfig.AssessmentFile, "assessmentFile", `hs-assessments.yaml`, "Explicit path to the assessment YAML file.")
cmd.Flags().StringSliceVar(&stepConfig.BuildDescriptorExcludeList, "buildDescriptorExcludeList", []string{`unit-tests/pom.xml`, `integration-tests/pom.xml`}, "List of build descriptors and therefore modules to exclude from the scan and assessment activities.")
cmd.Flags().StringVar(&stepConfig.BuildDescriptorFile, "buildDescriptorFile", os.Getenv("PIPER_buildDescriptorFile"), "Explicit path to the build descriptor file.")
cmd.Flags().StringVar(&stepConfig.BuildTool, "buildTool", os.Getenv("PIPER_buildTool"), "Defines the tool which is used for building the artifact.")
@ -429,6 +431,15 @@ func whitesourceExecuteScanMetadata() config.StepData {
Aliases: []config.Alias{},
Default: false,
},
{
Name: "assessmentFile",
ResourceRef: []config.ResourceReference{},
Scope: []string{"PARAMETERS", "STAGES", "STEPS"},
Type: "string",
Mandatory: false,
Aliases: []config.Alias{},
Default: `hs-assessments.yaml`,
},
{
Name: "buildDescriptorExcludeList",
ResourceRef: []config.ResourceReference{},

View File

@ -7,6 +7,7 @@ import (
"testing"
"time"
"github.com/SAP/jenkins-library/pkg/format"
"github.com/SAP/jenkins-library/pkg/mock"
"github.com/SAP/jenkins-library/pkg/piperutils"
"github.com/SAP/jenkins-library/pkg/reporting"
@ -613,7 +614,7 @@ func TestCheckProjectSecurityViolations(t *testing.T) {
systemMock.Alerts = []ws.Alert{}
influx := whitesourceExecuteScanInflux{}
severeVulnerabilities, alerts, err := checkProjectSecurityViolations(&ScanOptions{FailOnSevereVulnerabilities: true}, 7.0, project, systemMock, &influx)
severeVulnerabilities, alerts, err := checkProjectSecurityViolations(&ScanOptions{FailOnSevereVulnerabilities: true}, 7.0, project, systemMock, &[]format.Assessment{}, &influx)
assert.NoError(t, err)
assert.Equal(t, 0, severeVulnerabilities)
assert.Equal(t, 0, len(alerts))
@ -622,23 +623,37 @@ func TestCheckProjectSecurityViolations(t *testing.T) {
t.Run("error - some vulnerabilities", func(t *testing.T) {
systemMock := ws.NewSystemMock("ignored")
systemMock.Alerts = []ws.Alert{
{Vulnerability: ws.Vulnerability{CVSS3Score: 7}},
{Vulnerability: ws.Vulnerability{CVSS3Score: 6}},
{Vulnerability: ws.Vulnerability{CVSS3Score: 7, Name: "CVE-2025-001"}, Library: ws.Library{KeyID: 42, Name: "test", GroupID: "com.sap", ArtifactID: "test", Version: "1.2.3", LibType: "MAVEN_ARTIFACT"}},
{Vulnerability: ws.Vulnerability{CVSS3Score: 6, Name: "CVE-2025-002"}, Library: ws.Library{KeyID: 42, Name: "test", GroupID: "com.sap", ArtifactID: "test", Version: "1.2.3", LibType: "MAVEN_ARTIFACT"}},
}
influx := whitesourceExecuteScanInflux{}
severeVulnerabilities, alerts, err := checkProjectSecurityViolations(&ScanOptions{FailOnSevereVulnerabilities: true}, 7.0, project, systemMock, &influx)
severeVulnerabilities, alerts, err := checkProjectSecurityViolations(&ScanOptions{FailOnSevereVulnerabilities: true}, 7.0, project, systemMock, &[]format.Assessment{}, &influx)
assert.Contains(t, fmt.Sprint(err), "1 Open Source Software Security vulnerabilities")
assert.Equal(t, 1, severeVulnerabilities)
assert.Equal(t, 2, len(alerts))
})
t.Run("success - assessed vulnerabilities", func(t *testing.T) {
systemMock := ws.NewSystemMock("ignored")
systemMock.Alerts = []ws.Alert{
{Vulnerability: ws.Vulnerability{CVSS3Score: 7, Name: "CVE-2025-001"}, Library: ws.Library{KeyID: 42, Name: "test", GroupID: "com.sap", ArtifactID: "test", Version: "1.2.3", LibType: "MAVEN_ARTIFACT"}},
{Vulnerability: ws.Vulnerability{CVSS3Score: 6, Name: "CVE-2025-002"}, Library: ws.Library{KeyID: 42, Name: "test", GroupID: "com.sap", ArtifactID: "test", Version: "1.2.3", LibType: "MAVEN_ARTIFACT"}},
}
influx := whitesourceExecuteScanInflux{}
severeVulnerabilities, alerts, err := checkProjectSecurityViolations(&ScanOptions{FailOnSevereVulnerabilities: true}, 7.0, project, systemMock, &[]format.Assessment{{Vulnerability: "CVE-2025-001", Purls: []format.Purl{{Purl: "pkg:/maven/com.sap/test@1.2.3"}}}, {Vulnerability: "CVE-2025-002", Purls: []format.Purl{{Purl: "pkg:/maven/com.sap/test@1.2.3"}}}}, &influx)
assert.NoError(t, err)
assert.Equal(t, 0, severeVulnerabilities)
assert.Equal(t, 0, len(alerts))
})
t.Run("error - WhiteSource failure", func(t *testing.T) {
systemMock := ws.NewSystemMock("ignored")
systemMock.AlertError = fmt.Errorf("failed to read alerts")
influx := whitesourceExecuteScanInflux{}
_, _, err := checkProjectSecurityViolations(&ScanOptions{FailOnSevereVulnerabilities: true}, 7.0, project, systemMock, &influx)
_, _, err := checkProjectSecurityViolations(&ScanOptions{FailOnSevereVulnerabilities: true}, 7.0, project, systemMock, &[]format.Assessment{}, &influx)
assert.Contains(t, fmt.Sprint(err), "failed to retrieve project alerts from WhiteSource")
})
}

2
go.mod
View File

@ -40,6 +40,7 @@ require (
github.com/microsoft/azure-devops-go-api/azuredevops v1.0.0-b5
github.com/mitchellh/mapstructure v1.4.3
github.com/motemen/go-nuts v0.0.0-20210915132349-615a782f2c69
github.com/package-url/packageurl-go v0.1.0
github.com/pelletier/go-toml v1.9.4
github.com/piper-validation/fortify-client-go v0.0.0-20220126145513-7b3e9a72af01
github.com/pkg/errors v0.9.1
@ -81,6 +82,7 @@ require (
github.com/Azure/go-autorest/logger v0.2.1 // indirect
github.com/Azure/go-autorest/tracing v0.6.0 // indirect
github.com/BurntSushi/toml v0.4.1 // indirect
github.com/CycloneDX/cyclonedx-go v0.6.0
github.com/DataDog/datadog-go v3.2.0+incompatible // indirect
github.com/Jeffail/gabs v1.1.1 // indirect
github.com/Masterminds/goutils v1.1.1 // indirect

6
go.sum
View File

@ -146,6 +146,8 @@ github.com/BurntSushi/toml v0.4.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbi
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
github.com/CloudyKit/fastprinter v0.0.0-20200109182630-33d98a066a53/go.mod h1:+3IMCy2vIlbG1XG/0ggNQv0SvxCAIpPM5b1nCz56Xno=
github.com/CloudyKit/jet/v3 v3.0.0/go.mod h1:HKQPgSJmdK8hdoAbKUUWajkHyHo4RaU5rMdUywE7VMo=
github.com/CycloneDX/cyclonedx-go v0.6.0 h1:SizWGbZzFTC/O/1yh072XQBMxfvsoWqd//oKCIyzFyE=
github.com/CycloneDX/cyclonedx-go v0.6.0/go.mod h1:nQCiF4Tvrg5Ieu8qPhYMvzPGMu5I7fANZkrSsJjl5mg=
github.com/DATA-DOG/go-sqlmock v1.5.0/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM=
github.com/DataDog/datadog-go v2.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ=
github.com/DataDog/datadog-go v3.2.0+incompatible h1:qSG2N4FghB1He/r2mFrWKCaL7dXCilEuNEeAn20fdD4=
@ -331,6 +333,8 @@ github.com/bndr/gojenkins v1.1.1-0.20210520222939-90ed82bfdff6 h1:yHK3nXjSRklq0S
github.com/bndr/gojenkins v1.1.1-0.20210520222939-90ed82bfdff6/go.mod h1:QeskxN9F/Csz0XV/01IC8y37CapKKWvOHa0UHLLX1fM=
github.com/boltdb/bolt v1.3.1/go.mod h1:clJnj/oiGkjum5o1McbSZDSLxVThjynRyGBgiAx27Ps=
github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc h1:biVzkmvwrH8WK8raXaxBx6fRVTlJILwEwQGL1I/ByEI=
github.com/bradleyjkemp/cupaloy/v2 v2.7.0 h1:AT0vOjO68RcLyenLCHOGZzSNiuto7ziqzq6Q1/3xzMQ=
github.com/bradleyjkemp/cupaloy/v2 v2.7.0/go.mod h1:bm7JXdkRd4BHJk9HpwqAI8BoAY1lps46Enkdqw6aRX0=
github.com/briankassouf/jose v0.9.2-0.20180619214549-d2569464773f h1:ZMEzE7R0WNqgbHplzSBaYJhJi5AZWTCK9baU0ebzG6g=
github.com/bshuster-repo/logrus-logstash-hook v0.4.1/go.mod h1:zsTqEiSzDgAa/8GZR7E1qaXrhYNDKBYy5/dWPTIflbk=
github.com/bshuster-repo/logrus-logstash-hook v1.0.0 h1:e+C0SB5R1pu//O4MQ3f9cFuPGoOVeF2fE4Og9otCc70=
@ -1553,6 +1557,8 @@ github.com/oracle/oci-go-sdk v13.1.0+incompatible h1:inwbT0b/mMbnTfzYoW2xcU1cCMI
github.com/oracle/oci-go-sdk v13.1.0+incompatible/go.mod h1:VQb79nF8Z2cwLkLS35ukwStZIg5F66tcBccjip/j888=
github.com/ory/dockertest v3.3.5+incompatible h1:iLLK6SQwIhcbrG783Dghaaa3WPzGc+4Emza6EbVUUGA=
github.com/ory/dockertest/v3 v3.8.0 h1:i5b0cJCd801qw0cVQUOH6dSpI9fT3j5tdWu0jKu90ks=
github.com/package-url/packageurl-go v0.1.0 h1:efWBc98O/dBZRg1pw2xiDzovnlMjCa9NPnfaiBduh8I=
github.com/package-url/packageurl-go v0.1.0/go.mod h1:C/ApiuWpmbpni4DIOECf6WCjFUZV7O1Fx7VAzrZHgBw=
github.com/packethost/packngo v0.1.1-0.20180711074735-b9cb5096f54c h1:vwpFWvAO8DeIZfFeqASzZfsxuWPno9ncAebBEP0N3uE=
github.com/packethost/packngo v0.1.1-0.20180711074735-b9cb5096f54c/go.mod h1:otzZQXgoO96RTzDB/Hycg0qZcXZsWJGJRSXbmEIJ+4M=
github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=

72
pkg/format/assessment.go Normal file
View File

@ -0,0 +1,72 @@
package format
import (
"fmt"
"io"
"io/ioutil"
"github.com/ghodss/yaml"
"github.com/package-url/packageurl-go"
"github.com/pkg/errors"
)
// Assessment format related JSON structs
type Assessments struct {
List []Assessment `json:"ignore"`
}
type Assessment struct {
Vulnerability string `json:"vulnerability"`
Status AssessmentStatus `json:"status"`
Analysis AssessmentAnalysis `json:"analysis"`
Purls []Purl `json:"purls"`
}
type AssessmentStatus string
const (
NotAssessed AssessmentStatus = "notAssessed" //"Not Assessed"
Relevant AssessmentStatus = "relevant" //"Relevant (True Positive)"
NotRelevant AssessmentStatus = "notRelevant" //"Not Relevant (False Positive)"
InProcess AssessmentStatus = "inProcess" //"In Process"
)
type AssessmentAnalysis string
const (
WaitingForFix AssessmentAnalysis = "waitingForFix" //"Waiting for OSS community fix"
RiskAccepted AssessmentAnalysis = "riskAccepted" //"Risk Accepted"
//Others AssessmentAnalysis = "others" //"Others"
NotPresent AssessmentAnalysis = "notPresent" //"Affected parts of the OSS library are not present"
NotUsed AssessmentAnalysis = "notUsed" //"Affected parts of the OSS library are not used"
AssessmentPropagation AssessmentAnalysis = "assessmentPropagation" //"Assessment Propagation"
//BuildVersionOutdated AssessmentAnalysis = "buildVersionOutdated" //"Build Version is outdated"
FixedByDevTeam AssessmentAnalysis = "fixedByDevTeam" //"OSS Component fixed by development team"
Mitigated AssessmentAnalysis = "mitigated" //"Mitigated by the Application"
WronglyReported AssessmentAnalysis = "wronglyReported" //"Wrongly reported CVE"
)
type Purl struct {
Purl string `json:"purl"`
}
func (p Purl) ToPackageUrl() (packageurl.PackageURL, error) {
return packageurl.FromString(p.Purl)
}
// ReadAssessment loads the assessments and returns their contents
func ReadAssessments(assessmentFile io.ReadCloser) (*[]Assessment, error) {
defer assessmentFile.Close()
assessments := &[]Assessment{}
content, err := ioutil.ReadAll(assessmentFile)
if err != nil {
return nil, errors.Wrapf(err, "error reading %v", assessmentFile)
}
err = yaml.Unmarshal(content, assessments)
if err != nil {
return nil, NewParseError(fmt.Sprintf("format of assessment file is invalid %q: %v", content, err))
}
return assessments, nil
}

18
pkg/format/errors.go Normal file
View File

@ -0,0 +1,18 @@
package format
// ParseError defines an error type for assessment file parsing errors
type ParseError struct {
message string
}
// NewParseError creates a new ParseError
func NewParseError(message string) *ParseError {
return &ParseError{
message: message,
}
}
// Error returns the message of the ParseError
func (e *ParseError) Error() string {
return e.message
}

View File

@ -0,0 +1,7 @@
ignore:
# This is the full set of supported rule fields:
- vulnerability: CVE-2008-4318
status: notRelevant
analysis: mitigated
purls:
- purl: "pkg:npm/observer@0.3.2"

18
pkg/piperutils/maps.go Normal file
View File

@ -0,0 +1,18 @@
package piperutils
func Keys[M ~map[K]V, K comparable, V any](m M) []K {
r := make([]K, 0, len(m))
for k := range m {
r = append(r, k)
}
return r
}
//Values returns the slice of values of the map provided
func Values[M ~map[K]V, K comparable, V any](m M) []V {
r := make([]V, 0, len(m))
for _, v := range m {
r = append(r, v)
}
return r
}

View File

@ -0,0 +1,31 @@
package piperutils
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestKeys(t *testing.T) {
intStringMap := map[int]string{1: "eins", 2: "zwei", 3: "drei", 4: "vier"}
intList := Keys(intStringMap)
assert.Equal(t, 4, len(intList))
assert.Equal(t, true, ContainsInt(intList, 1))
assert.Equal(t, true, ContainsInt(intList, 2))
assert.Equal(t, true, ContainsInt(intList, 3))
assert.Equal(t, true, ContainsInt(intList, 4))
}
func TestValues(t *testing.T) {
intStringMap := map[int]string{1: "eins", 2: "zwei", 3: "drei", 4: "vier"}
intList := Values(intStringMap)
assert.Equal(t, 4, len(intList))
assert.Equal(t, true, ContainsString(intList, "eins"))
assert.Equal(t, true, ContainsString(intList, "zwei"))
assert.Equal(t, true, ContainsString(intList, "drei"))
assert.Equal(t, true, ContainsString(intList, "vier"))
}

View File

@ -1,6 +1,7 @@
package whitesource
import (
"bytes"
"crypto/sha1"
"encoding/json"
"fmt"
@ -9,6 +10,9 @@ import (
"strings"
"time"
cdx "github.com/CycloneDX/cyclonedx-go"
"github.com/package-url/packageurl-go"
"github.com/SAP/jenkins-library/pkg/format"
"github.com/SAP/jenkins-library/pkg/log"
"github.com/SAP/jenkins-library/pkg/piperutils"
@ -284,3 +288,229 @@ func WriteSarifFile(sarif *format.SARIF, utils piperutils.FileUtils) ([]piperuti
return reportPaths, nil
}
func transformToCdxSeverity(severity string) cdx.Severity {
switch severity {
case "info":
return cdx.SeverityInfo
case "low":
return cdx.SeverityLow
case "medium":
return cdx.SeverityMedium
case "high":
return cdx.SeverityHigh
case "critical":
return cdx.SeverityCritical
case "":
return cdx.SeverityNone
}
return cdx.SeverityUnknown
}
func transformBuildToPurlType(buildType string) string {
switch buildType {
case "maven":
return packageurl.TypeMaven
case "npm":
return packageurl.TypeNPM
case "docker":
return packageurl.TypeDocker
case "kaniko":
return packageurl.TypeDocker
case "golang":
return packageurl.TypeGolang
case "mta":
return packageurl.TypeComposer
}
return packageurl.TypeGeneric
}
func CreateCycloneSBOM(scan *Scan, libraries *[]Library, alerts *[]Alert) ([]byte, error) {
ppurl := packageurl.NewPackageURL(transformBuildToPurlType(scan.BuildTool), scan.Coordinates.GroupID, scan.Coordinates.ArtifactID, scan.Coordinates.Version, nil, "")
metadata := cdx.Metadata{
// Define metadata about the main component
// (the component which the BOM will describe)
// TODO check whether we can identify library vs. application
Component: &cdx.Component{
BOMRef: ppurl.ToString(),
Type: cdx.ComponentTypeLibrary,
Name: scan.Coordinates.ArtifactID,
Group: scan.Coordinates.GroupID,
Version: scan.Coordinates.Version,
},
// Use properties to include an internal identifier for this BOM
// https://cyclonedx.org/use-cases/#properties--name-value-store
Properties: &[]cdx.Property{
{
Name: "internal:bom-identifier",
Value: strings.Join(scan.ScannedProjectNames(), ", "),
},
},
}
components := []cdx.Component{}
flatUniqueLibrariesMap := map[string]Library{}
transformToUniqueFlatList(libraries, &flatUniqueLibrariesMap)
flatUniqueLibraries := piperutils.Values(flatUniqueLibrariesMap)
sort.Slice(flatUniqueLibraries, func(i, j int) bool {
return flatUniqueLibraries[i].ToPackageUrl().ToString() < flatUniqueLibraries[j].ToPackageUrl().ToString()
})
for _, lib := range flatUniqueLibraries {
purl := lib.ToPackageUrl()
// Define the components that the product ships with
// https://cyclonedx.org/use-cases/#inventory
component := cdx.Component{
BOMRef: purl.ToString(),
Type: cdx.ComponentTypeLibrary,
Author: lib.GroupID,
Name: lib.ArtifactID,
Version: lib.Version,
PackageURL: purl.ToString(),
}
components = append(components, component)
}
dependencies := []cdx.Dependency{}
declareDependency(ppurl, libraries, &dependencies)
vulnerabilities := []cdx.Vulnerability{}
for _, alert := range *alerts {
// Define the vulnerabilities in VEX
// https://cyclonedx.org/use-cases/#vulnerability-exploitability
purl := alert.Library.ToPackageUrl()
vuln := cdx.Vulnerability{
BOMRef: purl.ToString(),
ID: alert.Vulnerability.Name,
Source: &cdx.Source{URL: alert.Vulnerability.URL},
Tools: &[]cdx.Tool{
{
Name: scan.AgentName,
Version: scan.AgentVersion,
Vendor: "Mend",
ExternalReferences: &[]cdx.ExternalReference{
{
URL: "https://www.mend.io/",
Type: cdx.ERTypeBuildMeta,
},
},
},
},
Recommendation: alert.Vulnerability.FixResolutionText,
Detail: alert.Vulnerability.Description,
Ratings: &[]cdx.VulnerabilityRating{
{
Score: &alert.Vulnerability.CVSS3Score,
Severity: transformToCdxSeverity(alert.Vulnerability.Severity),
Method: cdx.ScoringMethodCVSSv3,
},
{
Score: &alert.Vulnerability.Score,
Severity: transformToCdxSeverity(alert.Vulnerability.Severity),
Method: cdx.ScoringMethodCVSSv2,
},
},
Advisories: &[]cdx.Advisory{
{
Title: alert.Vulnerability.TopFix.Vulnerability,
URL: alert.Vulnerability.TopFix.Origin,
},
},
Description: alert.Description,
Created: alert.CreationDate,
Published: alert.Vulnerability.PublishDate,
Updated: alert.ModifiedDate,
Affects: &[]cdx.Affects{
{
Ref: purl.ToString(),
Range: &[]cdx.AffectedVersions{
{
Version: alert.Library.Version,
Status: cdx.VulnerabilityStatus(alert.Status),
},
},
},
},
}
references := []cdx.VulnerabilityReference{}
for _, ref := range alert.Vulnerability.References {
reference := cdx.VulnerabilityReference{
Source: &cdx.Source{Name: ref.Homepage, URL: ref.URL},
ID: ref.GenericPackageIndex,
}
references = append(references, reference)
}
vuln.References = &references
vulnerabilities = append(vulnerabilities, vuln)
}
// Assemble the BOM
bom := cdx.NewBOM()
bom.Vulnerabilities = &vulnerabilities
bom.Metadata = &metadata
bom.Components = &components
bom.Dependencies = &dependencies
// Encode the BOM
var outputBytes []byte
buffer := bytes.NewBuffer(outputBytes)
encoder := cdx.NewBOMEncoder(buffer, cdx.BOMFileFormatXML)
encoder.SetPretty(true)
if err := encoder.Encode(bom); err != nil {
return nil, err
}
return buffer.Bytes(), nil
}
func WriteCycloneSBOM(sbom []byte, utils piperutils.FileUtils) ([]piperutils.Path, error) {
paths := []piperutils.Path{}
if err := utils.MkdirAll(ReportsDirectory, 0777); err != nil {
return paths, errors.Wrapf(err, "failed to create report directory")
}
sbomPath := filepath.Join(ReportsDirectory, "piper_whitesource_sbom.xml")
// Write file
if err := utils.FileWrite(sbomPath, sbom, 0666); err != nil {
log.SetErrorCategory(log.ErrorConfiguration)
return paths, errors.Wrapf(err, "failed to write SARIF file")
}
paths = append(paths, piperutils.Path{Name: "WhiteSource SBOM file", Target: sbomPath})
return paths, nil
}
func transformToUniqueFlatList(libraries *[]Library, flatMapRef *map[string]Library) {
for _, lib := range *libraries {
key := lib.ToPackageUrl().ToString()
flatMap := *flatMapRef
lookup := flatMap[key]
if lookup.KeyID != lib.KeyID {
flatMap[key] = lib
if len(lib.Dependencies) > 0 {
transformToUniqueFlatList(&lib.Dependencies, flatMapRef)
}
}
}
}
func declareDependency(parentPurl *packageurl.PackageURL, dependents *[]Library, collection *[]cdx.Dependency) {
localDependencies := []cdx.Dependency{}
for _, lib := range *dependents {
purl := lib.ToPackageUrl()
// Define the dependency graph
// https://cyclonedx.org/use-cases/#dependency-graph
localDependency := cdx.Dependency{Ref: purl.ToString()}
localDependencies = append(localDependencies, localDependency)
if len(lib.Dependencies) > 0 {
declareDependency(purl, &lib.Dependencies, collection)
}
}
dependency := cdx.Dependency{
Ref: parentPurl.ToString(),
Dependencies: &localDependencies,
}
*collection = append(*collection, dependency)
}

View File

@ -1,14 +1,19 @@
package whitesource
import (
"bytes"
"fmt"
"io/ioutil"
"path/filepath"
"testing"
cdx "github.com/CycloneDX/cyclonedx-go"
"github.com/SAP/jenkins-library/pkg/format"
"github.com/SAP/jenkins-library/pkg/mock"
"github.com/SAP/jenkins-library/pkg/piperutils"
"github.com/SAP/jenkins-library/pkg/reporting"
"github.com/SAP/jenkins-library/pkg/versioning"
"github.com/stretchr/testify/assert"
)
@ -56,6 +61,99 @@ func TestCreateCustomVulnerabilityReport(t *testing.T) {
})
}
func TestCreateCycloneSBOM(t *testing.T) {
t.Parallel()
t.Run("success case", func(t *testing.T) {
config := &ScanOptions{}
scan := &Scan{
AgentName: "Mend Unified Agent",
AgentVersion: "3.3.3",
AggregateProjectName: config.ProjectName,
BuildTool: "maven",
ProductVersion: config.ProductVersion,
Coordinates: versioning.Coordinates{GroupID: "com.sap", ArtifactID: "myproduct", Version: "1.3.4"},
}
scan.AppendScannedProject("testProject")
alerts := []Alert{
{Library: Library{KeyID: 42, Name: "log4j", GroupID: "apache-logging", ArtifactID: "log4j", Filename: "vul1"}, Vulnerability: Vulnerability{CVSS3Score: 7.0, Score: 6}},
{Library: Library{KeyID: 43, Name: "commons-lang", GroupID: "apache-commons", ArtifactID: "commons-lang", Filename: "vul2"}, Vulnerability: Vulnerability{CVSS3Score: 8.0, TopFix: Fix{Message: "this is the top fix"}}},
{Library: Library{KeyID: 42, Name: "log4j", GroupID: "apache-logging", ArtifactID: "log4j", Filename: "vul3"}, Vulnerability: Vulnerability{Score: 6}},
}
libraries := []Library{
{KeyID: 42, Name: "log4j", GroupID: "apache-logging", ArtifactID: "log4j", Filename: "vul1", Dependencies: []Library{{KeyID: 43, Name: "commons-lang", GroupID: "apache-commons", ArtifactID: "commons-lang", Filename: "vul2"}}},
{KeyID: 42, Name: "log4j", GroupID: "apache-logging", ArtifactID: "log4j", Filename: "vul3"},
}
contents, err := CreateCycloneSBOM(scan, &libraries, &alerts)
assert.NoError(t, err, "unexpected error")
buffer := bytes.NewBuffer(contents)
decoder := cdx.NewBOMDecoder(buffer, cdx.BOMFileFormatXML)
bom := cdx.NewBOM()
decoder.Decode(bom)
assert.NotNil(t, bom, "BOM was nil")
assert.NotEmpty(t, bom.SpecVersion)
components := *bom.Components
assert.Equal(t, 2, len(components))
assert.Equal(t, true, components[0].Name == "log4j" || components[0].Name == "commons-lang")
assert.Equal(t, true, components[1].Name == "log4j" || components[1].Name == "commons-lang")
assert.Equal(t, true, components[0].Name != components[1].Name)
})
t.Run("success - golden", func(t *testing.T) {
config := &ScanOptions{ProjectName: "myproduct - 1.3.4", ProductVersion: "1"}
scan := &Scan{
AgentName: "Mend Unified Agent",
AgentVersion: "3.3.3",
AggregateProjectName: config.ProjectName,
BuildTool: "maven",
ProductVersion: config.ProductVersion,
Coordinates: versioning.Coordinates{GroupID: "com.sap", ArtifactID: "myproduct", Version: "1.3.4"},
}
scan.AppendScannedProject("testProject")
alerts := []Alert{
{Library: Library{KeyID: 42, Name: "log4j", GroupID: "apache-logging", ArtifactID: "log4j", Version: "1.14", LibType: "MAVEN_ARTIFACT", Filename: "vul1"}, Vulnerability: Vulnerability{Name: "CVE-2022-001", CVSS3Score: 7.0, Score: 6, Severity: "medium", PublishDate: "01.01.2022"}},
{Library: Library{KeyID: 43, Name: "commons-lang", GroupID: "apache-commons", ArtifactID: "commons-lang", Version: "2.4.30", LibType: "MAVEN_ARTIFACT", Filename: "vul2"}, Vulnerability: Vulnerability{Name: "CVE-2022-002", CVSS3Score: 8.0, Severity: "high", PublishDate: "02.01.2022", TopFix: Fix{Message: "this is the top fix"}}},
{Library: Library{KeyID: 42, Name: "log4j", GroupID: "apache-logging", ArtifactID: "log4j", Version: "3.25", LibType: "MAVEN_ARTIFACT", Filename: "vul3"}, Vulnerability: Vulnerability{Name: "CVE-2022-003", Score: 6, Severity: "medium", PublishDate: "03.01.2022"}},
}
libraries := []Library{
{KeyID: 42, Name: "log4j", GroupID: "apache-logging", ArtifactID: "log4j", Version: "1.14", LibType: "MAVEN_ARTIFACT", Filename: "vul1", Dependencies: []Library{{KeyID: 43, Name: "commons-lang", GroupID: "apache-commons", ArtifactID: "commons-lang", Version: "2.4.30", LibType: "MAVEN_ARTIFACT", Filename: "vul2"}}},
{KeyID: 44, Name: "log4j", GroupID: "apache-logging", ArtifactID: "log4j", Version: "3.25", LibType: "MAVEN_ARTIFACT", Filename: "vul3", Dependencies: []Library{{KeyID: 45, Name: "commons-lang", GroupID: "apache-commons", ArtifactID: "commons-lang", Version: "3.15", LibType: "MAVEN_ARTIFACT", Filename: "vul2"}}},
}
contents, err := CreateCycloneSBOM(scan, &libraries, &alerts)
assert.NoError(t, err, "unexpected error")
goldenFilePath := filepath.Join("testdata", "sbom.golden")
expected, err := ioutil.ReadFile(goldenFilePath)
assert.NoError(t, err)
assert.Equal(t, string(expected), string(contents))
})
}
func TestWriteCycloneSBOM(t *testing.T) {
t.Parallel()
var utilsMock piperutils.FileUtils
utilsMock = &mock.FilesMock{}
t.Run("success case", func(t *testing.T) {
paths, err := WriteCycloneSBOM([]byte{1, 2, 3, 4}, utilsMock)
assert.NoError(t, err, "unexpexted error")
assert.Equal(t, 1, len(paths))
assert.Equal(t, "whitesource/piper_whitesource_sbom.xml", paths[0].Target)
exists, err := utilsMock.FileExists(paths[0].Target)
assert.NoError(t, err)
assert.True(t, exists)
})
}
func TestCreateSarifResultFile(t *testing.T) {
scan := &Scan{ProductVersion: "1"}
scan.AppendScannedProject("project1")

View File

@ -8,6 +8,7 @@ import (
"github.com/SAP/jenkins-library/pkg/log"
"github.com/SAP/jenkins-library/pkg/piperutils"
"github.com/SAP/jenkins-library/pkg/versioning"
)
// Scan stores information about scanned WhiteSource projects (modules).
@ -16,11 +17,13 @@ type Scan struct {
// It does not include the ProductVersion.
AggregateProjectName string
// ProductVersion is the global version that is used across all Projects (modules) during the scan.
BuildTool string
ProductVersion string
scannedProjects map[string]Project
scanTimes map[string]time.Time
AgentName string
AgentVersion string
Coordinates versioning.Coordinates
}
func (s *Scan) init() {

View File

@ -8,6 +8,7 @@ import (
"path/filepath"
"github.com/SAP/jenkins-library/pkg/log"
"github.com/pkg/errors"
)
const whiteSourceConfig = "whitesource.config.json"
@ -151,6 +152,9 @@ func getNpmProjectName(modulePath string, utils Utils) (string, error) {
}
var packageJSON = make(map[string]interface{})
err = json.Unmarshal(fileContents, &packageJSON)
if err != nil {
return "", errors.Wrapf(err, "failed to unmarshall the file '%s'", modulePath)
}
projectNameEntry, exists := packageJSON["name"]
if !exists {

View File

@ -124,6 +124,11 @@ func (m *SystemMock) GetProjectLibraryLocations(projectToken string) ([]Library,
return m.Libraries, nil
}
// GetProjectHierarchy returns the libraries stored in the SystemMock.
func (m *SystemMock) GetProjectHierarchy(projectToken string, inHouse bool) ([]Library, error) {
return m.Libraries, nil
}
// NewSystemMockWithProjectName returns a pointer to a new instance of SystemMock using a project with a defined name.
func NewSystemMockWithProjectName(lastUpdateDate, projectName string) *SystemMock {
mockLibrary := Library{

191
pkg/whitesource/testdata/sbom.golden vendored Normal file
View File

@ -0,0 +1,191 @@
<?xml version="1.0" encoding="UTF-8"?>
<bom xmlns="http://cyclonedx.org/schema/bom/1.4" version="1">
<metadata>
<component bom-ref="pkg:maven/com.sap/myproduct@1.3.4" type="library">
<group>com.sap</group>
<name>myproduct</name>
<version>1.3.4</version>
</component>
<properties>
<property name="internal:bom-identifier">testProject - 1</property>
</properties>
</metadata>
<components>
<component bom-ref="pkg:maven/apache-commons/commons-lang@2.4.30" type="library">
<author>apache-commons</author>
<name>commons-lang</name>
<version>2.4.30</version>
<purl>pkg:maven/apache-commons/commons-lang@2.4.30</purl>
</component>
<component bom-ref="pkg:maven/apache-commons/commons-lang@3.15" type="library">
<author>apache-commons</author>
<name>commons-lang</name>
<version>3.15</version>
<purl>pkg:maven/apache-commons/commons-lang@3.15</purl>
</component>
<component bom-ref="pkg:maven/apache-logging/log4j@1.14" type="library">
<author>apache-logging</author>
<name>log4j</name>
<version>1.14</version>
<purl>pkg:maven/apache-logging/log4j@1.14</purl>
</component>
<component bom-ref="pkg:maven/apache-logging/log4j@3.25" type="library">
<author>apache-logging</author>
<name>log4j</name>
<version>3.25</version>
<purl>pkg:maven/apache-logging/log4j@3.25</purl>
</component>
</components>
<dependencies>
<dependency ref="pkg:maven/apache-logging/log4j@1.14">
<dependency ref="pkg:maven/apache-commons/commons-lang@2.4.30"></dependency>
</dependency>
<dependency ref="pkg:maven/apache-logging/log4j@3.25">
<dependency ref="pkg:maven/apache-commons/commons-lang@3.15"></dependency>
</dependency>
<dependency ref="pkg:maven/com.sap/myproduct@1.3.4">
<dependency ref="pkg:maven/apache-logging/log4j@1.14"></dependency>
<dependency ref="pkg:maven/apache-logging/log4j@3.25"></dependency>
</dependency>
</dependencies>
<vulnerabilities>
<vulnerability bom-ref="pkg:maven/apache-logging/log4j@1.14">
<id>CVE-2022-001</id>
<source></source>
<references></references>
<ratings>
<rating>
<score>0</score>
<severity>medium</severity>
<method>CVSSv3</method>
</rating>
<rating>
<score>6</score>
<severity>medium</severity>
<method>CVSSv2</method>
</rating>
</ratings>
<advisories>
<advisory>
<url></url>
</advisory>
</advisories>
<published>01.01.2022</published>
<tools>
<tool>
<vendor>Mend</vendor>
<name>Mend Unified Agent</name>
<version>3.3.3</version>
<externalReferences>
<reference type="build-meta">
<url>https://www.mend.io/</url>
</reference>
</externalReferences>
</tool>
</tools>
<affects>
<target>
<ref>pkg:maven/apache-logging/log4j@1.14</ref>
<versions>
<version>
<version>1.14</version>
<status></status>
</version>
</versions>
</target>
</affects>
</vulnerability>
<vulnerability bom-ref="pkg:maven/apache-commons/commons-lang@2.4.30">
<id>CVE-2022-002</id>
<source></source>
<references></references>
<ratings>
<rating>
<score>0</score>
<severity>high</severity>
<method>CVSSv3</method>
</rating>
<rating>
<score>6</score>
<severity>high</severity>
<method>CVSSv2</method>
</rating>
</ratings>
<advisories>
<advisory>
<url></url>
</advisory>
</advisories>
<published>02.01.2022</published>
<tools>
<tool>
<vendor>Mend</vendor>
<name>Mend Unified Agent</name>
<version>3.3.3</version>
<externalReferences>
<reference type="build-meta">
<url>https://www.mend.io/</url>
</reference>
</externalReferences>
</tool>
</tools>
<affects>
<target>
<ref>pkg:maven/apache-commons/commons-lang@2.4.30</ref>
<versions>
<version>
<version>2.4.30</version>
<status></status>
</version>
</versions>
</target>
</affects>
</vulnerability>
<vulnerability bom-ref="pkg:maven/apache-logging/log4j@3.25">
<id>CVE-2022-003</id>
<source></source>
<references></references>
<ratings>
<rating>
<score>0</score>
<severity>medium</severity>
<method>CVSSv3</method>
</rating>
<rating>
<score>6</score>
<severity>medium</severity>
<method>CVSSv2</method>
</rating>
</ratings>
<advisories>
<advisory>
<url></url>
</advisory>
</advisories>
<published>03.01.2022</published>
<tools>
<tool>
<vendor>Mend</vendor>
<name>Mend Unified Agent</name>
<version>3.3.3</version>
<externalReferences>
<reference type="build-meta">
<url>https://www.mend.io/</url>
</reference>
</externalReferences>
</tool>
</tools>
<affects>
<target>
<ref>pkg:maven/apache-logging/log4j@3.25</ref>
<versions>
<version>
<version>3.25</version>
<status></status>
</version>
</versions>
</target>
</affects>
</vulnerability>
</vulnerabilities>
</bom>

View File

@ -8,9 +8,11 @@ import (
"net/http"
"time"
"github.com/SAP/jenkins-library/pkg/format"
piperhttp "github.com/SAP/jenkins-library/pkg/http"
"github.com/SAP/jenkins-library/pkg/log"
"github.com/SAP/jenkins-library/pkg/reporting"
"github.com/package-url/packageurl-go"
"github.com/pkg/errors"
)
@ -64,6 +66,44 @@ func (a Alert) Title() string {
return fmt.Sprintf("%v %v %v ", a.Type, a.Vulnerability.Name, a.Library.ArtifactID)
}
func (a Alert) ContainedIn(assessments *[]format.Assessment) (bool, error) {
localPurl := a.Library.ToPackageUrl().ToString()
for _, assessment := range *assessments {
if assessment.Vulnerability == a.Vulnerability.Name {
for _, purl := range assessment.Purls {
assessmentPurl, err := purl.ToPackageUrl()
assessmentPurlStr := assessmentPurl.ToString()
if err != nil {
log.SetErrorCategory(log.ErrorConfiguration)
log.Entry().WithError(err).Errorf("assessment from file ignored due to invalid packageUrl '%s'", purl)
return false, err
}
if assessmentPurlStr == localPurl {
return true, nil
}
}
}
}
return false, nil
}
func transformLibToPurlType(libType string) string {
// TODO verify and complete, only maven is proven so far
switch libType {
case "MAVEN_ARTIFACT":
return packageurl.TypeMaven
case "NODE_ARTIFACT":
return packageurl.TypeNPM
case "GOLANG_ARTIFACT":
return packageurl.TypeGolang
case "DOCKER_ARTIFACT":
return packageurl.TypeGolang
case "UNKNOWN_ARTIFACT":
return packageurl.TypeGeneric
}
return packageurl.TypeGeneric
}
func consolidate(cvss2severity, cvss3severity string, cvss2score, cvss3score float64) string {
switch cvss3severity {
case "low":
@ -152,11 +192,22 @@ Link: [%v](%v)`,
// Library
type Library struct {
Name string `json:"name,omitempty"`
Filename string `json:"filename,omitempty"`
ArtifactID string `json:"artifactId,omitempty"`
GroupID string `json:"groupId,omitempty"`
Version string `json:"version,omitempty"`
KeyUUID string `json:"keyUuid,omitempty"`
KeyID int `json:"keyId,omitempty"`
Name string `json:"name,omitempty"`
Filename string `json:"filename,omitempty"`
ArtifactID string `json:"artifactId,omitempty"`
GroupID string `json:"groupId,omitempty"`
Version string `json:"version,omitempty"`
Sha1 string `json:"sha1,omitempty"`
LibType string `json:"type,omitempty"`
Coordinates string `json:"coordinates,omitempty"`
Dependencies []Library `json:"dependencies,omitempty"`
}
// ToPackageUrl constructs and returns the package URL of the library
func (l Library) ToPackageUrl() *packageurl.PackageURL {
return packageurl.NewPackageURL(transformLibToPurlType(l.LibType), l.GroupID, l.ArtifactID, l.Version, nil, "")
}
// Vulnerability defines a vulnerability as returned by WhiteSource
@ -221,6 +272,7 @@ type Request struct {
AlertsEmailReceivers *Assignment `json:"alertsEmailReceivers,omitempty"`
ProductApprovers *Assignment `json:"productApprovers,omitempty"`
ProductIntegrators *Assignment `json:"productIntegrators,omitempty"`
IncludeInHouseData bool `json:"includeInHouseData,omitempty"`
}
// System defines a WhiteSource System including respective tokens (e.g. org token, user token)
@ -346,6 +398,28 @@ func (s *System) GetProjectsMetaInfo(productToken string) ([]Project, error) {
return wsResponse.ProjectVitals, nil
}
// GetProjectHierarchy retrieves the full set of libraries that the project depends on
func (s *System) GetProjectHierarchy(projectToken string, includeInHouse bool) ([]Library, error) {
wsResponse := struct {
Libraries []Library `json:"libraries"`
}{
Libraries: []Library{},
}
req := Request{
RequestType: "getProjectHierarchy",
ProductToken: projectToken,
IncludeInHouseData: includeInHouse,
}
err := s.sendRequestAndDecodeJSON(req, &wsResponse)
if err != nil {
return nil, err
}
return wsResponse.Libraries, nil
}
// GetProjectToken returns the project token for a project with a given name
func (s *System) GetProjectToken(productToken, projectName string) (string, error) {
project, err := s.GetProjectByName(productToken, projectName)

View File

@ -55,6 +55,7 @@ func TestGetProductsMetaInfo(t *testing.T) {
sys := System{serverURL: "https://my.test.server", httpClient: &myTestClient, orgToken: "test_org_token", userToken: "test_user_token"}
products, err := sys.GetProductsMetaInfo()
assert.NoError(t, err)
requestBody, err := ioutil.ReadAll(myTestClient.requestBody)
assert.NoError(t, err)
@ -167,6 +168,7 @@ func TestGetProjectsMetaInfo(t *testing.T) {
sys := System{serverURL: "https://my.test.server", httpClient: &myTestClient, orgToken: "test_org_token", userToken: "test_user_token"}
projects, err := sys.GetProjectsMetaInfo("test_product_token")
assert.NoError(t, err)
requestBody, err := ioutil.ReadAll(myTestClient.requestBody)
assert.NoError(t, err)
@ -293,6 +295,138 @@ func TestGetProductName(t *testing.T) {
assert.Equal(t, "Test Product", productName)
}
func TestGetProjectHierarchy(t *testing.T) {
myTestClient := whitesourceMockClient{
responseBody: `{
"libraries": [
{
"keyUuid": "1f9ee6ec-eded-45d3-8fdb-2d0d735e5b14",
"keyId": 43,
"filename": "log4j-1.2.17.jar",
"name": "log4j",
"groupId": "log4j",
"artifactId": "log4j",
"version": "1.2.17",
"sha1": "5af35056b4d257e4b64b9e8069c0746e8b08629f",
"type": "UNKNOWN_ARTIFACT",
"coordinates": "log4j:log4j:1.2.17"
},
{
"keyUuid": "f362c53f-ce25-4d0c-b53b-ee2768b32d1a",
"keyId": 45,
"filename": "akka-actor_2.11-2.5.2.jar",
"name": "akka-actor",
"groupId": "com.typesafe.akka",
"artifactId": "akka-actor_2.11",
"version": "2.5.2",
"sha1": "183ccaed9002bfa10628a5df48e7bac6f1c03f7b",
"type": "MAVEN_ARTIFACT",
"coordinates": "com.typesafe.akka:akka-actor_2.11:2.5.2",
"dependencies": [
{
"keyUuid": "49c6840d-bf96-470f-8892-6c2a536c91eb",
"keyId": 44,
"filename": "scala-library-2.11.11.jar",
"name": "Scala Library",
"groupId": "org.scala-lang",
"artifactId": "scala-library",
"version": "2.11.11",
"sha1": "e283d2b7fde6504f6a86458b1f6af465353907cc",
"type": "MAVEN_ARTIFACT",
"coordinates": "org.scala-lang:scala-library:2.11.11"
},
{
"keyUuid": "e5e730d1-8b41-4d2d-a8c5-610a374b6501",
"keyId": 46,
"filename": "scala-java8-compat_2.11-0.7.0.jar",
"name": "scala-java8-compat_2.11",
"groupId": "org.scala-lang.modules",
"artifactId": "scala-java8-compat_2.11",
"version": "0.7.0",
"sha1": "a31b1b36bcf0d53657733b5d40c78d5f090a5dea",
"type": "UNKNOWN_ARTIFACT",
"coordinates": "org.scala-lang.modules:scala-java8-compat_2.11:0.7.0"
},
{
"keyUuid": "426c0056-f180-4cac-a9dd-c266a76b32c9",
"keyId": 47,
"filename": "config-1.3.1.jar",
"name": "config",
"groupId": "com.typesafe",
"artifactId": "config",
"version": "1.3.1",
"sha1": "2cf7a6cc79732e3bdf1647d7404279900ca63eb0",
"type": "UNKNOWN_ARTIFACT",
"coordinates": "com.typesafe:config:1.3.1"
}
]
},
{
"keyUuid": "25a8ceaa-4548-4fe4-9819-8658b8cbe9aa",
"keyId": 48,
"filename": "kafka-clients-0.10.2.1.jar",
"name": "Apache Kafka",
"groupId": "org.apache.kafka",
"artifactId": "kafka-clients",
"version": "0.10.2.1",
"sha1": "3dd2aa4c9f87ac54175d017bcb63b4bb5dca63dd",
"type": "MAVEN_ARTIFACT",
"coordinates": "org.apache.kafka:kafka-clients:0.10.2.1",
"dependencies": [
{
"keyUuid": "71065ffb-e509-4e2d-88bc-9184bc50888d",
"keyId": 49,
"filename": "lz4-1.3.0.jar",
"name": "LZ4 and xxHash",
"groupId": "net.jpountz.lz4",
"artifactId": "lz4",
"version": "1.3.0",
"sha1": "c708bb2590c0652a642236ef45d9f99ff842a2ce",
"type": "MAVEN_ARTIFACT",
"coordinates": "net.jpountz.lz4:lz4:1.3.0"
},
{
"keyUuid": "e44ab569-de95-4562-8efa-a2ebfe808471",
"keyId": 50,
"filename": "slf4j-api-1.7.21.jar",
"name": "SLF4J API Module",
"groupId": "org.slf4j",
"artifactId": "slf4j-api",
"version": "1.7.21",
"sha1": "139535a69a4239db087de9bab0bee568bf8e0b70",
"type": "MAVEN_ARTIFACT",
"coordinates": "org.slf4j:slf4j-api:1.7.21"
},
{
"keyUuid": "72ecad5e-9f35-466c-9ed8-0974e7ce4e29",
"keyId": 51,
"filename": "snappy-java-1.1.2.6.jar",
"name": "snappy-java",
"groupId": "org.xerial.snappy",
"artifactId": "snappy-java",
"version": "1.1.2.6",
"sha1": "48d92871ca286a47f230feb375f0bbffa83b85f6",
"type": "UNKNOWN_ARTIFACT",
"coordinates": "org.xerial.snappy:snappy-java:1.1.2.6"
}
]
}
]
}`,
}
sys := System{serverURL: "https://my.test.server", httpClient: &myTestClient, orgToken: "test_org_token", userToken: "test_user_token"}
libraries, err := sys.GetProjectHierarchy("test_project_token", true)
assert.NoError(t, err)
assert.Equal(t, 3, len(libraries))
assert.Nil(t, libraries[0].Dependencies)
assert.NotNil(t, libraries[1].Dependencies)
assert.Equal(t, 3, len(libraries[1].Dependencies))
assert.NotNil(t, libraries[2].Dependencies)
assert.Equal(t, 3, len(libraries[2].Dependencies))
}
func TestGetProjectsByIDs(t *testing.T) {
responseBody :=
`{

View File

@ -19,6 +19,9 @@ spec:
- name: checkmarxCredentialsId
description: Jenkins 'Username with password' credentials ID containing username and password to communicate with the Checkmarx backend.
type: jenkins
- name: githubTokenCredentialsId
description: Jenkins 'Secret text' credentials ID containing token to authenticate to GitHub.
type: jenkins
resources:
- name: checkmarx
type: stash

View File

@ -18,6 +18,9 @@ spec:
- name: apiTokenCredentialsId
description: Jenkins 'Secret text' credentials ID containing the API token used to authenticate with the Synopsis Detect (formerly BlackDuck) Server.
type: jenkins
- name: githubTokenCredentialsId
description: Jenkins 'Secret text' credentials ID containing token to authenticate to GitHub.
type: jenkins
params:
- name: token
aliases:

View File

@ -81,6 +81,14 @@ spec:
- PARAMETERS
- STAGES
- STEPS
- name: assessmentFile
type: string
description: "Explicit path to the assessment YAML file."
scope:
- PARAMETERS
- STAGES
- STEPS
default: "hs-assessments.yaml"
- name: buildDescriptorExcludeList
type: "[]string"
description: "List of build descriptors and therefore modules to exclude from the scan and assessment activities."