From 6dad12436707be202cc5de95808bfed58b18f8aa Mon Sep 17 00:00:00 2001 From: sumeet patil Date: Fri, 28 Apr 2023 15:47:05 +0200 Subject: [PATCH] feat(codeqlExecuteScan): CodeQL compliance report and check (#4335) * CodeQL compliance report and check * fix test cases --------- Co-authored-by: Daria Kuznetsova --- cmd/codeqlExecuteScan.go | 307 +++++++++++++--------- cmd/codeqlExecuteScan_generated.go | 59 ++++- cmd/codeqlExecuteScan_test.go | 275 +++++++++---------- pkg/codeql/codeql.go | 65 +++++ pkg/codeql/codeql_test.go | 47 ++++ pkg/codeql/reporting.go | 44 ++++ resources/metadata/codeqlExecuteScan.yaml | 25 ++ 7 files changed, 547 insertions(+), 275 deletions(-) create mode 100644 pkg/codeql/codeql.go create mode 100644 pkg/codeql/codeql_test.go create mode 100644 pkg/codeql/reporting.go diff --git a/cmd/codeqlExecuteScan.go b/cmd/codeqlExecuteScan.go index 206d3c656a..312414db0e 100644 --- a/cmd/codeqlExecuteScan.go +++ b/cmd/codeqlExecuteScan.go @@ -3,9 +3,11 @@ package cmd import ( "fmt" "os" + "path/filepath" "regexp" "strings" + "github.com/SAP/jenkins-library/pkg/codeql" "github.com/SAP/jenkins-library/pkg/command" "github.com/SAP/jenkins-library/pkg/log" "github.com/SAP/jenkins-library/pkg/orchestrator" @@ -26,6 +28,7 @@ type RepoInfo struct { repo string commitId string ref string + owner string } type codeqlExecuteScanUtilsBundle struct { @@ -48,7 +51,9 @@ func codeqlExecuteScan(config codeqlExecuteScanOptions, telemetryData *telemetry utils := newCodeqlExecuteScanUtils() - err := runCodeqlExecuteScan(&config, telemetryData, utils) + reports, err := runCodeqlExecuteScan(&config, telemetryData, utils) + piperutils.PersistReportsAndLinks("codeqlExecuteScan", "./", utils, reports, nil) + if err != nil { log.Entry().WithError(err).Fatal("Codeql scan failed") } @@ -96,82 +101,99 @@ func getGitRepoInfo(repoUri string, repoInfo *RepoInfo) error { if len(matches) > 0 { match := matches[0] repoInfo.serverUrl = "https://" + match[3] - repoInfo.repo = strings.TrimSuffix(match[4], ".git") + repoData := strings.Split(strings.TrimSuffix(match[4], ".git"), "/") + if len(repoData) != 2 { + return fmt.Errorf("Invalid repository %s", repoUri) + } + + repoInfo.owner = repoData[0] + repoInfo.repo = repoData[1] return nil } return fmt.Errorf("Invalid repository %s", repoUri) } -func uploadResults(config *codeqlExecuteScanOptions, utils codeqlExecuteScanUtils) error { - if config.UploadResults { - if len(config.GithubToken) == 0 { - return errors.New("failed running upload-results as github token was not specified") - } +func initGitInfo(config *codeqlExecuteScanOptions) RepoInfo { + var repoInfo RepoInfo + err := getGitRepoInfo(config.Repository, &repoInfo) + if err != nil { + log.Entry().Error(err) + } + repoInfo.ref = config.AnalyzedRef + repoInfo.commitId = config.CommitID - if config.CommitID == "NA" { - return errors.New("failed running upload-results as gitCommitId is not available") + provider, err := orchestrator.NewOrchestratorSpecificConfigProvider() + if err != nil { + log.Entry().Warn("No orchestrator found. We assume piper is running locally.") + } else { + if repoInfo.ref == "" { + repoInfo.ref = provider.GetReference() } - var repoInfo RepoInfo - err := getGitRepoInfo(config.Repository, &repoInfo) - if err != nil { - log.Entry().Error(err) + if repoInfo.commitId == "" || repoInfo.commitId == "NA" { + repoInfo.commitId = provider.GetCommit() } - repoInfo.ref = config.AnalyzedRef - repoInfo.commitId = config.CommitID - provider, err := orchestrator.NewOrchestratorSpecificConfigProvider() - if err != nil { - log.Entry().Error(err) - } else { - if repoInfo.ref == "" { - repoInfo.ref = provider.GetReference() + if repoInfo.serverUrl == "" { + err = getGitRepoInfo(provider.GetRepoURL(), &repoInfo) + if err != nil { + log.Entry().Error(err) } + } + } - if repoInfo.commitId == "" { - repoInfo.commitId = provider.GetCommit() - } + return repoInfo +} - if repoInfo.serverUrl == "" { - err = getGitRepoInfo(provider.GetRepoURL(), &repoInfo) - if err != nil { - log.Entry().Error(err) - } - } - } +func getToken(config *codeqlExecuteScanOptions) (bool, string) { + if len(config.GithubToken) > 0 { + return true, config.GithubToken + } - cmd := []string{"github", "upload-results", "--sarif=" + fmt.Sprintf("%vtarget/codeqlReport.sarif", config.ModulePath), "-a=" + config.GithubToken} + envVal, isEnvGithubToken := os.LookupEnv("GITHUB_TOKEN") + if isEnvGithubToken { + return true, envVal + } - if repoInfo.commitId != "" { - cmd = append(cmd, "--commit="+repoInfo.commitId) - } + return false, "" +} - if repoInfo.serverUrl != "" { - cmd = append(cmd, "--github-url="+repoInfo.serverUrl) - } +func uploadResults(config *codeqlExecuteScanOptions, repoInfo RepoInfo, token string, utils codeqlExecuteScanUtils) error { + cmd := []string{"github", "upload-results", "--sarif=" + filepath.Join(config.ModulePath, "target", "codeqlReport.sarif")} - if repoInfo.repo != "" { - cmd = append(cmd, "--repository="+repoInfo.repo) - } + if config.GithubToken != "" { + cmd = append(cmd, "-a="+token) + } - if repoInfo.ref != "" { - cmd = append(cmd, "--ref="+repoInfo.ref) - } + if repoInfo.commitId != "" { + cmd = append(cmd, "--commit="+repoInfo.commitId) + } - //if no git pramas are passed(commitId, reference, serverUrl, repository), then codeql tries to auto populate it based on git information of the checkout repository. - //It also depends on the orchestrator. Some orchestrator keep git information and some not. - err = execute(utils, cmd, GeneralConfig.Verbose) - if err != nil { - log.Entry().Error("failed to upload sarif results") - return err - } + if repoInfo.serverUrl != "" { + cmd = append(cmd, "--github-url="+repoInfo.serverUrl) + } + + if repoInfo.repo != "" { + cmd = append(cmd, "--repository="+(repoInfo.owner+"/"+repoInfo.repo)) + } + + if repoInfo.ref != "" { + cmd = append(cmd, "--ref="+repoInfo.ref) + } + + //if no git pramas are passed(commitId, reference, serverUrl, repository), then codeql tries to auto populate it based on git information of the checkout repository. + //It also depends on the orchestrator. Some orchestrator keep git information and some not. + err := execute(utils, cmd, GeneralConfig.Verbose) + if err != nil { + log.Entry().Error("failed to upload sarif results") + return err } return nil } -func runCodeqlExecuteScan(config *codeqlExecuteScanOptions, telemetryData *telemetry.CustomData, utils codeqlExecuteScanUtils) error { +func runCodeqlExecuteScan(config *codeqlExecuteScanOptions, telemetryData *telemetry.CustomData, utils codeqlExecuteScanUtils) ([]piperutils.Path, error) { codeqlVersion, err := os.ReadFile("/etc/image-version") if err != nil { log.Entry().Infof("CodeQL image version: unknown") @@ -186,9 +208,9 @@ func runCodeqlExecuteScan(config *codeqlExecuteScanOptions, telemetryData *telem if len(language) == 0 && len(config.Language) == 0 { if config.BuildTool == "custom" { - return fmt.Errorf("as the buildTool is custom. please atleast specify the language parameter") + return reports, fmt.Errorf("as the buildTool is custom. please specify the language parameter") } else { - return fmt.Errorf("the step could not recognize the specified buildTool %s. please specify valid buildtool", config.BuildTool) + return reports, fmt.Errorf("the step could not recognize the specified buildTool %s. please specify valid buildtool", config.BuildTool) } } if len(language) > 0 { @@ -207,130 +229,153 @@ func runCodeqlExecuteScan(config *codeqlExecuteScanOptions, telemetryData *telem err = execute(utils, cmd, GeneralConfig.Verbose) if err != nil { log.Entry().Error("failed running command codeql database create") - return err + return reports, err } - err = os.MkdirAll(fmt.Sprintf("%vtarget", config.ModulePath), os.ModePerm) + err = os.MkdirAll(filepath.Join(config.ModulePath, "target"), os.ModePerm) if err != nil { - return fmt.Errorf("failed to create directory: %w", err) + return reports, fmt.Errorf("failed to create directory: %w", err) } cmd = nil - cmd = append(cmd, "database", "analyze", "--format=sarif-latest", fmt.Sprintf("--output=%vtarget/codeqlReport.sarif", config.ModulePath), config.Database) + cmd = append(cmd, "database", "analyze", "--format=sarif-latest", fmt.Sprintf("--output=%v", filepath.Join(config.ModulePath, "target", "codeqlReport.sarif")), config.Database) cmd = append(cmd, getRamAndThreadsFromConfig(config)...) cmd = codeqlQuery(cmd, config.QuerySuite) err = execute(utils, cmd, GeneralConfig.Verbose) if err != nil { log.Entry().Error("failed running command codeql database analyze for sarif generation") - return err + return reports, err } - reports = append(reports, piperutils.Path{Target: fmt.Sprintf("%vtarget/codeqlReport.sarif", config.ModulePath)}) + reports = append(reports, piperutils.Path{Target: filepath.Join(config.ModulePath, "target", "codeqlReport.sarif")}) cmd = nil - cmd = append(cmd, "database", "analyze", "--format=csv", fmt.Sprintf("--output=%vtarget/codeqlReport.csv", config.ModulePath), config.Database) + cmd = append(cmd, "database", "analyze", "--format=csv", fmt.Sprintf("--output=%v", filepath.Join(config.ModulePath, "target", "codeqlReport.csv")), config.Database) cmd = append(cmd, getRamAndThreadsFromConfig(config)...) cmd = codeqlQuery(cmd, config.QuerySuite) err = execute(utils, cmd, GeneralConfig.Verbose) if err != nil { log.Entry().Error("failed running command codeql database analyze for csv generation") - return err + return reports, err } - reports = append(reports, piperutils.Path{Target: fmt.Sprintf("%vtarget/codeqlReport.csv", config.ModulePath)}) - err = uploadResults(config, utils) - if err != nil { - log.Entry().Error("failed to upload results") - return err + reports = append(reports, piperutils.Path{Target: filepath.Join(config.ModulePath, "target", "codeqlReport.csv")}) + + repoInfo := initGitInfo(config) + repoUrl := fmt.Sprintf("%s/%s/%s", repoInfo.serverUrl, repoInfo.owner, repoInfo.repo) + repoReference, err := buildRepoReference(repoUrl, repoInfo.ref) + repoCodeqlScanUrl := fmt.Sprintf("%s/security/code-scanning?query=is:open+ref:%s", repoUrl, repoInfo.ref) + + if !config.UploadResults { + log.Entry().Warn("The sarif results will not be uploaded to the repository and compliance report will not be generated as uploadResults is set to false.") + } else { + hasToken, token := getToken(config) + if !hasToken { + return reports, errors.New("failed running upload-results as githubToken was not specified") + } + + err = uploadResults(config, repoInfo, token, utils) + if err != nil { + + return reports, err + } + + codeqlScanAuditInstance := codeql.NewCodeqlScanAuditInstance(config.GithubAPIURL, repoInfo.owner, repoInfo.repo, token, []string{}) + scanResults, err := codeqlScanAuditInstance.GetVulnerabilities(repoInfo.ref) + if err != nil { + return reports, errors.Wrap(err, "failed to get scan results") + } + + unaudited := (scanResults.Total - scanResults.Audited) + if unaudited > config.VulnerabilityThresholdTotal { + msg := fmt.Sprintf("Your repository %v with ref %v is not compliant. Total unaudited issues are %v which is greater than the VulnerabilityThresholdTotal count %v", repoUrl, repoInfo.ref, unaudited, config.VulnerabilityThresholdTotal) + if config.CheckForCompliance { + + return reports, errors.Errorf(msg) + } + + log.Entry().Warning(msg) + } + + codeqlAudit := codeql.CodeqlAudit{ToolName: "codeql", RepositoryUrl: repoUrl, CodeScanningLink: repoCodeqlScanUrl, RepositoryReferenceUrl: repoReference, ScanResults: scanResults} + paths, err := codeql.WriteJSONReport(codeqlAudit, config.ModulePath) + if err != nil { + return reports, errors.Wrap(err, "failed to write json compliance report") + } + + reports = append(reports, paths...) } - // create toolrecord file - toolRecordFileName, err := createToolRecordCodeql(utils, "./", *config) + toolRecordFileName, err := createAndPersistToolRecord(utils, repoInfo, repoReference, repoUrl, repoCodeqlScanUrl) if err != nil { - // do not fail until the framework is well established log.Entry().Warning("TR_CODEQL: Failed to create toolrecord file ...", err) } else { reports = append(reports, piperutils.Path{Target: toolRecordFileName}) } - piperutils.PersistReportsAndLinks("codeqlExecuteScan", "./", utils, reports, nil) - - return nil + return reports, nil } -func createToolRecordCodeql(utils codeqlExecuteScanUtils, workspace string, config codeqlExecuteScanOptions) (string, error) { - repoURL := strings.TrimSuffix(config.Repository, ".git") - toolInstance, orgName, repoName, err := parseRepositoryURL(repoURL) +func createAndPersistToolRecord(utils codeqlExecuteScanUtils, repoInfo RepoInfo, repoReference string, repoUrl string, repoCodeqlScanUrl string) (string, error) { + toolRecord, err := createToolRecordCodeql(utils, repoInfo, repoReference, repoUrl, repoCodeqlScanUrl) if err != nil { return "", err } - record := toolrecord.New(utils, workspace, "codeql", toolInstance) - record.DisplayName = fmt.Sprintf("%s %s - %s %s", orgName, repoName, config.AnalyzedRef, config.CommitID) - record.DisplayURL = fmt.Sprintf("%s/security/code-scanning?query=is:open+ref:%s", repoURL, config.AnalyzedRef) - // Repository - err = record.AddKeyData("repository", - fmt.Sprintf("%s/%s", orgName, repoName), - fmt.Sprintf("%s %s", orgName, repoName), - config.Repository) + + toolRecordFileName, err := persistToolRecord(toolRecord) if err != nil { return "", err } - // Repository Reference - repoReference, err := buildRepoReference(repoURL, config.AnalyzedRef) + + return toolRecordFileName, nil +} + +func createToolRecordCodeql(utils codeqlExecuteScanUtils, repoInfo RepoInfo, repoUrl string, repoReference string, repoCodeqlScanUrl string) (*toolrecord.Toolrecord, error) { + record := toolrecord.New(utils, "./", "codeql", repoInfo.serverUrl) + + if repoInfo.serverUrl == "" { + return record, errors.New("Repository not set") + } + + if repoInfo.commitId == "" || repoInfo.commitId == "NA" { + return record, errors.New("CommitId not set") + } + + if repoInfo.ref == "" { + return record, errors.New("Analyzed Reference not set") + } + + record.DisplayName = fmt.Sprintf("%s %s - %s %s", repoInfo.owner, repoInfo.repo, repoInfo.ref, repoInfo.commitId) + record.DisplayURL = fmt.Sprintf("%s/security/code-scanning?query=is:open+ref:%s", repoUrl, repoInfo.ref) + + err := record.AddKeyData("repository", + fmt.Sprintf("%s/%s", repoInfo.owner, repoInfo.repo), + fmt.Sprintf("%s %s", repoInfo.owner, repoInfo.repo), + repoUrl) if err != nil { - log.Entry().WithError(err).Warn("Failed to build repository reference") + return record, err } + err = record.AddKeyData("repositoryReference", - config.AnalyzedRef, - fmt.Sprintf("%s - %s", repoName, config.AnalyzedRef), + repoInfo.ref, + fmt.Sprintf("%s - %s", repoInfo.repo, repoInfo.ref), repoReference) if err != nil { - return "", err + return record, err } - // Scan Results + err = record.AddKeyData("scanResult", - fmt.Sprintf("%s/%s", config.AnalyzedRef, config.CommitID), - fmt.Sprintf("%s %s - %s %s", orgName, repoName, config.AnalyzedRef, config.CommitID), - fmt.Sprintf("%s/security/code-scanning?query=is:open+ref:%s", repoURL, config.AnalyzedRef)) + fmt.Sprintf("%s/%s", repoInfo.ref, repoInfo.commitId), + fmt.Sprintf("%s %s - %s %s", repoInfo.owner, repoInfo.repo, repoInfo.ref, repoInfo.commitId), + fmt.Sprintf("%s/security/code-scanning?query=is:open+ref:%s", repoUrl, repoInfo.ref)) if err != nil { - return "", err - } - err = record.Persist() - if err != nil { - return "", err + return record, err } - return record.GetFileName(), nil -} -func parseRepositoryURL(repository string) (toolInstance, orgName, repoName string, err error) { - if repository == "" { - err = errors.New("Repository param is not set") - return - } - fullRepo := strings.TrimSuffix(repository, ".git") - // regexp for toolInstance - re := regexp.MustCompile(`^[a-zA-Z0-9]+://[a-zA-Z0-9-_.]+/`) - matchedHost := re.FindAllString(fullRepo, -1) - if len(matchedHost) == 0 { - err = errors.New("Unable to parse tool instance from repository url") - return - } - orgRepoNames := strings.Split(strings.TrimPrefix(fullRepo, matchedHost[0]), "/") - if len(orgRepoNames) < 2 { - err = errors.New("Unable to parse organization and repo names from repository url") - return - } - - toolInstance = strings.Trim(matchedHost[0], "/") - orgName = orgRepoNames[0] - repoName = orgRepoNames[1] - return + return record, nil } func buildRepoReference(repository, analyzedRef string) (string, error) { - if repository == "" || analyzedRef == "" { - return "", errors.New("Repository or analyzedRef param is not set") - } ref := strings.Split(analyzedRef, "/") if len(ref) < 3 { return "", errors.New(fmt.Sprintf("Wrong analyzedRef format: %s", analyzedRef)) @@ -344,6 +389,14 @@ func buildRepoReference(repository, analyzedRef string) (string, error) { return fmt.Sprintf("%s/tree/%s", repository, ref[2]), nil } +func persistToolRecord(toolRecord *toolrecord.Toolrecord) (string, error) { + err := toolRecord.Persist() + if err != nil { + return "", err + } + return toolRecord.GetFileName(), nil +} + func getRamAndThreadsFromConfig(config *codeqlExecuteScanOptions) []string { params := make([]string, 0, 2) if len(config.Threads) > 0 { diff --git a/cmd/codeqlExecuteScan_generated.go b/cmd/codeqlExecuteScan_generated.go index 90238708fd..8c551a0885 100644 --- a/cmd/codeqlExecuteScan_generated.go +++ b/cmd/codeqlExecuteScan_generated.go @@ -20,19 +20,22 @@ import ( ) type codeqlExecuteScanOptions struct { - GithubToken string `json:"githubToken,omitempty"` - BuildTool string `json:"buildTool,omitempty" validate:"possible-values=custom maven golang npm pip yarn"` - BuildCommand string `json:"buildCommand,omitempty"` - Language string `json:"language,omitempty"` - ModulePath string `json:"modulePath,omitempty"` - Database string `json:"database,omitempty"` - QuerySuite string `json:"querySuite,omitempty"` - UploadResults bool `json:"uploadResults,omitempty"` - Threads string `json:"threads,omitempty"` - Ram string `json:"ram,omitempty"` - AnalyzedRef string `json:"analyzedRef,omitempty"` - Repository string `json:"repository,omitempty"` - CommitID string `json:"commitId,omitempty"` + GithubToken string `json:"githubToken,omitempty"` + GithubAPIURL string `json:"githubApiUrl,omitempty"` + BuildTool string `json:"buildTool,omitempty" validate:"possible-values=custom maven golang npm pip yarn"` + BuildCommand string `json:"buildCommand,omitempty"` + Language string `json:"language,omitempty"` + ModulePath string `json:"modulePath,omitempty"` + Database string `json:"database,omitempty"` + QuerySuite string `json:"querySuite,omitempty"` + UploadResults bool `json:"uploadResults,omitempty"` + Threads string `json:"threads,omitempty"` + Ram string `json:"ram,omitempty"` + AnalyzedRef string `json:"analyzedRef,omitempty"` + Repository string `json:"repository,omitempty"` + CommitID string `json:"commitId,omitempty"` + VulnerabilityThresholdTotal int `json:"vulnerabilityThresholdTotal,omitempty"` + CheckForCompliance bool `json:"checkForCompliance,omitempty"` } type codeqlExecuteScanReports struct { @@ -173,6 +176,7 @@ and Java plus Maven.`, func addCodeqlExecuteScanFlags(cmd *cobra.Command, stepConfig *codeqlExecuteScanOptions) { cmd.Flags().StringVar(&stepConfig.GithubToken, "githubToken", os.Getenv("PIPER_githubToken"), "GitHub personal access token in plain text. NEVER set this parameter in a file commited to a source code repository. This parameter is intended to be used from the command line or set securely via the environment variable listed below. In most pipeline use-cases, you should instead either store the token in Vault (where it can be automatically retrieved by the step from one of the paths listed below) or store it as a Jenkins secret and configure the secret's id via the `githubTokenCredentialsId` parameter.") + cmd.Flags().StringVar(&stepConfig.GithubAPIURL, "githubApiUrl", `https://api.github.com`, "Set the GitHub API URL.") cmd.Flags().StringVar(&stepConfig.BuildTool, "buildTool", `maven`, "Defines the build tool which is used for building the project.") cmd.Flags().StringVar(&stepConfig.BuildCommand, "buildCommand", os.Getenv("PIPER_buildCommand"), "Command to build the project") cmd.Flags().StringVar(&stepConfig.Language, "language", os.Getenv("PIPER_language"), "The programming language used to analyze.") @@ -185,6 +189,8 @@ func addCodeqlExecuteScanFlags(cmd *cobra.Command, stepConfig *codeqlExecuteScan cmd.Flags().StringVar(&stepConfig.AnalyzedRef, "analyzedRef", os.Getenv("PIPER_analyzedRef"), "Name of the ref that was analyzed.") cmd.Flags().StringVar(&stepConfig.Repository, "repository", os.Getenv("PIPER_repository"), "URL of the GitHub instance") cmd.Flags().StringVar(&stepConfig.CommitID, "commitId", os.Getenv("PIPER_commitId"), "SHA of commit that was analyzed.") + cmd.Flags().IntVar(&stepConfig.VulnerabilityThresholdTotal, "vulnerabilityThresholdTotal", 0, "Threashold for maximum number of allowed vulnerabilities.") + cmd.Flags().BoolVar(&stepConfig.CheckForCompliance, "checkForCompliance", false, "If set to true, the piper step checks for compliance based on vulnerability threadholds. Example - If total vulnerabilites are 10 and vulnerabilityThresholdTotal is set as 0, then the steps throws an compliance error.") cmd.MarkFlagRequired("buildTool") } @@ -228,6 +234,15 @@ func codeqlExecuteScanMetadata() config.StepData { Aliases: []config.Alias{{Name: "access_token"}}, Default: os.Getenv("PIPER_githubToken"), }, + { + Name: "githubApiUrl", + ResourceRef: []config.ResourceReference{}, + Scope: []string{"GENERAL", "PARAMETERS", "STAGES", "STEPS"}, + Type: "string", + Mandatory: false, + Aliases: []config.Alias{}, + Default: `https://api.github.com`, + }, { Name: "buildTool", ResourceRef: []config.ResourceReference{}, @@ -351,6 +366,24 @@ func codeqlExecuteScanMetadata() config.StepData { Aliases: []config.Alias{}, Default: os.Getenv("PIPER_commitId"), }, + { + Name: "vulnerabilityThresholdTotal", + ResourceRef: []config.ResourceReference{}, + Scope: []string{"PARAMETERS", "STAGES", "STEPS"}, + Type: "int", + Mandatory: false, + Aliases: []config.Alias{}, + Default: 0, + }, + { + Name: "checkForCompliance", + ResourceRef: []config.ResourceReference{}, + Scope: []string{"PARAMETERS", "STAGES", "STEPS"}, + Type: "bool", + Mandatory: false, + Aliases: []config.Alias{}, + Default: false, + }, }, }, Containers: []config.Container{ diff --git a/cmd/codeqlExecuteScan_test.go b/cmd/codeqlExecuteScan_test.go index 4e81ab0ca2..6cc03013ad 100644 --- a/cmd/codeqlExecuteScan_test.go +++ b/cmd/codeqlExecuteScan_test.go @@ -1,9 +1,11 @@ package cmd import ( + "fmt" "testing" "github.com/SAP/jenkins-library/pkg/mock" + "github.com/SAP/jenkins-library/pkg/orchestrator" "github.com/stretchr/testify/assert" ) @@ -24,42 +26,50 @@ func TestRunCodeqlExecuteScan(t *testing.T) { t.Run("Valid CodeqlExecuteScan", func(t *testing.T) { config := codeqlExecuteScanOptions{BuildTool: "maven", ModulePath: "./"} - assert.Equal(t, nil, runCodeqlExecuteScan(&config, nil, newCodeqlExecuteScanTestsUtils())) + _, err := runCodeqlExecuteScan(&config, nil, newCodeqlExecuteScanTestsUtils()) + assert.NoError(t, err) }) t.Run("No auth token passed on upload results", func(t *testing.T) { config := codeqlExecuteScanOptions{BuildTool: "maven", UploadResults: true, ModulePath: "./"} - assert.Error(t, runCodeqlExecuteScan(&config, nil, newCodeqlExecuteScanTestsUtils())) + _, err := runCodeqlExecuteScan(&config, nil, newCodeqlExecuteScanTestsUtils()) + assert.Error(t, err) }) t.Run("GitCommitID is NA on upload results", func(t *testing.T) { config := codeqlExecuteScanOptions{BuildTool: "maven", UploadResults: true, ModulePath: "./", CommitID: "NA"} - assert.Error(t, runCodeqlExecuteScan(&config, nil, newCodeqlExecuteScanTestsUtils())) + _, err := runCodeqlExecuteScan(&config, nil, newCodeqlExecuteScanTestsUtils()) + assert.Error(t, err) }) - t.Run("Upload results with token", func(t *testing.T) { + t.Run("Upload results fails as repository not specified", func(t *testing.T) { config := codeqlExecuteScanOptions{BuildTool: "maven", ModulePath: "./", UploadResults: true, GithubToken: "test"} - assert.Equal(t, nil, runCodeqlExecuteScan(&config, nil, newCodeqlExecuteScanTestsUtils())) + _, err := runCodeqlExecuteScan(&config, nil, newCodeqlExecuteScanTestsUtils()) + assert.Error(t, err) }) t.Run("Custom buildtool", func(t *testing.T) { - config := codeqlExecuteScanOptions{BuildTool: "custom", Language: "javascript", ModulePath: "./", GithubToken: "test"} - assert.Equal(t, nil, runCodeqlExecuteScan(&config, nil, newCodeqlExecuteScanTestsUtils())) + config := codeqlExecuteScanOptions{BuildTool: "custom", Language: "javascript", ModulePath: "./"} + _, err := runCodeqlExecuteScan(&config, nil, newCodeqlExecuteScanTestsUtils()) + assert.NoError(t, err) }) t.Run("Custom buildtool but no language specified", func(t *testing.T) { config := codeqlExecuteScanOptions{BuildTool: "custom", ModulePath: "./", GithubToken: "test"} - assert.Error(t, runCodeqlExecuteScan(&config, nil, newCodeqlExecuteScanTestsUtils())) + _, err := runCodeqlExecuteScan(&config, nil, newCodeqlExecuteScanTestsUtils()) + assert.Error(t, err) }) t.Run("Invalid buildtool and no language specified", func(t *testing.T) { config := codeqlExecuteScanOptions{BuildTool: "test", ModulePath: "./", GithubToken: "test"} - assert.Error(t, runCodeqlExecuteScan(&config, nil, newCodeqlExecuteScanTestsUtils())) + _, err := runCodeqlExecuteScan(&config, nil, newCodeqlExecuteScanTestsUtils()) + assert.Error(t, err) }) t.Run("Invalid buildtool but language specified", func(t *testing.T) { config := codeqlExecuteScanOptions{BuildTool: "test", Language: "javascript", ModulePath: "./", GithubToken: "test"} - assert.Equal(t, nil, runCodeqlExecuteScan(&config, nil, newCodeqlExecuteScanTestsUtils())) + _, err := runCodeqlExecuteScan(&config, nil, newCodeqlExecuteScanTestsUtils()) + assert.NoError(t, err) }) } @@ -69,7 +79,8 @@ func TestGetGitRepoInfo(t *testing.T) { err := getGitRepoInfo("https://github.hello.test/Testing/fortify.git", &repoInfo) assert.NoError(t, err) assert.Equal(t, "https://github.hello.test", repoInfo.serverUrl) - assert.Equal(t, "Testing/fortify", repoInfo.repo) + assert.Equal(t, "fortify", repoInfo.repo) + assert.Equal(t, "Testing", repoInfo.owner) }) t.Run("Valid URL2", func(t *testing.T) { @@ -77,14 +88,16 @@ func TestGetGitRepoInfo(t *testing.T) { err := getGitRepoInfo("https://github.hello.test/Testing/fortify", &repoInfo) assert.NoError(t, err) assert.Equal(t, "https://github.hello.test", repoInfo.serverUrl) - assert.Equal(t, "Testing/fortify", repoInfo.repo) + assert.Equal(t, "fortify", repoInfo.repo) + assert.Equal(t, "Testing", repoInfo.owner) }) t.Run("Valid URL1 with dots", func(t *testing.T) { var repoInfo RepoInfo err := getGitRepoInfo("https://github.hello.test/Testing/com.sap.fortify.git", &repoInfo) assert.NoError(t, err) assert.Equal(t, "https://github.hello.test", repoInfo.serverUrl) - assert.Equal(t, "Testing/com.sap.fortify", repoInfo.repo) + assert.Equal(t, "com.sap.fortify", repoInfo.repo) + assert.Equal(t, "Testing", repoInfo.owner) }) t.Run("Valid URL2 with dots", func(t *testing.T) { @@ -92,14 +105,16 @@ func TestGetGitRepoInfo(t *testing.T) { err := getGitRepoInfo("https://github.hello.test/Testing/com.sap.fortify", &repoInfo) assert.NoError(t, err) assert.Equal(t, "https://github.hello.test", repoInfo.serverUrl) - assert.Equal(t, "Testing/com.sap.fortify", repoInfo.repo) + assert.Equal(t, "com.sap.fortify", repoInfo.repo) + assert.Equal(t, "Testing", repoInfo.owner) }) t.Run("Valid URL1 with username and token", func(t *testing.T) { var repoInfo RepoInfo err := getGitRepoInfo("https://username:token@github.hello.test/Testing/fortify.git", &repoInfo) assert.NoError(t, err) assert.Equal(t, "https://github.hello.test", repoInfo.serverUrl) - assert.Equal(t, "Testing/fortify", repoInfo.repo) + assert.Equal(t, "fortify", repoInfo.repo) + assert.Equal(t, "Testing", repoInfo.owner) }) t.Run("Valid URL2 with username and token", func(t *testing.T) { @@ -107,7 +122,8 @@ func TestGetGitRepoInfo(t *testing.T) { err := getGitRepoInfo("https://username:token@github.hello.test/Testing/fortify", &repoInfo) assert.NoError(t, err) assert.Equal(t, "https://github.hello.test", repoInfo.serverUrl) - assert.Equal(t, "Testing/fortify", repoInfo.repo) + assert.Equal(t, "fortify", repoInfo.repo) + assert.Equal(t, "Testing", repoInfo.owner) }) t.Run("Invalid URL as no org/owner passed", func(t *testing.T) { @@ -121,58 +137,78 @@ func TestGetGitRepoInfo(t *testing.T) { }) } -func TestParseRepositoryURL(t *testing.T) { - t.Run("Valid repository", func(t *testing.T) { - repository := "https://github.hello.test/Testing/fortify.git" - toolInstance, orgName, repoName, err := parseRepositoryURL(repository) - assert.NoError(t, err) - assert.Equal(t, "https://github.hello.test", toolInstance) - assert.Equal(t, "Testing", orgName) - assert.Equal(t, "fortify", repoName) +func TestInitGitInfo(t *testing.T) { + t.Run("Valid URL1", func(t *testing.T) { + config := codeqlExecuteScanOptions{Repository: "https://github.hello.test/Testing/codeql.git", AnalyzedRef: "refs/head/branch", CommitID: "abcd1234"} + repoInfo := initGitInfo(&config) + assert.Equal(t, "abcd1234", repoInfo.commitId) + assert.Equal(t, "Testing", repoInfo.owner) + assert.Equal(t, "codeql", repoInfo.repo) + assert.Equal(t, "refs/head/branch", repoInfo.ref) + assert.Equal(t, "https://github.hello.test", repoInfo.serverUrl) }) - t.Run("valid repository 2", func(t *testing.T) { - repository := "https://github.hello.test/Testing/fortify" - toolInstance, orgName, repoName, err := parseRepositoryURL(repository) - assert.NoError(t, err) - assert.Equal(t, "https://github.hello.test", toolInstance) - assert.Equal(t, "Testing", orgName) - assert.Equal(t, "fortify", repoName) + + t.Run("Valid URL2", func(t *testing.T) { + config := codeqlExecuteScanOptions{Repository: "https://github.hello.test/Testing/codeql", AnalyzedRef: "refs/head/branch", CommitID: "abcd1234"} + repoInfo := initGitInfo(&config) + assert.Equal(t, "abcd1234", repoInfo.commitId) + assert.Equal(t, "Testing", repoInfo.owner) + assert.Equal(t, "codeql", repoInfo.repo) + assert.Equal(t, "refs/head/branch", repoInfo.ref) + assert.Equal(t, "https://github.hello.test", repoInfo.serverUrl) }) - t.Run("Invalid repository without repo name", func(t *testing.T) { - repository := "https://github.hello.test/Testing" - toolInstance, orgName, repoName, err := parseRepositoryURL(repository) - assert.Error(t, err) - assert.ErrorContains(t, err, "Unable to parse organization and repo names") - assert.Equal(t, "", toolInstance) - assert.Equal(t, "", orgName) - assert.Equal(t, "", repoName) - }) - t.Run("Invalid repository without organization name", func(t *testing.T) { - repository := "https://github.hello.test/fortify" - toolInstance, orgName, repoName, err := parseRepositoryURL(repository) - assert.Error(t, err) - assert.ErrorContains(t, err, "Unable to parse organization and repo names") - assert.Equal(t, "", toolInstance) - assert.Equal(t, "", orgName) - assert.Equal(t, "", repoName) - }) - t.Run("Invalid repository without tool instance", func(t *testing.T) { - repository := "/Testing/fortify" - toolInstance, orgName, repoName, err := parseRepositoryURL(repository) - assert.Error(t, err) - assert.ErrorContains(t, err, "Unable to parse tool instance") - assert.Equal(t, "", toolInstance) - assert.Equal(t, "", orgName) - assert.Equal(t, "", repoName) - }) - t.Run("Empty repository", func(t *testing.T) { - repository := "" - toolInstance, orgName, repoName, err := parseRepositoryURL(repository) - assert.Error(t, err) - assert.ErrorContains(t, err, "Repository param is not set") - assert.Equal(t, "", toolInstance) - assert.Equal(t, "", orgName) - assert.Equal(t, "", repoName) + + t.Run("Valid url with dots URL1", func(t *testing.T) { + config := codeqlExecuteScanOptions{Repository: "https://github.hello.test/Testing/com.sap.codeql.git", AnalyzedRef: "refs/head/branch", CommitID: "abcd1234"} + repoInfo := initGitInfo(&config) + assert.Equal(t, "abcd1234", repoInfo.commitId) + assert.Equal(t, "Testing", repoInfo.owner) + assert.Equal(t, "com.sap.codeql", repoInfo.repo) + assert.Equal(t, "refs/head/branch", repoInfo.ref) + assert.Equal(t, "https://github.hello.test", repoInfo.serverUrl) + }) + + t.Run("Valid url with dots URL2", func(t *testing.T) { + config := codeqlExecuteScanOptions{Repository: "https://github.hello.test/Testing/com.sap.codeql", AnalyzedRef: "refs/head/branch", CommitID: "abcd1234"} + repoInfo := initGitInfo(&config) + assert.Equal(t, "abcd1234", repoInfo.commitId) + assert.Equal(t, "Testing", repoInfo.owner) + assert.Equal(t, "com.sap.codeql", repoInfo.repo) + assert.Equal(t, "refs/head/branch", repoInfo.ref) + assert.Equal(t, "https://github.hello.test", repoInfo.serverUrl) + }) + + t.Run("Valid url with username and token URL1", func(t *testing.T) { + config := codeqlExecuteScanOptions{Repository: "https://username:token@github.hello.test/Testing/codeql.git", AnalyzedRef: "refs/head/branch", CommitID: "abcd1234"} + repoInfo := initGitInfo(&config) + assert.Equal(t, "abcd1234", repoInfo.commitId) + assert.Equal(t, "Testing", repoInfo.owner) + assert.Equal(t, "codeql", repoInfo.repo) + assert.Equal(t, "refs/head/branch", repoInfo.ref) + assert.Equal(t, "https://github.hello.test", repoInfo.serverUrl) + }) + + t.Run("Valid url with username and token URL2", func(t *testing.T) { + config := codeqlExecuteScanOptions{Repository: "https://username:token@github.hello.test/Testing/codeql", AnalyzedRef: "refs/head/branch", CommitID: "abcd1234"} + repoInfo := initGitInfo(&config) + assert.Equal(t, "abcd1234", repoInfo.commitId) + assert.Equal(t, "Testing", repoInfo.owner) + assert.Equal(t, "codeql", repoInfo.repo) + assert.Equal(t, "refs/head/branch", repoInfo.ref) + assert.Equal(t, "https://github.hello.test", repoInfo.serverUrl) + }) + + t.Run("Invalid URL with no org/reponame", func(t *testing.T) { + config := codeqlExecuteScanOptions{Repository: "https://github.hello.test", AnalyzedRef: "refs/head/branch", CommitID: "abcd1234"} + repoInfo := initGitInfo(&config) + _, err := orchestrator.NewOrchestratorSpecificConfigProvider() + assert.Equal(t, "abcd1234", repoInfo.commitId) + assert.Equal(t, "refs/head/branch", repoInfo.ref) + if err != nil { + assert.Equal(t, "", repoInfo.owner) + assert.Equal(t, "", repoInfo.repo) + assert.Equal(t, "", repoInfo.serverUrl) + } }) } @@ -207,87 +243,56 @@ func TestBuildRepoReference(t *testing.T) { assert.ErrorContains(t, err, "Wrong analyzedRef format") assert.Equal(t, "", ref) }) - t.Run("Empty repository", func(t *testing.T) { - repository := "" - analyzedRef := "refs/pull/merge" - ref, err := buildRepoReference(repository, analyzedRef) - assert.Error(t, err) - assert.ErrorContains(t, err, "Repository or analyzedRef param is not set") - assert.Equal(t, "", ref) - }) - t.Run("Empty analyzedRef", func(t *testing.T) { - repository := "https://github.hello.test/Testing/fortify" - analyzedRef := "" - ref, err := buildRepoReference(repository, analyzedRef) - assert.Error(t, err) - assert.ErrorContains(t, err, "Repository or analyzedRef param is not set") - assert.Equal(t, "", ref) - }) } +func getRepoReferences(repoInfo RepoInfo) (string, string, string) { + repoUrl := fmt.Sprintf("%s/%s/%s", repoInfo.serverUrl, repoInfo.owner, repoInfo.repo) + repoReference, _ := buildRepoReference(repoUrl, repoInfo.ref) + repoCodeqlScanUrl := fmt.Sprintf("%s/security/code-scanning?query=is:open+ref:%s", repoUrl, repoInfo.ref) + return repoUrl, repoReference, repoCodeqlScanUrl +} func TestCreateToolRecordCodeql(t *testing.T) { t.Run("Valid toolrun file", func(t *testing.T) { - config := codeqlExecuteScanOptions{ - Repository: "https://github.hello.test/Testing/fortify.git", - AnalyzedRef: "refs/head/branch", - CommitID: "test", - } - fileName, err := createToolRecordCodeql(newCodeqlExecuteScanTestsUtils(), "test", config) + repoInfo := RepoInfo{serverUrl: "https://github.hello.test", commitId: "test", ref: "refs/head/branch", owner: "Testing", repo: "fortify"} + repoUrl, repoReference, repoCodeqlScanUrl := getRepoReferences(repoInfo) + toolRecord, err := createToolRecordCodeql(newCodeqlExecuteScanTestsUtils(), repoInfo, repoUrl, repoReference, repoCodeqlScanUrl) assert.NoError(t, err) - assert.Contains(t, fileName, "toolrun_codeql") + assert.Equal(t, toolRecord.ToolName, "codeql") + assert.Equal(t, toolRecord.ToolInstance, "https://github.hello.test") + assert.Equal(t, toolRecord.DisplayName, "Testing fortify - refs/head/branch test") + assert.Equal(t, toolRecord.DisplayURL, "https://github.hello.test/Testing/fortify/security/code-scanning?query=is:open+ref:refs/head/branch") }) t.Run("Empty repository URL", func(t *testing.T) { - config := codeqlExecuteScanOptions{ - Repository: "", - AnalyzedRef: "refs/head/branch", - CommitID: "test", - } - fileName, err := createToolRecordCodeql(newCodeqlExecuteScanTestsUtils(), "", config) - assert.Error(t, err) - assert.ErrorContains(t, err, "Repository param is not set") - assert.Empty(t, fileName) - }) - t.Run("Invalid repository URL", func(t *testing.T) { - config := codeqlExecuteScanOptions{ - Repository: "https://github.hello.test/Testing", - AnalyzedRef: "refs/head/branch", - CommitID: "test", - } - fileName, err := createToolRecordCodeql(newCodeqlExecuteScanTestsUtils(), "test", config) - assert.Error(t, err) - assert.Regexp(t, "^Unable to parse [a-z ]+ from repository url$", err.Error()) - assert.Empty(t, fileName) - }) - t.Run("Empty workspace", func(t *testing.T) { - config := codeqlExecuteScanOptions{ - Repository: "https://github.hello.test/Testing/fortify.git", - AnalyzedRef: "refs/head/branch", - CommitID: "test", - } - fileName, err := createToolRecordCodeql(newCodeqlExecuteScanTestsUtils(), "", config) + repoInfo := RepoInfo{serverUrl: "", commitId: "test", ref: "refs/head/branch", owner: "Testing", repo: "fortify"} + repoUrl, repoReference, repoCodeqlScanUrl := getRepoReferences(repoInfo) + _, err := createToolRecordCodeql(newCodeqlExecuteScanTestsUtils(), repoInfo, repoUrl, repoReference, repoCodeqlScanUrl) + assert.Error(t, err) - assert.ErrorContains(t, err, "TR_PERSIST: empty workspace") - assert.Empty(t, fileName) + assert.ErrorContains(t, err, "Repository not set") }) + t.Run("Empty analyzedRef", func(t *testing.T) { - config := codeqlExecuteScanOptions{ - Repository: "https://github.hello.test/Testing/fortify.git", - AnalyzedRef: "", - CommitID: "test", - } - fileName, err := createToolRecordCodeql(newCodeqlExecuteScanTestsUtils(), "test", config) + repoInfo := RepoInfo{serverUrl: "https://github.hello.test", commitId: "test", ref: "", owner: "Testing", repo: "fortify"} + repoUrl, repoReference, repoCodeqlScanUrl := getRepoReferences(repoInfo) + _, err := createToolRecordCodeql(newCodeqlExecuteScanTestsUtils(), repoInfo, repoUrl, repoReference, repoCodeqlScanUrl) + assert.Error(t, err) - assert.ErrorContains(t, err, "TR_ADD_KEY: empty keyvalue") - assert.Empty(t, fileName, "toolrun_codeql") + assert.ErrorContains(t, err, "Analyzed Reference not set") + }) + + t.Run("Empty CommitId", func(t *testing.T) { + repoInfo := RepoInfo{serverUrl: "https://github.hello.test", commitId: "", ref: "refs/head/branch", owner: "Testing", repo: "fortify"} + repoUrl, repoReference, repoCodeqlScanUrl := getRepoReferences(repoInfo) + _, err := createToolRecordCodeql(newCodeqlExecuteScanTestsUtils(), repoInfo, repoUrl, repoReference, repoCodeqlScanUrl) + + assert.Error(t, err) + assert.ErrorContains(t, err, "CommitId not set") }) t.Run("Invalid analyzedRef", func(t *testing.T) { - config := codeqlExecuteScanOptions{ - Repository: "https://github.hello.test/Testing/fortify.git", - AnalyzedRef: "refs/head", - CommitID: "test", - } - fileName, err := createToolRecordCodeql(newCodeqlExecuteScanTestsUtils(), "test", config) - assert.NoError(t, err) - assert.Contains(t, fileName, "toolrun_codeql") + repoInfo := RepoInfo{serverUrl: "https://github.hello.test", commitId: "", ref: "refs/branch", owner: "Testing", repo: "fortify"} + repoUrl, repoReference, repoCodeqlScanUrl := getRepoReferences(repoInfo) + _, err := createToolRecordCodeql(newCodeqlExecuteScanTestsUtils(), repoInfo, repoUrl, repoReference, repoCodeqlScanUrl) + + assert.Error(t, err) }) } diff --git a/pkg/codeql/codeql.go b/pkg/codeql/codeql.go new file mode 100644 index 0000000000..7e7f82c99c --- /dev/null +++ b/pkg/codeql/codeql.go @@ -0,0 +1,65 @@ +package codeql + +import ( + "context" + + sapgithub "github.com/SAP/jenkins-library/pkg/github" + "github.com/google/go-github/v45/github" +) + +type CodeqlScanAudit interface { + GetVulnerabilities(analyzedRef string, state string) error +} + +type githubCodeqlScanningService interface { + ListAlertsForRepo(ctx context.Context, owner, repo string, opts *github.AlertListOptions) ([]*github.Alert, *github.Response, error) +} + +const auditStateOpen = "open" + +func NewCodeqlScanAuditInstance(apiURL, owner, repository, token string, trustedCerts []string) CodeqlScanAuditInstance { + return CodeqlScanAuditInstance{apiURL: apiURL, owner: owner, repository: repository, token: token, trustedCerts: trustedCerts} +} + +type CodeqlScanAuditInstance struct { + apiURL string + owner string + repository string + token string + trustedCerts []string + alertListoptions github.AlertListOptions +} + +func (codeqlScanAudit *CodeqlScanAuditInstance) GetVulnerabilities(analyzedRef string) (CodeqlScanning, error) { + ctx, client, err := sapgithub.NewClient(codeqlScanAudit.token, codeqlScanAudit.apiURL, "", codeqlScanAudit.trustedCerts) + if err != nil { + return CodeqlScanning{}, err + } + + return getVulnerabilitiesFromClient(ctx, client.CodeScanning, analyzedRef, codeqlScanAudit) +} + +func getVulnerabilitiesFromClient(ctx context.Context, codeScanning githubCodeqlScanningService, analyzedRef string, codeqlScanAudit *CodeqlScanAuditInstance) (CodeqlScanning, error) { + alertOptions := github.AlertListOptions{ + State: "", + Ref: analyzedRef, + ListOptions: github.ListOptions{}, + } + + alerts, _, err := codeScanning.ListAlertsForRepo(ctx, codeqlScanAudit.owner, codeqlScanAudit.repository, &alertOptions) + if err != nil { + return CodeqlScanning{}, err + } + + openStateCount := 0 + for _, alert := range alerts { + if *alert.State == auditStateOpen { + openStateCount = openStateCount + 1 + } + } + + codeqlScanning := CodeqlScanning{} + codeqlScanning.Total = len(alerts) + codeqlScanning.Audited = (codeqlScanning.Total - openStateCount) + return codeqlScanning, nil +} diff --git a/pkg/codeql/codeql_test.go b/pkg/codeql/codeql_test.go new file mode 100644 index 0000000000..c0f64aee64 --- /dev/null +++ b/pkg/codeql/codeql_test.go @@ -0,0 +1,47 @@ +package codeql + +import ( + "context" + "errors" + "testing" + + "github.com/google/go-github/v45/github" + "github.com/stretchr/testify/assert" +) + +type githubCodeqlScanningMock struct { +} + +func (g *githubCodeqlScanningMock) ListAlertsForRepo(ctx context.Context, owner, repo string, opts *github.AlertListOptions) ([]*github.Alert, *github.Response, error) { + openState := "open" + closedState := "closed" + alerts := []*github.Alert{{State: &openState}, {State: &openState}, {State: &closedState}} + return alerts, nil, nil +} + +type githubCodeqlScanningErrorMock struct { +} + +func (g *githubCodeqlScanningErrorMock) ListAlertsForRepo(ctx context.Context, owner, repo string, opts *github.AlertListOptions) ([]*github.Alert, *github.Response, error) { + return []*github.Alert{}, nil, errors.New("Some error") +} + +func TestGetVulnerabilitiesFromClient(t *testing.T) { + ctx := context.Background() + t.Parallel() + t.Run("Success", func(t *testing.T) { + ghCodeqlScanningMock := githubCodeqlScanningMock{} + codeqlScanAuditInstance := NewCodeqlScanAuditInstance("", "", "", "", []string{}) + codeScanning, err := getVulnerabilitiesFromClient(ctx, &ghCodeqlScanningMock, "ref", &codeqlScanAuditInstance) + assert.NoError(t, err) + assert.Equal(t, 3, codeScanning.Total) + assert.Equal(t, 1, codeScanning.Audited) + }) + + t.Run("Error", func(t *testing.T) { + ghCodeqlScanningErrorMock := githubCodeqlScanningErrorMock{} + codeqlScanAuditInstance := NewCodeqlScanAuditInstance("", "", "", "", []string{}) + _, err := getVulnerabilitiesFromClient(ctx, &ghCodeqlScanningErrorMock, "ref", &codeqlScanAuditInstance) + assert.Error(t, err) + }) +} diff --git a/pkg/codeql/reporting.go b/pkg/codeql/reporting.go new file mode 100644 index 0000000000..e2ac9fa1f0 --- /dev/null +++ b/pkg/codeql/reporting.go @@ -0,0 +1,44 @@ +package codeql + +import ( + "encoding/json" + "path/filepath" + + "github.com/SAP/jenkins-library/pkg/log" + "github.com/SAP/jenkins-library/pkg/piperutils" + "github.com/pkg/errors" +) + +type CodeqlAudit struct { + ToolName string `json:"toolName"` + RepositoryUrl string `json:"repositoryUrl"` + RepositoryReferenceUrl string `json:"repositoryReferenceUrl"` //URL of PR or Branch where scan was performed + CodeScanningLink string `json:"codeScanningLink"` + ScanResults CodeqlScanning `json:"scanResults"` +} + +type CodeqlScanning struct { + Total int `json:"total"` + Audited int `json:"audited"` +} + +func WriteJSONReport(jsonReport CodeqlAudit, modulePath string) ([]piperutils.Path, error) { + utils := piperutils.Files{} + reportPaths := []piperutils.Path{} + + reportsDirectory := filepath.Join(modulePath, "codeql") + jsonComplianceReportPath := filepath.Join(reportsDirectory, "piper_codeql_report.json") + if err := utils.MkdirAll(reportsDirectory, 0777); err != nil { + return reportPaths, errors.Wrapf(err, "failed to create report directory") + } + + file, _ := json.Marshal(jsonReport) + if err := utils.FileWrite(jsonComplianceReportPath, file, 0666); err != nil { + log.SetErrorCategory(log.ErrorConfiguration) + return reportPaths, errors.Wrapf(err, "failed to write codeql json compliance report") + } + + reportPaths = append(reportPaths, piperutils.Path{Name: "Codeql JSON Compliance Report", Target: jsonComplianceReportPath}) + + return reportPaths, nil +} diff --git a/resources/metadata/codeqlExecuteScan.yaml b/resources/metadata/codeqlExecuteScan.yaml index 65d5dce95d..42e9280cd9 100644 --- a/resources/metadata/codeqlExecuteScan.yaml +++ b/resources/metadata/codeqlExecuteScan.yaml @@ -39,6 +39,15 @@ spec: - type: vaultSecret default: github name: githubVaultSecretName + - name: githubApiUrl + description: "Set the GitHub API URL." + scope: + - GENERAL + - PARAMETERS + - STAGES + - STEPS + type: string + default: "https://api.github.com" - name: buildTool type: string description: Defines the build tool which is used for building the project. @@ -142,6 +151,22 @@ spec: - name: commonPipelineEnvironment param: git/remoteCommitId type: string + - name: vulnerabilityThresholdTotal + description: "Threashold for maximum number of allowed vulnerabilities." + type: int + default: 0 + scope: + - PARAMETERS + - STAGES + - STEPS + - name: checkForCompliance + description: "If set to true, the piper step checks for compliance based on vulnerability threadholds. Example - If total vulnerabilites are 10 and vulnerabilityThresholdTotal is set as 0, then the steps throws an compliance error." + type: bool + default: false + scope: + - PARAMETERS + - STAGES + - STEPS containers: - image: "" outputs: