You've already forked sap-jenkins-library
mirror of
https://github.com/SAP/jenkins-library.git
synced 2025-09-16 09:26:22 +02:00
Upload Fortify scan results to GitHub issue (#3300)
* fix(fortifyExecuteScan): Propagate translation errors Force translation related errors to stop the execution of the step. * Extend testcase * Update fortifyExecuteScan.go * Fix fmt and test * Fix code * feat(fortifyExecuteScan): Create GitHub issue * Fix expectation * Fix fmt * Fix fmt add test * Added tests * Go fmt * Add switch * Rewrite githubCreateIssue * Fix tests * Added switch * Issue only in case of violations * Fix CPE reference * Add debug message to issue creation/update * Update fortifyExecuteScan.go * Add credential for GH to groovy wrapper * Update fortifyExecuteScan.go
This commit is contained in:
@@ -158,9 +158,13 @@ func runFortifyScan(config fortifyExecuteScanOptions, sys fortify.System, utils
|
||||
}
|
||||
log.Entry().Debugf("Looked up / created project version with ID %v for PR %v", projectVersion.ID, fortifyProjectVersion)
|
||||
} else {
|
||||
prID := determinePullRequestMerge(config)
|
||||
prID, prAuthor := determinePullRequestMerge(config)
|
||||
if len(prID) > 0 {
|
||||
log.Entry().Debugf("Determined PR ID '%v' for merge check", prID)
|
||||
if len(prAuthor) > 0 && !piperutils.ContainsString(config.Assignees, prAuthor) {
|
||||
log.Entry().Debugf("Determined PR Author '%v' for result assignment", prAuthor)
|
||||
config.Assignees = append(config.Assignees, prAuthor)
|
||||
}
|
||||
pullRequestProjectName := fmt.Sprintf("PR-%v", prID)
|
||||
err = sys.MergeProjectVersionStateOfPRIntoMaster(config.FprDownloadEndpoint, config.FprUploadEndpoint, project.ID, projectVersion.ID, pullRequestProjectName)
|
||||
if err != nil {
|
||||
@@ -294,12 +298,22 @@ func verifyFFProjectCompliance(config fortifyExecuteScanOptions, sys fortify.Sys
|
||||
|
||||
fortifyReportingData := prepareReportData(influx)
|
||||
scanReport := fortify.CreateCustomReport(fortifyReportingData, issueGroups)
|
||||
paths, err := fortify.WriteCustomReports(scanReport, influx.fortify_data.fields.projectName, influx.fortify_data.fields.projectVersion)
|
||||
paths, err := fortify.WriteCustomReports(scanReport)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to write custom reports"), reports
|
||||
}
|
||||
reports = append(reports, paths...)
|
||||
|
||||
log.Entry().Debug("Checking whether GitHub issue creation/update is active")
|
||||
log.Entry().Debugf("%v, %v, %v, %v, %v, %v", config.CreateResultIssue, numberOfViolations > 0, len(config.GithubToken) > 0, len(config.GithubAPIURL) > 0, len(config.Owner) > 0, len(config.Repository) > 0)
|
||||
if config.CreateResultIssue && numberOfViolations > 0 && len(config.GithubToken) > 0 && len(config.GithubAPIURL) > 0 && len(config.Owner) > 0 && len(config.Repository) > 0 {
|
||||
log.Entry().Debug("Creating/updating GitHub issue with scan results")
|
||||
err = fortify.UploadReportToGithub(scanReport, config.GithubToken, config.GithubAPIURL, config.Owner, config.Repository, config.Assignees)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to upload scan results into GitHub"), reports
|
||||
}
|
||||
}
|
||||
|
||||
jsonReport := fortify.CreateJSONReport(fortifyReportingData, spotChecksCountByCategory, config.ServerURL)
|
||||
paths, err = fortify.WriteJSONReport(jsonReport)
|
||||
if err != nil {
|
||||
@@ -330,6 +344,7 @@ func prepareReportData(influx *fortifyExecuteScanInflux) fortify.FortifyReportDa
|
||||
output.Suppressed = input.suppressed
|
||||
output.Suspicious = input.suspicious
|
||||
output.ProjectVersionID = input.projectVersionID
|
||||
output.Violations = input.violations
|
||||
return output
|
||||
}
|
||||
|
||||
@@ -805,7 +820,10 @@ func triggerFortifyScan(config fortifyExecuteScanOptions, utils fortifyUtils, bu
|
||||
return fmt.Errorf("buildTool '%s' is not supported by this step", config.BuildTool)
|
||||
}
|
||||
|
||||
translateProject(&config, utils, buildID, classpath)
|
||||
err = translateProject(&config, utils, buildID, classpath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return scanProject(&config, utils, buildID, buildLabel, buildProject)
|
||||
}
|
||||
@@ -851,7 +869,7 @@ func populateMavenTranslate(config *fortifyExecuteScanOptions, classpath string)
|
||||
return string(translateJSON), err
|
||||
}
|
||||
|
||||
func translateProject(config *fortifyExecuteScanOptions, utils fortifyUtils, buildID, classpath string) {
|
||||
func translateProject(config *fortifyExecuteScanOptions, utils fortifyUtils, buildID, classpath string) error {
|
||||
var translateList []map[string]string
|
||||
json.Unmarshal([]byte(config.Translate), &translateList)
|
||||
log.Entry().Debugf("Translating with options: %v", translateList)
|
||||
@@ -859,8 +877,12 @@ func translateProject(config *fortifyExecuteScanOptions, utils fortifyUtils, bui
|
||||
if len(classpath) > 0 {
|
||||
translate["autoClasspath"] = classpath
|
||||
}
|
||||
handleSingleTranslate(config, utils, buildID, translate)
|
||||
err := handleSingleTranslate(config, utils, buildID, translate)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func handleSingleTranslate(config *fortifyExecuteScanOptions, command fortifyUtils, buildID string, t map[string]string) error {
|
||||
@@ -917,14 +939,15 @@ func scanProject(config *fortifyExecuteScanOptions, command fortifyUtils, buildI
|
||||
return nil
|
||||
}
|
||||
|
||||
func determinePullRequestMerge(config fortifyExecuteScanOptions) string {
|
||||
func determinePullRequestMerge(config fortifyExecuteScanOptions) (string, string) {
|
||||
author := ""
|
||||
ctx, client, err := piperGithub.NewClient(config.GithubToken, config.GithubAPIURL, "")
|
||||
if err == nil {
|
||||
result, err := determinePullRequestMergeGithub(ctx, config, client.PullRequests)
|
||||
prID, author, err := determinePullRequestMergeGithub(ctx, config, client.PullRequests)
|
||||
if err != nil {
|
||||
log.Entry().WithError(err).Warn("Failed to get PR metadata via GitHub client")
|
||||
} else {
|
||||
return result
|
||||
return prID, author
|
||||
}
|
||||
}
|
||||
|
||||
@@ -932,18 +955,18 @@ func determinePullRequestMerge(config fortifyExecuteScanOptions) string {
|
||||
r, _ := regexp.Compile(config.PullRequestMessageRegex)
|
||||
matches := r.FindSubmatch([]byte(config.CommitMessage))
|
||||
if matches != nil && len(matches) > 1 {
|
||||
return string(matches[config.PullRequestMessageRegexGroup])
|
||||
return string(matches[config.PullRequestMessageRegexGroup]), author
|
||||
}
|
||||
return ""
|
||||
return "", ""
|
||||
}
|
||||
|
||||
func determinePullRequestMergeGithub(ctx context.Context, config fortifyExecuteScanOptions, pullRequestServiceInstance pullRequestService) (string, error) {
|
||||
func determinePullRequestMergeGithub(ctx context.Context, config fortifyExecuteScanOptions, pullRequestServiceInstance pullRequestService) (string, string, error) {
|
||||
options := github.PullRequestListOptions{State: "closed", Sort: "updated", Direction: "desc"}
|
||||
prList, _, err := pullRequestServiceInstance.ListPullRequestsWithCommit(ctx, config.Owner, config.Repository, config.CommitID, &options)
|
||||
if err == nil && len(prList) > 0 {
|
||||
return fmt.Sprintf("%v", prList[0].GetNumber()), nil
|
||||
return fmt.Sprintf("%v", prList[0].GetNumber()), *prList[0].User.Email, nil
|
||||
}
|
||||
return "", err
|
||||
return "", "", err
|
||||
}
|
||||
|
||||
func appendToOptions(config *fortifyExecuteScanOptions, options []string, t map[string]string) []string {
|
||||
|
@@ -19,6 +19,7 @@ import (
|
||||
|
||||
type fortifyExecuteScanOptions struct {
|
||||
AdditionalScanParameters []string `json:"additionalScanParameters,omitempty"`
|
||||
Assignees []string `json:"assignees,omitempty"`
|
||||
AuthToken string `json:"authToken,omitempty"`
|
||||
BuildDescriptorExcludeList []string `json:"buildDescriptorExcludeList,omitempty"`
|
||||
CustomScanVersion string `json:"customScanVersion,omitempty"`
|
||||
@@ -72,6 +73,7 @@ type fortifyExecuteScanOptions struct {
|
||||
M2Path string `json:"m2Path,omitempty"`
|
||||
VerifyOnly bool `json:"verifyOnly,omitempty"`
|
||||
InstallArtifacts bool `json:"installArtifacts,omitempty"`
|
||||
CreateResultIssue bool `json:"createResultIssue,omitempty"`
|
||||
}
|
||||
|
||||
type fortifyExecuteScanInflux struct {
|
||||
@@ -246,6 +248,7 @@ Besides triggering a scan the step verifies the results after they have been upl
|
||||
|
||||
func addFortifyExecuteScanFlags(cmd *cobra.Command, stepConfig *fortifyExecuteScanOptions) {
|
||||
cmd.Flags().StringSliceVar(&stepConfig.AdditionalScanParameters, "additionalScanParameters", []string{}, "List of additional scan parameters to be used for Fortify sourceanalyzer command execution.")
|
||||
cmd.Flags().StringSliceVar(&stepConfig.Assignees, "assignees", []string{``}, "Defines the assignees for the Github Issue created/updated with the results of the scan.")
|
||||
cmd.Flags().StringVar(&stepConfig.AuthToken, "authToken", os.Getenv("PIPER_authToken"), "The FortifyToken to use for authentication")
|
||||
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.CustomScanVersion, "customScanVersion", os.Getenv("PIPER_customScanVersion"), "Custom version of the Fortify project used as source.")
|
||||
@@ -299,6 +302,7 @@ func addFortifyExecuteScanFlags(cmd *cobra.Command, stepConfig *fortifyExecuteSc
|
||||
cmd.Flags().StringVar(&stepConfig.M2Path, "m2Path", os.Getenv("PIPER_m2Path"), "Path to the location of the local repository that should be used.")
|
||||
cmd.Flags().BoolVar(&stepConfig.VerifyOnly, "verifyOnly", false, "Whether the step shall only apply verification checks or whether it does a full scan and check cycle")
|
||||
cmd.Flags().BoolVar(&stepConfig.InstallArtifacts, "installArtifacts", false, "If enabled, it will install all artifacts to the local maven repository to make them available before running Fortify. This is required if any maven module has dependencies to other modules in the repository and they were not installed before.")
|
||||
cmd.Flags().BoolVar(&stepConfig.CreateResultIssue, "createResultIssue", false, "Whether the step creates a GitHub issue containing the scan results in the originating repo. Since optimized pipelines are headless the creation is implicitly activated for schedules runs.")
|
||||
|
||||
cmd.MarkFlagRequired("authToken")
|
||||
cmd.MarkFlagRequired("serverUrl")
|
||||
@@ -335,6 +339,15 @@ func fortifyExecuteScanMetadata() config.StepData {
|
||||
Aliases: []config.Alias{},
|
||||
Default: []string{},
|
||||
},
|
||||
{
|
||||
Name: "assignees",
|
||||
ResourceRef: []config.ResourceReference{},
|
||||
Scope: []string{"PARAMETERS", "STAGES", "STEPS"},
|
||||
Type: "[]string",
|
||||
Mandatory: false,
|
||||
Aliases: []config.Alias{},
|
||||
Default: []string{``},
|
||||
},
|
||||
{
|
||||
Name: "authToken",
|
||||
ResourceRef: []config.ResourceReference{
|
||||
@@ -870,6 +883,20 @@ func fortifyExecuteScanMetadata() config.StepData {
|
||||
Aliases: []config.Alias{},
|
||||
Default: false,
|
||||
},
|
||||
{
|
||||
Name: "createResultIssue",
|
||||
ResourceRef: []config.ResourceReference{
|
||||
{
|
||||
Name: "commonPipelineEnvironment",
|
||||
Param: "custom/optimizedAndScheduled",
|
||||
},
|
||||
},
|
||||
Scope: []string{"PARAMETERS", "STAGES", "STEPS"},
|
||||
Type: "bool",
|
||||
Mandatory: false,
|
||||
Aliases: []config.Alias{},
|
||||
Default: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
Containers: []config.Container{
|
||||
|
@@ -19,6 +19,7 @@ import (
|
||||
|
||||
"github.com/SAP/jenkins-library/pkg/fortify"
|
||||
"github.com/SAP/jenkins-library/pkg/log"
|
||||
"github.com/SAP/jenkins-library/pkg/piperutils"
|
||||
"github.com/SAP/jenkins-library/pkg/versioning"
|
||||
|
||||
"github.com/google/go-github/v32/github"
|
||||
@@ -27,6 +28,8 @@ import (
|
||||
"github.com/piper-validation/fortify-client-go/models"
|
||||
)
|
||||
|
||||
const author string = "john.doe@dummy.com"
|
||||
|
||||
type fortifyTestUtilsBundle struct {
|
||||
*execRunnerMock
|
||||
*mock.FilesMock
|
||||
@@ -277,11 +280,13 @@ func (f *fortifyMock) DownloadResultFile(endpoint string, projectVersionID int64
|
||||
type pullRequestServiceMock struct{}
|
||||
|
||||
func (prService pullRequestServiceMock) ListPullRequestsWithCommit(ctx context.Context, owner, repo, sha string, opts *github.PullRequestListOptions) ([]*github.PullRequest, *github.Response, error) {
|
||||
authorString := author
|
||||
user := github.User{Email: &authorString}
|
||||
if owner == "A" {
|
||||
result := 17
|
||||
return []*github.PullRequest{{Number: &result}}, &github.Response{}, nil
|
||||
return []*github.PullRequest{{Number: &result, User: &user}}, &github.Response{}, nil
|
||||
} else if owner == "C" {
|
||||
return []*github.PullRequest{}, &github.Response{}, errors.New("Test error")
|
||||
return []*github.PullRequest{{User: &user}}, &github.Response{}, errors.New("Test error")
|
||||
}
|
||||
return []*github.PullRequest{}, &github.Response{}, nil
|
||||
}
|
||||
@@ -333,6 +338,9 @@ func (er *execRunnerMock) Stderr(err io.Writer) {
|
||||
func (er *execRunnerMock) RunExecutable(e string, p ...string) error {
|
||||
er.numExecutions++
|
||||
er.currentExecution().executable = e
|
||||
if len(p) > 0 && piperutils.ContainsString(p, "--failTranslate") {
|
||||
return errors.New("Translate failed")
|
||||
}
|
||||
er.currentExecution().parameters = p
|
||||
classpathPip := "/usr/lib/python35.zip;/usr/lib/python3.5;/usr/lib/python3.5/plat-x86_64-linux-gnu;/usr/lib/python3.5/lib-dynload;/home/piper/.local/lib/python3.5/site-packages;/usr/local/lib/python3.5/dist-packages;/usr/lib/python3/dist-packages;./lib"
|
||||
classpathMaven := "some.jar;someother.jar"
|
||||
@@ -386,6 +394,12 @@ func TestExecutions(t *testing.T) {
|
||||
config: fortifyExecuteScanOptions{BuildTool: "golang", BuildDescriptorFile: "go.mod", VerifyOnly: true},
|
||||
expectedReportsLength: 0,
|
||||
},
|
||||
{
|
||||
nameOfRun: "maven scan and verify",
|
||||
config: fortifyExecuteScanOptions{BuildTool: "maven", BuildDescriptorFile: "pom.xml", UpdateRulePack: true, Reporting: true, UploadResults: true},
|
||||
expectedReportsLength: 2,
|
||||
expectedReports: []string{"target/fortify-scan.*", "target/*.fpr"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, data := range testData {
|
||||
@@ -729,14 +743,16 @@ func TestDeterminePullRequestMerge(t *testing.T) {
|
||||
config := fortifyExecuteScanOptions{CommitMessage: "Merge pull request #2462 from branch f-test", PullRequestMessageRegex: `(?m).*Merge pull request #(\d+) from.*`, PullRequestMessageRegexGroup: 1}
|
||||
|
||||
t.Run("success", func(t *testing.T) {
|
||||
match := determinePullRequestMerge(config)
|
||||
match, authorString := determinePullRequestMerge(config)
|
||||
assert.Equal(t, "2462", match, "Expected different result")
|
||||
assert.Equal(t, "", authorString, "Expected different result")
|
||||
})
|
||||
|
||||
t.Run("no match", func(t *testing.T) {
|
||||
config.CommitMessage = "Some test commit"
|
||||
match := determinePullRequestMerge(config)
|
||||
match, authorString := determinePullRequestMerge(config)
|
||||
assert.Equal(t, "", match, "Expected different result")
|
||||
assert.Equal(t, "", authorString, "Expected different result")
|
||||
})
|
||||
}
|
||||
|
||||
@@ -744,21 +760,24 @@ func TestDeterminePullRequestMergeGithub(t *testing.T) {
|
||||
prServiceMock := pullRequestServiceMock{}
|
||||
|
||||
t.Run("success", func(t *testing.T) {
|
||||
match, err := determinePullRequestMergeGithub(nil, fortifyExecuteScanOptions{Owner: "A"}, prServiceMock)
|
||||
match, authorString, err := determinePullRequestMergeGithub(nil, fortifyExecuteScanOptions{Owner: "A"}, prServiceMock)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "17", match, "Expected different result")
|
||||
assert.Equal(t, author, authorString, "Expected different result")
|
||||
})
|
||||
|
||||
t.Run("no match", func(t *testing.T) {
|
||||
match, err := determinePullRequestMergeGithub(nil, fortifyExecuteScanOptions{Owner: "B"}, prServiceMock)
|
||||
match, authorString, err := determinePullRequestMergeGithub(nil, fortifyExecuteScanOptions{Owner: "B"}, prServiceMock)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "", match, "Expected different result")
|
||||
assert.Equal(t, "", authorString, "Expected different result")
|
||||
})
|
||||
|
||||
t.Run("error", func(t *testing.T) {
|
||||
match, err := determinePullRequestMergeGithub(nil, fortifyExecuteScanOptions{Owner: "C"}, prServiceMock)
|
||||
match, authorString, err := determinePullRequestMergeGithub(nil, fortifyExecuteScanOptions{Owner: "C"}, prServiceMock)
|
||||
assert.EqualError(t, err, "Test error")
|
||||
assert.Equal(t, "", match, "Expected different result")
|
||||
assert.Equal(t, "", authorString, "Expected different result")
|
||||
})
|
||||
}
|
||||
|
||||
@@ -794,6 +813,14 @@ func TestTranslateProject(t *testing.T) {
|
||||
assert.Equal(t, "sourceanalyzer", utils.executions[0].executable, "Expected different executable")
|
||||
assert.Equal(t, []string{"-verbose", "-64", "-b", "/commit/7267658798797", "-Xmx2G", "-cp", "./WEB-INF/lib/*.jar", "-extdirs", "tmp/", "-source", "1.8", "-jdk", "1.8.0-21", "-sourcepath", "src/ext/", "./**/*"}, utils.executions[0].parameters, "Expected different parameters")
|
||||
})
|
||||
|
||||
t.Run("failure propagated", func(t *testing.T) {
|
||||
utils := newFortifyTestUtilsBundle()
|
||||
config := fortifyExecuteScanOptions{BuildTool: "maven", Memory: "-Xmx2G", Translate: `[{"classpath":"./classes/*.jar", "extdirs":"tmp/","jdk":"1.8.0-21","source":"1.8","sourcepath":"src/ext/","src":"./**/*"}]`}
|
||||
err := translateProject(&config, &utils, "--failTranslate", "./WEB-INF/lib/*.jar")
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, "failed to execute sourceanalyzer translate command with options [-verbose -64 -b --failTranslate -Xmx2G -cp ./WEB-INF/lib/*.jar -extdirs tmp/ -source 1.8 -jdk 1.8.0-21 -sourcepath src/ext/ ./**/*]: Translate failed", err.Error())
|
||||
})
|
||||
}
|
||||
|
||||
func TestScanProject(t *testing.T) {
|
||||
|
@@ -1,108 +1,53 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
|
||||
"github.com/SAP/jenkins-library/pkg/log"
|
||||
"github.com/SAP/jenkins-library/pkg/telemetry"
|
||||
"github.com/google/go-github/v32/github"
|
||||
"github.com/pkg/errors"
|
||||
|
||||
piperGithub "github.com/SAP/jenkins-library/pkg/github"
|
||||
)
|
||||
|
||||
type githubCreateIssueService interface {
|
||||
Create(ctx context.Context, owner string, repo string, issue *github.IssueRequest) (*github.Issue, *github.Response, error)
|
||||
}
|
||||
|
||||
type githubSearchIssuesService interface {
|
||||
Issues(ctx context.Context, query string, opts *github.SearchOptions) (*github.IssuesSearchResult, *github.Response, error)
|
||||
}
|
||||
|
||||
type githubCreateCommentService interface {
|
||||
CreateComment(ctx context.Context, owner string, repo string, number int, comment *github.IssueComment) (*github.IssueComment, *github.Response, error)
|
||||
}
|
||||
|
||||
func githubCreateIssue(config githubCreateIssueOptions, telemetryData *telemetry.CustomData) {
|
||||
ctx, client, err := piperGithub.NewClient(config.Token, config.APIURL, "")
|
||||
if err != nil {
|
||||
log.Entry().WithError(err).Fatal("Failed to get GitHub client")
|
||||
}
|
||||
err = runGithubCreateIssue(ctx, &config, telemetryData, client.Issues, client.Search, client.Issues, ioutil.ReadFile)
|
||||
err := runGithubCreateIssue(&config, telemetryData)
|
||||
if err != nil {
|
||||
log.Entry().WithError(err).Fatal("Failed to comment on issue")
|
||||
}
|
||||
}
|
||||
|
||||
func runGithubCreateIssue(ctx context.Context, config *githubCreateIssueOptions, _ *telemetry.CustomData, ghCreateIssueService githubCreateIssueService, ghSearchIssuesService githubSearchIssuesService, ghCreateCommentService githubCreateCommentService, readFile func(string) ([]byte, error)) error {
|
||||
func runGithubCreateIssue(config *githubCreateIssueOptions, _ *telemetry.CustomData) error {
|
||||
|
||||
options := piperGithub.CreateIssueOptions{}
|
||||
err := transformConfig(config, &options, ioutil.ReadFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return piperGithub.CreateIssue(&options)
|
||||
}
|
||||
|
||||
func transformConfig(config *githubCreateIssueOptions, options *piperGithub.CreateIssueOptions, readFile func(string) ([]byte, error)) error {
|
||||
options.Token = config.Token
|
||||
options.APIURL = config.APIURL
|
||||
options.Owner = config.Owner
|
||||
options.Repository = config.Repository
|
||||
options.Title = config.Title
|
||||
options.Body = []byte(config.Body)
|
||||
options.Assignees = config.Assignees
|
||||
options.UpdateExisting = config.UpdateExisting
|
||||
|
||||
if len(config.Body)+len(config.BodyFilePath) == 0 {
|
||||
return fmt.Errorf("either parameter `body` or parameter `bodyFilePath` is required")
|
||||
}
|
||||
|
||||
issue := github.IssueRequest{
|
||||
Title: &config.Title,
|
||||
}
|
||||
|
||||
if len(config.Body) > 0 {
|
||||
issue.Body = &config.Body
|
||||
} else {
|
||||
if len(config.Body) == 0 {
|
||||
issueContent, err := readFile(config.BodyFilePath)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "failed to read file '%v'", config.BodyFilePath)
|
||||
}
|
||||
body := string(issueContent)
|
||||
issue.Body = &body
|
||||
options.Body = issueContent
|
||||
}
|
||||
|
||||
if len(config.Assignees) > 0 {
|
||||
issue.Assignees = &config.Assignees
|
||||
} else {
|
||||
issue.Assignees = &[]string{}
|
||||
}
|
||||
|
||||
var existingIssue *github.Issue = nil
|
||||
|
||||
if config.UpdateExisting {
|
||||
queryString := fmt.Sprintf("is:open is:issue repo:%v/%v in:title %v", config.Owner, config.Repository, config.Title)
|
||||
searchResult, resp, err := ghSearchIssuesService.Issues(ctx, queryString, nil)
|
||||
if err != nil {
|
||||
if resp != nil {
|
||||
log.Entry().Errorf("GitHub response code %v", resp.Status)
|
||||
}
|
||||
return errors.Wrap(err, "error occurred when looking for existing issue")
|
||||
} else {
|
||||
for _, value := range searchResult.Issues {
|
||||
if value != nil && *value.Title == config.Title {
|
||||
existingIssue = value
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if existingIssue != nil {
|
||||
comment := &github.IssueComment{Body: issue.Body}
|
||||
_, resp, err := ghCreateCommentService.CreateComment(ctx, config.Owner, config.Repository, *existingIssue.Number, comment)
|
||||
if err != nil {
|
||||
if resp != nil {
|
||||
log.Entry().Errorf("GitHub response code %v", resp.Status)
|
||||
}
|
||||
return errors.Wrap(err, "error occurred when looking for existing issue")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if existingIssue == nil {
|
||||
newIssue, resp, err := ghCreateIssueService.Create(ctx, config.Owner, config.Repository, &issue)
|
||||
if err != nil {
|
||||
if resp != nil {
|
||||
log.Entry().Errorf("GitHub response code %v", resp.Status)
|
||||
}
|
||||
return errors.Wrap(err, "error occurred when creating issue")
|
||||
}
|
||||
log.Entry().Debugf("New issue created: %v", newIssue)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
@@ -1,114 +1,21 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"testing"
|
||||
|
||||
"github.com/SAP/jenkins-library/pkg/mock"
|
||||
|
||||
"github.com/google/go-github/v32/github"
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
piperGithub "github.com/SAP/jenkins-library/pkg/github"
|
||||
)
|
||||
|
||||
type ghCreateIssueMock struct {
|
||||
issue *github.IssueRequest
|
||||
issueID int64
|
||||
issueError error
|
||||
owner string
|
||||
repo string
|
||||
number int
|
||||
assignees []string
|
||||
}
|
||||
|
||||
func (g *ghCreateIssueMock) Create(ctx context.Context, owner string, repo string, issue *github.IssueRequest) (*github.Issue, *github.Response, error) {
|
||||
g.issue = issue
|
||||
g.owner = owner
|
||||
g.repo = repo
|
||||
g.assignees = *issue.Assignees
|
||||
|
||||
issueResponse := github.Issue{ID: &g.issueID, Title: issue.Title, Body: issue.Body}
|
||||
|
||||
ghRes := github.Response{Response: &http.Response{Status: "200"}}
|
||||
if g.issueError != nil {
|
||||
ghRes.Status = "401"
|
||||
}
|
||||
|
||||
return &issueResponse, &ghRes, g.issueError
|
||||
}
|
||||
|
||||
type ghSearchIssuesMock struct {
|
||||
issueID int64
|
||||
issueNumber int
|
||||
issueTitle string
|
||||
issueBody string
|
||||
issuesSearchResult *github.IssuesSearchResult
|
||||
issuesSearchError error
|
||||
}
|
||||
|
||||
func (g *ghSearchIssuesMock) Issues(ctx context.Context, query string, opts *github.SearchOptions) (*github.IssuesSearchResult, *github.Response, error) {
|
||||
|
||||
regex := regexp.MustCompile(`.*in:title (?P<Title>(.*))`)
|
||||
matches := regex.FindStringSubmatch(query)
|
||||
|
||||
g.issueTitle = matches[1]
|
||||
|
||||
issues := []*github.Issue{
|
||||
{
|
||||
ID: &g.issueID,
|
||||
Number: &g.issueNumber,
|
||||
Title: &g.issueTitle,
|
||||
Body: &g.issueBody,
|
||||
},
|
||||
}
|
||||
|
||||
total := len(issues)
|
||||
incompleteResults := false
|
||||
|
||||
g.issuesSearchResult = &github.IssuesSearchResult{
|
||||
Issues: issues,
|
||||
Total: &total,
|
||||
IncompleteResults: &incompleteResults,
|
||||
}
|
||||
|
||||
ghRes := github.Response{Response: &http.Response{Status: "200"}}
|
||||
if g.issuesSearchError != nil {
|
||||
ghRes.Status = "401"
|
||||
}
|
||||
|
||||
return g.issuesSearchResult, &ghRes, g.issuesSearchError
|
||||
}
|
||||
|
||||
type ghCreateCommentMock struct {
|
||||
issueComment *github.IssueComment
|
||||
issueCommentError error
|
||||
}
|
||||
|
||||
func (g *ghCreateCommentMock) CreateComment(ctx context.Context, owner string, repo string, number int, comment *github.IssueComment) (*github.IssueComment, *github.Response, error) {
|
||||
g.issueComment = comment
|
||||
ghRes := github.Response{Response: &http.Response{Status: "200"}}
|
||||
if g.issueCommentError != nil {
|
||||
ghRes.Status = "401"
|
||||
}
|
||||
return g.issueComment, &ghRes, g.issueCommentError
|
||||
}
|
||||
|
||||
func TestRunGithubCreateIssue(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
func TestTransformConfig(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
t.Run("Success", func(t *testing.T) {
|
||||
// init
|
||||
filesMock := mock.FilesMock{}
|
||||
ghCreateIssueService := ghCreateIssueMock{
|
||||
issueID: 1,
|
||||
}
|
||||
ghSearchIssuesMock := ghSearchIssuesMock{
|
||||
issueID: 1,
|
||||
}
|
||||
ghCreateCommentMock := ghCreateCommentMock{}
|
||||
config := githubCreateIssueOptions{
|
||||
Owner: "TEST",
|
||||
Repository: "test",
|
||||
@@ -116,119 +23,61 @@ func TestRunGithubCreateIssue(t *testing.T) {
|
||||
Title: "This is my title",
|
||||
Assignees: []string{"userIdOne", "userIdTwo"},
|
||||
}
|
||||
options := piperGithub.CreateIssueOptions{}
|
||||
|
||||
// test
|
||||
err := runGithubCreateIssue(ctx, &config, nil, &ghCreateIssueService, &ghSearchIssuesMock, &ghCreateCommentMock, filesMock.FileRead)
|
||||
err := transformConfig(&config, &options, filesMock.FileRead)
|
||||
|
||||
// assert
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, config.Owner, ghCreateIssueService.owner)
|
||||
assert.Equal(t, config.Repository, ghCreateIssueService.repo)
|
||||
assert.Equal(t, config.Body, ghCreateIssueService.issue.GetBody())
|
||||
assert.Equal(t, config.Title, ghCreateIssueService.issue.GetTitle())
|
||||
assert.Equal(t, config.Assignees, ghCreateIssueService.issue.GetAssignees())
|
||||
assert.Nil(t, ghSearchIssuesMock.issuesSearchResult)
|
||||
assert.Nil(t, ghCreateCommentMock.issueComment)
|
||||
assert.Equal(t, config.Token, options.Token)
|
||||
assert.Equal(t, config.APIURL, options.APIURL)
|
||||
assert.Equal(t, config.Owner, options.Owner)
|
||||
assert.Equal(t, config.Repository, options.Repository)
|
||||
assert.Equal(t, []byte(config.Body), options.Body)
|
||||
assert.Equal(t, config.Title, options.Title)
|
||||
assert.Equal(t, config.Assignees, options.Assignees)
|
||||
assert.Equal(t, config.UpdateExisting, options.UpdateExisting)
|
||||
})
|
||||
|
||||
t.Run("Success - body from file", func(t *testing.T) {
|
||||
t.Run("Success bodyFilePath", func(t *testing.T) {
|
||||
// init
|
||||
filesMock := mock.FilesMock{}
|
||||
filesMock.AddFile("test.md", []byte("Test markdown"))
|
||||
ghCreateIssueService := ghCreateIssueMock{
|
||||
issueID: 1,
|
||||
}
|
||||
config := githubCreateIssueOptions{
|
||||
Owner: "TEST",
|
||||
Repository: "test",
|
||||
BodyFilePath: "test.md",
|
||||
Title: "This is my title",
|
||||
Assignees: []string{"userIdOne", "userIdTwo"},
|
||||
}
|
||||
options := piperGithub.CreateIssueOptions{}
|
||||
|
||||
// test
|
||||
err := runGithubCreateIssue(ctx, &config, nil, &ghCreateIssueService, nil, nil, filesMock.FileRead)
|
||||
err := transformConfig(&config, &options, filesMock.FileRead)
|
||||
|
||||
// assert
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, config.Owner, ghCreateIssueService.owner)
|
||||
assert.Equal(t, config.Repository, ghCreateIssueService.repo)
|
||||
assert.Equal(t, "Test markdown", ghCreateIssueService.issue.GetBody())
|
||||
assert.Equal(t, config.Title, ghCreateIssueService.issue.GetTitle())
|
||||
assert.Empty(t, ghCreateIssueService.issue.GetAssignees())
|
||||
})
|
||||
|
||||
t.Run("Success update existing", func(t *testing.T) {
|
||||
// init
|
||||
filesMock := mock.FilesMock{}
|
||||
ghSearchIssuesMock := ghSearchIssuesMock{
|
||||
issueID: 1,
|
||||
}
|
||||
ghCreateCommentMock := ghCreateCommentMock{}
|
||||
config := githubCreateIssueOptions{
|
||||
Owner: "TEST",
|
||||
Repository: "test",
|
||||
Body: "This is my test body",
|
||||
Title: "This is my title",
|
||||
Assignees: []string{"userIdOne", "userIdTwo"},
|
||||
UpdateExisting: true,
|
||||
}
|
||||
|
||||
// test
|
||||
err := runGithubCreateIssue(ctx, &config, nil, nil, &ghSearchIssuesMock, &ghCreateCommentMock, filesMock.FileRead)
|
||||
|
||||
// assert
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, ghSearchIssuesMock.issuesSearchResult)
|
||||
assert.NotNil(t, ghCreateCommentMock.issueComment)
|
||||
assert.Equal(t, config.Title, ghSearchIssuesMock.issueTitle)
|
||||
assert.Equal(t, config.Title, *ghSearchIssuesMock.issuesSearchResult.Issues[0].Title)
|
||||
assert.Equal(t, config.Body, ghCreateCommentMock.issueComment.GetBody())
|
||||
})
|
||||
|
||||
t.Run("Error", func(t *testing.T) {
|
||||
// init
|
||||
filesMock := mock.FilesMock{}
|
||||
ghCreateIssueService := ghCreateIssueMock{
|
||||
issueError: fmt.Errorf("error creating issue"),
|
||||
}
|
||||
config := githubCreateIssueOptions{
|
||||
Body: "test content",
|
||||
}
|
||||
|
||||
// test
|
||||
err := runGithubCreateIssue(ctx, &config, nil, &ghCreateIssueService, nil, nil, filesMock.FileRead)
|
||||
|
||||
// assert
|
||||
assert.EqualError(t, err, "error occurred when creating issue: error creating issue")
|
||||
assert.Equal(t, config.Token, options.Token)
|
||||
assert.Equal(t, config.APIURL, options.APIURL)
|
||||
assert.Equal(t, config.Owner, options.Owner)
|
||||
assert.Equal(t, config.Repository, options.Repository)
|
||||
assert.Equal(t, []byte("Test markdown"), options.Body)
|
||||
assert.Equal(t, config.Title, options.Title)
|
||||
assert.Equal(t, config.Assignees, options.Assignees)
|
||||
assert.Equal(t, config.UpdateExisting, options.UpdateExisting)
|
||||
})
|
||||
|
||||
t.Run("Error - missing issue body", func(t *testing.T) {
|
||||
// init
|
||||
filesMock := mock.FilesMock{}
|
||||
ghCreateIssueService := ghCreateIssueMock{}
|
||||
ghSearchIssuesMock := ghSearchIssuesMock{}
|
||||
ghCreateCommentMock := ghCreateCommentMock{}
|
||||
config := githubCreateIssueOptions{}
|
||||
options := piperGithub.CreateIssueOptions{}
|
||||
|
||||
// test
|
||||
err := runGithubCreateIssue(ctx, &config, nil, &ghCreateIssueService, &ghSearchIssuesMock, &ghCreateCommentMock, filesMock.FileRead)
|
||||
err := transformConfig(&config, &options, filesMock.FileRead)
|
||||
|
||||
// assert
|
||||
assert.EqualError(t, err, "either parameter `body` or parameter `bodyFilePath` is required")
|
||||
})
|
||||
|
||||
t.Run("Error - missing body file", func(t *testing.T) {
|
||||
// init
|
||||
filesMock := mock.FilesMock{}
|
||||
ghCreateIssueService := ghCreateIssueMock{}
|
||||
config := githubCreateIssueOptions{
|
||||
BodyFilePath: "test.md",
|
||||
}
|
||||
|
||||
// test
|
||||
err := runGithubCreateIssue(ctx, &config, nil, &ghCreateIssueService, nil, nil, filesMock.FileRead)
|
||||
|
||||
// assert
|
||||
assert.Contains(t, fmt.Sprint(err), "failed to read file 'test.md'")
|
||||
})
|
||||
}
|
||||
|
@@ -9,6 +9,7 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
piperGithub "github.com/SAP/jenkins-library/pkg/github"
|
||||
"github.com/SAP/jenkins-library/pkg/log"
|
||||
"github.com/SAP/jenkins-library/pkg/piperutils"
|
||||
"github.com/SAP/jenkins-library/pkg/reporting"
|
||||
@@ -129,7 +130,7 @@ func WriteJSONReport(jsonReport FortifyReportData) ([]piperutils.Path, error) {
|
||||
return reportPaths, nil
|
||||
}
|
||||
|
||||
func WriteCustomReports(scanReport reporting.ScanReport, projectName string, projectVersion string) ([]piperutils.Path, error) {
|
||||
func WriteCustomReports(scanReport reporting.ScanReport) ([]piperutils.Path, error) {
|
||||
utils := piperutils.Files{}
|
||||
reportPaths := []piperutils.Path{}
|
||||
|
||||
@@ -146,24 +147,33 @@ func WriteCustomReports(scanReport reporting.ScanReport, projectName string, pro
|
||||
}
|
||||
reportPaths = append(reportPaths, piperutils.Path{Name: "Fortify Report", Target: htmlReportPath})
|
||||
|
||||
// JSON reports are used by step pipelineCreateSummary in order to e.g. prepare an issue creation in GitHub
|
||||
// ignore JSON errors since structure is in our hands
|
||||
jsonReport, _ := scanReport.ToJSON()
|
||||
if exists, _ := utils.DirExists(reporting.StepReportDirectory); !exists {
|
||||
err := utils.MkdirAll(reporting.StepReportDirectory, 0777)
|
||||
if err != nil {
|
||||
return reportPaths, errors.Wrap(err, "failed to create reporting directory")
|
||||
}
|
||||
}
|
||||
if err := utils.FileWrite(filepath.Join(reporting.StepReportDirectory, fmt.Sprintf("fortifyExecuteScan_sast_%v.json", reportShaFortify([]string{projectName, projectVersion}))), jsonReport, 0666); err != nil {
|
||||
return reportPaths, errors.Wrapf(err, "failed to write json report")
|
||||
}
|
||||
// we do not add the json report to the overall list of reports for now,
|
||||
// since it is just an intermediary report used as input for later
|
||||
// and there does not seem to be real benefit in archiving it.
|
||||
return reportPaths, nil
|
||||
}
|
||||
|
||||
func UploadReportToGithub(scanReport reporting.ScanReport, token, APIURL, owner, repository string, assignees []string) error {
|
||||
// JSON reports are used by step pipelineCreateSummary in order to e.g. prepare an issue creation in GitHub
|
||||
// ignore JSON errors since structure is in our hands
|
||||
markdownReport, _ := scanReport.ToMarkdown()
|
||||
options := piperGithub.CreateIssueOptions{
|
||||
Token: token,
|
||||
APIURL: APIURL,
|
||||
Owner: owner,
|
||||
Repository: repository,
|
||||
Title: "Fortify SAST Results",
|
||||
Body: markdownReport,
|
||||
Assignees: assignees,
|
||||
UpdateExisting: true,
|
||||
}
|
||||
err := piperGithub.CreateIssue(&options)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to upload fortify results into GitHub issue")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func reportShaFortify(parts []string) string {
|
||||
reportShaData := []byte(strings.Join(parts, ","))
|
||||
return fmt.Sprintf("%x", sha1.Sum(reportShaData))
|
||||
|
@@ -2,13 +2,40 @@ package github
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"github.com/SAP/jenkins-library/pkg/log"
|
||||
"github.com/google/go-github/v32/github"
|
||||
"github.com/pkg/errors"
|
||||
"golang.org/x/oauth2"
|
||||
)
|
||||
|
||||
type githubCreateIssueService interface {
|
||||
Create(ctx context.Context, owner string, repo string, issue *github.IssueRequest) (*github.Issue, *github.Response, error)
|
||||
}
|
||||
|
||||
type githubSearchIssuesService interface {
|
||||
Issues(ctx context.Context, query string, opts *github.SearchOptions) (*github.IssuesSearchResult, *github.Response, error)
|
||||
}
|
||||
|
||||
type githubCreateCommentService interface {
|
||||
CreateComment(ctx context.Context, owner string, repo string, number int, comment *github.IssueComment) (*github.IssueComment, *github.Response, error)
|
||||
}
|
||||
|
||||
// CreateIssueOptions to configure the creation
|
||||
type CreateIssueOptions struct {
|
||||
APIURL string `json:"apiUrl,omitempty"`
|
||||
Assignees []string `json:"assignees,omitempty"`
|
||||
Body []byte `json:"body,omitempty"`
|
||||
Owner string `json:"owner,omitempty"`
|
||||
Repository string `json:"repository,omitempty"`
|
||||
Title string `json:"title,omitempty"`
|
||||
UpdateExisting bool `json:"updateExisting,omitempty"`
|
||||
Token string `json:"token,omitempty"`
|
||||
}
|
||||
|
||||
//NewClient creates a new GitHub client using an OAuth token for authentication
|
||||
func NewClient(token, apiURL, uploadURL string) (context.Context, *github.Client, error) {
|
||||
ctx := context.Background()
|
||||
@@ -39,3 +66,73 @@ func NewClient(token, apiURL, uploadURL string) (context.Context, *github.Client
|
||||
client.UploadURL = uploadTargetURL
|
||||
return ctx, client, nil
|
||||
}
|
||||
|
||||
func CreateIssue(ghCreateIssueOptions *CreateIssueOptions) error {
|
||||
ctx, client, err := NewClient(ghCreateIssueOptions.Token, ghCreateIssueOptions.APIURL, "")
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to get GitHub client")
|
||||
}
|
||||
return createIssueLocal(ctx, ghCreateIssueOptions, client.Issues, client.Search, client.Issues)
|
||||
}
|
||||
|
||||
func createIssueLocal(ctx context.Context, ghCreateIssueOptions *CreateIssueOptions, ghCreateIssueService githubCreateIssueService, ghSearchIssuesService githubSearchIssuesService, ghCreateCommentService githubCreateCommentService) error {
|
||||
|
||||
issue := github.IssueRequest{
|
||||
Title: &ghCreateIssueOptions.Title,
|
||||
}
|
||||
var bodyString string
|
||||
if len(ghCreateIssueOptions.Body) > 0 {
|
||||
bodyString = string(ghCreateIssueOptions.Body)
|
||||
} else {
|
||||
bodyString = ""
|
||||
}
|
||||
issue.Body = &bodyString
|
||||
if len(ghCreateIssueOptions.Assignees) > 0 {
|
||||
issue.Assignees = &ghCreateIssueOptions.Assignees
|
||||
} else {
|
||||
issue.Assignees = &[]string{}
|
||||
}
|
||||
|
||||
var existingIssue *github.Issue = nil
|
||||
|
||||
if ghCreateIssueOptions.UpdateExisting {
|
||||
queryString := fmt.Sprintf("is:open is:issue repo:%v/%v in:title %v", ghCreateIssueOptions.Owner, ghCreateIssueOptions.Repository, ghCreateIssueOptions.Title)
|
||||
searchResult, resp, err := ghSearchIssuesService.Issues(ctx, queryString, nil)
|
||||
if err != nil {
|
||||
if resp != nil {
|
||||
log.Entry().Errorf("GitHub response code %v", resp.Status)
|
||||
}
|
||||
return errors.Wrap(err, "error occurred when looking for existing issue")
|
||||
} else {
|
||||
for _, value := range searchResult.Issues {
|
||||
if value != nil && *value.Title == ghCreateIssueOptions.Title {
|
||||
existingIssue = value
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if existingIssue != nil {
|
||||
comment := &github.IssueComment{Body: issue.Body}
|
||||
_, resp, err := ghCreateCommentService.CreateComment(ctx, ghCreateIssueOptions.Owner, ghCreateIssueOptions.Repository, *existingIssue.Number, comment)
|
||||
if err != nil {
|
||||
if resp != nil {
|
||||
log.Entry().Errorf("GitHub response code %v", resp.Status)
|
||||
}
|
||||
return errors.Wrap(err, "error occurred when looking for existing issue")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if existingIssue == nil {
|
||||
newIssue, resp, err := ghCreateIssueService.Create(ctx, ghCreateIssueOptions.Owner, ghCreateIssueOptions.Repository, &issue)
|
||||
if err != nil {
|
||||
if resp != nil {
|
||||
log.Entry().Errorf("GitHub response code %v", resp.Status)
|
||||
}
|
||||
return errors.Wrap(err, "error occurred when creating issue")
|
||||
}
|
||||
log.Entry().Debugf("New issue created: %v", newIssue)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
203
pkg/github/github_test.go
Normal file
203
pkg/github/github_test.go
Normal file
@@ -0,0 +1,203 @@
|
||||
package github
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-github/v32/github"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
type ghCreateIssueMock struct {
|
||||
issue *github.IssueRequest
|
||||
issueID int64
|
||||
issueError error
|
||||
owner string
|
||||
repo string
|
||||
number int
|
||||
assignees []string
|
||||
}
|
||||
|
||||
func (g *ghCreateIssueMock) Create(ctx context.Context, owner string, repo string, issue *github.IssueRequest) (*github.Issue, *github.Response, error) {
|
||||
g.issue = issue
|
||||
g.owner = owner
|
||||
g.repo = repo
|
||||
g.assignees = *issue.Assignees
|
||||
|
||||
issueResponse := github.Issue{ID: &g.issueID, Title: issue.Title, Body: issue.Body}
|
||||
|
||||
ghRes := github.Response{Response: &http.Response{Status: "200"}}
|
||||
if g.issueError != nil {
|
||||
ghRes.Status = "401"
|
||||
}
|
||||
|
||||
return &issueResponse, &ghRes, g.issueError
|
||||
}
|
||||
|
||||
type ghSearchIssuesMock struct {
|
||||
issueID int64
|
||||
issueNumber int
|
||||
issueTitle string
|
||||
issueBody string
|
||||
issuesSearchResult *github.IssuesSearchResult
|
||||
issuesSearchError error
|
||||
}
|
||||
|
||||
func (g *ghSearchIssuesMock) Issues(ctx context.Context, query string, opts *github.SearchOptions) (*github.IssuesSearchResult, *github.Response, error) {
|
||||
|
||||
regex := regexp.MustCompile(`.*in:title (?P<Title>(.*))`)
|
||||
matches := regex.FindStringSubmatch(query)
|
||||
|
||||
g.issueTitle = matches[1]
|
||||
|
||||
issues := []*github.Issue{
|
||||
{
|
||||
ID: &g.issueID,
|
||||
Number: &g.issueNumber,
|
||||
Title: &g.issueTitle,
|
||||
Body: &g.issueBody,
|
||||
},
|
||||
}
|
||||
|
||||
total := len(issues)
|
||||
incompleteResults := false
|
||||
|
||||
g.issuesSearchResult = &github.IssuesSearchResult{
|
||||
Issues: issues,
|
||||
Total: &total,
|
||||
IncompleteResults: &incompleteResults,
|
||||
}
|
||||
|
||||
ghRes := github.Response{Response: &http.Response{Status: "200"}}
|
||||
if g.issuesSearchError != nil {
|
||||
ghRes.Status = "401"
|
||||
}
|
||||
|
||||
return g.issuesSearchResult, &ghRes, g.issuesSearchError
|
||||
}
|
||||
|
||||
type ghCreateCommentMock struct {
|
||||
issueComment *github.IssueComment
|
||||
issueCommentError error
|
||||
}
|
||||
|
||||
func (g *ghCreateCommentMock) CreateComment(ctx context.Context, owner string, repo string, number int, comment *github.IssueComment) (*github.IssueComment, *github.Response, error) {
|
||||
g.issueComment = comment
|
||||
ghRes := github.Response{Response: &http.Response{Status: "200"}}
|
||||
if g.issueCommentError != nil {
|
||||
ghRes.Status = "401"
|
||||
}
|
||||
return g.issueComment, &ghRes, g.issueCommentError
|
||||
}
|
||||
|
||||
func TestRunGithubCreateIssue(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
t.Parallel()
|
||||
|
||||
t.Run("Success", func(t *testing.T) {
|
||||
// init
|
||||
ghCreateIssueService := ghCreateIssueMock{
|
||||
issueID: 1,
|
||||
}
|
||||
ghSearchIssuesMock := ghSearchIssuesMock{
|
||||
issueID: 1,
|
||||
}
|
||||
ghCreateCommentMock := ghCreateCommentMock{}
|
||||
config := CreateIssueOptions{
|
||||
Owner: "TEST",
|
||||
Repository: "test",
|
||||
Body: []byte("This is my test body"),
|
||||
Title: "This is my title",
|
||||
Assignees: []string{"userIdOne", "userIdTwo"},
|
||||
}
|
||||
|
||||
// test
|
||||
err := createIssueLocal(ctx, &config, &ghCreateIssueService, &ghSearchIssuesMock, &ghCreateCommentMock)
|
||||
|
||||
// assert
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, config.Owner, ghCreateIssueService.owner)
|
||||
assert.Equal(t, config.Repository, ghCreateIssueService.repo)
|
||||
assert.Equal(t, "This is my test body", ghCreateIssueService.issue.GetBody())
|
||||
assert.Equal(t, config.Title, ghCreateIssueService.issue.GetTitle())
|
||||
assert.Equal(t, config.Assignees, ghCreateIssueService.issue.GetAssignees())
|
||||
assert.Nil(t, ghSearchIssuesMock.issuesSearchResult)
|
||||
assert.Nil(t, ghCreateCommentMock.issueComment)
|
||||
})
|
||||
|
||||
t.Run("Success update existing", func(t *testing.T) {
|
||||
// init
|
||||
ghSearchIssuesMock := ghSearchIssuesMock{
|
||||
issueID: 1,
|
||||
}
|
||||
ghCreateCommentMock := ghCreateCommentMock{}
|
||||
config := CreateIssueOptions{
|
||||
Owner: "TEST",
|
||||
Repository: "test",
|
||||
Body: []byte("This is my test body"),
|
||||
Title: "This is my title",
|
||||
Assignees: []string{"userIdOne", "userIdTwo"},
|
||||
UpdateExisting: true,
|
||||
}
|
||||
|
||||
// test
|
||||
err := createIssueLocal(ctx, &config, nil, &ghSearchIssuesMock, &ghCreateCommentMock)
|
||||
|
||||
// assert
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, ghSearchIssuesMock.issuesSearchResult)
|
||||
assert.NotNil(t, ghCreateCommentMock.issueComment)
|
||||
assert.Equal(t, config.Title, ghSearchIssuesMock.issueTitle)
|
||||
assert.Equal(t, config.Title, *ghSearchIssuesMock.issuesSearchResult.Issues[0].Title)
|
||||
assert.Equal(t, "This is my test body", ghCreateCommentMock.issueComment.GetBody())
|
||||
})
|
||||
|
||||
t.Run("Empty body", func(t *testing.T) {
|
||||
// init
|
||||
ghCreateIssueService := ghCreateIssueMock{
|
||||
issueID: 1,
|
||||
}
|
||||
ghSearchIssuesMock := ghSearchIssuesMock{
|
||||
issueID: 1,
|
||||
}
|
||||
ghCreateCommentMock := ghCreateCommentMock{}
|
||||
config := CreateIssueOptions{
|
||||
Owner: "TEST",
|
||||
Repository: "test",
|
||||
Body: []byte(""),
|
||||
Title: "This is my title",
|
||||
Assignees: []string{"userIdOne", "userIdTwo"},
|
||||
UpdateExisting: true,
|
||||
}
|
||||
|
||||
// test
|
||||
err := createIssueLocal(ctx, &config, &ghCreateIssueService, &ghSearchIssuesMock, &ghCreateCommentMock)
|
||||
|
||||
// assert
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, ghSearchIssuesMock.issuesSearchResult)
|
||||
assert.NotNil(t, ghCreateCommentMock.issueComment)
|
||||
assert.Equal(t, config.Title, ghSearchIssuesMock.issueTitle)
|
||||
assert.Equal(t, config.Title, *ghSearchIssuesMock.issuesSearchResult.Issues[0].Title)
|
||||
assert.Equal(t, "", ghCreateCommentMock.issueComment.GetBody())
|
||||
})
|
||||
|
||||
t.Run("Create error", func(t *testing.T) {
|
||||
// init
|
||||
ghCreateIssueService := ghCreateIssueMock{
|
||||
issueError: fmt.Errorf("error creating issue"),
|
||||
}
|
||||
config := CreateIssueOptions{
|
||||
Body: []byte("test content"),
|
||||
}
|
||||
|
||||
// test
|
||||
err := createIssueLocal(ctx, &config, &ghCreateIssueService, nil, nil)
|
||||
|
||||
// assert
|
||||
assert.EqualError(t, err, "error occurred when creating issue: error creating issue")
|
||||
})
|
||||
}
|
@@ -44,6 +44,15 @@ spec:
|
||||
- PARAMETERS
|
||||
- STAGES
|
||||
- STEPS
|
||||
- name: assignees
|
||||
description: Defines the assignees for the Github Issue created/updated with the results of the scan.
|
||||
scope:
|
||||
- PARAMETERS
|
||||
- STAGES
|
||||
- STEPS
|
||||
type: "[]string"
|
||||
default: []
|
||||
mandatory: false
|
||||
- name: authToken
|
||||
type: string
|
||||
description: "The FortifyToken to use for authentication"
|
||||
@@ -597,6 +606,18 @@ spec:
|
||||
- STEPS
|
||||
- STAGES
|
||||
- PARAMETERS
|
||||
- name: createResultIssue
|
||||
type: bool
|
||||
description: "Whether the step creates a GitHub issue containing the scan results in the originating repo.
|
||||
Since optimized pipelines are headless the creation is implicitly activated for schedules runs."
|
||||
resourceRef:
|
||||
- name: commonPipelineEnvironment
|
||||
param: custom/optimizedAndScheduled
|
||||
scope:
|
||||
- PARAMETERS
|
||||
- STAGES
|
||||
- STEPS
|
||||
default: false
|
||||
containers:
|
||||
- image: ""
|
||||
outputs:
|
||||
|
@@ -13,6 +13,6 @@ void call(Map parameters = [:]) {
|
||||
final script = checkScript(this, parameters) ?: this
|
||||
parameters = DownloadCacheUtils.injectDownloadCacheInParameters(script, parameters, BuildTool.MAVEN)
|
||||
|
||||
List credentials = [[type: 'token', id: 'fortifyCredentialsId', env: ['PIPER_authToken']]]
|
||||
List credentials = [[type: 'token', id: 'fortifyCredentialsId', env: ['PIPER_authToken']], [type: 'token', id: 'githubTokenCredentialsId', env: ['PIPER_githubToken']]]
|
||||
piperExecuteBin(parameters, STEP_NAME, METADATA_FILE, credentials)
|
||||
}
|
||||
|
Reference in New Issue
Block a user