1
0
mirror of https://github.com/SAP/jenkins-library.git synced 2025-09-16 09:26:22 +02:00

feat(codeqlExecuteScan): multi language support (#5462)

This commit is contained in:
Timur Akhmadiev
2025-08-29 14:01:14 +04:00
committed by GitHub
parent 88d899fe18
commit d3a0c8fcad
5 changed files with 399 additions and 70 deletions

View File

@@ -8,6 +8,9 @@ import (
"path/filepath"
"strings"
"github.com/google/shlex"
"github.com/pkg/errors"
"github.com/SAP/jenkins-library/pkg/codeql"
"github.com/SAP/jenkins-library/pkg/command"
piperhttp "github.com/SAP/jenkins-library/pkg/http"
@@ -15,8 +18,6 @@ import (
"github.com/SAP/jenkins-library/pkg/maven"
"github.com/SAP/jenkins-library/pkg/piperutils"
"github.com/SAP/jenkins-library/pkg/telemetry"
"github.com/google/shlex"
"github.com/pkg/errors"
)
type codeqlExecuteScanUtils interface {
@@ -141,7 +142,7 @@ func runCodeqlExecuteScan(config *codeqlExecuteScanOptions, telemetryData *telem
var reports []piperutils.Path
dbCreateCustomFlags := codeql.ParseCustomFlags(config.DatabaseCreateFlags)
err := runDatabaseCreate(config, dbCreateCustomFlags, utils)
isMultiLang, err := runDatabaseCreate(config, dbCreateCustomFlags, utils)
if err != nil {
log.Entry().WithError(err).Error("failed to create codeql database")
return reports, err
@@ -154,7 +155,7 @@ func runCodeqlExecuteScan(config *codeqlExecuteScanOptions, telemetryData *telem
}
dbAnalyzeCustomFlags := codeql.ParseCustomFlags(config.DatabaseAnalyzeFlags)
scanReports, err := runDatabaseAnalyze(config, dbAnalyzeCustomFlags, utils)
scanReports, sarifFiles, err := runDatabaseAnalyze(config, dbAnalyzeCustomFlags, utils, isMultiLang)
if err != nil {
log.Entry().WithError(err).Error("failed to analyze codeql database")
return reports, err
@@ -194,7 +195,7 @@ func runCodeqlExecuteScan(config *codeqlExecuteScanOptions, telemetryData *telem
return reports, fmt.Errorf("failed running upload-results as githubToken was not specified")
}
err = uploadSarifResults(config, token, repoInfo, utils)
err = uploadSarifResults(config, token, repoInfo, sarifFiles, utils)
if err != nil {
log.Entry().WithError(err).Error("failed to upload sarif results")
return reports, err
@@ -242,33 +243,77 @@ func runCodeqlExecuteScan(config *codeqlExecuteScanOptions, telemetryData *telem
return reports, nil
}
func runDatabaseCreate(config *codeqlExecuteScanOptions, customFlags map[string]string, utils codeqlExecuteScanUtils) error {
cmd, err := prepareCmdForDatabaseCreate(customFlags, config, utils)
func runDatabaseCreate(config *codeqlExecuteScanOptions, customFlags map[string]string, utils codeqlExecuteScanUtils) (bool, error) {
isMultiLang, cmd, err := prepareCmdForDatabaseCreate(customFlags, config, utils)
if err != nil {
log.Entry().Error("failed to prepare command for codeql database create")
return err
return isMultiLang, err
}
if err = execute(utils, cmd, GeneralConfig.Verbose); err != nil {
log.Entry().Error("failed running command codeql database create")
return err
return isMultiLang, err
}
return nil
return isMultiLang, nil
}
func runDatabaseAnalyze(config *codeqlExecuteScanOptions, customFlags map[string]string, utils codeqlExecuteScanUtils) ([]piperutils.Path, error) {
sarifReport, err := executeAnalysis("sarif-latest", "codeqlReport.sarif", customFlags, config, utils)
if err != nil {
return nil, err
func runDatabaseAnalyze(config *codeqlExecuteScanOptions, customFlags map[string]string, utils codeqlExecuteScanUtils, isMultiLang bool) ([]piperutils.Path, []string, error) {
var reports []piperutils.Path
var sarifFiles []string
if !isMultiLang {
sarifReport, sarifPath, err := executeAnalysis("sarif-latest", "codeqlReport.sarif", customFlags, config, utils, config.Database, "")
if err != nil {
return nil, nil, err
}
reports = append(reports, sarifReport...)
if sarifPath != "" {
sarifFiles = append(sarifFiles, sarifPath)
}
csvReport, _, err := executeAnalysis("csv", "codeqlReport.csv", customFlags, config, utils, config.Database, "")
if err != nil {
return nil, nil, err
}
reports = append(reports, csvReport...)
return reports, sarifFiles, nil
}
csvReport, err := executeAnalysis("csv", "codeqlReport.csv", customFlags, config, utils)
if err != nil {
return nil, err
languages := getLanguageList(config)
for _, lang := range languages {
lang = strings.TrimSpace(lang)
if lang == "" {
continue
}
dbPath := filepath.Join(config.Database, lang)
sarifOut := fmt.Sprintf("%s.sarif", lang)
localFlags := cloneFlags(customFlags)
if !codeql.IsFlagSetByUser(localFlags, []string{"--sarif-category"}) {
localFlags["--sarif-category"] = fmt.Sprintf("--sarif-category=%s", lang)
}
sarifReport, sarifPath, err := executeAnalysis("sarif-latest", sarifOut, localFlags, config, utils, dbPath, lang)
if err != nil {
return nil, nil, err
}
reports = append(reports, sarifReport...)
if sarifPath != "" {
sarifFiles = append(sarifFiles, sarifPath)
}
csvOut := fmt.Sprintf("%s.csv", lang)
csvReport, _, err := executeAnalysis("csv", csvOut, customFlags, config, utils, dbPath, lang)
if err != nil {
return nil, nil, err
}
reports = append(reports, csvReport...)
}
return append(sarifReport, csvReport...), nil
return reports, sarifFiles, nil
}
func runGithubUploadResults(config *codeqlExecuteScanOptions, repoInfo *codeql.RepoInfo, token string, utils codeqlExecuteScanUtils) (string, error) {
cmd := prepareCmdForUploadResults(config, repoInfo, token)
func runGithubUploadResults(repoInfo *codeql.RepoInfo, token string, sarifPath string, utils codeqlExecuteScanUtils) (string, error) {
cmd := prepareCmdForUploadResults(repoInfo, token, sarifPath)
var bufferOut, bufferErr bytes.Buffer
utils.Stdout(&bufferOut)
@@ -289,41 +334,59 @@ func runGithubUploadResults(config *codeqlExecuteScanOptions, repoInfo *codeql.R
return url, nil
}
func executeAnalysis(format, reportName string, customFlags map[string]string, config *codeqlExecuteScanOptions, utils codeqlExecuteScanUtils) ([]piperutils.Path, error) {
func executeAnalysis(format, reportPath string, customFlags map[string]string, config *codeqlExecuteScanOptions, utils codeqlExecuteScanUtils, databasePath string, langForLog string) ([]piperutils.Path, string, error) {
moduleTargetPath := filepath.Join(config.ModulePath, "target")
report := filepath.Join(moduleTargetPath, reportName)
cmd, err := prepareCmdForDatabaseAnalyze(utils, customFlags, config, format, report)
report := filepath.Join(moduleTargetPath, reportPath)
cmd, err := prepareCmdForDatabaseAnalyze(utils, customFlags, config, format, report, databasePath)
if err != nil {
log.Entry().Errorf("failed to prepare command for codeql database analyze (format=%s)", format)
return nil, err
if langForLog == "" {
log.Entry().Errorf("failed to prepare command for codeql database analyze (format=%s)", format)
} else {
log.Entry().Errorf("failed to prepare command for codeql database analyze (format=%s, lang=%s)", format, langForLog)
}
return nil, "", err
}
if err = execute(utils, cmd, GeneralConfig.Verbose); err != nil {
log.Entry().Errorf("failed running command codeql database analyze for %s generation", format)
return nil, err
if langForLog == "" {
log.Entry().Errorf("failed running command codeql database analyze for %s generation", format)
} else {
log.Entry().Errorf("failed running command codeql database analyze for %s generation (lang=%s)", format, langForLog)
}
return nil, "", err
}
return []piperutils.Path{
{Target: report},
}, nil
{Target: report},
}, func() string {
if strings.HasPrefix(format, "sarif") {
return report
}
return ""
}(), nil
}
func prepareCmdForDatabaseCreate(customFlags map[string]string, config *codeqlExecuteScanOptions, utils codeqlExecuteScanUtils) ([]string, error) {
func prepareCmdForDatabaseCreate(customFlags map[string]string, config *codeqlExecuteScanOptions, utils codeqlExecuteScanUtils) (bool, []string, error) {
cmd := []string{"database", "create", config.Database}
cmd = codeql.AppendFlagIfNotSetByUser(cmd, []string{"--overwrite", "--no-overwrite"}, []string{"--overwrite"}, customFlags)
cmd = codeql.AppendFlagIfNotSetByUser(cmd, []string{"--source-root", "-s"}, []string{"--source-root", "."}, customFlags)
cmd = codeql.AppendFlagIfNotSetByUser(cmd, []string{"--working-dir"}, []string{"--working-dir", config.ModulePath}, customFlags)
isMultiLang := false
if !codeql.IsFlagSetByUser(customFlags, []string{"--language", "-l"}) {
language := getLangFromBuildTool(config.BuildTool)
if len(language) == 0 && len(config.Language) == 0 {
if config.BuildTool == "custom" {
return nil, fmt.Errorf("as the buildTool is custom. please specify the language parameter")
return false, nil, fmt.Errorf("as the buildTool is custom. please specify the language parameter")
} else {
return nil, fmt.Errorf("the step could not recognize the specified buildTool %s. please specify valid buildtool", config.BuildTool)
return false, nil, fmt.Errorf("the step could not recognize the specified buildTool %s. please specify valid buildtool", config.BuildTool)
}
}
if len(language) > 0 {
cmd = append(cmd, "--language="+language)
} else {
if strings.Contains(config.Language, ",") { // coma separation used to specify multiple languages
isMultiLang = true
cmd = append(cmd, "--db-cluster")
}
cmd = append(cmd, "--language="+config.Language)
}
}
@@ -341,19 +404,19 @@ func prepareCmdForDatabaseCreate(customFlags map[string]string, config *codeqlEx
}
cmd = codeql.AppendCustomFlags(cmd, customFlags)
return cmd, nil
return isMultiLang, cmd, nil
}
func prepareCmdForDatabaseAnalyze(utils codeqlExecuteScanUtils, customFlags map[string]string, config *codeqlExecuteScanOptions, format, reportName string) ([]string, error) {
cmd := []string{"database", "analyze", "--format=" + format, "--output=" + reportName, config.Database}
func prepareCmdForDatabaseAnalyze(utils codeqlExecuteScanUtils, customFlags map[string]string, config *codeqlExecuteScanOptions, format, reportPath, databasePath string) ([]string, error) {
cmd := []string{"database", "analyze", "--format=" + format, "--output=" + reportPath, databasePath}
cmd = codeql.AppendThreadsAndRam(cmd, config.Threads, config.Ram, customFlags)
cmd = codeql.AppendCustomFlags(cmd, customFlags)
cmd = appendCodeqlQuerySuite(utils, cmd, config.QuerySuite, config.TransformQuerySuite)
return cmd, nil
}
func prepareCmdForUploadResults(config *codeqlExecuteScanOptions, repoInfo *codeql.RepoInfo, token string) []string {
cmd := []string{"github", "upload-results", "--sarif=" + filepath.Join(config.ModulePath, "target", "codeqlReport.sarif")}
func prepareCmdForUploadResults(repoInfo *codeql.RepoInfo, token string, sarifPath string) []string {
cmd := []string{"github", "upload-results", "--sarif=" + sarifPath}
//if no git params 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.
@@ -380,16 +443,22 @@ func prepareCmdForUploadResults(config *codeqlExecuteScanOptions, repoInfo *code
return cmd
}
func uploadSarifResults(config *codeqlExecuteScanOptions, token string, repoInfo *codeql.RepoInfo, utils codeqlExecuteScanUtils) error {
sarifUrl, err := runGithubUploadResults(config, repoInfo, token, utils)
if err != nil {
return err
func uploadSarifResults(config *codeqlExecuteScanOptions, token string, repoInfo *codeql.RepoInfo, sarifFiles []string, utils codeqlExecuteScanUtils) error {
// fallback
if len(sarifFiles) == 0 {
sarifFiles = []string{filepath.Join(config.ModulePath, "target", "codeqlReport.sarif")}
}
codeqlSarifUploader := codeql.NewCodeqlSarifUploaderInstance(sarifUrl, token)
err = codeql.WaitSarifUploaded(config.SarifCheckMaxRetries, config.SarifCheckRetryInterval, &codeqlSarifUploader)
if err != nil {
return errors.Wrap(err, "failed to upload sarif")
for _, sarifPath := range sarifFiles {
sarifUrl, err := runGithubUploadResults(repoInfo, token, sarifPath, utils)
if err != nil {
return err
}
codeqlSarifUploader := codeql.NewCodeqlSarifUploaderInstance(sarifUrl, token)
if err := codeql.WaitSarifUploaded(config.SarifCheckMaxRetries, config.SarifCheckRetryInterval, &codeqlSarifUploader); err != nil {
return errors.Wrapf(err, "failed to upload sarif %s", sarifPath)
}
}
return nil
}
@@ -499,3 +568,35 @@ func updateCmdFlag(config *codeqlExecuteScanOptions, customFlags map[string]stri
customFlags["--command"] = buildCmd
delete(customFlags, "-c")
}
func getLanguageList(config *codeqlExecuteScanOptions) []string {
// prefer explicit config.Language if present; otherwise derive from build tool
if strings.Contains(config.Language, ",") {
parts := strings.Split(config.Language, ",")
out := make([]string, 0, len(parts))
for _, p := range parts {
p = strings.TrimSpace(p)
if p != "" {
out = append(out, p)
}
}
return out
}
if config.Language != "" {
return []string{strings.TrimSpace(config.Language)}
}
// fall back to inferred language (single)
inferred := getLangFromBuildTool(config.BuildTool)
if inferred != "" {
return []string{inferred}
}
return nil
}
func cloneFlags(src map[string]string) map[string]string {
dst := make(map[string]string, len(src))
for k, v := range src {
dst[k] = v
}
return dst
}

View File

@@ -284,7 +284,7 @@ func addCodeqlExecuteScanFlags(cmd *cobra.Command, stepConfig *codeqlExecuteScan
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.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.")
cmd.Flags().StringVar(&stepConfig.Language, "language", os.Getenv("PIPER_language"), "The programming language used to analyze. Use coma separation and select custom build tool to analyze multiple languages")
cmd.Flags().StringVar(&stepConfig.ModulePath, "modulePath", `./`, "Allows providing the path for the module to scan")
cmd.Flags().StringVar(&stepConfig.Database, "database", `codeqlDB`, "Path to the CodeQL database to create. This directory will be created, and must not already exist.")
cmd.Flags().StringVar(&stepConfig.QuerySuite, "querySuite", os.Getenv("PIPER_querySuite"), "The name of a CodeQL query suite. If omitted, the default query suite for the language of the database being analyzed will be used.")

View File

@@ -11,9 +11,11 @@ import (
"strings"
"testing"
"github.com/stretchr/testify/assert"
"github.com/SAP/jenkins-library/pkg/codeql"
"github.com/SAP/jenkins-library/pkg/mock"
"github.com/stretchr/testify/assert"
"github.com/SAP/jenkins-library/pkg/piperutils"
)
type codeqlExecuteScanMockUtils struct {
@@ -364,9 +366,10 @@ func TestPrepareCmdForDatabaseCreate(t *testing.T) {
BuildTool: "maven",
BuildCommand: "mvn clean install",
}
cmd, err := prepareCmdForDatabaseCreate(map[string]string{}, config, newCodeqlExecuteScanTestsUtils())
isMultiLang, cmd, err := prepareCmdForDatabaseCreate(map[string]string{}, config, newCodeqlExecuteScanTestsUtils())
assert.NoError(t, err)
assert.NotEmpty(t, cmd)
assert.False(t, isMultiLang)
assert.Equal(t, 10, len(cmd))
assert.Equal(t, "database create codeqlDB --overwrite --source-root . --working-dir ./ --language=java --command=mvn clean install",
strings.Join(cmd, " "))
@@ -379,9 +382,10 @@ func TestPrepareCmdForDatabaseCreate(t *testing.T) {
BuildTool: "custom",
Language: "javascript",
}
cmd, err := prepareCmdForDatabaseCreate(map[string]string{}, config, newCodeqlExecuteScanTestsUtils())
isMultiLang, cmd, err := prepareCmdForDatabaseCreate(map[string]string{}, config, newCodeqlExecuteScanTestsUtils())
assert.NoError(t, err)
assert.NotEmpty(t, cmd)
assert.False(t, isMultiLang)
assert.Equal(t, 9, len(cmd))
assert.Equal(t, "database create codeqlDB --overwrite --source-root . --working-dir ./ --language=javascript",
strings.Join(cmd, " "))
@@ -393,7 +397,7 @@ func TestPrepareCmdForDatabaseCreate(t *testing.T) {
ModulePath: "./",
BuildTool: "custom",
}
_, err := prepareCmdForDatabaseCreate(map[string]string{}, config, newCodeqlExecuteScanTestsUtils())
_, _, err := prepareCmdForDatabaseCreate(map[string]string{}, config, newCodeqlExecuteScanTestsUtils())
assert.Error(t, err)
})
@@ -403,7 +407,7 @@ func TestPrepareCmdForDatabaseCreate(t *testing.T) {
ModulePath: "./",
BuildTool: "test",
}
_, err := prepareCmdForDatabaseCreate(map[string]string{}, config, newCodeqlExecuteScanTestsUtils())
_, _, err := prepareCmdForDatabaseCreate(map[string]string{}, config, newCodeqlExecuteScanTestsUtils())
assert.Error(t, err)
})
@@ -414,9 +418,10 @@ func TestPrepareCmdForDatabaseCreate(t *testing.T) {
BuildTool: "test",
Language: "javascript",
}
cmd, err := prepareCmdForDatabaseCreate(map[string]string{}, config, newCodeqlExecuteScanTestsUtils())
isMultiLang, cmd, err := prepareCmdForDatabaseCreate(map[string]string{}, config, newCodeqlExecuteScanTestsUtils())
assert.NoError(t, err)
assert.NotEmpty(t, cmd)
assert.False(t, isMultiLang)
assert.Equal(t, 9, len(cmd))
assert.Equal(t, "database create codeqlDB --overwrite --source-root . --working-dir ./ --language=javascript",
strings.Join(cmd, " "))
@@ -431,9 +436,10 @@ func TestPrepareCmdForDatabaseCreate(t *testing.T) {
customFlags := map[string]string{
"--source-root": "--source-root=customSrcRoot/",
}
cmd, err := prepareCmdForDatabaseCreate(customFlags, config, newCodeqlExecuteScanTestsUtils())
isMultiLang, cmd, err := prepareCmdForDatabaseCreate(customFlags, config, newCodeqlExecuteScanTestsUtils())
assert.NoError(t, err)
assert.NotEmpty(t, cmd)
assert.False(t, isMultiLang)
assert.Equal(t, 8, len(cmd))
assert.Equal(t, "database create codeqlDB --overwrite --working-dir ./ --language=javascript --source-root=customSrcRoot/",
strings.Join(cmd, " "))
@@ -451,14 +457,47 @@ func TestPrepareCmdForDatabaseCreate(t *testing.T) {
"--source-root": "--source-root=customSrcRoot/",
"-j": "-j=1",
}
cmd, err := prepareCmdForDatabaseCreate(customFlags, config, newCodeqlExecuteScanTestsUtils())
isMultiLang, cmd, err := prepareCmdForDatabaseCreate(customFlags, config, newCodeqlExecuteScanTestsUtils())
assert.NoError(t, err)
assert.NotEmpty(t, cmd)
assert.False(t, isMultiLang)
assert.Equal(t, 10, len(cmd))
assert.True(t, "database create codeqlDB --overwrite --working-dir ./ --language=javascript --ram=2000 -j=1 --source-root=customSrcRoot/" == strings.Join(cmd, " ") ||
"database create codeqlDB --overwrite --working-dir ./ --language=javascript --ram=2000 --source-root=customSrcRoot/ -j=1" == strings.Join(cmd, " "))
})
t.Run("Multi-language adds --db-cluster", func(t *testing.T) {
config := &codeqlExecuteScanOptions{
Database: "codeqlDB",
ModulePath: "./",
BuildTool: "custom",
Language: "javascript,python",
}
isMultiLang, cmd, err := prepareCmdForDatabaseCreate(map[string]string{}, config, newCodeqlExecuteScanTestsUtils())
assert.NoError(t, err)
assert.NotEmpty(t, cmd)
assert.True(t, isMultiLang)
assert.Equal(t, 10, len(cmd))
assert.Equal(t, "database create codeqlDB --overwrite --source-root . --working-dir ./ --db-cluster --language=javascript,python",
strings.Join(cmd, " "))
})
t.Run("Multi-language with build command", func(t *testing.T) {
config := &codeqlExecuteScanOptions{
Database: "codeqlDB",
ModulePath: "./",
BuildTool: "custom",
Language: "go,python",
BuildCommand: "make build",
}
isMultiLang, cmd, err := prepareCmdForDatabaseCreate(map[string]string{}, config, newCodeqlExecuteScanTestsUtils())
assert.NoError(t, err)
assert.NotEmpty(t, cmd)
assert.True(t, isMultiLang)
assert.Equal(t, 11, len(cmd))
assert.Equal(t, "database create codeqlDB --overwrite --source-root . --working-dir ./ --db-cluster --language=go,python --command=make build",
strings.Join(cmd, " "))
})
}
func TestPrepareCmdForDatabaseAnalyze(t *testing.T) {
@@ -469,7 +508,7 @@ func TestPrepareCmdForDatabaseAnalyze(t *testing.T) {
config := &codeqlExecuteScanOptions{
Database: "codeqlDB",
}
cmd, err := prepareCmdForDatabaseAnalyze(utils, map[string]string{}, config, "sarif-latest", "target/codeqlReport.sarif")
cmd, err := prepareCmdForDatabaseAnalyze(utils, map[string]string{}, config, "sarif-latest", "target/codeqlReport.sarif", config.Database)
assert.NoError(t, err)
assert.NotEmpty(t, cmd)
assert.Equal(t, 5, len(cmd))
@@ -480,7 +519,7 @@ func TestPrepareCmdForDatabaseAnalyze(t *testing.T) {
config := &codeqlExecuteScanOptions{
Database: "codeqlDB",
}
cmd, err := prepareCmdForDatabaseAnalyze(utils, map[string]string{}, config, "csv", "target/codeqlReport.csv")
cmd, err := prepareCmdForDatabaseAnalyze(utils, map[string]string{}, config, "csv", "target/codeqlReport.csv", config.Database)
assert.NoError(t, err)
assert.NotEmpty(t, cmd)
assert.Equal(t, 5, len(cmd))
@@ -492,7 +531,7 @@ func TestPrepareCmdForDatabaseAnalyze(t *testing.T) {
Database: "codeqlDB",
QuerySuite: "security.ql",
}
cmd, err := prepareCmdForDatabaseAnalyze(utils, map[string]string{}, config, "sarif-latest", "target/codeqlReport.sarif")
cmd, err := prepareCmdForDatabaseAnalyze(utils, map[string]string{}, config, "sarif-latest", "target/codeqlReport.sarif", config.Database)
assert.NoError(t, err)
assert.NotEmpty(t, cmd)
assert.Equal(t, 6, len(cmd))
@@ -506,7 +545,7 @@ func TestPrepareCmdForDatabaseAnalyze(t *testing.T) {
Threads: "1",
Ram: "2000",
}
cmd, err := prepareCmdForDatabaseAnalyze(utils, map[string]string{}, config, "sarif-latest", "target/codeqlReport.sarif")
cmd, err := prepareCmdForDatabaseAnalyze(utils, map[string]string{}, config, "sarif-latest", "target/codeqlReport.sarif", config.Database)
assert.NoError(t, err)
assert.NotEmpty(t, cmd)
assert.Equal(t, 8, len(cmd))
@@ -523,7 +562,7 @@ func TestPrepareCmdForDatabaseAnalyze(t *testing.T) {
customFlags := map[string]string{
"--threads": "--threads=2",
}
cmd, err := prepareCmdForDatabaseAnalyze(utils, customFlags, config, "sarif-latest", "target/codeqlReport.sarif")
cmd, err := prepareCmdForDatabaseAnalyze(utils, customFlags, config, "sarif-latest", "target/codeqlReport.sarif", config.Database)
assert.NoError(t, err)
assert.NotEmpty(t, cmd)
assert.Equal(t, 8, len(cmd))
@@ -540,7 +579,7 @@ func TestPrepareCmdForDatabaseAnalyze(t *testing.T) {
customFlags := map[string]string{
"-j": "-j=2",
}
cmd, err := prepareCmdForDatabaseAnalyze(utils, customFlags, config, "sarif-latest", "target/codeqlReport.sarif")
cmd, err := prepareCmdForDatabaseAnalyze(utils, customFlags, config, "sarif-latest", "target/codeqlReport.sarif", config.Database)
assert.NoError(t, err)
assert.NotEmpty(t, cmd)
assert.Equal(t, 8, len(cmd))
@@ -557,7 +596,7 @@ func TestPrepareCmdForDatabaseAnalyze(t *testing.T) {
customFlags := map[string]string{
"--no-download": "--no-download",
}
cmd, err := prepareCmdForDatabaseAnalyze(utils, customFlags, config, "sarif-latest", "target/codeqlReport.sarif")
cmd, err := prepareCmdForDatabaseAnalyze(utils, customFlags, config, "sarif-latest", "target/codeqlReport.sarif", config.Database)
assert.NoError(t, err)
assert.NotEmpty(t, cmd)
assert.Equal(t, 9, len(cmd))
@@ -580,7 +619,7 @@ func TestPrepareCmdForUploadResults(t *testing.T) {
Owner: "owner",
AnalyzedRef: "refs/heads/branch",
}
cmd := prepareCmdForUploadResults(config, repoInfo, "token")
cmd := prepareCmdForUploadResults(repoInfo, "token", filepath.Join(config.ModulePath, "target", "codeqlReport.sarif"))
assert.NotEmpty(t, cmd)
assert.Equal(t, 8, len(cmd))
})
@@ -591,7 +630,7 @@ func TestPrepareCmdForUploadResults(t *testing.T) {
ServerUrl: "http://github.com",
Repo: "repo",
}
cmd := prepareCmdForUploadResults(config, repoInfo, "token")
cmd := prepareCmdForUploadResults(repoInfo, "token", filepath.Join(config.ModulePath, "target", "codeqlReport.sarif"))
assert.NotEmpty(t, cmd)
assert.Equal(t, 6, len(cmd))
})
@@ -604,14 +643,14 @@ func TestPrepareCmdForUploadResults(t *testing.T) {
Owner: "owner",
AnalyzedRef: "refs/heads/branch",
}
cmd := prepareCmdForUploadResults(config, repoInfo, "")
cmd := prepareCmdForUploadResults(repoInfo, "", filepath.Join(config.ModulePath, "target", "codeqlReport.sarif"))
assert.NotEmpty(t, cmd)
assert.Equal(t, 7, len(cmd))
})
t.Run("Empty configs and token", func(t *testing.T) {
repoInfo := &codeql.RepoInfo{}
cmd := prepareCmdForUploadResults(config, repoInfo, "")
cmd := prepareCmdForUploadResults(repoInfo, "", filepath.Join(config.ModulePath, "target", "codeqlReport.sarif"))
assert.NotEmpty(t, cmd)
assert.Equal(t, 3, len(cmd))
})
@@ -858,3 +897,183 @@ func TestCheckForCompliance(t *testing.T) {
assert.NoError(t, checkForCompliance(scanResults, config, repoInfo))
})
}
func TestGetLanguageList(t *testing.T) {
t.Parallel()
t.Run("Comma separated with spaces and empties", func(t *testing.T) {
cfg := &codeqlExecuteScanOptions{
Language: "javascript, python, ,go ,",
BuildTool: "npm",
}
got := getLanguageList(cfg)
assert.Equal(t, []string{"javascript", "python", "go"}, got)
})
t.Run("Single explicit language", func(t *testing.T) {
cfg := &codeqlExecuteScanOptions{
Language: "python",
}
got := getLanguageList(cfg)
assert.Equal(t, []string{"python"}, got)
})
t.Run("Inferred from build tool", func(t *testing.T) {
cfg := &codeqlExecuteScanOptions{
BuildTool: "maven",
}
got := getLanguageList(cfg)
assert.Equal(t, []string{"java"}, got)
})
t.Run("None available returns nil", func(t *testing.T) {
cfg := &codeqlExecuteScanOptions{}
got := getLanguageList(cfg)
assert.Nil(t, got)
})
}
func TestCloneFlags(t *testing.T) {
src := map[string]string{"--threads": "--threads=2", "--foo": "bar"}
dst := cloneFlags(src)
assert.Equal(t, src, dst)
// check that changes in the dst map does not affect scr
dst["--threads"] = "--threads=4"
delete(dst, "--foo")
assert.Equal(t, "--threads=2", src["--threads"])
assert.Equal(t, "bar", src["--foo"])
}
func TestRunDatabaseAnalyze_SingleLanguage(t *testing.T) {
t.Parallel()
var calls []string
utils := codeqlExecuteScanMockUtils{
ExecMockRunner: &mock.ExecMockRunner{
Stub: func(call string, stdoutReturn map[string]string, shouldFailOnCommand map[string]error, stdout io.Writer) error {
calls = append(calls, call)
return nil
},
},
FilesMock: &mock.FilesMock{},
HttpClientMock: &mock.HttpClientMock{},
}
cfg := &codeqlExecuteScanOptions{
Database: "codeqlDB",
ModulePath: ".",
Language: "javascript",
}
custom := map[string]string{}
reports, sarifs, err := runDatabaseAnalyze(cfg, custom, utils, false)
assert.NoError(t, err)
expectSarif := filepath.Join(".", "target", "codeqlReport.sarif")
expectCSV := filepath.Join(".", "target", "codeqlReport.csv")
assert.ElementsMatch(t,
[]piperutils.Path{{Target: expectSarif}, {Target: expectCSV}},
reports,
)
assert.Equal(t, []string{expectSarif}, sarifs)
joined := strings.Join(calls, "\n")
assert.Contains(t, joined, "database analyze --format=sarif-latest --output="+expectSarif+" codeqlDB")
assert.Contains(t, joined, "database analyze --format=csv --output="+expectCSV+" codeqlDB")
}
func TestRunDatabaseAnalyze_MultiLanguage(t *testing.T) {
t.Parallel()
var calls []string
utils := codeqlExecuteScanMockUtils{
ExecMockRunner: &mock.ExecMockRunner{
Stub: func(call string, stdoutReturn map[string]string, shouldFailOnCommand map[string]error, stdout io.Writer) error {
calls = append(calls, call)
return nil
},
},
FilesMock: &mock.FilesMock{},
HttpClientMock: &mock.HttpClientMock{},
}
cfg := &codeqlExecuteScanOptions{
Database: "codeqlDB",
ModulePath: ".",
Language: "javascript,python",
}
custom := map[string]string{}
reports, sarifs, err := runDatabaseAnalyze(cfg, custom, utils, true)
assert.NoError(t, err)
jsSarif := filepath.Join(".", "target", "javascript.sarif")
pySarif := filepath.Join(".", "target", "python.sarif")
jsCSV := filepath.Join(".", "target", "javascript.csv")
pyCSV := filepath.Join(".", "target", "python.csv")
// reports should include all 4 files
assert.ElementsMatch(t,
[]piperutils.Path{{Target: jsSarif}, {Target: jsCSV}, {Target: pySarif}, {Target: pyCSV}},
reports,
)
// sarifFiles should be both per-language sarif outputs
assert.ElementsMatch(t, []string{jsSarif, pySarif}, sarifs)
joined := strings.Join(calls, "\n")
assert.Contains(t, joined, "database analyze --format=sarif-latest --output="+jsSarif+" codeqlDB/javascript")
assert.Contains(t, joined, "database analyze --format=sarif-latest --output="+pySarif+" codeqlDB/python")
assert.Contains(t, joined, "--sarif-category=javascript")
assert.Contains(t, joined, "--sarif-category=python")
assert.Contains(t, joined, "database analyze --format=csv --output="+jsCSV+" codeqlDB/javascript")
assert.Contains(t, joined, "database analyze --format=csv --output="+pyCSV+" codeqlDB/python")
}
func TestRunCustomCommand(t *testing.T) {
t.Parallel()
t.Run("Success: simple command with args", func(t *testing.T) {
var calls []string
utils := codeqlExecuteScanMockUtils{
ExecMockRunner: &mock.ExecMockRunner{
Stub: func(call string, _ map[string]string, _ map[string]error, _ io.Writer) error {
calls = append(calls, call)
return nil
},
},
FilesMock: &mock.FilesMock{},
HttpClientMock: &mock.HttpClientMock{},
}
err := runCustomCommand(utils, `echo "hello world"`)
assert.NoError(t, err)
if assert.Len(t, calls, 1) {
assert.Equal(t, "echo hello world", calls[0])
}
})
t.Run("Parse error: invalid quoting", func(t *testing.T) {
utils := newCodeqlExecuteScanTestsUtils() // stub isn't invoked because split fails first
err := runCustomCommand(utils, `echo "unterminated`)
assert.Error(t, err)
})
t.Run("Exec error: command fails to run", func(t *testing.T) {
utils := codeqlExecuteScanMockUtils{
ExecMockRunner: &mock.ExecMockRunner{
Stub: func(call string, _ map[string]string, _ map[string]error, _ io.Writer) error {
return fmt.Errorf("boom")
},
},
FilesMock: &mock.FilesMock{},
HttpClientMock: &mock.HttpClientMock{},
}
err := runCustomCommand(utils, `false --flag`)
assert.Error(t, err)
})
}

View File

@@ -34,6 +34,11 @@ func AppendFlagIfNotSetByUser(cmd []string, flagToCheck []string, flagToAppend [
return cmd
}
// AppendCustomFlags appends custom flags from the flags map to the command slice.
// The flags map should contain flags in their complete form (e.g., key: "--flag", value: "--flag=value").
// Only non-empty flags (after trimming whitespace) are appended to avoid adding
// empty or whitespace-only entries to the command.
// Returns a new slice with the original command elements followed by the valid flags.
func AppendCustomFlags(cmd []string, flags map[string]string) []string {
for _, flag := range flags {
if strings.TrimSpace(flag) != "" {
@@ -100,6 +105,10 @@ func removeDuplicateFlags(customFlags map[string]string, shortFlags map[string]s
}
}
// ParseCustomFlags parses flagsStr and returns a map where each flag is mapped to its complete form.
// For flags with values (e.g., "--flag=value"), the key is the flag name and value is the complete flag.
// For flags without values (e.g., "--flag"), both key and value are set to the flag name.
// Duplicate flags (long/short variants) are removed based on longShortFlagsMap.
func ParseCustomFlags(flagsStr string) map[string]string {
flagsMap := make(map[string]string)
parsedFlags := parseFlags(flagsStr)

View File

@@ -44,7 +44,7 @@ spec:
description: Defines the build tool which is used for building the project.
longDescription: |-
Based on the build tool the step will try to auto build the project. The step will try to auto select
the language and the build command. You can override the language and the build command by specifiying it seperatly.
the language and the build command.
mandatory: true
scope:
- GENERAL
@@ -68,7 +68,7 @@ spec:
- STEPS
- name: language
type: string
description: "The programming language used to analyze."
description: "The programming language used to analyze. Use coma separation and select custom build tool to analyze multiple languages"
scope:
- PARAMETERS
- STAGES
@@ -225,10 +225,10 @@ spec:
type: string
description: "A space-separated string of flags for the 'codeql database analyze' command."
longDescription: |-
A space-separated string of flags for the 'codeql database analyze' command.
A space-separated string of flags for the 'codeql database analyze' command.
If both long and short forms of the same flag are provided, the long form takes precedence.
Example input: "--threads=1 --ram=2000"
If both long and short forms of the same flag are provided, the long form takes precedence.
Example input: "--threads=1 --ram=2000"
scope:
- STEPS
- STAGES