diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2472ae737..7c638f67a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -4,7 +4,7 @@ on: pull_request: env: - GO_VERSION: '1.21.5' + GO_VERSION: '1.21.8' jobs: unit-tests: @@ -68,10 +68,10 @@ jobs: PR_GITHUB_REPO_NAME: "ast-cli" PR_GITHUB_NUMBER: 418 PR_GITLAB_TOKEN : ${{ secrets.PR_GITLAB_TOKEN }} - PR_GITLAB_NAMESPACE: "tiagobcx" - PR_GITLAB_REPO_NAME: "testProject" - PR_GITLAB_PROJECT_ID: 40227565 - PR_GITLAB_IID: 19 + PR_GITLAB_NAMESPACE: ${{ secrets.PR_GITLAB_NAMESPACE }} + PR_GITLAB_REPO_NAME: ${{ secrets.PR_GITLAB_REPO_NAME }} + PR_GITLAB_PROJECT_ID: ${{ secrets.PR_GITLAB_PROJECT_ID }} + PR_GITLAB_IID: ${{ secrets.PR_GITLAB_IID }} AZURE_ORG: ${{ secrets.AZURE_ORG }} AZURE_PROJECT: ${{ secrets.AZURE_PROJECT }} AZURE_REPOS: ${{ secrets.AZURE_REPOS }} @@ -116,7 +116,7 @@ jobs: go-version: ${{ env.GO_VERSION }} - run: go version - name: golangci-lint - uses: golangci/golangci-lint-action@v3 + uses: golangci/golangci-lint-action@3a919529898de77ec3da873e3063ca4b10e7f5cc #v3 with: skip-pkg-cache: true version: v1.54.2 @@ -127,7 +127,7 @@ jobs: name: govulncheck steps: - id: govulncheck - uses: golang/govulncheck-action@v1 + uses: golang/govulncheck-action@7da72f730e37eeaad891fcff0a532d27ed737cd4 #v1 with: go-version-input: ${{ env.GO_VERSION }} go-package: ./... \ No newline at end of file diff --git a/.github/workflows/dependabot-auto-merge.yml b/.github/workflows/dependabot-auto-merge.yml index c64d58ceb..0c4850b98 100644 --- a/.github/workflows/dependabot-auto-merge.yml +++ b/.github/workflows/dependabot-auto-merge.yml @@ -11,7 +11,7 @@ jobs: steps: - name: Dependabot metadata id: metadata - uses: dependabot/fetch-metadata@v1.3.1 + uses: dependabot/fetch-metadata@bfac3fa29cc6834ca2e3fd659343da191a65d971 # v1.3.1 with: github-token: "${{ secrets.GH_TOKEN }}" - name: Enable auto-merge for Dependabot PRs @@ -20,6 +20,6 @@ jobs: GITHUB_TOKEN: ${{secrets.GH_TOKEN}} run: gh pr merge --auto --merge "$PR_URL" - name: Auto approve dependabot PRs - uses: hmarr/auto-approve-action@v2 + uses: hmarr/auto-approve-action@7782c7e2bdf62b4d79bdcded8332808fd2f179cd #v2 with: github-token: ${{ secrets.PERSONAL_ACCESS_TOKEN }} diff --git a/.github/workflows/jira_notify.yml b/.github/workflows/jira_notify.yml index 89f3744ba..6fcd9547a 100644 --- a/.github/workflows/jira_notify.yml +++ b/.github/workflows/jira_notify.yml @@ -27,7 +27,7 @@ jobs: JIRA_URL: "https://checkmarx.atlassian.net/" steps: - name: Jira Login - uses: atlassian/gajira-login@v3 + uses: atlassian/gajira-login@ca13f8850ea309cf44a6e4e0c49d9aa48ac3ca4c #v3 env: JIRA_BASE_URL: ${{ env.JIRA_URL }} JIRA_USER_EMAIL: ${{ secrets.AST_JIRA_USER_EMAIL }} @@ -35,7 +35,7 @@ jobs: - name: Jira Create issue id: create_jira_issue - uses: atlassian/gajira-create@v3 + uses: atlassian/gajira-create@1ff0b6bd115a780592b47bfbb63fc4629132e6ec #v3 with: project: AST issuetype: Task @@ -55,7 +55,7 @@ jobs: }) - name: Send a teams notification - uses: thechetantalwar/teams-notify@v2 + uses: thechetantalwar/teams-notify@8a78811f5e8f58cdd204efebd79158006428c46b #v2 with: teams_webhook_url: ${{ secrets.TEAMS_WEBHOOK_URI }} message: "Github issue created ${{ github.repository }} - Link - ${{inputs.html_url}} - Jira Issue - ${{ env.JIRA_URL }}/browse/${{ steps.create_jira_issue.outputs.issue }}" diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml index c1ace59c0..bb1990ecd 100644 --- a/.github/workflows/nightly.yml +++ b/.github/workflows/nightly.yml @@ -14,7 +14,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Delete release - uses: dev-drprasad/delete-tag-and-release@v0.2.1 + uses: dev-drprasad/delete-tag-and-release@5eafd8668311bf3e4d6c1e9898f32a317103de68 #v0.2.1 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: diff --git a/.github/workflows/pr-label.yml b/.github/workflows/pr-label.yml index 8a2d8ac41..8c7862108 100644 --- a/.github/workflows/pr-label.yml +++ b/.github/workflows/pr-label.yml @@ -12,7 +12,7 @@ jobs: pull-requests: write # for TimonVS/pr-labeler-action to add labels in PR runs-on: ubuntu-latest steps: - - uses: TimonVS/pr-labeler-action@v3 + - uses: TimonVS/pr-labeler-action@8447391d87bc7648ce6bf97159c17b642576afb0 #v3 with: configuration-path: .github/pr-labeler.yml # optional, .github/pr-labeler.yml is the default value env: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 2ae7ecb22..17e999861 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -45,7 +45,7 @@ jobs: with: go-version: '^1.21.5' - name: Import Code-Signing Certificates - uses: Apple-Actions/import-codesign-certs@v1 + uses: Apple-Actions/import-codesign-certs@253ddeeac23f2bdad1646faac5c8c2832e800071 #v1 with: # The certificates in a PKCS12 file encoded as a base64 string p12-file-base64: ${{ secrets.APPLE_DEVELOPER_CERTIFICATE_P12_BASE64 }} @@ -61,8 +61,7 @@ jobs: brew --version - name: Install gon run: | - brew tap mitchellh/gon - brew install mitchellh/gon/gon + brew install Bearer/tap/gon - name: Install and start docker if: inputs.dev == false run: | @@ -76,12 +75,12 @@ jobs: docker info - name: Login to Docker Hub if: inputs.dev == false - uses: docker/login-action@v1 + uses: docker/login-action@dd4fa0671be5250ee6f50aedf4cb05514abda2c7 #v1 with: username: ${{ secrets.DOCKER_USERNAME }} password: ${{ secrets.DOCKER_PASSWORD }} - name: Configure AWS Credentials - uses: aws-actions/configure-aws-credentials@v2 + uses: aws-actions/configure-aws-credentials@5fd3084fc36e372ff1fff382a39b10d03659f355 #v2 with: role-to-assume: ${{ secrets.AWS_ASSUME_ROLE_ARN }} aws-region: ${{ secrets.AWS_ASSUME_ROLE_REGION }} @@ -105,7 +104,7 @@ jobs: - name: Echo GoReleaser Args run: echo ${{ env.GR_ARGS }} - name: Run GoReleaser - uses: goreleaser/goreleaser-action@v3 + uses: goreleaser/goreleaser-action@b508e2e3ef3b19d4e4146d4f8fb3ba9db644a757 #v3 with: version: v1.18.2 args: ${{ env.GR_ARGS }} @@ -131,7 +130,7 @@ jobs: - name: Converts Markdown to HTML id: convert - uses: lifepal/markdown-to-html@v1.1 + uses: lifepal/markdown-to-html@71ed74a56602597c05dd7dd0e561631557158ed5 #v1.1 with: text: "${{ steps.release.outputs.body_release }}" @@ -144,7 +143,7 @@ jobs: - name: Send a Notification id: notify - uses: thechetantalwar/teams-notify@v2 + uses: thechetantalwar/teams-notify@8a78811f5e8f58cdd204efebd79158006428c46b #v2 with: teams_webhook_url: ${{ secrets.TEAMS_WEBHOOK_URI }} message: "${{ steps.clean.outputs.clean }}" diff --git a/.golangci.yml b/.golangci.yml index 47ec9efd7..ba0a3c767 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -60,7 +60,7 @@ linters-settings: misspell: locale: US linters: - # please, do not use `enable-all`: it's deprecated and will be removed soon. + # please, do not use `enable-all`: it's deprecated and will be removed soon. # inverted configuration with `enable-all` and `disable` is not scalable during updates of golangci-lint disable-all: true enable: diff --git a/Dockerfile b/Dockerfile index 98228ffca..768e430d3 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,6 +1,6 @@ -FROM alpine:3.19.0 +FROM alpine:3.19.1 -RUN apk add --no-cache bash +RUN apk add bash RUN adduser --system --disabled-password cxuser USER cxuser diff --git a/cmd/main.go b/cmd/main.go index 03dd653ef..8ce4a8aa5 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -32,6 +32,7 @@ func main() { groups := viper.GetString(params.GroupsPathKey) logs := viper.GetString(params.LogsPathKey) projects := viper.GetString(params.ProjectsPathKey) + applications := viper.GetString(params.ApplicationsPathKey) results := viper.GetString(params.ResultsPathKey) scanSummary := viper.GetString(params.ScanSummaryPathKey) scaPackage := viper.GetString(params.ScaPackagePathKey) @@ -49,6 +50,7 @@ func main() { featureFlagsPath := viper.GetString(params.FeatureFlagsKey) policyEvaluationPath := viper.GetString(params.PolicyEvaluationPathKey) sastMetadataPath := viper.GetString(params.SastMetadataPathKey) + accessManagementPath := viper.GetString(params.AccessManagementPathKey) scansWrapper := wrappers.NewHTTPScansWrapper(scans) resultsPdfReportsWrapper := wrappers.NewResultsPdfReportsHTTPWrapper(resultsPdfPath) @@ -57,6 +59,7 @@ func main() { logsWrapper := wrappers.NewLogsWrapper(logs) uploadsWrapper := wrappers.NewUploadsHTTPWrapper(uploads) projectsWrapper := wrappers.NewHTTPProjectsWrapper(projects) + applicationsWrapper := wrappers.NewApplicationsHTTPWrapper(applications) risksOverviewWrapper := wrappers.NewHTTPRisksOverviewWrapper(risksOverview) resultsWrapper := wrappers.NewHTTPResultsWrapper(results, scaPackage, scanSummary) authWrapper := wrappers.NewAuthHTTPWrapper() @@ -77,8 +80,10 @@ func main() { featureFlagsWrapper := wrappers.NewFeatureFlagsHTTPWrapper(featureFlagsPath) policyWrapper := wrappers.NewHTTPPolicyWrapper(policyEvaluationPath) sastMetadataWrapper := wrappers.NewSastIncrementalHTTPWrapper(sastMetadataPath) + accessManagementWrapper := wrappers.NewAccessManagementHTTPWrapper(accessManagementPath) astCli := commands.NewAstCLI( + applicationsWrapper, scansWrapper, resultsSbomReportsWrapper, resultsPdfReportsWrapper, @@ -106,6 +111,7 @@ func main() { featureFlagsWrapper, policyWrapper, sastMetadataWrapper, + accessManagementWrapper, ) exitListener() err = astCli.Execute() diff --git a/go.mod b/go.mod index 2d3f021b9..4e16b4478 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/checkmarx/ast-cli -go 1.21.5 +go 1.21.8 require ( github.com/MakeNowJust/heredoc v1.0.0 @@ -8,14 +8,14 @@ require ( github.com/golang-jwt/jwt v3.2.2+incompatible github.com/gomarkdown/markdown v0.0.0-20230922112808-5421fefb8386 github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 - github.com/google/uuid v1.5.0 + github.com/google/uuid v1.6.0 github.com/gookit/color v1.5.4 github.com/mssola/user_agent v0.6.0 github.com/pkg/errors v0.9.1 github.com/spf13/cobra v1.8.0 github.com/spf13/viper v1.18.2 github.com/tomnomnom/linkheader v0.0.0-20180905144013-02ca5825eb80 - golang.org/x/crypto v0.18.0 + golang.org/x/crypto v0.21.0 golang.org/x/text v0.14.0 gotest.tools v2.2.0+incompatible ) @@ -39,7 +39,8 @@ require ( go.uber.org/atomic v1.9.0 // indirect go.uber.org/multierr v1.9.0 // indirect golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect - golang.org/x/sys v0.16.0 // indirect + golang.org/x/sys v0.18.0 // indirect gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect -) + +) \ No newline at end of file diff --git a/go.sum b/go.sum index 5dcb04082..6f08f88e2 100644 --- a/go.sum +++ b/go.sum @@ -19,8 +19,8 @@ github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= -github.com/google/uuid v1.5.0 h1:1p67kYwdtXjb0gL0BPiP1Av9wiZPo5A8z2cWkTZ+eyU= -github.com/google/uuid v1.5.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gookit/color v1.5.4 h1:FZmqs7XOyGgCAxmWyPslpiok1k05wmY3SJTytgvYFs0= github.com/gookit/color v1.5.4/go.mod h1:pZJOeOS8DM43rXbp4AZo1n9zCU2qjpcRko0b6/QJi9w= github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= @@ -83,10 +83,20 @@ go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI= go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ= golang.org/x/crypto v0.18.0 h1:PGVlW0xEltQnzFZ55hkuX5+KLyrMYhHld1YHO4AKcdc= golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg= +golang.org/x/crypto v0.20.0 h1:jmAMJJZXr5KiCw05dfYK9QnqaqKLYXijU23lsEdcQqg= +golang.org/x/crypto v0.20.0/go.mod h1:Xwo95rrVNIoSMx9wa1JroENMToLWn3RNVrTBpLHgZPQ= +golang.org/x/crypto v0.20.1-0.20240228204720-0d2316b26734 h1:HutZC8sRIg57ztz3rVaQYl4yxgM+UF0Jal0kAWUSeFU= +golang.org/x/crypto v0.20.1-0.20240228204720-0d2316b26734/go.mod h1:Xwo95rrVNIoSMx9wa1JroENMToLWn3RNVrTBpLHgZPQ= +golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA= +golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs= golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g= golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k= golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU= golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y= +golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4= +golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/gon b/gon index cca60664f..5b5cac869 100755 Binary files a/gon and b/gon differ diff --git a/gonMac.hcl b/gonMac.hcl index 4ce752ebf..f7707915b 100644 --- a/gonMac.hcl +++ b/gonMac.hcl @@ -4,7 +4,7 @@ bundle_id = "com.checkmarx.cli" apple_id { username = "tiago.baptista@checkmarx.com" - password = "@env:AC_PASSWORD" + provider = "Z68SAQG5BR" } sign { diff --git a/internal/commands/.scripts/integration_down.sh b/internal/commands/.scripts/integration_down.sh old mode 100644 new mode 100755 diff --git a/internal/commands/.scripts/integration_up.sh b/internal/commands/.scripts/integration_up.sh old mode 100644 new mode 100755 index c503ee3af..7b16af05d --- a/internal/commands/.scripts/integration_up.sh +++ b/internal/commands/.scripts/integration_up.sh @@ -13,7 +13,7 @@ rm -rf ScaResolver-linux64.tar.gz go test \ -tags integration \ -v \ - -timeout 60m \ + -timeout 90m \ -coverpkg github.com/checkmarx/ast-cli/internal/commands,github.com/checkmarx/ast-cli/internal/wrappers \ -coverprofile cover.out \ github.com/checkmarx/ast-cli/test/integration diff --git a/internal/commands/chat-kics.go b/internal/commands/chat-kics.go index 34f9e8a3e..25e2461af 100644 --- a/internal/commands/chat-kics.go +++ b/internal/commands/chat-kics.go @@ -48,10 +48,11 @@ type OutputModel struct { func ChatKicsSubCommand(chatWrapper wrappers.ChatWrapper) *cobra.Command { chatKicsCmd := &cobra.Command{ - Use: "kics", - Short: "Chat about KICS result with OpenAI models", - Long: "Chat about KICS result with OpenAI models", - RunE: runChatKics(chatWrapper), + Use: "kics", + Short: "Chat about KICS result with OpenAI models", + Long: "Chat about KICS result with OpenAI models", + Hidden: true, + RunE: runChatKics(chatWrapper), } chatKicsCmd.Flags().String(params.ChatAPIKey, "", "OpenAI API key") diff --git a/internal/commands/chat-sast.go b/internal/commands/chat-sast.go index b37d6ad48..88ceb623f 100644 --- a/internal/commands/chat-sast.go +++ b/internal/commands/chat-sast.go @@ -2,8 +2,8 @@ package commands import ( "fmt" + "strconv" - "github.com/checkmarx/ast-cli/internal/commands/chatsast" "github.com/checkmarx/ast-cli/internal/commands/util/printer" "github.com/checkmarx/ast-cli/internal/logger" "github.com/checkmarx/ast-cli/internal/params" @@ -20,13 +20,15 @@ import ( const ScanResultsFileErrorFormat = "Error reading and parsing scan results %s" const CreatePromptErrorFormat = "Error creating prompt for result ID %s" const UserInputRequiredErrorFormat = "%s is required when %s is provided" +const AiGuidedRemediationDisabledError = "The AI Guided Remediation is disabled in your tenant account" -func ChatSastSubCommand(chatWrapper wrappers.ChatWrapper) *cobra.Command { +func ChatSastSubCommand(chatWrapper wrappers.ChatWrapper, tenantWrapper wrappers.TenantConfigurationWrapper) *cobra.Command { chatSastCmd := &cobra.Command{ - Use: "sast", - Short: "OpenAI-based SAST results remediation", - Long: "Use OpenAI models to remediate SAST results and chat about them", - RunE: runChatSast(chatWrapper), + Use: "sast", + Short: "OpenAI-based SAST results remediation", + Long: "Use OpenAI models to remediate SAST results and chat about them", + Hidden: true, + RunE: runChatSast(chatWrapper, tenantWrapper), } chatSastCmd.Flags().String(params.ChatAPIKey, "", "OpenAI API key") @@ -45,8 +47,11 @@ func ChatSastSubCommand(chatWrapper wrappers.ChatWrapper) *cobra.Command { return chatSastCmd } -func runChatSast(chatWrapper wrappers.ChatWrapper) func(cmd *cobra.Command, args []string) error { +func runChatSast(chatWrapper wrappers.ChatWrapper, tenantWrapper wrappers.TenantConfigurationWrapper) func(cmd *cobra.Command, args []string) error { return func(cmd *cobra.Command, args []string) error { + if !isAiGuidedRemediationEnabled(tenantWrapper) { + return outputError(cmd, uuid.Nil, errors.Errorf(AiGuidedRemediationDisabledError)) + } chatAPIKey, _ := cmd.Flags().GetString(params.ChatAPIKey) chatConversationID, _ := cmd.Flags().GetString(params.ChatConversationID) chatModel, _ := cmd.Flags().GetString(params.ChatModel) @@ -104,6 +109,8 @@ func runChatSast(chatWrapper wrappers.ChatWrapper) func(cmd *cobra.Command, args responseContent := getMessageContents(response) + responseContent = addDescriptionForIdentifier(responseContent) + return printer.Print(cmd.OutOrStdout(), &OutputModel{ ConversationID: id.String(), Response: responseContent, @@ -111,8 +118,27 @@ func runChatSast(chatWrapper wrappers.ChatWrapper) func(cmd *cobra.Command, args } } +func isAiGuidedRemediationEnabled(tenantWrapper wrappers.TenantConfigurationWrapper) bool { + tenantConfigurationResponse, errorModel, err := tenantWrapper.GetTenantConfiguration() + if err != nil { + return false + } + if errorModel != nil { + return false + } + if tenantConfigurationResponse != nil { + for _, resp := range *tenantConfigurationResponse { + if resp.Key == AiGuidedRemediationEnabled { + isEnabled, _ := strconv.ParseBool(resp.Value) + return isEnabled + } + } + } + return false +} + func buildPrompt(scanResultsFile, sastResultID, sourceDir string) (systemPrompt, userPrompt string, err error) { - scanResults, err := chatsast.ReadResultsSAST(scanResultsFile) + scanResults, err := ReadResultsSAST(scanResultsFile) if err != nil { return "", "", fmt.Errorf("error in build-prompt: %s: %w", fmt.Sprintf(ScanResultsFileErrorFormat, scanResultsFile), err) } @@ -121,22 +147,22 @@ func buildPrompt(scanResultsFile, sastResultID, sourceDir string) (systemPrompt, return "", "", errors.Errorf(fmt.Sprintf("error in build-prompt: currently only --%s is supported", params.ChatSastResultID)) } - sastResult, err := chatsast.GetResultByID(scanResults, sastResultID) + sastResult, err := GetResultByID(scanResults, sastResultID) if err != nil { return "", "", fmt.Errorf("error in build-prompt: %w", err) } - sources, err := chatsast.GetSourcesForResult(sastResult, sourceDir) + sources, err := GetSourcesForResult(sastResult, sourceDir) if err != nil { return "", "", fmt.Errorf("error in build-prompt: %w", err) } - prompt, err := chatsast.CreateUserPrompt(sastResult, sources) + prompt, err := CreateUserPrompt(sastResult, sources) if err != nil { return "", "", fmt.Errorf("error in build-prompt: %s: %w", fmt.Sprintf(CreatePromptErrorFormat, sastResultID), err) } - return chatsast.GetSystemPrompt(), prompt, nil + return GetSystemPrompt(), prompt, nil } func getMessageContents(response []message.Message) []string { diff --git a/internal/commands/chat-sast_test.go b/internal/commands/chat-sast_test.go index fff40ff1e..6673a8e34 100644 --- a/internal/commands/chat-sast_test.go +++ b/internal/commands/chat-sast_test.go @@ -6,6 +6,8 @@ import ( "strings" "testing" + "github.com/checkmarx/ast-cli/internal/wrappers" + "github.com/checkmarx/ast-cli/internal/wrappers/mock" "github.com/google/uuid" "gotest.tools/assert" ) @@ -69,6 +71,30 @@ func TestChatSastInvalideResultId(t *testing.T) { assert.Assert(t, strings.Contains(s, "result ID invalidResultId not found"), s) } +func TestChatSastAiGuidedRemediationDisabled(t *testing.T) { + mock.TenantConfiguration = []*wrappers.TenantConfigurationResponse{{ + Key: "scan.config.plugins.ideScans", + Value: "true", + }, + { + Key: "scan.config.plugins.aiGuidedRemediation", + Value: "false", + }, + } + + buffer, err := executeRedirectedTestCommand("chat", "sast", + "--chat-apikey", "apiKey", + "--scan-results-file", "./data/cx_result.json", + "--source-dir", "dir", + "--sast-result-id", "13588362") + assert.NilError(t, err) + output, err := io.ReadAll(buffer) + assert.NilError(t, err) + s := string(output) + assert.Assert(t, strings.Contains(s, AiGuidedRemediationDisabledError), s) + mock.TenantConfiguration = []*wrappers.TenantConfigurationResponse{} +} + func TestChatSastInvalidSourceDir(t *testing.T) { buffer, err := executeRedirectedTestCommand("chat", "sast", "--chat-apikey", "apiKey", diff --git a/internal/commands/chat.go b/internal/commands/chat.go index 0a188b8cb..28c205462 100644 --- a/internal/commands/chat.go +++ b/internal/commands/chat.go @@ -5,16 +5,20 @@ import ( "github.com/spf13/cobra" ) -const ConversationIDErrorFormat = "Invalid conversation ID %s" +const ( + ConversationIDErrorFormat = "Invalid conversation ID %s" + AiGuidedRemediationEnabled = "scan.config.plugins.aiGuidedRemediation" +) -func NewChatCommand(chatWrapper wrappers.ChatWrapper) *cobra.Command { +func NewChatCommand(chatWrapper wrappers.ChatWrapper, tenantWrapper wrappers.TenantConfigurationWrapper) *cobra.Command { chatCmd := &cobra.Command{ - Use: "chat", - Short: "Chat with OpenAI models", - Long: "Chat with OpenAI models regarding KICS or SAST results", + Use: "chat", + Short: "Chat with OpenAI models", + Long: "Chat with OpenAI models regarding KICS or SAST results", + Hidden: true, } chatKicsCmd := ChatKicsSubCommand(chatWrapper) - chatSastCmd := ChatSastSubCommand(chatWrapper) + chatSastCmd := ChatSastSubCommand(chatWrapper, tenantWrapper) chatCmd.AddCommand(chatKicsCmd, chatSastCmd) return chatCmd diff --git a/internal/commands/groups.go b/internal/commands/groups.go new file mode 100644 index 000000000..67dfd24c0 --- /dev/null +++ b/internal/commands/groups.go @@ -0,0 +1,111 @@ +package commands + +import ( + "encoding/json" + "strings" + + commonParams "github.com/checkmarx/ast-cli/internal/params" + "github.com/checkmarx/ast-cli/internal/wrappers" + "github.com/pkg/errors" + "github.com/spf13/cobra" +) + +const accessManagementEnabled = "ACCESS_MANAGEMENT_ENABLED" // feature flag + +func createGroupsMap(groupsStr string, groupsWrapper wrappers.GroupsWrapper) ([]*wrappers.Group, error) { + groups := strings.Split(groupsStr, ",") + var groupsMap []*wrappers.Group + var groupsNotFound []string + for _, group := range groups { + if len(group) > 0 { + groupsFromEnv, err := groupsWrapper.Get(group) + if err != nil { + groupsNotFound = append(groupsNotFound, group) + } else { + findGroup := findGroupByName(groupsFromEnv, group) + if findGroup != nil && findGroup.Name != "" { + groupsMap = append(groupsMap, findGroup) + } else { + groupsNotFound = append(groupsNotFound, group) + } + } + } + } + if len(groupsNotFound) > 0 { + return nil, errors.Errorf("%s: %v", failedFindingGroup, groupsNotFound) + } + return groupsMap, nil +} + +func findGroupByName(groups []wrappers.Group, name string) *wrappers.Group { + for i := 0; i < len(groups); i++ { + if groups[i].Name == name { + return &groups[i] + } + } + return nil +} + +func updateGroupValues(input *[]byte, cmd *cobra.Command, groupsWrapper wrappers.GroupsWrapper) ([]*wrappers.Group, error) { + groupListStr, _ := cmd.Flags().GetString(commonParams.GroupList) + groups, err := createGroupsMap(groupListStr, groupsWrapper) + if err != nil { + return groups, err + } + if !wrappers.FeatureFlags[accessManagementEnabled] { + var info map[string]interface{} + _ = json.Unmarshal(*input, &info) + info["groups"] = getGroupIds(groups) + *input, _ = json.Marshal(info) + } + return groups, nil +} +func getGroupsForRequest(groups []*wrappers.Group) []string { + if !wrappers.FeatureFlags[accessManagementEnabled] { + return getGroupIds(groups) + } + return nil +} +func getGroupIds(groups []*wrappers.Group) []string { + var groupIds []string + for _, group := range groups { + groupIds = append(groupIds, group.ID) + } + return groupIds +} + +func assignGroupsToProject(projectID string, projectName string, groups []*wrappers.Group, + accessManagement wrappers.AccessManagementWrapper) error { + if !wrappers.FeatureFlags[accessManagementEnabled] { + return nil + } + groupsAssignedToTheProject, err := accessManagement.GetGroups(projectID) + if err != nil { + return err + } + groupsToAssign := getGroupsToAssign(groups, groupsAssignedToTheProject) + if len(groupsToAssign) == 0 { + return nil + } + + err = accessManagement.CreateGroupsAssignment(projectID, projectName, groupsToAssign) + if err != nil { + return err + } + return nil +} + +func getGroupsToAssign(receivedGroups, existingGroups []*wrappers.Group) []*wrappers.Group { + var groupsToAssign []*wrappers.Group + var groupsMap = make(map[string]bool) + for _, existingGroup := range existingGroups { + groupsMap[existingGroup.ID] = true + } + for _, receivedGroup := range receivedGroups { + find := groupsMap[receivedGroup.ID] + if !find { + groupsToAssign = append(groupsToAssign, receivedGroup) + } + } + return groupsToAssign +} diff --git a/internal/commands/groups_test.go b/internal/commands/groups_test.go new file mode 100644 index 000000000..4b507908b --- /dev/null +++ b/internal/commands/groups_test.go @@ -0,0 +1,46 @@ +package commands + +import ( + "testing" + + "github.com/checkmarx/ast-cli/internal/wrappers" + "github.com/checkmarx/ast-cli/internal/wrappers/mock" +) + +func TestCreateScanAndProjectWithGroupFFTrue(t *testing.T) { + mock.Flags = wrappers.FeatureFlagsResponseModel{{Name: "ACCESS_MANAGEMENT_ENABLED", Status: true}} + execCmdNilAssertion( + t, + "scan", "create", "--project-name", "new-project", "-b", "dummy_branch", "-s", ".", "--project-groups", "group", + ) +} + +func TestCreateScanAndProjectWithGroupFFFalse(t *testing.T) { + mock.Flags = wrappers.FeatureFlagsResponseModel{{Name: "ACCESS_MANAGEMENT_ENABLED", Status: false}} + execCmdNilAssertion( + t, + "scan", "create", "--project-name", "new-project", "-b", "dummy_branch", "-s", ".", "--project-groups", "group", + ) +} +func TestCreateProjectWithGroupFFTrue(t *testing.T) { + mock.Flags = wrappers.FeatureFlagsResponseModel{{Name: "ACCESS_MANAGEMENT_ENABLED", Status: true}} + execCmdNilAssertion( + t, "project", "create", "--project-name", "new-project", "--groups", "group", + ) +} + +func TestCreateProjectWithGroupFFFalse(t *testing.T) { + mock.Flags = wrappers.FeatureFlagsResponseModel{{Name: "ACCESS_MANAGEMENT_ENABLED", Status: false}} + execCmdNilAssertion( + t, + "project", "create", "--project-name", "new-project", "--groups", "group", + ) +} + +func TestCreateScanForExistingProjectWithGroupFFTrue(t *testing.T) { + mock.Flags = wrappers.FeatureFlagsResponseModel{{Name: "ACCESS_MANAGEMENT_ENABLED", Status: true}} + execCmdNilAssertion( + t, + "scan", "create", "--project-name", "MOCK", "-b", "dummy_branch", "-s", ".", "--project-groups", "group", + ) +} diff --git a/internal/commands/policymanagement/policy.go b/internal/commands/policymanagement/policy.go new file mode 100644 index 000000000..9c9df3446 --- /dev/null +++ b/internal/commands/policymanagement/policy.go @@ -0,0 +1,121 @@ +package policymanagement + +import ( + "fmt" + "log" + "math" + "time" + + "github.com/checkmarx/ast-cli/internal/logger" + commonParams "github.com/checkmarx/ast-cli/internal/params" + "github.com/checkmarx/ast-cli/internal/wrappers" + "github.com/spf13/cobra" + "github.com/spf13/viper" +) + +const ( + failedGetting = "Failed showing a scan" + maxPollingWaitTime = 60 + completedPolicy = "COMPLETED" + nonePolicy = "NONE" + evaluatingPolicy = "EVALUATING" +) + +func HandlePolicyWait( + waitDelay, + timeoutMinutes int, + policyWrapper wrappers.PolicyWrapper, + scanID, + projectID string, + cmd *cobra.Command, +) (*wrappers.PolicyResponseModel, error) { + policyResponseModel, err := waitForPolicyCompletion( + waitDelay, + timeoutMinutes, + policyWrapper, + scanID, + projectID, + cmd) + if err != nil { + verboseFlag, _ := cmd.Flags().GetBool(commonParams.DebugFlag) + if verboseFlag { + logger.PrintIfVerbose("Policy evaluation failed") + } + return nil, err + } + return policyResponseModel, nil +} + +func waitForPolicyCompletion( + waitDelay int, + timeoutMinutes int, + policyWrapper wrappers.PolicyWrapper, + scanID, + projectID string, + cmd *cobra.Command, +) (*wrappers.PolicyResponseModel, error) { + logger.PrintIfVerbose("Waiting for policy evaluation to complete for scanID:" + scanID + " and projectID:" + projectID) + var policyResponseModel *wrappers.PolicyResponseModel + timeout := time.Now().Add(time.Duration(timeoutMinutes) * time.Minute) + fixedWait := time.Duration(waitDelay) * time.Second + i := uint64(0) + if !cmd.Flags().Changed(commonParams.RetryDelayFlag) { + viper.Set(commonParams.RetryDelayFlag, commonParams.RetryDelayPollingDefault) + } + for { + variableWait := time.Duration(math.Min(float64(i/uint64(waitDelay)), maxPollingWaitTime)) * time.Second + waitDuration := fixedWait + variableWait + logger.PrintfIfVerbose("Sleeping %v before polling", waitDuration) + time.Sleep(waitDuration) + evaluated := false + var err error + evaluated, policyResponseModel, err = isPolicyEvaluated(policyWrapper, scanID, projectID) + if err != nil { + return nil, err + } + if evaluated { + break + } + if timeoutMinutes > 0 && time.Now().After(timeout) { + logger.PrintfIfVerbose("Timeout of %d minute(s) for policy evaluation reached", timeoutMinutes) + return nil, nil + } + i++ + } + logger.PrintIfVerbose("Policy evaluation completed with status" + policyResponseModel.Status) + return policyResponseModel, nil +} + +func isPolicyEvaluated( + policyWrapper wrappers.PolicyWrapper, + scanID, + projectID string, +) (bool, *wrappers.PolicyResponseModel, error) { + var errorModel *wrappers.WebError + var err error + var policyResponseModel *wrappers.PolicyResponseModel + var params = make(map[string]string) + + params["scanId"] = scanID + params["astProjectId"] = projectID + + policyResponseModel, errorModel, err = policyWrapper.EvaluatePolicy(params) + if err != nil { + return false, nil, err + } + if errorModel != nil { + log.Fatalf(fmt.Sprintf("%s: CODE: %d, %s", failedGetting, errorModel.Code, errorModel.Message)) + } else if policyResponseModel != nil { + if policyResponseModel.Status == evaluatingPolicy { + log.Println("Policy status: ", policyResponseModel.Status) + return false, nil, nil + } + } + // Case the policy is evaluated or None + logger.PrintIfVerbose("Policy evaluation finished with status: " + policyResponseModel.Status) + if policyResponseModel.Status == completedPolicy || policyResponseModel.Status == nonePolicy { + logger.PrintIfVerbose("Policy status: " + policyResponseModel.Status) + return true, policyResponseModel, nil + } + return true, nil, nil +} diff --git a/internal/commands/project.go b/internal/commands/project.go index 79256295c..d95638499 100644 --- a/internal/commands/project.go +++ b/internal/commands/project.go @@ -6,6 +6,8 @@ import ( "strings" "time" + applicationErrors "github.com/checkmarx/ast-cli/internal/errors" + "github.com/MakeNowJust/heredoc" "github.com/checkmarx/ast-cli/internal/commands/util" "github.com/checkmarx/ast-cli/internal/commands/util/printer" @@ -61,7 +63,8 @@ var ( ) ) -func NewProjectCommand(projectsWrapper wrappers.ProjectsWrapper, groupsWrapper wrappers.GroupsWrapper) *cobra.Command { +func NewProjectCommand(applicationsWrapper wrappers.ApplicationsWrapper, projectsWrapper wrappers.ProjectsWrapper, groupsWrapper wrappers.GroupsWrapper, + accessManagementWrapper wrappers.AccessManagementWrapper) *cobra.Command { projCmd := &cobra.Command{ Use: "project", Short: "Manage projects", @@ -91,7 +94,7 @@ func NewProjectCommand(projectsWrapper wrappers.ProjectsWrapper, groupsWrapper w `, ), }, - RunE: runCreateProjectCommand(projectsWrapper, groupsWrapper), + RunE: runCreateProjectCommand(applicationsWrapper, projectsWrapper, groupsWrapper, accessManagementWrapper), } createProjCmd.PersistentFlags().String(commonParams.TagList, "", "List of tags, ex: (tagA,tagB:val,etc)") createProjCmd.PersistentFlags().String(commonParams.GroupList, "", "List of groups, ex: (PowerUsers,etc)") @@ -99,6 +102,7 @@ func NewProjectCommand(projectsWrapper wrappers.ProjectsWrapper, groupsWrapper w createProjCmd.PersistentFlags().StringP(commonParams.MainBranchFlag, "", "", "Main branch") createProjCmd.PersistentFlags().String(commonParams.SSHKeyFlag, "", "Path to ssh private key") createProjCmd.PersistentFlags().String(commonParams.RepoURLFlag, "", "Repository URL") + createProjCmd.PersistentFlags().String(commonParams.ApplicationName, "", "Name of the application to assign with the project") listProjectsCmd := &cobra.Command{ Use: "list", @@ -222,75 +226,37 @@ func updateProjectRequestValues(input *[]byte, cmd *cobra.Command) error { return nil } -func updateGroupValues(input *[]byte, cmd *cobra.Command, groupsWrapper wrappers.GroupsWrapper) error { - groupListStr, _ := cmd.Flags().GetString(commonParams.GroupList) - - var groupMap []string - var info map[string]interface{} - _ = json.Unmarshal(*input, &info) - if _, ok := info["groups"]; !ok { - _ = json.Unmarshal([]byte("[]"), &groupMap) - info["groups"] = groupMap - } - groups, err := createGroupsMap(groupListStr, groupsWrapper) - if err != nil { - return err - } - - info["groups"] = groups - *input, _ = json.Marshal(info) - - return nil -} - -func createGroupsMap(groupsStr string, groupsWrapper wrappers.GroupsWrapper) ([]string, error) { - groups := strings.Split(groupsStr, ",") - var groupMap []string - var groupsNotFound []string - for _, group := range groups { - if len(group) > 0 { - groupIds, err := groupsWrapper.Get(group) - if err != nil { - groupsNotFound = append(groupsNotFound, group) - } else { - groupID := findGroupID(groupIds, group) - if groupID != "" { - groupMap = append(groupMap, groupID) - } else { - groupsNotFound = append(groupsNotFound, group) - } - } - } - } - - if len(groupsNotFound) > 0 { - return nil, errors.Errorf("%s: %v", failedFindingGroup, groupsNotFound) - } - - return groupMap, nil -} - -func findGroupID(groups []wrappers.Group, name string) string { - for i := 0; i < len(groups); i++ { - if groups[i].Name == name { - return groups[i].ID - } - } - return "" -} - func runCreateProjectCommand( + applicationsWrapper wrappers.ApplicationsWrapper, projectsWrapper wrappers.ProjectsWrapper, groupsWrapper wrappers.GroupsWrapper, + accessManagementWrapper wrappers.AccessManagementWrapper, ) func(cmd *cobra.Command, args []string) error { return func(cmd *cobra.Command, args []string) error { + applicationName, err := cmd.Flags().GetString(commonParams.ApplicationName) + if err != nil { + return err + } + + var applicationID []string + + if applicationName != "" { + application, getAppErr := getApplication(applicationName, applicationsWrapper) + if getAppErr != nil { + return getAppErr + } + if application == nil { + return errors.Errorf(applicationErrors.ApplicationDoesntExistOrNoPermission) + } + applicationID = []string{application.ID} + } + var input = []byte("{}") - var err error err = updateProjectRequestValues(&input, cmd) if err != nil { return err } - err = updateGroupValues(&input, cmd, groupsWrapper) + groups, err := updateGroupValues(&input, cmd, groupsWrapper) if err != nil { return err } @@ -300,6 +266,7 @@ func runCreateProjectCommand( return err } var projModel = wrappers.Project{} + projModel.ApplicationIds = applicationID var projResponseModel *wrappers.ProjectResponseModel var errorModel *wrappers.ErrorModel // Try to parse to a project model in order to manipulate the request payload @@ -324,7 +291,10 @@ func runCreateProjectCommand( return errors.Wrapf(err, "%s", failedCreatingProj) } } - + err = assignGroupsToProject(projResponseModel.ID, projResponseModel.Name, groups, accessManagementWrapper) + if err != nil { + return err + } err = updateProjectConfigurationIfNeeded(cmd, projectsWrapper, projResponseModel.ID) if err != nil { return err @@ -575,20 +545,22 @@ func toProjectViews(models []wrappers.ProjectResponseModel) []projectView { func toProjectView(model wrappers.ProjectResponseModel) projectView { //nolint:gocritic return projectView{ - ID: model.ID, - Name: model.Name, - CreatedAt: model.CreatedAt, - UpdatedAt: model.UpdatedAt, - Tags: model.Tags, - Groups: model.Groups, + ID: model.ID, + Name: model.Name, + CreatedAt: model.CreatedAt, + UpdatedAt: model.UpdatedAt, + Tags: model.Tags, + Groups: model.Groups, + ApplicationIds: model.ApplicationIds, } } type projectView struct { - ID string `format:"name:Project ID"` - Name string - CreatedAt time.Time `format:"name:Created at;time:01-02-06 15:04:05"` - UpdatedAt time.Time `format:"name:Updated at;time:01-02-06 15:04:05"` - Tags map[string]string - Groups []string + ID string `format:"name:Project ID"` + Name string + CreatedAt time.Time `format:"name:Created at;time:01-02-06 15:04:05"` + UpdatedAt time.Time `format:"name:Updated at;time:01-02-06 15:04:05"` + Tags map[string]string + Groups []string + ApplicationIds []string } diff --git a/internal/commands/project_test.go b/internal/commands/project_test.go index 133ce5f73..347e5e937 100644 --- a/internal/commands/project_test.go +++ b/internal/commands/project_test.go @@ -5,6 +5,9 @@ package commands import ( "testing" + applicationErrors "github.com/checkmarx/ast-cli/internal/errors" + "github.com/checkmarx/ast-cli/internal/wrappers/mock" + "gotest.tools/assert" "github.com/checkmarx/ast-cli/internal/commands/util" @@ -22,6 +25,25 @@ func TestRunCreateProjectCommandWithFile(t *testing.T) { execCmdNilAssertion(t, "project", "create", "--project-name", "test_project") } +func TestProjectCreate_ExistingApplication_CreateProjectUnderApplicationSuccessfully(t *testing.T) { + execCmdNilAssertion(t, "project", "create", "--project-name", "test_project", "--application-name", "MOCK") +} + +func TestProjectCreate_ExistingApplicationWithNoPermission_FailToCreateProject(t *testing.T) { + err := execCmdNotNilAssertion(t, "project", "create", "--project-name", "test_project", "--application-name", mock.NoPermissionApp) + assert.Assert(t, err.Error() == applicationErrors.ApplicationDoesntExistOrNoPermission) +} + +func TestProjectCreate_OnReceivingHttpBadRequestStatusCode_FailedToCreateScan(t *testing.T) { + err := execCmdNotNilAssertion(t, "project", "create", "--project-name", "test_project", "--application-name", mock.FakeHTTPStatusBadRequest) + assert.Assert(t, err.Error() == applicationErrors.FailedToGetApplication) +} + +func TestProjectCreate_OnReceivingHttpInternalServerErrorStatusCode_FailedToCreateScan(t *testing.T) { + err := execCmdNotNilAssertion(t, "project", "create", "--project-name", "test_project", "--application-name", mock.FakeHTTPStatusInternalServerError) + assert.Assert(t, err.Error() == applicationErrors.FailedToGetApplication) +} + func TestRunCreateProjectCommandWithNoInput(t *testing.T) { err := execCmdNotNilAssertion(t, "project", "create") assert.Assert(t, err.Error() == "Project name is required") diff --git a/internal/commands/result.go b/internal/commands/result.go index 6fd361b61..56b8bdcc3 100644 --- a/internal/commands/result.go +++ b/internal/commands/result.go @@ -14,6 +14,7 @@ import ( "time" "github.com/MakeNowJust/heredoc" + "github.com/checkmarx/ast-cli/internal/commands/policymanagement" "github.com/checkmarx/ast-cli/internal/commands/util" "github.com/checkmarx/ast-cli/internal/commands/util/printer" "github.com/checkmarx/ast-cli/internal/logger" @@ -56,10 +57,15 @@ const ( criticalCx = "CRITICAL" codeBashingKey = "cb-url" failedGettingBfl = "Failed getting BFL" - notAvailableString = "N/A" + notAvailableString = "-" + scanFailedString = "Failed" + scanCanceledString = "Canceled" + scanSuccessString = "Completed" notAvailableNumber = -1 + scanFailedNumber = -2 + scanCanceledNumber = -3 defaultPaddingSize = -13 - defaultResultsPaddingSize = -15 + boldFormat = "\033[1m%s\033[0m" scanPendingMessage = "Scan triggered in asynchronous mode or still running. Click more details to get the full status." directDependencyType = "Direct Dependency" indirectDependencyType = "Transitive Dependency" @@ -87,6 +93,8 @@ const ( summaryCreatedAtLayout = "2006-01-02, 15:04:05" glTimeFormat = "2006-01-02T15:04:05" sarifNodeFileLength = 2 + fixLabel = "fix" + redundantLabel = "redundant" ) var summaryFormats = []string{ @@ -222,6 +230,8 @@ func resultShowSubCommand( "Cancel the policy evaluation and fail after the timeout in minutes", ) resultShowCmd.PersistentFlags().Bool(commonParams.IgnorePolicyFlag, false, "Do not evaluate policies") + resultShowCmd.PersistentFlags().Bool(commonParams.SastRedundancyFlag, false, + "Populate SAST results 'data.redundancy' with values '"+fixLabel+"' (to fix) or '"+redundantLabel+"' (no need to fix)") return resultShowCmd } @@ -352,6 +362,13 @@ func convertScanToResultsSummary(scanInfo *wrappers.ScanResponseModel, resultsWr sastIssues := 0 scaIssues := 0 kicsIssues := 0 + enginesStatusCode := map[string]int{ + commonParams.SastType: 0, + commonParams.ScaType: 0, + commonParams.KicsType: 0, + commonParams.APISecType: 0, + } + if len(scanInfo.StatusDetails) > 0 { for _, statusDetailItem := range scanInfo.StatusDetails { if statusDetailItem.Status == wrappers.ScanFailed || statusDetailItem.Status == wrappers.ScanCanceled { @@ -363,6 +380,12 @@ func convertScanToResultsSummary(scanInfo *wrappers.ScanResponseModel, resultsWr kicsIssues = notAvailableNumber } } + switch statusDetailItem.Status { + case wrappers.ScanFailed: + handleScanStatus(statusDetailItem, enginesStatusCode, scanFailedNumber) + case wrappers.ScanCanceled: + handleScanStatus(statusDetailItem, enginesStatusCode, scanCanceledNumber) + } } } summary := &wrappers.ResultSummary{ @@ -384,6 +407,12 @@ func convertScanToResultsSummary(scanInfo *wrappers.ScanResponseModel, resultsWr ProjectName: scanInfo.ProjectName, BranchName: scanInfo.Branch, EnginesEnabled: scanInfo.Engines, + EnginesResult: map[string]*wrappers.EngineResultSummary{ + commonParams.SastType: {StatusCode: enginesStatusCode[commonParams.SastType]}, + commonParams.ScaType: {StatusCode: enginesStatusCode[commonParams.ScaType]}, + commonParams.KicsType: {StatusCode: enginesStatusCode[commonParams.KicsType]}, + commonParams.APISecType: {StatusCode: enginesStatusCode[commonParams.APISecType]}, + }, } baseURI, err := resultsWrapper.GetResultsURL(summary.ProjectID) @@ -400,6 +429,12 @@ func convertScanToResultsSummary(scanInfo *wrappers.ScanResponseModel, resultsWr return summary, nil } +func handleScanStatus(statusDetailItem wrappers.StatusInfo, targetTypes map[string]int, statusCode int) { + if _, ok := targetTypes[statusDetailItem.Name]; ok { + targetTypes[statusDetailItem.Name] = statusCode + } +} + func summaryReport( summary *wrappers.ResultSummary, policies *wrappers.PolicyResponseModel, @@ -424,13 +459,14 @@ func summaryReport( setNotAvailableNumberIfZero(summary, &summary.ScaIssues, commonParams.ScaType) setNotAvailableNumberIfZero(summary, &summary.KicsIssues, commonParams.KicsType) setRiskMsgAndStyle(summary) + setNotAvailableEnginesStatusCode(summary) return summary, nil } -func setNotAvailableNumberIfZero(summary *wrappers.ResultSummary, counter *int, engineType string) { - if *counter == 0 && !contains(summary.EnginesEnabled, engineType) { - *counter = notAvailableNumber +func setNotAvailableEnginesStatusCode(summary *wrappers.ResultSummary) { + for engineName, engineResult := range summary.EnginesResult { + setNotAvailableNumberIfZero(summary, &engineResult.StatusCode, engineName) } } @@ -452,11 +488,22 @@ func setRiskMsgAndStyle(summary *wrappers.ResultSummary) { } } +func setNotAvailableNumberIfZero(summary *wrappers.ResultSummary, counter *int, engineType string) { + if *counter == 0 && !contains(summary.EnginesEnabled, engineType) { + *counter = notAvailableNumber + } +} + func enhanceWithScanSummary(summary *wrappers.ResultSummary, results *wrappers.ScanResultsCollection) { for _, result := range results.Results { countResult(summary, result) } - summary.TotalIssues = summary.SastIssues + summary.ScaIssues + summary.KicsIssues + if summary.HasAPISecurity() { + summary.EnginesResult[commonParams.APISecType].Low = summary.APISecurity.Risks[3] + summary.EnginesResult[commonParams.APISecType].Medium = summary.APISecurity.Risks[2] + summary.EnginesResult[commonParams.APISecType].High = summary.APISecurity.Risks[1] + } + summary.TotalIssues = summary.SastIssues + summary.ScaIssues + summary.KicsIssues + summary.GetAPISecurityDocumentationTotal() } func writeHTMLSummary(targetFile string, summary *wrappers.ResultSummary) error { @@ -503,64 +550,16 @@ func writeConsoleSummary(summary *wrappers.ResultSummary) error { " Risk Level: %s \n", summary.RiskMsg, ) - fmt.Printf(" -------------------------------------- \n") - if summary.HasAPISecurity() { - fmt.Printf( - " API Security - Total Detected APIs: %d \n", - summary.APISecurity.APICount) - } if summary.Policies != nil && !strings.EqualFold(summary.Policies.Status, policeManagementNoneStatus) { - fmt.Printf(" -------------------------------------- \n\n") - if summary.Policies.BreakBuild { - fmt.Printf(" Policy Management Violation - Break Build Enabled: \n") - } else { - fmt.Printf(" Policy Management Violation: \n") - } - if len(summary.Policies.Polices) > 0 { - for _, police := range summary.Policies.Polices { - if len(police.RulesViolated) > 0 { - fmt.Printf(" Policy: %s | Break Build: %t | Violated Rules: ", police.Name, police.BreakBuild) - for _, violatedRule := range police.RulesViolated { - fmt.Printf("%s;", violatedRule) - } - } - fmt.Printf("\n") - } - } - fmt.Printf("\n") + printPoliciesSummary(summary) } - fmt.Printf(" Total Results: %d \n", summary.TotalIssues) - fmt.Printf(" ----------------------------------- \n") - fmt.Printf(" | Critical: %*d| \n", defaultPaddingSize, summary.CriticalIssues) - fmt.Printf(" | High: %*d| \n", defaultPaddingSize, summary.HighIssues) - fmt.Printf(" | Medium: %*d| \n", defaultPaddingSize, summary.MediumIssues) - fmt.Printf(" | Low: %*d| \n", defaultPaddingSize, summary.LowIssues) - fmt.Printf(" | Info: %*d| \n", defaultPaddingSize, summary.InfoIssues) - fmt.Printf(" ----------------------------------- \n") - - if summary.KicsIssues == notAvailableNumber { - fmt.Printf(" | IAC-SECURITY: %*s| \n", defaultPaddingSize, notAvailableString) - } else { - fmt.Printf(" | IAC-SECURITY: %*d| \n", defaultPaddingSize, summary.KicsIssues) - } - if summary.SastIssues == notAvailableNumber { - fmt.Printf(" | SAST: %*s| \n", defaultPaddingSize, notAvailableString) - } else { - fmt.Printf(" | SAST: %*d| \n", defaultPaddingSize, summary.SastIssues) - if summary.HasAPISecurity() { - fmt.Printf(" | APIS WITH RISK: %*d| \n", defaultPaddingSize, summary.APISecurity.TotalRisksCount) - if summary.HasAPISecurityDocumentation() { - fmt.Printf(" | APIS DOCUMENTATION: %*d| \n", defaultPaddingSize, summary.GetAPISecurityDocumentationTotal()) - } - } - } - if summary.ScaIssues == notAvailableNumber { - fmt.Printf(" | SCA: %*s| \n", defaultPaddingSize, notAvailableString) - } else { - fmt.Printf(" | SCA: %*d| \n", defaultPaddingSize, summary.ScaIssues) + printResultsSummaryTable(summary) + + if summary.HasAPISecurity() { + printAPIsSecuritySummary(summary) } - fmt.Printf(" -------------------------------------- \n\n") + fmt.Printf(" Checkmarx One - Scan Summary & Details: %s\n", summary.BaseURI) } else { fmt.Printf("Scan executed in asynchronous mode or still running. Hence, no results generated.\n") @@ -569,6 +568,74 @@ func writeConsoleSummary(summary *wrappers.ResultSummary) error { return nil } +func printPoliciesSummary(summary *wrappers.ResultSummary) { + fmt.Printf(" -------------------------------------- \n") + if summary.Policies.BreakBuild { + fmt.Printf(" Policy Management Violation - Break Build Enabled: \n") + } else { + fmt.Printf(" Policy Management Violation: \n") + } + if len(summary.Policies.Policies) > 0 { + for _, police := range summary.Policies.Policies { + if len(police.RulesViolated) > 0 { + fmt.Printf(" Policy: %s | Break Build: %t | Violated Rules: ", police.Name, police.BreakBuild) + for _, violatedRule := range police.RulesViolated { + fmt.Printf("%s;", violatedRule) + } + } + fmt.Printf("\n") + } + } + fmt.Printf("\n") +} + +func printAPIsSecuritySummary(summary *wrappers.ResultSummary) { + fmt.Printf(" API Security - Total Detected APIs: %d \n", summary.APISecurity.APICount) + fmt.Printf(" APIS WITH RISK: %*d \n", defaultPaddingSize, summary.APISecurity.TotalRisksCount) + if summary.HasAPISecurityDocumentation() { + fmt.Printf(" APIS DOCUMENTATION: %*d \n", defaultPaddingSize, summary.GetAPISecurityDocumentationTotal()) + } + fmt.Printf(" ---------------------------------------------------------------- \n\n") +} + +func printTableRow(title string, counts *wrappers.EngineResultSummary, statusNumber int) { + formatString := " | %-4s %4d %4d %6d %4d %4d %-9s |\n" + notAvailableFormatString := " | %-4s %4s %4s %6s %4s %4s %5s |\n" + + switch statusNumber { + case notAvailableNumber: + fmt.Printf(notAvailableFormatString, title, notAvailableString, notAvailableString, notAvailableString, notAvailableString, notAvailableString, notAvailableString) + case scanFailedNumber: + fmt.Printf(formatString, title, counts.Critical, counts.High, counts.Medium, counts.Low, counts.Info, scanFailedString) + case scanCanceledNumber: + fmt.Printf(formatString, title, counts.Critical, counts.High, counts.Medium, counts.Low, counts.Info, scanCanceledString) + default: + fmt.Printf(formatString, title, counts.Critical, counts.High, counts.Medium, counts.Low, counts.Info, scanSuccessString) + } +} + +func printResultsSummaryTable(summary *wrappers.ResultSummary) { + totalCriticalIssues := summary.EnginesResult.GetCriticalIssues() + totalHighIssues := summary.EnginesResult.GetHighIssues() + totalMediumIssues := summary.EnginesResult.GetMediumIssues() + totalLowIssues := summary.EnginesResult.GetLowIssues() + totalInfoIssues := summary.EnginesResult.GetInfoIssues() + fmt.Printf(" ---------------------------------------------------------------- \n\n") + fmt.Printf(" Total Results: %d \n", summary.TotalIssues) + fmt.Println(" ---------------------------------------------------------------- ") + fmt.Println(" | Critical High Medium Low Info Status |") + + printTableRow("APIs", summary.EnginesResult[commonParams.APISecType], summary.EnginesResult[commonParams.APISecType].StatusCode) + printTableRow("IAC", summary.EnginesResult[commonParams.KicsType], summary.EnginesResult[commonParams.KicsType].StatusCode) + printTableRow("SAST", summary.EnginesResult[commonParams.SastType], summary.EnginesResult[commonParams.SastType].StatusCode) + printTableRow("SCA", summary.EnginesResult[commonParams.ScaType], summary.EnginesResult[commonParams.ScaType].StatusCode) + + fmt.Println(" ---------------------------------------------------------------- ") + fmt.Printf(" | %-4s %4d %4d %6d %4d %4d %-9s |\n", + fmt.Sprintf(boldFormat, "TOTAL"), totalCriticalIssues, totalHighIssues, totalMediumIssues, totalLowIssues, totalInfoIssues, summary.Status) + fmt.Printf(" ---------------------------------------------------------------- \n\n") +} + func generateScanSummaryURL(summary *wrappers.ResultSummary) string { summaryURL := fmt.Sprintf( strings.Replace(summary.BaseURI, "overview", "scans?id=%s&branch=%s", 1), @@ -594,6 +661,7 @@ func runGetResultCommand( formatSbomOptions, _ := cmd.Flags().GetString(commonParams.ReportSbomFormatFlag) useSCALocalFlow, _ := cmd.Flags().GetBool(commonParams.ReportSbomFormatLocalFlowFlag) retrySBOM, _ := cmd.Flags().GetInt(commonParams.RetrySBOMFlag) + sastRedundancy, _ := cmd.Flags().GetBool(commonParams.SastRedundancyFlag) scanID, _ := cmd.Flags().GetString(commonParams.ScanIDFlag) if scanID == "" { @@ -619,13 +687,18 @@ func runGetResultCommand( if policyTimeout < 0 { return errors.Errorf("--%s should be equal or higher than 0", commonParams.PolicyTimeoutFlag) } - policyResponseModel, err = handlePolicyWait(waitDelay, policyTimeout, policyWrapper, scan, cmd) + policyResponseModel, err = policymanagement.HandlePolicyWait(waitDelay, policyTimeout, policyWrapper, scan.ID, scan.ProjectID, cmd) if err != nil { return err } } else { logger.PrintIfVerbose("Skipping policy evaluation") } + + if sastRedundancy { + params[commonParams.SastRedundancyFlag] = "" + } + return CreateScanReport( resultsWrapper, risksOverviewWrapper, @@ -741,6 +814,7 @@ func CreateScanReport( func countResult(summary *wrappers.ResultSummary, result *wrappers.ScanResult) { engineType := strings.TrimSpace(result.Type) + severity := strings.ToLower(result.Severity) if contains(summary.EnginesEnabled, engineType) && isExploitable(result.State) { if engineType == commonParams.SastType { summary.SastIssues++ @@ -752,8 +826,9 @@ func countResult(summary *wrappers.ResultSummary, result *wrappers.ScanResult) { summary.KicsIssues++ summary.TotalIssues++ } - severity := strings.ToLower(result.Severity) - if severity == highLabel { + if severity == criticalLabel { + summary.CriticalIssues++ + } else if severity == highLabel { summary.HighIssues++ } else if severity == lowLabel { summary.LowIssues++ @@ -762,6 +837,7 @@ func countResult(summary *wrappers.ResultSummary, result *wrappers.ScanResult) { } else if severity == infoLabel { summary.InfoIssues++ } + summary.UpdateEngineResultSummary(engineType, severity) } } @@ -971,6 +1047,12 @@ func enrichScaResults( resultsModel = addPackageInformation(resultsModel, scaPackageModel, scaTypeModel) } } + _, sastRedundancy := params[commonParams.SastRedundancyFlag] + + if util.Contains(scan.Engines, commonParams.SastType) && sastRedundancy { + // Compute SAST results redundancy + resultsModel = ComputeRedundantSastResults(resultsModel) + } return resultsModel, nil } @@ -1343,8 +1425,8 @@ func createSarifRun(results *wrappers.ScanResultsCollection) wrappers.SarifRun { } func parseResults(results *wrappers.ScanResultsCollection) ([]wrappers.SarifDriverRule, []wrappers.SarifScanResult) { - var sarifRules []wrappers.SarifDriverRule - var sarifResults []wrappers.SarifScanResult + var sarifRules = make([]wrappers.SarifDriverRule, 0) + var sarifResults = make([]wrappers.SarifScanResult, 0) if results != nil { ruleIds := map[interface{}]bool{} for _, result := range results.Results { @@ -1729,11 +1811,11 @@ func addPackageInformation( head.SupportsQuickFix = head.SupportsQuickFix && util.IsPackageFileSupported(*location) } currentPackage.SupportsQuickFix = currentPackage.SupportsQuickFix || head.SupportsQuickFix - } - if result.VulnerabilityDetails.CveName != "" { - currentPackage.FixLink = "https://devhub.checkmarx.com/cve-details/" + result.VulnerabilityDetails.CveName - } else { - currentPackage.FixLink = "" + if result.ID != "" { + currentPackage.FixLink = "https://devhub.checkmarx.com/cve-details/" + result.ID + } else { + currentPackage.FixLink = "" + } } if currentPackage.IsDirectDependency { currentPackage.TypeOfDependency = directDependencyType @@ -1751,12 +1833,12 @@ func addPackageInformation( func filterViolatedRules(policyModel wrappers.PolicyResponseModel) *wrappers.PolicyResponseModel { i := 0 - for _, policy := range policyModel.Polices { + for _, policy := range policyModel.Policies { if len(policy.RulesViolated) > 0 { - policyModel.Polices[i] = policy + policyModel.Policies[i] = policy i++ } } - policyModel.Polices = policyModel.Polices[:i] + policyModel.Policies = policyModel.Policies[:i] return &policyModel } diff --git a/internal/commands/result_test.go b/internal/commands/result_test.go index d27d70422..525c759bd 100644 --- a/internal/commands/result_test.go +++ b/internal/commands/result_test.go @@ -62,6 +62,13 @@ func TestRunGetResultsByScanIdJsonFormat(t *testing.T) { os.Remove(fmt.Sprintf("%s.%s", fileName, printer.FormatJSON)) } +func TestRunGetResultsByScanIdJsonFormatWithSastRedundancy(t *testing.T) { + execCmdNilAssertion(t, "results", "show", "--scan-id", "MOCK", "--report-format", "json", "--sast-redundancy") + + // Remove generated json file + os.Remove(fmt.Sprintf("%s.%s", fileName, printer.FormatJSON)) +} + func TestRunGetResultsByScanIdSummaryJsonFormat(t *testing.T) { execCmdNilAssertion(t, "results", "show", "--scan-id", "MOCK", "--report-format", "summaryJSON") @@ -325,3 +332,124 @@ func TestRunGetResultsByScanIdGLFormat(t *testing.T) { // Run test for gl-sast report type os.Remove(fmt.Sprintf("%s.%s", fileName, printer.FormatGL)) } + +func Test_addPackageInformation(t *testing.T) { + var dependencyPath = wrappers.DependencyPath{ID: "test-1"} + var dependencyArray = [][]wrappers.DependencyPath{{dependencyPath}} + resultsModel := &wrappers.ScanResultsCollection{ + Results: []*wrappers.ScanResult{ + { + Type: "sca", // Assuming this matches commonParams.ScaType + ScanResultData: wrappers.ScanResultData{ + PackageIdentifier: "pkg-123", + }, + ID: "CVE-2021-23-424", + VulnerabilityDetails: wrappers.VulnerabilityDetails{ + CvssScore: 5.0, + CveName: "cwe-789", + }, + }, + }, + } + scaPackageModel := &[]wrappers.ScaPackageCollection{ + { + ID: "pkg-123", + FixLink: "", + DependencyPathArray: dependencyArray, + }, + } + scaTypeModel := &[]wrappers.ScaTypeCollection{ + {}} + + resultsModel = addPackageInformation(resultsModel, scaPackageModel, scaTypeModel) + + expectedFixLink := "https://devhub.checkmarx.com/cve-details/CVE-2021-23-424" + actualFixLink := resultsModel.Results[0].ScanResultData.ScaPackageCollection.FixLink + assert.Equal(t, expectedFixLink, actualFixLink, "FixLink should match the result ID") +} + +func Test_setRiskMsgAndStyle_critical(t *testing.T) { + var summary wrappers.ResultSummary + summary.CriticalIssues = 1 + setRiskMsgAndStyle(&summary) + assert.Equal(t, criticalLabel, summary.RiskStyle, "Incorrect Risk Style for critical issues.") + assert.Equal(t, "Critical Risk", summary.RiskMsg, "Incorrect Risk Message for critical issues.") +} +func Test_setRiskMsgAndStyle_high(t *testing.T) { + var summary wrappers.ResultSummary + summary.CriticalIssues = 0 + summary.HighIssues = 1 + setRiskMsgAndStyle(&summary) + assert.Equal(t, highLabel, summary.RiskStyle, "Incorrect Risk Style for high issues.") + assert.Equal(t, "High Risk", summary.RiskMsg, "Incorrect Risk Message for high issues.") +} +func Test_setRiskMsgAndStyle_criticalAndHigh(t *testing.T) { + var summary wrappers.ResultSummary + summary.CriticalIssues = 1 + summary.HighIssues = 1 + setRiskMsgAndStyle(&summary) + assert.Equal(t, criticalLabel, summary.RiskStyle, "Incorrect Risk Style for critical issues.") + assert.Equal(t, "Critical Risk", summary.RiskMsg, "Incorrect Risk Message for critical issues.") +} +func Test_countResult(t *testing.T) { + var result wrappers.ScanResult + result.Type = params.SastType + result.Severity = criticalLabel + result.State = "EXPLOITABLE" + + var summary wrappers.ResultSummary + engineEnabled := []string{params.SastType} + summary.EnginesEnabled = engineEnabled + summary.SastIssues = 100 + summary.TotalIssues = 1000 + summary.CriticalIssues = 10 + var engineResultSummary wrappers.EngineResultSummary + engineResultSummary.Critical = 0 + var engineResult = make(map[string]*wrappers.EngineResultSummary) + engineResult[params.SastType] = &engineResultSummary + summary.EnginesResult = engineResult + + countResult(&summary, &result) + + assert.Equal(t, 101, summary.SastIssues, "Critical issues in summary SAST issues are not counted properly") + assert.Equal(t, 1001, summary.TotalIssues, "Critical issues in summary total issues are not counted properly") + assert.Equal(t, 11, summary.CriticalIssues, "Critical issues in summary are not counted properly") + assert.Equal(t, 1, summary.EnginesResult[params.SastType].Critical, "Critical issues in summary for SAST are not counted properly") +} +func Test_countResult_high(t *testing.T) { + var result wrappers.ScanResult + result.Type = params.ScaType + result.Severity = highLabel + result.State = "EXPLOITABLE" + + var summary wrappers.ResultSummary + engineEnabled := []string{params.ScaType} + summary.EnginesEnabled = engineEnabled + summary.ScaIssues = 100 + summary.TotalIssues = 1000 + summary.HighIssues = 10 + var engineResultSummary wrappers.EngineResultSummary + engineResultSummary.High = 0 + var engineResult = make(map[string]*wrappers.EngineResultSummary) + engineResult[params.ScaType] = &engineResultSummary + summary.EnginesResult = engineResult + + countResult(&summary, &result) + + assert.Equal(t, 101, summary.ScaIssues, "High issues in summary SCA issues are not counted properly") + assert.Equal(t, 1001, summary.TotalIssues, "High issues in summary total issues are not counted properly") + assert.Equal(t, 11, summary.HighIssues, "High issues in summary are not counted properly") + assert.Equal(t, 1, summary.EnginesResult[params.ScaType].High, "High issues in summary for SCA are not counted properly") +} +func Test_findSarifLevel_critical(t *testing.T) { + var result wrappers.ScanResult + result.Severity = criticalCx + var sarifLevel = findSarifLevel(&result) + assert.Equal(t, highSarif, sarifLevel, "Incorrect sarif level for critical issues.") +} +func Test_findSarifLevel_high(t *testing.T) { + var result wrappers.ScanResult + result.Severity = highCx + var sarifLevel = findSarifLevel(&result) + assert.Equal(t, highSarif, sarifLevel, "Incorrect sarif level for high issues.") +} diff --git a/internal/commands/results-redundancy.go b/internal/commands/results-redundancy.go new file mode 100644 index 000000000..4d57aaa2e --- /dev/null +++ b/internal/commands/results-redundancy.go @@ -0,0 +1,279 @@ +package commands + +import ( + "crypto/sha1" + "encoding/hex" + "fmt" + "math" + "sort" + "strings" + + "github.com/checkmarx/ast-cli/internal/wrappers" +) + +const ( + precision = 2 + ten = 10 + half = 0.5 +) + +func ComputeRedundantSastResults(resultsModel *wrappers.ScanResultsCollection) *wrappers.ScanResultsCollection { + languages := GetLanguages(resultsModel) + queriesByLanguage := GetQueries(resultsModel, languages) + + for language := range queriesByLanguage { + for query := range queriesByLanguage[language] { + resultsModel = ComputeRedundantSastResultsForQuery(resultsModel, language, query) + } + } + return resultsModel +} + +func GetLanguages(resultsModel *wrappers.ScanResultsCollection) map[string]bool { + languages := make(map[string]bool) + for _, result := range resultsModel.Results { + if result.ScanResultData.LanguageName != "" { + languages[result.ScanResultData.LanguageName] = true + } + } + return languages +} + +func GetQueries(resultsModel *wrappers.ScanResultsCollection, languages map[string]bool) map[string]map[string]bool { + queriesByLanguage := make(map[string]map[string]bool) + for _, result := range resultsModel.Results { + if _, exist := languages[result.ScanResultData.LanguageName]; !exist { + continue + } + if _, exist := queriesByLanguage[result.ScanResultData.LanguageName]; !exist { + queriesByLanguage[result.ScanResultData.LanguageName] = make(map[string]bool) + } + queriesByLanguage[result.ScanResultData.LanguageName][result.ScanResultData.QueryName] = true + } + return queriesByLanguage +} + +func ComputeRedundantSastResultsForQuery(resultsModel *wrappers.ScanResultsCollection, language, query string) *wrappers.ScanResultsCollection { + queryResults := GetResultsForQuery(resultsModel, language, query) + if len(queryResults) == 0 { + return resultsModel + } + + flows := buildFlows(queryResults) + + subFlows := compareFlows(flows) + + redundantResults := computeRedundantResults(subFlows, queryResults) + + labelRedundantResults(queryResults, redundantResults) + return resultsModel +} + +func GetResultsForQuery(resultsModel *wrappers.ScanResultsCollection, language, query string) []*wrappers.ScanResult { + var queryResults []*wrappers.ScanResult + for _, result := range resultsModel.Results { + if result.ScanResultData.LanguageName != language || result.ScanResultData.QueryName != query { + continue + } + queryResults = append(queryResults, result) + } + + sort.Slice(queryResults, func(i, j int) bool { + return queryResults[i].ID < queryResults[j].ID + }) + + return queryResults +} + +func labelRedundantResults(queryResults []*wrappers.ScanResult, redundantResults map[string]map[string]bool) { + resultsByID := make(map[string]*wrappers.ScanResult) + for _, result := range queryResults { + resultsByID[result.ID] = result + } + + for resultID, redundantResult := range redundantResults { + if len(redundantResult) == 0 { + resultsByID[resultID].ScanResultData.Redundancy = fixLabel + } else { + resultsByID[resultID].ScanResultData.Redundancy = redundantLabel + } + } +} + +func computeRedundantResults(subFlows map[string]*SubFlow, queryResults []*wrappers.ScanResult) map[string]map[string]bool { + redundantResults := make(map[string]map[string]bool) + for _, result := range queryResults { + redundantResults[result.ID] = make(map[string]bool) + } + + sortedSubFlowIDs := sortSubFlowIDs(subFlows) + for _, key := range sortedSubFlowIDs { + sortedResults := sortSubFlowResultIDs(subFlows[key]) + coverage := make(map[string]float64) + for _, resultID := range sortedResults { + result := getResultForID(queryResults, resultID) + coverage[resultID] = roundFloat(float64(len(subFlows[key].Flow))/float64(len(result.ScanResultData.Nodes)), precision) + } + maxCoverageResultID, maxCoverage := getMaxCoverage(coverage) + + if maxCoverage < half { + continue + } + for resultID := range coverage { + if resultID == maxCoverageResultID { + continue + } + redundantResults[resultID][maxCoverageResultID] = true + } + } + return redundantResults +} + +func getMaxCoverage(coverage map[string]float64) (maxCoverageResultID string, maxCoverage float64) { + var sortedResultsIDs []string + for resultID := range coverage { + sortedResultsIDs = append(sortedResultsIDs, resultID) + } + sort.Strings(sortedResultsIDs) + + maxCoverageResultID = "" + maxCoverage = 0.0 + for _, resultID := range sortedResultsIDs { + c := coverage[resultID] + if c > maxCoverage { + maxCoverage = c + maxCoverageResultID = resultID + } + } + return maxCoverageResultID, maxCoverage +} + +func roundFloat(val float64, precision uint) float64 { + ratio := math.Pow(ten, float64(precision)) + return math.Round(val*ratio) / ratio +} + +func getResultForID(queryResults []*wrappers.ScanResult, resultID string) *wrappers.ScanResult { + for _, result := range queryResults { + if result.ID == resultID { + return result + } + } + return nil +} + +func sortSubFlowIDs(subFlows map[string]*SubFlow) []string { + var subFlowIDs []string + for key := range subFlows { + subFlowIDs = append(subFlowIDs, key) + } + sort.Strings(subFlowIDs) + return subFlowIDs +} + +func sortSubFlowResultIDs(subFlow *SubFlow) []string { + var results []string + for result := range subFlow.Results { + results = append(results, result) + } + sort.Strings(results) + return results +} + +func buildFlows(queryResults []*wrappers.ScanResult) map[string][]string { + flowsByResult := make(map[string][]string) + for _, result := range queryResults { + var resultNodes []string + for _, node := range result.ScanResultData.Nodes { + nodeStr := fmt.Sprintf("%s:%d:%d", node.FileName, node.Line, node.Column) + resultNodes = append(resultNodes, nodeStr) + } + flowsByResult[result.ID] = resultNodes + } + return flowsByResult +} + +type SubFlow struct { + ShaOne string + Flow []string + Results map[string]bool +} + +func compareFlows(flows map[string][]string) map[string]*SubFlow { + subFlows := make(map[string]*SubFlow) + comparedFlows := make(map[string]bool) + for r1, f1 := range flows { + for r2, f2 := range flows { + if r1 == r2 { + continue + } + key := GetKey(r1, r2) + if _, exist := comparedFlows[key]; exist { + continue + } + comparedFlows[key] = true + + // compute the subflow of f1 and f2 + exist, sf := computeSubFlow(f1, f2) + if !exist { + continue + } + if _, exist := subFlows[sf.ShaOne]; !exist { + sf.Results = make(map[string]bool) + subFlows[sf.ShaOne] = sf + } + subFlows[sf.ShaOne].Results[r1] = true + subFlows[sf.ShaOne].Results[r2] = true + } + } + return subFlows +} + +func GetKey(r1, r2 string) string { + if r1 <= r2 { + return r1 + "," + r2 + } + return r2 + "," + r1 +} + +func computeSubFlow(f1, f2 []string) (bool, *SubFlow) { + var subFlow []string + for i1 := 0; i1 < len(f1); { + for i2 := 0; i2 < len(f2) && i1 < len(f1); { + if f1[i1] == f2[i2] { + subFlow = append(subFlow, f1[i1]) + i1++ + i2++ + } else { + if len(subFlow) > 0 { + break + } + i2++ + } + } + if len(subFlow) > 0 { + break + } + i1++ + } + if len(subFlow) == 0 { + return false, nil + } + sha1String := getSha1String(subFlow) + return true, &SubFlow{sha1String, subFlow, nil} +} + +func getSha1String(lines []string) string { + h := sha1.New() + + // Write the bytes of the input string into the hash + h.Write([]byte(strings.Join(lines, ""))) + + // Get the final hash result as a byte slice + bs := h.Sum(nil) + + // Convert the byte slice to a hexadecimal string + sha1String := hex.EncodeToString(bs) + + return sha1String +} diff --git a/internal/commands/root.go b/internal/commands/root.go index 86dc1e615..ed879366f 100644 --- a/internal/commands/root.go +++ b/internal/commands/root.go @@ -23,6 +23,7 @@ const ErrorCodeFormat = "%s: CODE: %d, %s\n" // NewAstCLI Return a Checkmarx One CLI root command to execute func NewAstCLI( + applicationsWrapper wrappers.ApplicationsWrapper, scansWrapper wrappers.ScansWrapper, resultsSbomWrapper wrappers.ResultsSbomWrapper, resultsPdfReportsWrapper wrappers.ResultsPdfWrapper, @@ -50,6 +51,7 @@ func NewAstCLI( featureFlagsWrapper wrappers.FeatureFlagsWrapper, policyWrapper wrappers.PolicyWrapper, sastMetadataWrapper wrappers.SastMetadataWrapper, + accessManagementWrapper wrappers.AccessManagementWrapper, ) *cobra.Command { // Create the root rootCmd := &cobra.Command{ @@ -143,6 +145,7 @@ func NewAstCLI( // Create the CLI command structure scanCmd := NewScanCommand( + applicationsWrapper, scansWrapper, resultsSbomWrapper, resultsPdfReportsWrapper, @@ -156,8 +159,9 @@ func NewAstCLI( scaRealTimeWrapper, policyWrapper, sastMetadataWrapper, + accessManagementWrapper, ) - projectCmd := NewProjectCommand(projectsWrapper, groupsWrapper) + projectCmd := NewProjectCommand(applicationsWrapper, projectsWrapper, groupsWrapper, accessManagementWrapper) resultsCmd := NewResultsCommand( resultsWrapper, scansWrapper, @@ -181,11 +185,13 @@ func NewAstCLI( learnMoreWrapper, tenantWrapper, chatWrapper, + policyWrapper, + scansWrapper, ) configCmd := util.NewConfigCommand() triageCmd := NewResultsPredicatesCommand(resultsPredicatesWrapper) - chatCmd := NewChatCommand(chatWrapper) + chatCmd := NewChatCommand(chatWrapper, tenantWrapper) rootCmd.AddCommand( scanCmd, diff --git a/internal/commands/root_test.go b/internal/commands/root_test.go index c7394f2e7..e301c4676 100644 --- a/internal/commands/root_test.go +++ b/internal/commands/root_test.go @@ -30,6 +30,7 @@ func TestMain(m *testing.M) { } func createASTTestCommand() *cobra.Command { + applicationWrapper := &mock.ApplicationsMockWrapper{} scansMockWrapper := &mock.ScansMockWrapper{} resultsSbomWrapper := &mock.ResultsSbomWrapper{} resultsPdfWrapper := &mock.ResultsPdfWrapper{} @@ -57,8 +58,10 @@ func createASTTestCommand() *cobra.Command { featureFlagsMockWrapper := &mock.FeatureFlagsMockWrapper{} policyWrapper := &mock.PolicyMockWrapper{} sastMetadataWrapper := &mock.SastMetadataMockWrapper{} + accessManagementWrapper := &mock.AccessManagementMockWrapper{} return NewAstCLI( + applicationWrapper, scansMockWrapper, resultsSbomWrapper, resultsPdfWrapper, @@ -86,6 +89,7 @@ func createASTTestCommand() *cobra.Command { featureFlagsMockWrapper, policyWrapper, sastMetadataWrapper, + accessManagementWrapper, ) } diff --git a/internal/commands/chatsast/sast-prompt.go b/internal/commands/sast-prompt.go similarity index 70% rename from internal/commands/chatsast/sast-prompt.go rename to internal/commands/sast-prompt.go index bc2e1bdaa..f6f85dd55 100644 --- a/internal/commands/chatsast/sast-prompt.go +++ b/internal/commands/sast-prompt.go @@ -1,4 +1,4 @@ -package chatsast +package commands import ( "fmt" @@ -11,13 +11,29 @@ about the results. You should also be capable of delivering clear, concise, and If a question irrelevant to the mentioned source code or SAST result is asked, answer 'I am the AI Guided Remediation assistant and can answer only on questions related to source code or SAST results or SAST Queries'.` +const ( + confidence = "**CONFIDENCE:**" + explanation = "**EXPLANATION:**" + fix = "**PROPOSED REMEDIATION:**" + code = "```" +) + +const ( + confidenceDescription = " A score between 0 (low) and 100 (high) indicating the degree of confidence in the exploitability of this vulnerability in the context of your code.
" + explanationDescription = " An OpenAI generated description of the vulnerability.
" + fixDescription = " A customized snippet, generated by OpenAI, that can be used to remediate the vulnerability in your code.
" +) + +// This constant is used to format the identifiers (confidence, explanation, fix) and their descriptions with HTML tags +const identifierTitleForamt = "%s%s" + const userPromptTemplate = `Checkmarx Static Application Security Testing (SAST) detected the %s vulnerability within the provided %s code snippet. The attack vector is presented by code snippets annotated by comments in the form ` + "`//SAST Node #X: element (element-type)`" + ` where X is the node index in the result, ` + "`element`" + ` is the name of the element through which the data flows, and the ` + "`element-type`" + ` is it's type. The first and last nodes are indicated by ` + "`(input ...)` and `(output ...)`" + ` respectively: -` + "```" + ` +` + code + ` %s -` + "```" + ` +` + code + ` Please review the code above and provide a confidence score ranging from 0 to 100. A score of 0 means you believe the result is completely incorrect, unexploitable, and a false positive. A score of 100 means you believe the result is completely correct, exploitable, and a true positive. @@ -35,12 +51,15 @@ or it's a false positive. Please provide a brief explanation for your confidence score, don't mention all the instruction above. -Next, please provide code that fixes the vulnerability so that a developer can copy paste instead of the snippet above. +Next, please provide code that remediates the vulnerability so that a developer can copy paste instead of the snippet above. -Your analysis should be presented in the following format: - CONFIDENCE: num - EXPLANATION: short_text - FIX: fixed_snippet` +Your analysis MUST be presented in the following format: +` + confidence + + `number +` + "\n" + explanation + + `short_text +` + "\n" + fix + ":" + + `fixed_snippet` func GetSystemPrompt() string { return systemPrompt @@ -83,7 +102,17 @@ func createSourceForPrompt(result *Result, sources map[string][]string) (string, } else { edge = "" } - methodLines[lineInMethod] += fmt.Sprintf("//SAST Node #%d%s: %s (%s)", i, edge, node.Name, node.DomType) + + // change UnknownReference to something more informational like VariableReference or TypeNameReference + nodeType := node.DomType + if node.DomType == "UnknownReference" { + if node.TypeName == "" { + nodeType = "VariableReference" + } else { + nodeType = node.TypeName + "Reference" + } + } + methodLines[lineInMethod] += fmt.Sprintf("//SAST Node #%d%s: %s (%s)", i, edge, node.Name, nodeType) methodsInPrompt[sourceFilename+":"+node.Method] = methodLines } @@ -117,3 +146,23 @@ func GetMethodByMethodLine(filename string, lines []string, methodLineNumber, no } return methodLines, nil } + +func addDescriptionForIdentifier(responseContent []string) []string { + identifiersDescription := map[string]string{ + confidence: confidenceDescription, + explanation: explanationDescription, + fix: fixDescription, + } + if len(responseContent) > 0 { + for i := 0; i < len(responseContent); i++ { + for identifier, description := range identifiersDescription { + responseContent[i] = replaceIdentifierTitleIfNeeded(responseContent[i], identifier, description) + } + } + } + return responseContent +} + +func replaceIdentifierTitleIfNeeded(input, identifier, identifierDescription string) string { + return strings.Replace(input, identifier, fmt.Sprintf(identifierTitleForamt, identifier, identifierDescription), 1) +} diff --git a/internal/commands/sast-prompt_test.go b/internal/commands/sast-prompt_test.go new file mode 100644 index 000000000..9af903994 --- /dev/null +++ b/internal/commands/sast-prompt_test.go @@ -0,0 +1,61 @@ +package commands + +import ( + "fmt" + "testing" +) + +const expectedOutputFormat = "**CONFIDENCE:** " + + "A score between 0 (low) and 100 (high) indicating the degree of confidence in the exploitability of this vulnerability in the context of your code. " + + "
%s**EXPLANATION:** " + + "An OpenAI generated description of the vulnerability.
%s**PROPOSED REMEDIATION:** " + + "A customized snippet, generated by OpenAI, that can be used to remediate the vulnerability in your code.
%s" + +func getExpectedOutput(confidenceNumber, explanationText, fixText string) string { + return fmt.Sprintf(expectedOutputFormat, confidenceNumber, explanationText, fixText) +} + +func TestAddDescriptionForIdentifiers(t *testing.T) { + input := confidence + " 35 " + explanation + " this is a short explanation." + fix + " a fixed snippet" + expected := getExpectedOutput(" 35 ", " this is a short explanation.", " a fixed snippet") + output := getActual(input, t) + + if output[len(output)-1] != expected { + t.Errorf("Expected %q, but got %q", expected, output) + } +} + +func TestAddNewlinesIfNecessarySomeNewlines(t *testing.T) { + input := confidence + " 35 " + explanation + " this is a short explanation.\n" + fix + " a fixed snippet" + expected := getExpectedOutput(" 35 ", " this is a short explanation.\n", " a fixed snippet") + + output := getActual(input, t) + + if output[len(output)-1] != expected { + t.Errorf("Expected %q, but got %q", expected, output) + } +} + +func TestAddNewlinesIfNecessaryAllNewlines(t *testing.T) { + input := confidence + " 35\n " + explanation + " this is a short explanation.\n" + fix + " a fixed snippet" + expected := getExpectedOutput(" 35\n ", " this is a short explanation.\n", " a fixed snippet") + + output := getActual(input, t) + + if output[len(output)-1] != expected { + t.Errorf("Expected %q, but got %q", expected, output) + } +} + +func getActual(input string, t *testing.T) []string { + someText := "some text" + response := []string{someText, someText, input} + output := addDescriptionForIdentifier(response) + for i := 0; i < len(output)-1; i++ { + if output[i] != response[i] { + t.Errorf("All strings except last expected to stay the same") + } + } + return output +} diff --git a/internal/commands/chatsast/sast-results-json.go b/internal/commands/sast-results-json.go similarity index 93% rename from internal/commands/chatsast/sast-results-json.go rename to internal/commands/sast-results-json.go index 6b4959891..8c8e64963 100644 --- a/internal/commands/chatsast/sast-results-json.go +++ b/internal/commands/sast-results-json.go @@ -1,4 +1,4 @@ -package chatsast +package commands import ( "encoding/json" @@ -87,22 +87,6 @@ type ScanResults struct { ScanID string `json:"scanID"` } -func ReadResultsAll(filename string) (*ScanResults, error) { - bytes, err := os.ReadFile(filename) - if err != nil { - return nil, err - } - - // Unmarshal the JSON data into the ScanResults struct - var scanResults ScanResults - err = json.Unmarshal(bytes, &scanResults) - if err != nil { - return nil, err - } - - return &scanResults, nil -} - func ReadResultsSAST(filename string) (*ScanResults, error) { bytes, err := os.ReadFile(filename) if err != nil { diff --git a/internal/commands/chatsast/sast-sources.go b/internal/commands/sast-sources.go similarity index 63% rename from internal/commands/chatsast/sast-sources.go rename to internal/commands/sast-sources.go index 4846b9852..ece464391 100644 --- a/internal/commands/chatsast/sast-sources.go +++ b/internal/commands/sast-sources.go @@ -1,4 +1,4 @@ -package chatsast +package commands import ( "bufio" @@ -22,26 +22,6 @@ func GetSourcesForResult(scanResult *Result, sourceDir string) (map[string][]str return fileContents, nil } -func GetSourcesForQuery(scanResults *ScanResults, sourceDir, language, query string) (map[string][]string, error) { - sourceFilenames := make(map[string]bool) - for _, scanResult := range scanResults.Results { - if scanResult.Data.LanguageName != language || scanResult.Data.QueryName != query { - continue - } - for i := range scanResult.Data.Nodes { - sourceFilename := strings.ReplaceAll(scanResult.Data.Nodes[i].FileName, "\\", "/") - sourceFilenames[sourceFilename] = true - } - } - - fileContents, err := GetFileContents(sourceFilenames, sourceDir) - if err != nil { - return nil, err - } - - return fileContents, nil -} - func GetFileContents(filenames map[string]bool, sourceDir string) (map[string][]string, error) { fileContents := make(map[string][]string) diff --git a/internal/commands/scan.go b/internal/commands/scan.go index c7bbda601..f3b7b098a 100644 --- a/internal/commands/scan.go +++ b/internal/commands/scan.go @@ -18,6 +18,8 @@ import ( "strings" "time" + applicationErrors "github.com/checkmarx/ast-cli/internal/errors" + "github.com/checkmarx/ast-cli/internal/commands/scarealtime" "github.com/checkmarx/ast-cli/internal/commands/util" "github.com/checkmarx/ast-cli/internal/commands/util/printer" @@ -27,6 +29,7 @@ import ( "github.com/pkg/errors" "github.com/MakeNowJust/heredoc" + "github.com/checkmarx/ast-cli/internal/commands/policymanagement" commonParams "github.com/checkmarx/ast-cli/internal/params" "github.com/checkmarx/ast-cli/internal/wrappers" "github.com/mssola/user_agent" @@ -121,6 +124,7 @@ var ( ) func NewScanCommand( + applicationsWrapper wrappers.ApplicationsWrapper, scansWrapper wrappers.ScansWrapper, resultsSbomWrapper wrappers.ResultsSbomWrapper, resultsPdfReportsWrapper wrappers.ResultsPdfWrapper, @@ -134,6 +138,7 @@ func NewScanCommand( scaRealTimeWrapper wrappers.ScaRealTimeWrapper, policyWrapper wrappers.PolicyWrapper, sastMetadataWrapper wrappers.SastMetadataWrapper, + accessManagementWrapper wrappers.AccessManagementWrapper, ) *cobra.Command { scanCmd := &cobra.Command{ Use: "scan", @@ -159,6 +164,8 @@ func NewScanCommand( riskOverviewWrapper, jwtWrapper, policyWrapper, + accessManagementWrapper, + applicationsWrapper, ) listScansCmd := scanListSubCommand(scansWrapper, sastMetadataWrapper) @@ -246,16 +253,16 @@ func scanLogsSubCommand(logsWrapper wrappers.LogsWrapper) *cobra.Command { logsCmd := &cobra.Command{ Use: "logs", Short: "Download scan log for selected scan type", - Long: "Accepts a scan-id and scan type (sast, iac-security or sca) and downloads the related scan log", + Long: "Accepts a scan-id and scan type (sast, iac-security) and downloads the related scan log", Example: heredoc.Doc( ` - $ cx scan logs --scan-id --scan-type + $ cx scan logs --scan-id --scan-type `, ), RunE: runDownloadLogs(logsWrapper), } logsCmd.PersistentFlags().String(commonParams.ScanIDFlag, "", "Scan ID to retrieve log for.") - logsCmd.PersistentFlags().String(commonParams.ScanTypeFlag, "", "Scan type to pull log for, ex: sast, iac-security or sca.") + logsCmd.PersistentFlags().String(commonParams.ScanTypeFlag, "", "Scan type to pull log for, ex: sast, iac-security.") markFlagAsRequired(logsCmd, commonParams.ScanIDFlag) markFlagAsRequired(logsCmd, commonParams.ScanTypeFlag) @@ -409,6 +416,8 @@ func scanCreateSubCommand( risksOverviewWrapper wrappers.RisksOverviewWrapper, jwtWrapper wrappers.JWTWrapper, policyWrapper wrappers.PolicyWrapper, + accessManagementWrapper wrappers.AccessManagementWrapper, + applicationsWrapper wrappers.ApplicationsWrapper, ) *cobra.Command { createScanCmd := &cobra.Command{ Use: "create", @@ -437,6 +446,8 @@ func scanCreateSubCommand( risksOverviewWrapper, jwtWrapper, policyWrapper, + accessManagementWrapper, + applicationsWrapper, ), } createScanCmd.PersistentFlags().Bool(commonParams.AsyncFlag, false, "Do not wait for scan completion") @@ -561,6 +572,8 @@ func scanCreateSubCommand( "Cancel the policy evaluation and fail after the timeout in minutes", ) createScanCmd.PersistentFlags().Bool(commonParams.IgnorePolicyFlag, false, "Do not evaluate policies") + + createScanCmd.PersistentFlags().String(commonParams.ApplicationName, "", "Name of the application to assign with the project") // Link the environment variables to the CLI argument(s). err = viper.BindPFlag(commonParams.BranchKey, createScanCmd.PersistentFlags().Lookup(commonParams.BranchFlag)) if err != nil { @@ -578,10 +591,12 @@ func scanCreateSubCommand( } func findProject( + applicationID []string, projectName string, cmd *cobra.Command, projectsWrapper wrappers.ProjectsWrapper, groupsWrapper wrappers.GroupsWrapper, + accessManagementWrapper wrappers.AccessManagementWrapper, ) (string, error) { params := make(map[string]string) params["names"] = projectName @@ -592,10 +607,10 @@ func findProject( for i := 0; i < len(resp.Projects); i++ { if resp.Projects[i].Name == projectName { - return updateProject(resp, cmd, projectsWrapper, groupsWrapper, projectName) + return updateProject(resp, cmd, projectsWrapper, groupsWrapper, accessManagementWrapper, projectName, applicationID) } } - projectID, err := createProject(projectName, cmd, projectsWrapper, groupsWrapper) + projectID, err := createProject(projectName, cmd, projectsWrapper, groupsWrapper, accessManagementWrapper, applicationID) if err != nil { return "", err } @@ -607,6 +622,8 @@ func createProject( cmd *cobra.Command, projectsWrapper wrappers.ProjectsWrapper, groupsWrapper wrappers.GroupsWrapper, + accessManagementWrapper wrappers.AccessManagementWrapper, + applicationID []string, ) (string, error) { projectGroups, _ := cmd.Flags().GetString(commonParams.ProjectGroupList) projectTags, _ := cmd.Flags().GetString(commonParams.ProjectTagList) @@ -617,7 +634,9 @@ func createProject( } var projModel = wrappers.Project{} projModel.Name = projectName - projModel.Groups = groupsMap + projModel.Groups = getGroupsForRequest(groupsMap) + projModel.ApplicationIds = applicationID + if projectPrivatePackage != "" { projModel.PrivatePackage, _ = strconv.ParseBool(projectPrivatePackage) } @@ -629,6 +648,7 @@ func createProject( } if err == nil { projectID = resp.ID + err = assignGroupsToProject(projectID, projectName, groupsMap, accessManagementWrapper) } return projectID, err } @@ -638,7 +658,9 @@ func updateProject( cmd *cobra.Command, projectsWrapper wrappers.ProjectsWrapper, groupsWrapper wrappers.GroupsWrapper, + accessManagementWrapper wrappers.AccessManagementWrapper, projectName string, + applicationID []string, ) (string, error) { var projectID string @@ -657,8 +679,8 @@ func updateProject( projModel.RepoURL = resp.Projects[i].RepoURL } } - if projectGroups == "" && projectTags == "" && projectPrivatePackage == "" { - logger.PrintIfVerbose("No groups or tags to update. Skipping project update.") + if projectGroups == "" && projectTags == "" && projectPrivatePackage == "" && len(applicationID) == 0 { + logger.PrintIfVerbose("No groups, applicationId or tags to update. Skipping project update.") return projectID, nil } if projectPrivatePackage != "" { @@ -674,18 +696,27 @@ func updateProject( projModel.Name = projModelResp.Name projModel.Groups = projModelResp.Groups projModel.Tags = projModelResp.Tags + projModel.ApplicationIds = projModelResp.ApplicationIds if projectGroups != "" { groupsMap, groupErr := createGroupsMap(projectGroups, groupsWrapper) if groupErr != nil { return "", errors.Errorf("%s: %v", failedUpdatingProj, groupErr) } logger.PrintIfVerbose("Updating project groups") - projModel.Groups = groupsMap + projModel.Groups = getGroupsForRequest(groupsMap) + err = assignGroupsToProject(projectID, projectName, groupsMap, accessManagementWrapper) + if err != nil { + return "", err + } } if projectTags != "" { logger.PrintIfVerbose("Updating project tags") projModel.Tags = createTagMap(projectTags) } + if len(applicationID) > 0 { + logger.PrintIfVerbose("Updating project applicationIds") + projModel.ApplicationIds = createApplicationIds(applicationID, projModelResp.ApplicationIds) + } err = projectsWrapper.Update(projectID, &projModel) if err != nil { return "", errors.Errorf("%s: %v", failedUpdatingProj, err) @@ -693,6 +724,15 @@ func updateProject( return projectID, nil } +func createApplicationIds(applicationID, existingApplicationIds []string) []string { + for _, id := range applicationID { + if !util.Contains(existingApplicationIds, id) { + existingApplicationIds = append(existingApplicationIds, id) + } + } + return existingApplicationIds +} + func setupScanTags(input *[]byte, cmd *cobra.Command) { tagListStr, _ := cmd.Flags().GetString(commonParams.TagList) tags := strings.Split(tagListStr, ",") @@ -738,6 +778,8 @@ func setupScanTypeProjectAndConfig( projectsWrapper wrappers.ProjectsWrapper, groupsWrapper wrappers.GroupsWrapper, scansWrapper wrappers.ScansWrapper, + applicationsWrapper wrappers.ApplicationsWrapper, + accessManagementWrapper wrappers.AccessManagementWrapper, ) error { var info map[string]interface{} newProjectName, _ := cmd.Flags().GetString(commonParams.ProjectName) @@ -754,15 +796,35 @@ func setupScanTypeProjectAndConfig( } else { return errors.Errorf("Project name is required") } + + applicationName, err := cmd.Flags().GetString(commonParams.ApplicationName) + if err != nil { + return err + } + + var applicationID []string + if applicationName != "" { + application, getAppErr := getApplication(applicationName, applicationsWrapper) + if getAppErr != nil { + return getAppErr + } + if application == nil { + return errors.Errorf(applicationErrors.ApplicationDoesntExistOrNoPermission) + } + applicationID = []string{application.ID} + } + // We need to convert the project name into an ID - projectID, err := findProject( + projectID, findProjectErr := findProject( + applicationID, info["project"].(map[string]interface{})["id"].(string), cmd, projectsWrapper, groupsWrapper, + accessManagementWrapper, ) - if err != nil { - return err + if findProjectErr != nil { + return findProjectErr } info["project"].(map[string]interface{})["id"] = projectID // Handle the scan configuration @@ -810,6 +872,35 @@ func setupScanTypeProjectAndConfig( return err } +func getApplication(applicationName string, applicationsWrapper wrappers.ApplicationsWrapper) (*wrappers.Application, error) { + if applicationName != "" { + params := make(map[string]string) + params["name"] = applicationName + resp, err := applicationsWrapper.Get(params) + if err != nil { + + return nil, err + } + if resp.Applications != nil && len(resp.Applications) > 0 { + application := verifyApplicationNameExactMatch(applicationName, resp) + + return application, nil + } + } + return nil, nil +} + +func verifyApplicationNameExactMatch(applicationName string, resp *wrappers.ApplicationsResponseModel) *wrappers.Application { + var application *wrappers.Application + for i := range resp.Applications { + if resp.Applications[i].Name == applicationName { + application = &resp.Applications[i] + break + } + } + return application +} + func getResubmitConfiguration(scansWrapper wrappers.ScansWrapper, projectID, userScanTypes string) ( []wrappers.Config, error, @@ -1327,7 +1418,6 @@ func UnzipFile(f string) (string, error) { defer func() { _ = archive.Close() }() - for _, f := range archive.File { filePath := filepath.Join(tempDir, f.Name) logger.PrintIfVerbose("unzipping file " + filePath + "...") @@ -1411,6 +1501,8 @@ func runCreateScanCommand( risksOverviewWrapper wrappers.RisksOverviewWrapper, jwtWrapper wrappers.JWTWrapper, policyWrapper wrappers.PolicyWrapper, + accessManagementWrapper wrappers.AccessManagementWrapper, + applicationsWrapper wrappers.ApplicationsWrapper, ) func(cmd *cobra.Command, args []string) error { return func(cmd *cobra.Command, args []string) error { err := validateScanTypes(cmd, jwtWrapper) @@ -1431,6 +1523,8 @@ func runCreateScanCommand( projectsWrapper, groupsWrapper, scansWrapper, + accessManagementWrapper, + applicationsWrapper, ) if err != nil { return errors.Errorf("%s", err) @@ -1474,7 +1568,7 @@ func runCreateScanCommand( if policyTimeout < 0 { return errors.Errorf("--%s should be equal or higher than 0", commonParams.PolicyTimeoutFlag) } - policyResponseModel, err = handlePolicyWait(waitDelay, policyTimeout, policyWrapper, scanResponseModel, cmd) + policyResponseModel, err = policymanagement.HandlePolicyWait(waitDelay, policyTimeout, policyWrapper, scanResponseModel.ID, scanResponseModel.ProjectID, cmd) if err != nil { return err } @@ -1499,7 +1593,7 @@ func runCreateScanCommand( cleanUpTempZip(zipFilePath) // verify break build from policy - if policyResponseModel != nil && len(policyResponseModel.Polices) > 0 && policyResponseModel.BreakBuild { + if policyResponseModel != nil && len(policyResponseModel.Policies) > 0 && policyResponseModel.BreakBuild { logger.PrintIfVerbose("Breaking the build due to policy violation") return errors.Errorf("Policy Violation - Break Build Enabled. To bypass the policy evaluation and continue with the build, you can use the `--ignore-policy` flag.") } @@ -1524,11 +1618,13 @@ func createScanModel( projectsWrapper wrappers.ProjectsWrapper, groupsWrapper wrappers.GroupsWrapper, scansWrapper wrappers.ScansWrapper, + accessManagementWrapper wrappers.AccessManagementWrapper, + applicationsWrapper wrappers.ApplicationsWrapper, ) (*wrappers.Scan, string, error) { var input = []byte("{}") // Define type, project and config in scan model - err := setupScanTypeProjectAndConfig(&input, cmd, projectsWrapper, groupsWrapper, scansWrapper) + err := setupScanTypeProjectAndConfig(&input, cmd, projectsWrapper, groupsWrapper, scansWrapper, applicationsWrapper, accessManagementWrapper) if err != nil { return nil, "", err } @@ -1669,29 +1765,6 @@ func handleWait( return nil } -func handlePolicyWait( - waitDelay, - timeoutMinutes int, - policyWrapper wrappers.PolicyWrapper, - scanResponseModel *wrappers.ScanResponseModel, - cmd *cobra.Command, -) (*wrappers.PolicyResponseModel, error) { - policyResponseModel, err := waitForPolicyCompletion( - waitDelay, - timeoutMinutes, - policyWrapper, - scanResponseModel, - cmd) - if err != nil { - verboseFlag, _ := cmd.Flags().GetBool(commonParams.DebugFlag) - if verboseFlag { - logger.PrintIfVerbose("Policy evaluation failed") - } - return nil, err - } - return policyResponseModel, nil -} - func createReportsAfterScan( cmd *cobra.Command, scanID string, @@ -1881,45 +1954,6 @@ func waitForScanCompletion( return nil } -func waitForPolicyCompletion( - waitDelay int, - timeoutMinutes int, - policyWrapper wrappers.PolicyWrapper, - scanResponseModel *wrappers.ScanResponseModel, - cmd *cobra.Command, -) (*wrappers.PolicyResponseModel, error) { - logger.PrintIfVerbose("Waiting for policy evaluation to complete for scanID:" + scanResponseModel.ID + " and projectID:" + scanResponseModel.ProjectID) - var policyResponseModel *wrappers.PolicyResponseModel - timeout := time.Now().Add(time.Duration(timeoutMinutes) * time.Minute) - fixedWait := time.Duration(waitDelay) * time.Second - i := uint64(0) - if !cmd.Flags().Changed(commonParams.RetryDelayFlag) { - viper.Set(commonParams.RetryDelayFlag, commonParams.RetryDelayPollingDefault) - } - for { - variableWait := time.Duration(math.Min(float64(i/uint64(waitDelay)), maxPollingWaitTime)) * time.Second - waitDuration := fixedWait + variableWait - logger.PrintfIfVerbose("Sleeping %v before polling", waitDuration) - time.Sleep(waitDuration) - evaluated := false - var err error - evaluated, policyResponseModel, err = isPolicyEvaluated(policyWrapper, scanResponseModel.ID, scanResponseModel.ProjectID) - if err != nil { - return nil, err - } - if evaluated { - break - } - if timeoutMinutes > 0 && time.Now().After(timeout) { - logger.PrintfIfVerbose("Timeout of %d minute(s) for policy evaluation reached", timeoutMinutes) - return nil, nil - } - i++ - } - logger.PrintIfVerbose("Policy evaluation completed with status" + policyResponseModel.Status) - return policyResponseModel, nil -} - func isScanRunning( scansWrapper wrappers.ScansWrapper, resultsSbomWrapper wrappers.ResultsSbomWrapper, @@ -1965,40 +1999,6 @@ func isScanRunning( return false, nil } -func isPolicyEvaluated( - policyWrapper wrappers.PolicyWrapper, - scanID, - projectID string, -) (bool, *wrappers.PolicyResponseModel, error) { - var errorModel *wrappers.WebError - var err error - var policyResponseModel *wrappers.PolicyResponseModel - var params = make(map[string]string) - - params["scanId"] = scanID - params["astProjectId"] = projectID - - policyResponseModel, errorModel, err = policyWrapper.EvaluatePolicy(params) - if err != nil { - return false, nil, err - } - if errorModel != nil { - log.Fatalf(fmt.Sprintf("%s: CODE: %d, %s", failedGetting, errorModel.Code, errorModel.Message)) - } else if policyResponseModel != nil { - if policyResponseModel.Status == evaluatingPolicy { - log.Println("Policy status: ", policyResponseModel.Status) - return false, nil, nil - } - } - // Case the policy is evaluated or None - logger.PrintIfVerbose("Policy evaluation finished with status: " + policyResponseModel.Status) - if policyResponseModel.Status == completedPolicy || policyResponseModel.Status == nonePolicy { - logger.PrintIfVerbose("Policy status: " + policyResponseModel.Status) - return true, policyResponseModel, nil - } - return true, nil, nil -} - func runListScansCommand(scansWrapper wrappers.ScansWrapper, sastMetadataWrapper wrappers.SastMetadataWrapper) func(cmd *cobra.Command, args []string) error { return func(cmd *cobra.Command, args []string) error { var allScansModel *wrappers.ScansCollectionResponseModel @@ -2017,7 +2017,11 @@ func runListScansCommand(scansWrapper wrappers.ScansWrapper, sastMetadataWrapper if errorModel != nil { return errors.Errorf(ErrorCodeFormat, failedGettingAll, errorModel.Code, errorModel.Message) } else if allScansModel != nil && allScansModel.Scans != nil { - err = printByFormat(cmd, toScanViews(allScansModel.Scans, sastMetadataWrapper)) + views, err := toScanViews(allScansModel.Scans, sastMetadataWrapper) + if err != nil { + return err + } + err = printByFormat(cmd, views) if err != nil { return err } @@ -2195,7 +2199,7 @@ type scanView struct { Engines []string } -func toScanViews(scans []wrappers.ScanResponseModel, sastMetadataWrapper wrappers.SastMetadataWrapper) []*scanView { +func toScanViews(scans []wrappers.ScanResponseModel, sastMetadataWrapper wrappers.SastMetadataWrapper) ([]*scanView, error) { scanIDs := make([]string, len(scans)) for i := range scans { scanIDs[i] = scans[i].ID @@ -2206,7 +2210,7 @@ func toScanViews(scans []wrappers.ScanResponseModel, sastMetadataWrapper wrapper sastMetadata, err := sastMetadataWrapper.GetSastMetadataByIDs(paramsToSast) if err != nil { logger.Printf("error getting sast metadata: %v", err) - return nil + return nil, err } metadataMap := make(map[string]bool) @@ -2220,7 +2224,7 @@ func toScanViews(scans []wrappers.ScanResponseModel, sastMetadataWrapper wrapper scans[i].SastIncremental = strconv.FormatBool(metadataMap[scans[i].ID]) views[i] = toScanView(&scans[i]) } - return views + return views, nil } func toScanView(scan *wrappers.ScanResponseModel) *scanView { diff --git a/internal/commands/scan_test.go b/internal/commands/scan_test.go index 5c2d7c80d..658661460 100644 --- a/internal/commands/scan_test.go +++ b/internal/commands/scan_test.go @@ -7,8 +7,10 @@ import ( "strings" "testing" + applicationErrors "github.com/checkmarx/ast-cli/internal/errors" commonParams "github.com/checkmarx/ast-cli/internal/params" "github.com/checkmarx/ast-cli/internal/wrappers" + "github.com/checkmarx/ast-cli/internal/wrappers/mock" "gotest.tools/assert" "github.com/checkmarx/ast-cli/internal/commands/util" @@ -122,6 +124,44 @@ func TestCreateScan(t *testing.T) { execCmdNilAssertion(t, "scan", "create", "--project-name", "MOCK", "-s", dummyRepo, "-b", "dummy_branch") } +func TestScanCreate_ExistingApplicationAndProject_CreateProjectUnderApplicationSuccessfully(t *testing.T) { + execCmdNilAssertion(t, "scan", "create", "--project-name", "MOCK", "--application-name", "MOCK", "-s", dummyRepo, "-b", "dummy_branch") +} + +func TestScanCreate_ApplicationNameIsNotExactMatch_FailedToCreateScan(t *testing.T) { + err := execCmdNotNilAssertion(t, "scan", "create", "--project-name", "MOCK", "--application-name", "MOC", "-s", dummyRepo, "-b", "dummy_branch") + assert.Assert(t, err.Error() == applicationErrors.ApplicationDoesntExistOrNoPermission) +} + +func TestScanCreate_ExistingProjectAndApplicationWithNoPermission_FailedToCreateScan(t *testing.T) { + err := execCmdNotNilAssertion(t, "scan", "create", "--project-name", "MOCK", "--application-name", mock.ApplicationDoesntExist, "-s", dummyRepo, "-b", "dummy_branch") + assert.Assert(t, err.Error() == applicationErrors.ApplicationDoesntExistOrNoPermission) +} + +func TestScanCreate_ExistingApplication_CreateNewProjectUnderApplicationSuccessfully(t *testing.T) { + execCmdNilAssertion(t, "scan", "create", "--project-name", "NewProject", "--application-name", "MOCK", "-s", dummyRepo, "-b", "dummy_branch") +} + +func TestScanCreate_ExistingApplicationWithNoPermission_FailedToCreateScan(t *testing.T) { + err := execCmdNotNilAssertion(t, "scan", "create", "--project-name", "NewProject", "--application-name", mock.NoPermissionApp, "-s", dummyRepo, "-b", "dummy_branch") + assert.Assert(t, err.Error() == applicationErrors.ApplicationDoesntExistOrNoPermission) +} + +func TestScanCreate_OnReceivingHttpBadRequestStatusCode_FailedToCreateScan(t *testing.T) { + err := execCmdNotNilAssertion(t, "scan", "create", "--project-name", "MOCK", "--application-name", mock.FakeHTTPStatusBadRequest, "-s", dummyRepo, "-b", "dummy_branch") + assert.Assert(t, err.Error() == applicationErrors.FailedToGetApplication) +} + +func TestScanCreate_OnReceivingHttpInternalServerErrorStatusCode_FailedToCreateScan(t *testing.T) { + err := execCmdNotNilAssertion(t, "scan", "create", "--project-name", "MOCK", "--application-name", mock.FakeHTTPStatusInternalServerError, "-s", dummyRepo, "-b", "dummy_branch") + assert.Assert(t, err.Error() == applicationErrors.FailedToGetApplication) +} + +func TestCreateScanInsideApplicationProjectExistNoPermissions(t *testing.T) { + err := execCmdNotNilAssertion(t, "scan", "create", "--project-name", "MOCK", "--application-name", mock.NoPermissionApp, "-s", dummyRepo, "-b", "dummy_branch") + assert.Assert(t, err.Error() == applicationErrors.ApplicationDoesntExistOrNoPermission) +} + func TestCreateScanSourceDirectory(t *testing.T) { baseArgs := []string{"scan", "create", "--project-name", "MOCK", "-b", "dummy_branch"} execCmdNilAssertion(t, append(baseArgs, "-s", "data", "--file-filter", "!.java")...) @@ -244,7 +284,7 @@ func TestCreateScanWithProjectGroup(t *testing.T) { t, "scan", "create", "--project-name", "invalidGroup", "-s", ".", "--project-groups", "invalidGroup", ) - assert.Assert(t, err.Error() == "Failed finding groups: [invalidGroup]") + assert.Assert(t, err.Error() == "Failed finding groups: [invalidGroup]", "\n the received error is:", err.Error()) } func TestScanWorkflowMissingID(t *testing.T) { diff --git a/internal/commands/scarealtime/sca-realtime-utils.go b/internal/commands/scarealtime/sca-realtime-utils.go index 02d119c66..dd0d8336f 100644 --- a/internal/commands/scarealtime/sca-realtime-utils.go +++ b/internal/commands/scarealtime/sca-realtime-utils.go @@ -22,6 +22,7 @@ var GetPackageManagerFromResolvingModuleType = map[string]string{ "composer": "Php", "gomodules": "Go", "pip": "Python", + "poetry": "Python", "rubygems": "Ruby", "npm": "Npm", "yarn": "Npm", @@ -34,6 +35,8 @@ var GetPackageManagerFromResolvingModuleType = map[string]string{ "swiftpm": "Ios", "carthage": "Ios", "cocoapods": "Ios", + "nuget": "Nuget", + "cpp": "Cpp", } // downloadSCAResolverAndHashFileIfNeeded Downloads SCA Realtime if it is not downloaded yet diff --git a/internal/commands/scarealtime/sca-realtime.go b/internal/commands/scarealtime/sca-realtime.go index 677d9d960..feed5b429 100644 --- a/internal/commands/scarealtime/sca-realtime.go +++ b/internal/commands/scarealtime/sca-realtime.go @@ -2,8 +2,8 @@ package scarealtime import ( "encoding/json" - "io/ioutil" "log" + "os" "strconv" "strings" @@ -129,34 +129,9 @@ func GetSCAVulnerabilities(scaRealTimeWrapper wrappers.ScaRealTimeWrapper) error var modelResults []wrappers.ScaVulnerabilitiesResponseModel var scaRealtimeScanErrors []wrappers.ScaRealtimeScanError - for _, dependencyResolutionResult := range scaResolverResults.DependencyResolutionResults { + for i, dependencyResolutionResult := range scaResolverResults.DependencyResolutionResults { // We're using a map to avoid adding repeated packages in request body - dependencyMap := make(map[string]wrappers.ScaDependencyBodyRequest) - - for i := range dependencyResolutionResult.Dependencies { - var dependency = dependencyResolutionResult.Dependencies[i] - var packageManager = GetPackageManagerFromResolvingModuleType[strings.ToLower(dependency.ResolvingModuleType)] - - // if no package manager is found uses the resolving module type - if packageManager == "" { - packageManager = strings.ToLower(dependency.ResolvingModuleType) - } - - dependencyMap[dependency.ID.NodeID] = wrappers.ScaDependencyBodyRequest{ - PackageName: dependency.ID.Name, - Version: dependency.ID.Version, - PackageManager: packageManager, - } - if len(dependency.Children) > 0 { - for _, dependencyChildren := range dependency.Children { - dependencyMap[dependencyChildren.NodeID] = wrappers.ScaDependencyBodyRequest{ - PackageName: dependencyChildren.Name, - Version: dependencyChildren.Version, - PackageManager: packageManager, - } - } - } - } + dependencyMap := createDependencyMapFromDependencyResolution(&scaResolverResults.DependencyResolutionResults[i]) // Get all ScaDependencyBodyRequest from the map to call SCA API var bodyRequest []wrappers.ScaDependencyBodyRequest @@ -211,6 +186,37 @@ func GetSCAVulnerabilities(scaRealTimeWrapper wrappers.ScaRealTimeWrapper) error return nil } +func createDependencyMapFromDependencyResolution(dependencyResolutionResult *DependencyResolution) map[string]wrappers.ScaDependencyBodyRequest { + // We're using a map to avoid adding repeated packages in request body + dependencyMap := make(map[string]wrappers.ScaDependencyBodyRequest) + + for i := range dependencyResolutionResult.Dependencies { + var dependency = dependencyResolutionResult.Dependencies[i] + var packageManager = GetPackageManagerFromResolvingModuleType[strings.ToLower(dependency.ResolvingModuleType)] + + // if no package manager is found uses the resolving module type + if packageManager == "" { + packageManager = strings.ToLower(dependency.ResolvingModuleType) + } + + dependencyMap[dependency.ID.NodeID] = wrappers.ScaDependencyBodyRequest{ + PackageName: dependency.ID.Name, + Version: dependency.ID.Version, + PackageManager: packageManager, + } + if len(dependency.Children) > 0 { + for _, dependencyChildren := range dependency.Children { + dependencyMap[dependencyChildren.NodeID] = wrappers.ScaDependencyBodyRequest{ + PackageName: dependencyChildren.Name, + Version: dependencyChildren.Version, + PackageManager: packageManager, + } + } + } + } + return dependencyMap +} + func GetScaVulnerabilitiesPackages(scaRealTimeWrapper wrappers.ScaRealTimeWrapper, bodyRequest []wrappers.ScaDependencyBodyRequest) (vulnerabilities []wrappers.ScaVulnerabilitiesResponseModel, err, err1 error) { //nolint:lll // We need to call the SCA API for each DependencyResolution so that we can save the file name vulnerabilitiesResponseModel, errorModel, errVulnerabilities := scaRealTimeWrapper.GetScaVulnerabilitiesPackages(bodyRequest) @@ -301,11 +307,10 @@ func validateProvidedProjectDirectory(cmd *cobra.Command) (string, error) { // readSCAResolverResultsFromFile Get SCA Resolver results from file to build SCA API request body func readSCAResolverResultsFromFile() (ScaResultsFile, error) { - file, err := ioutil.ReadFile(ScaResolverResultsFileNameDir) + file, err := os.ReadFile(ScaResolverResultsFileNameDir) if err != nil { return ScaResultsFile{}, err } - data := ScaResultsFile{} _ = json.Unmarshal(file, &data) diff --git a/internal/commands/scarealtime/sca-realtime_test.go b/internal/commands/scarealtime/sca-realtime_test.go index 90d8f9c98..891b5fd8b 100644 --- a/internal/commands/scarealtime/sca-realtime_test.go +++ b/internal/commands/scarealtime/sca-realtime_test.go @@ -57,3 +57,38 @@ func TestRequiredProjectDir(t *testing.T) { err := cmd.Execute() assert.Error(t, err, "Provided path does not exist: "+invalidProjectPath, err.Error()) } + +func TestCreateDependencyMapFromDependencyResolution_NugetDependencies_Success(t *testing.T) { + dependecyResolutionResult := DependencyResolution{ + Dependencies: []Dependency{ + NewDependency("8ce2d33f-5783-4fe1-b9a7-3ce2c9a3aae9", "Microsoft. NETCore. Platforms", + "1.1.0", "Nuget", []interface{}{"NetStandard20"}), + NewDependency("60b40261-18b2-4cf6-bdf5-e23ad408de3b", "NETStandard.Library", + "2.0.3", "Nuget", []interface{}{"NetStandard20"}), + }, + } + dependencyMap := createDependencyMapFromDependencyResolution(&dependecyResolutionResult) + assert.Equal(t, len(dependencyMap), 2) + assert.Equal(t, dependencyMap["60b40261-18b2-4cf6-bdf5-e23ad408de3b"].PackageManager, "Nuget") + assert.Equal(t, dependencyMap["60b40261-18b2-4cf6-bdf5-e23ad408de3b"].Version, "2.0.3") + assert.Equal(t, dependencyMap["60b40261-18b2-4cf6-bdf5-e23ad408de3b"].PackageName, "NETStandard.Library") + assert.Equal(t, dependencyMap["8ce2d33f-5783-4fe1-b9a7-3ce2c9a3aae9"].PackageManager, "Nuget") + assert.Equal(t, dependencyMap["8ce2d33f-5783-4fe1-b9a7-3ce2c9a3aae9"].Version, "1.1.0") + assert.Equal(t, dependencyMap["8ce2d33f-5783-4fe1-b9a7-3ce2c9a3aae9"].PackageName, "Microsoft. NETCore. Platforms") +} + +func NewDependency(nodeID, name, version, resolvingModuleType string, targetFrameworks []interface{}) Dependency { + return Dependency{ + ID: NewID(nodeID, name, version), + ResolvingModuleType: resolvingModuleType, + TargetFrameworks: targetFrameworks, + } +} + +func NewID(nodeID, name, version string) ID { + return ID{ + NodeID: nodeID, + Name: name, + Version: version, + } +} diff --git a/internal/commands/util/help.go b/internal/commands/util/help.go index b1ca4b203..f91412027 100644 --- a/internal/commands/util/help.go +++ b/internal/commands/util/help.go @@ -16,8 +16,10 @@ func RootHelpFunc(command *cobra.Command) { var commands []string for _, c := range command.Commands() { - s := rightPad(c.Name()+":", c.NamePadding()) + c.Short - commands = append(commands, s) + if !c.Hidden { + s := rightPad(c.Name()+":", c.NamePadding()) + c.Short + commands = append(commands, s) + } } type helpEntry struct { diff --git a/internal/commands/util/pr.go b/internal/commands/util/pr.go index aeaf1bc7b..0eacdf9b3 100644 --- a/internal/commands/util/pr.go +++ b/internal/commands/util/pr.go @@ -4,6 +4,7 @@ import ( "fmt" "github.com/MakeNowJust/heredoc" + "github.com/checkmarx/ast-cli/internal/commands/policymanagement" "github.com/checkmarx/ast-cli/internal/logger" "github.com/checkmarx/ast-cli/internal/params" "github.com/checkmarx/ast-cli/internal/wrappers" @@ -16,9 +17,12 @@ const ( failedCreatingGithubPrDecoration = "Failed creating github PR Decoration" failedCreatingGitlabPrDecoration = "Failed creating gitlab MR Decoration" errorCodeFormat = "%s: CODE: %d, %s\n" + policyErrorFormat = "%s: Failed to get scanID policy information" + waitDelayDefault = 5 + resultPolicyDefaultTimeout = 1 ) -func NewPRDecorationCommand(prWrapper wrappers.PRWrapper) *cobra.Command { +func NewPRDecorationCommand(prWrapper wrappers.PRWrapper, policyWrapper wrappers.PolicyWrapper, scansWrapper wrappers.ScansWrapper) *cobra.Command { cmd := &cobra.Command{ Use: "pr", Short: "Posts the comment with scan results on the Pull Request", @@ -29,15 +33,15 @@ func NewPRDecorationCommand(prWrapper wrappers.PRWrapper) *cobra.Command { ), } - prDecorationGithub := PRDecorationGithub(prWrapper) - prDecorationGitlab := PRDecorationGitlab(prWrapper) + prDecorationGithub := PRDecorationGithub(prWrapper, policyWrapper, scansWrapper) + prDecorationGitlab := PRDecorationGitlab(prWrapper, policyWrapper, scansWrapper) cmd.AddCommand(prDecorationGithub) cmd.AddCommand(prDecorationGitlab) return cmd } -func PRDecorationGithub(prWrapper wrappers.PRWrapper) *cobra.Command { +func PRDecorationGithub(prWrapper wrappers.PRWrapper, policyWrapper wrappers.PolicyWrapper, scansWrapper wrappers.ScansWrapper) *cobra.Command { prDecorationGithub := &cobra.Command{ Use: "github", Short: "Decorate github PR with vulnerabilities", @@ -54,7 +58,7 @@ func PRDecorationGithub(prWrapper wrappers.PRWrapper) *cobra.Command { `, ), }, - RunE: runPRDecoration(prWrapper), + RunE: runPRDecoration(prWrapper, policyWrapper, scansWrapper), } prDecorationGithub.Flags().String(params.ScanIDFlag, "", "Scan ID to retrieve results from") @@ -76,7 +80,7 @@ func PRDecorationGithub(prWrapper wrappers.PRWrapper) *cobra.Command { return prDecorationGithub } -func PRDecorationGitlab(prWrapper wrappers.PRWrapper) *cobra.Command { +func PRDecorationGitlab(prWrapper wrappers.PRWrapper, policyWrapper wrappers.PolicyWrapper, scansWrapper wrappers.ScansWrapper) *cobra.Command { prDecorationGitlab := &cobra.Command{ Use: "gitlab", Short: "Decorate gitlab PR with vulnerabilities", @@ -93,7 +97,7 @@ func PRDecorationGitlab(prWrapper wrappers.PRWrapper) *cobra.Command { `, ), }, - RunE: runPRDecorationGitlab(prWrapper), + RunE: runPRDecorationGitlab(prWrapper, policyWrapper, scansWrapper), } prDecorationGitlab.Flags().String(params.ScanIDFlag, "", "Scan ID to retrieve results from") @@ -117,7 +121,7 @@ func PRDecorationGitlab(prWrapper wrappers.PRWrapper) *cobra.Command { return prDecorationGitlab } -func runPRDecoration(prWrapper wrappers.PRWrapper) func(cmd *cobra.Command, args []string) error { +func runPRDecoration(prWrapper wrappers.PRWrapper, policyWrapper wrappers.PolicyWrapper, scansWrapper wrappers.ScansWrapper) func(cmd *cobra.Command, args []string) error { return func(cmd *cobra.Command, args []string) error { scanID, _ := cmd.Flags().GetString(params.ScanIDFlag) scmTokenFlag, _ := cmd.Flags().GetString(params.SCMTokenFlag) @@ -125,16 +129,22 @@ func runPRDecoration(prWrapper wrappers.PRWrapper) func(cmd *cobra.Command, args repoNameFlag, _ := cmd.Flags().GetString(params.RepoNameFlag) prNumberFlag, _ := cmd.Flags().GetInt(params.PRNumberFlag) + // Retrieve policies related to the scan and project to include in the PR decoration + policies, policyError := getScanViolatedPolicies(scansWrapper, policyWrapper, scanID, cmd) + if policyError != nil { + return errors.Errorf(policyErrorFormat, failedCreatingGithubPrDecoration) + } + + // Build and post the pr decoration prModel := &wrappers.PRModel{ ScanID: scanID, ScmToken: scmTokenFlag, Namespace: namespaceFlag, RepoName: repoNameFlag, PrNumber: prNumberFlag, + Policies: policies, } - prResponse, errorModel, err := prWrapper.PostPRDecoration(prModel) - if err != nil { return err } @@ -149,7 +159,7 @@ func runPRDecoration(prWrapper wrappers.PRWrapper) func(cmd *cobra.Command, args } } -func runPRDecorationGitlab(prWrapper wrappers.PRWrapper) func(cmd *cobra.Command, args []string) error { +func runPRDecorationGitlab(prWrapper wrappers.PRWrapper, policyWrapper wrappers.PolicyWrapper, scansWrapper wrappers.ScansWrapper) func(cmd *cobra.Command, args []string) error { return func(cmd *cobra.Command, args []string) error { scanID, _ := cmd.Flags().GetString(params.ScanIDFlag) scmTokenFlag, _ := cmd.Flags().GetString(params.SCMTokenFlag) @@ -158,6 +168,13 @@ func runPRDecorationGitlab(prWrapper wrappers.PRWrapper) func(cmd *cobra.Command iIDFlag, _ := cmd.Flags().GetInt(params.PRIidFlag) gitlabProjectIDFlag, _ := cmd.Flags().GetInt(params.PRGitlabProjectFlag) + // Retrieve policies related to the scan and project to include in the PR decoration + policies, policyError := getScanViolatedPolicies(scansWrapper, policyWrapper, scanID, cmd) + if policyError != nil { + return errors.Errorf(policyErrorFormat, failedCreatingGitlabPrDecoration) + } + + // Build and post the mr decoration prModel := &wrappers.GitlabPRModel{ ScanID: scanID, ScmToken: scmTokenFlag, @@ -165,6 +182,7 @@ func runPRDecorationGitlab(prWrapper wrappers.PRWrapper) func(cmd *cobra.Command RepoName: repoNameFlag, IiD: iIDFlag, GitlabProjectID: gitlabProjectIDFlag, + Policies: policies, } prResponse, errorModel, err := prWrapper.PostGitlabPRDecoration(prModel) @@ -182,3 +200,39 @@ func runPRDecorationGitlab(prWrapper wrappers.PRWrapper) func(cmd *cobra.Command return nil } } + +func getScanViolatedPolicies(scansWrapper wrappers.ScansWrapper, policyWrapper wrappers.PolicyWrapper, scanID string, cmd *cobra.Command) ([]wrappers.PrPolicy, error) { + // retrieve scan model to get the projectID + scanResponseModel, errorScanModel, err := scansWrapper.GetByID(scanID) + if err != nil { + return nil, err + } + if errorScanModel != nil { + return nil, err + } + // retrieve policy information to send to the PR service + policyResponseModel, err := policymanagement.HandlePolicyWait(waitDelayDefault, + resultPolicyDefaultTimeout, + policyWrapper, + scanID, + scanResponseModel.ProjectID, + cmd) + if err != nil { + return nil, err + } + // transform into the PR model for violated policies + violatedPolicies := policiesToPrPolicies(policyResponseModel.Policies) + return violatedPolicies, nil +} + +func policiesToPrPolicies(policies []wrappers.Policy) []wrappers.PrPolicy { + var prPolicies []wrappers.PrPolicy + for _, policy := range policies { + prPolicy := wrappers.PrPolicy{} + prPolicy.Name = policy.Name + prPolicy.BreakBuild = policy.BreakBuild + prPolicy.RulesNames = policy.RulesViolated + prPolicies = append(prPolicies, prPolicy) + } + return prPolicies +} diff --git a/internal/commands/util/pr_test.go b/internal/commands/util/pr_test.go index d731dd53c..1deca97b2 100644 --- a/internal/commands/util/pr_test.go +++ b/internal/commands/util/pr_test.go @@ -7,7 +7,7 @@ import ( ) func TestNewPRDecorationCommandMustExist(t *testing.T) { - cmd := PRDecorationGithub(nil) + cmd := PRDecorationGithub(nil, nil, nil) assert.Assert(t, cmd != nil, "PR decoration command must exist") err := cmd.Execute() @@ -15,7 +15,7 @@ func TestNewPRDecorationCommandMustExist(t *testing.T) { } func TestNewMRDecorationCommandMustExist(t *testing.T) { - cmd := PRDecorationGitlab(nil) + cmd := PRDecorationGitlab(nil, nil, nil) assert.Assert(t, cmd != nil, "MR decoration command must exist") err := cmd.Execute() diff --git a/internal/commands/util/remediation.go b/internal/commands/util/remediation.go index 24c6e780d..79081dd9f 100644 --- a/internal/commands/util/remediation.go +++ b/internal/commands/util/remediation.go @@ -3,7 +3,6 @@ package util import ( "encoding/json" "fmt" - "io/ioutil" "os" "os/exec" "path/filepath" @@ -22,6 +21,7 @@ import ( const ( npmPackageFilename = "package.json" permission = 0644 + permission0666 = 0666 containerStarting = "Starting kics container" filesContainerLocation = "/files/" filesContainerVolume = ":/files" @@ -298,7 +298,7 @@ func runKicsRemediation(cmd *cobra.Command, volumeMap, tempDir string) error { } func createKicsRemediateEnv(cmd *cobra.Command) (volume, kicsDir string, err error) { - kicsDir, err = ioutil.TempDir("", "kics") + kicsDir, err = os.MkdirTemp("", "kics") if err != nil { return "", "", errors.New(directoryError) } @@ -307,7 +307,7 @@ func createKicsRemediateEnv(cmd *cobra.Command) (volume, kicsDir string, err err if file == "" { return "", "", errors.New(" No results file was provided") } - kicsFile, err := ioutil.ReadFile(kicsResultsPath) + kicsFile, err := os.ReadFile(kicsResultsPath) if err != nil { return "", "", err } @@ -317,7 +317,7 @@ func createKicsRemediateEnv(cmd *cobra.Command) (volume, kicsDir string, err err return "", "", err } destinationFile := fmt.Sprintf("%s/%s", kicsDir, file) - err = ioutil.WriteFile(destinationFile, kicsFile, 0666) + err = os.WriteFile(destinationFile, kicsFile, permission0666) if err != nil { return "", "", errors.New(containerWriteFolderError) } diff --git a/internal/commands/util/utils.go b/internal/commands/util/utils.go index 7284f8533..52e98a8f0 100644 --- a/internal/commands/util/utils.go +++ b/internal/commands/util/utils.go @@ -28,6 +28,8 @@ func NewUtilsCommand( learnMoreWrapper wrappers.LearnMoreWrapper, tenantWrapper wrappers.TenantConfigurationWrapper, chatWrapper wrappers.ChatWrapper, + policyWrapper wrappers.PolicyWrapper, + scansWrapper wrappers.ScansWrapper, ) *cobra.Command { utilsCmd := &cobra.Command{ Use: "utils", @@ -50,7 +52,7 @@ func NewUtilsCommand( completionCmd := NewCompletionCommand() - prDecorationCmd := NewPRDecorationCommand(prWrapper) + prDecorationCmd := NewPRDecorationCommand(prWrapper, policyWrapper, scansWrapper) remediationCmd := NewRemediationCommand() diff --git a/internal/commands/util/utils_test.go b/internal/commands/util/utils_test.go index 4bbf5086a..7e5a52c1c 100644 --- a/internal/commands/util/utils_test.go +++ b/internal/commands/util/utils_test.go @@ -9,6 +9,7 @@ import ( const mockFormatErrorMessage = "Invalid format MOCK" func TestNewUtilsCommand(t *testing.T) { - cmd := NewUtilsCommand(nil, nil, nil, nil, nil, nil, nil, nil, nil) + cmd := NewUtilsCommand(nil, nil, nil, nil, + nil, nil, nil, nil, nil, nil, nil) assert.Assert(t, cmd != nil, "Utils command must exist") } diff --git a/internal/errors/application-errors.go b/internal/errors/application-errors.go new file mode 100644 index 000000000..92aebb0c0 --- /dev/null +++ b/internal/errors/application-errors.go @@ -0,0 +1,9 @@ +package applicationerrors + +const ( + ApplicationDoesntExistOrNoPermission = "Provided application does not exist or user has no permission to the application" +) + +const ( + FailedToGetApplication = "Failed to get application" +) diff --git a/internal/params/binds.go b/internal/params/binds.go index 6a790baff..f522daf70 100644 --- a/internal/params/binds.go +++ b/internal/params/binds.go @@ -15,6 +15,7 @@ var EnvVarsBinds = []struct { {CodeBashingPathKey, ScansPathEnv, "api/codebashing/lessons"}, {ScansPathKey, ScansPathEnv, "api/scans"}, {ProjectsPathKey, ProjectsPathEnv, "api/projects"}, + {ApplicationsPathKey, ApplicationsPathEnv, "api/applications"}, {GroupsPathKey, GroupsPathEnv, "auth/realms/organization/pip/groups"}, {ResultsPathKey, ResultsPathEnv, "api/results"}, {ScanSummaryPathKey, ScanSummaryPathEnv, "api/scan-summary"}, @@ -60,4 +61,5 @@ var EnvVarsBinds = []struct { {ResultsSbomReportProxyPathKey, ResultsSbomReportProxyPathEnv, "api/sca/risk-management/risk-reports"}, {FeatureFlagsKey, FeatureFlagsEnv, "api/flags"}, {PolicyEvaluationPathKey, PolicyEvaluationPathEnv, "api/policy_management_service_uri/evaluation"}, + {AccessManagementPathKey, AccessManagementPathEnv, "api/access-management"}, } diff --git a/internal/params/envs.go b/internal/params/envs.go index 2c55459fe..0d1a426d7 100644 --- a/internal/params/envs.go +++ b/internal/params/envs.go @@ -18,6 +18,7 @@ const ( GroupsPathEnv = "CX_GROUPS_PATH" AgentNameEnv = "CX_AGENT_NAME" ProjectsPathEnv = "CX_PROJECTS_PATH" + ApplicationsPathEnv = "CX_APPLICATIONS_PATH" ResultsPathEnv = "CX_RESULTS_PATH" ScanSummaryPathEnv = "CX_SCAN_SUMMARY_PATH" ScaPackagePathEnv = "CX_SCA_PACKAGE_PATH" @@ -58,5 +59,6 @@ const ( FeatureFlagsEnv = "CX_FEATURE_FLAGS_PATH" UploadURLEnv = "CX_UPLOAD_URL" PolicyEvaluationPathEnv = "CX_POLICY_EVALUATION_PATH" + AccessManagementPathEnv = "CX_ACCESS_MANAGEMENT_PATH" IgnoreProxyEnv = "CX_IGNORE_PROXY" ) diff --git a/internal/params/filters.go b/internal/params/filters.go index 2404e6305..656980b8d 100644 --- a/internal/params/filters.go +++ b/internal/params/filters.go @@ -130,6 +130,8 @@ var BaseFilters = []string{ "go.sum", "Podfile", "Podfile.lock", + "*.cmp", + "Directory.Packages.props", } var KicsBaseFilters = []string{ diff --git a/internal/params/flags.go b/internal/params/flags.go index f5831069b..08d86331e 100644 --- a/internal/params/flags.go +++ b/internal/params/flags.go @@ -4,6 +4,7 @@ package params const ( AgentFlag = "agent" AgentFlagUsage = "Scan origin name" + ApplicationName = "application-name" DefaultAgent = "ASTCLI" DebugFlag = "debug" DebugUsage = "Debug mode with detailed logs" @@ -134,6 +135,7 @@ const ( ExploitablePathFlag = "sca-exploitable-path" LastSastScanTime = "sca-last-sast-scan-time" ProjecPrivatePackageFlag = "project-private-package" + SastRedundancyFlag = "sast-redundancy" ScaPrivatePackageVersionFlag = "sca-private-package-version" @@ -222,6 +224,7 @@ const ( SastType = "sast" KicsType = "kics" APISecurityType = "api-security" + ContainersType = "containers" APIDocumentationFlag = "apisec-swagger-filter" IacType = "iac-security" IacLabel = "IaC Security" diff --git a/internal/params/keys.go b/internal/params/keys.go index 28c448a33..0ed4b1f98 100644 --- a/internal/params/keys.go +++ b/internal/params/keys.go @@ -18,6 +18,7 @@ var ( IgnoreProxyKey = strings.ToLower(IgnoreProxyEnv) CodeBashingPathKey = strings.ToLower(CodeBashingPathEnv) ProjectsPathKey = strings.ToLower(ProjectsPathEnv) + ApplicationsPathKey = strings.ToLower(ApplicationsPathEnv) ResultsPathKey = strings.ToLower(ResultsPathEnv) ScanSummaryPathKey = strings.ToLower(ScanSummaryPathEnv) RisksOverviewPathKey = strings.ToLower(RisksOverviewPathEnv) @@ -59,4 +60,5 @@ var ( ResultsSbomReportProxyPathKey = strings.ToLower(ResultsSbomReportProxyPathEnv) FeatureFlagsKey = strings.ToLower(FeatureFlagsEnv) PolicyEvaluationPathKey = strings.ToLower(PolicyEvaluationPathEnv) + AccessManagementPathKey = strings.ToLower(AccessManagementPathEnv) ) diff --git a/internal/wrappers/access-management-http.go b/internal/wrappers/access-management-http.go new file mode 100644 index 000000000..2ea265826 --- /dev/null +++ b/internal/wrappers/access-management-http.go @@ -0,0 +1,88 @@ +package wrappers + +import ( + "bytes" + "encoding/json" + "fmt" + "net/http" + + "github.com/checkmarx/ast-cli/internal/logger" + commonParams "github.com/checkmarx/ast-cli/internal/params" + "github.com/pkg/errors" + "github.com/spf13/viper" +) + +const ( + // APIs + createAssignmentPath = "" + entitiesForPath = "entities-for" + // EntityTypes + groupEntityType = "group" + projectResourceType = "project" +) + +type AccessManagementHTTPWrapper struct { + path string + clientTimeout uint +} + +func NewAccessManagementHTTPWrapper(path string) AccessManagementWrapper { + return &AccessManagementHTTPWrapper{ + path: path, + clientTimeout: viper.GetUint(commonParams.ClientTimeoutKey), + } +} +func (a *AccessManagementHTTPWrapper) CreateGroupsAssignment(projectID, projectName string, groups []*Group) error { + var resp *http.Response + for _, group := range groups { + assignment := AssignmentPayload{ + EntityID: group.ID, + EntityType: groupEntityType, + //EntityRoles: nil, // be used in the access-management phase 2 + ResourceID: projectID, + ResourceType: projectResourceType, + } + params, err := json.Marshal(assignment) + if err != nil { + return errors.Wrapf(err, "Failed to parse request body") + } + path := fmt.Sprintf("%s/%s", a.path, createAssignmentPath) + resp, err = SendHTTPRequestWithJSONContentType(http.MethodPost, path, bytes.NewBuffer(params), true, a.clientTimeout) + if err != nil { + return errors.Wrapf(err, "Failed to create groups assignment") + } + logger.PrintfIfVerbose("group '%s' assignment for project %s created", group.Name, projectName) + resp.Body.Close() + } + logger.PrintIfVerbose("Groups assignment created successfully") + return nil +} + +func (a *AccessManagementHTTPWrapper) GetGroups(projectID string) ([]*Group, error) { + path := fmt.Sprintf("%s/%s?resource-id=%s&resource-type=project", a.path, entitiesForPath, projectID) + resp, err := SendHTTPRequest(http.MethodGet, path, nil, true, a.clientTimeout) + if err != nil { + return nil, errors.Wrapf(err, "Failed to get groups") + } + defer resp.Body.Close() + if resp.StatusCode != http.StatusOK { + return nil, errors.Errorf("Failed to get groups, status code: %d", resp.StatusCode) + } + var assignments []*AssignmentResponse + var groups []*Group + decoder := json.NewDecoder(resp.Body) + err = decoder.Decode(&assignments) + if err != nil { + return nil, errors.Wrapf(err, "Failed to parse response body") + } + for _, assignment := range assignments { + if assignment.EntityType == groupEntityType { + group := &Group{ + ID: assignment.EntityID, + Name: assignment.EntityName, + } + groups = append(groups, group) + } + } + return groups, nil +} diff --git a/internal/wrappers/access-management.go b/internal/wrappers/access-management.go new file mode 100644 index 000000000..a578eaaf8 --- /dev/null +++ b/internal/wrappers/access-management.go @@ -0,0 +1,24 @@ +package wrappers + +type AssignmentResponse struct { + EntityID string `json:"entityID"` + EntityType string `json:"entityType"` + EntityName string `json:"entityName"` + EntityRoles []string `json:"entityRoles"` + ResourceID string `json:"resourceID"` + ResourceType string `json:"resourceType"` + ResourceName string `json:"resourceName"` +} + +type AccessManagementWrapper interface { + CreateGroupsAssignment(projectID, projectName string, groups []*Group) error + GetGroups(projectID string) ([]*Group, error) +} + +type AssignmentPayload struct { + EntityID string `json:"entityID"` + EntityType string `json:"entityType"` + EntityRoles []interface{} `json:"entityRoles"` + ResourceType string `json:"resourceType"` + ResourceID string `json:"resourceID"` +} diff --git a/internal/wrappers/application-http.go b/internal/wrappers/application-http.go new file mode 100644 index 000000000..c53ffdf17 --- /dev/null +++ b/internal/wrappers/application-http.go @@ -0,0 +1,60 @@ +package wrappers + +import ( + "encoding/json" + "net/http" + + applicationErrors "github.com/checkmarx/ast-cli/internal/errors" + commonParams "github.com/checkmarx/ast-cli/internal/params" + "github.com/pkg/errors" + "github.com/spf13/viper" +) + +type ApplicationsHTTPWrapper struct { + path string +} + +func NewApplicationsHTTPWrapper(path string) ApplicationsWrapper { + return &ApplicationsHTTPWrapper{ + path: path, + } +} + +func (a *ApplicationsHTTPWrapper) Get(params map[string]string) (*ApplicationsResponseModel, error) { + if _, ok := params[limit]; !ok { + params[limit] = limitValue + } + + clientTimeout := viper.GetUint(commonParams.ClientTimeoutKey) + + resp, err := SendHTTPRequestWithQueryParams(http.MethodGet, a.path, params, nil, clientTimeout) + defer func() { + if err == nil { + _ = resp.Body.Close() + } + }() + + if err != nil { + return nil, err + } + decoder := json.NewDecoder(resp.Body) + + switch resp.StatusCode { + case http.StatusBadRequest, http.StatusInternalServerError: + if err != nil { + return nil, errors.Errorf(applicationErrors.FailedToGetApplication) + } + return nil, nil + case http.StatusForbidden: + return nil, errors.Errorf(applicationErrors.ApplicationDoesntExistOrNoPermission) + case http.StatusOK: + model := ApplicationsResponseModel{} + err = decoder.Decode(&model) + if err != nil { + return nil, errors.Errorf(applicationErrors.FailedToGetApplication) + } + return &model, nil + default: + return nil, errors.Errorf("response status code %d", resp.StatusCode) + } +} diff --git a/internal/wrappers/application.go b/internal/wrappers/application.go new file mode 100644 index 000000000..6bb30d62a --- /dev/null +++ b/internal/wrappers/application.go @@ -0,0 +1,36 @@ +package wrappers + +import "time" + +type ApplicationsResponseModel struct { + TotalCount int `json:"totalCount"` + FilteredTotalCount int `json:"filteredTotalCount"` + Applications []Application `json:"applications"` +} + +type Application struct { + ID string `json:"id"` + Name string `json:"name"` + Description string `json:"description"` + Criticality int `json:"criticality"` + Rules []Rule `json:"rules"` + ProjectIds []string `json:"projectIds"` + CreatedAt time.Time `json:"createdAt"` + UpdatedAt time.Time `json:"updatedAt"` + Tags Tags `json:"tags"` +} + +type Rule struct { + ID string `json:"id"` + Type string `json:"type"` + Value string `json:"value"` +} + +type Tags struct { + Test string `json:"test"` + Priority string `json:"priority"` +} + +type ApplicationsWrapper interface { + Get(params map[string]string) (*ApplicationsResponseModel, error) +} diff --git a/internal/wrappers/azure-http.go b/internal/wrappers/azure-http.go index 735d26952..df8b98b52 100644 --- a/internal/wrappers/azure-http.go +++ b/internal/wrappers/azure-http.go @@ -112,7 +112,11 @@ func (g *AzureHTTPWrapper) get( if err != nil { return false, err } - defer resp.Body.Close() + defer func() { + if err == nil { + _ = resp.Body.Close() + } + }() logger.PrintResponse(resp, true) diff --git a/internal/wrappers/bfl-http.go b/internal/wrappers/bfl-http.go index b1c223b91..c633fc684 100644 --- a/internal/wrappers/bfl-http.go +++ b/internal/wrappers/bfl-http.go @@ -36,7 +36,11 @@ func (r *BflHTTPWrapper) GetBflByScanIDAndQueryID(params map[string]string) ( if err != nil { return nil, nil, err } - defer resp.Body.Close() + defer func() { + if err == nil { + _ = resp.Body.Close() + } + }() return handleBflResponseWithBody(resp, err) } diff --git a/internal/wrappers/bitbucket-http.go b/internal/wrappers/bitbucket-http.go index f840f76c2..8be2abf34 100644 --- a/internal/wrappers/bitbucket-http.go +++ b/internal/wrappers/bitbucket-http.go @@ -154,7 +154,11 @@ func (g *BitBucketHTTPWrapper) getFromBitBucket( if err != nil { return err } - defer resp.Body.Close() + defer func() { + if err == nil { + _ = resp.Body.Close() + } + }() switch resp.StatusCode { case http.StatusOK: err = json.NewDecoder(resp.Body).Decode(target) @@ -264,7 +268,11 @@ func getBitBucket(client *http.Client, token, url string, target interface{}, qu if err != nil { return err } - defer resp.Body.Close() + defer func() { + if err == nil { + _ = resp.Body.Close() + } + }() switch resp.StatusCode { case http.StatusOK: diff --git a/internal/wrappers/client.go b/internal/wrappers/client.go index 27841f2b8..174b8f09f 100644 --- a/internal/wrappers/client.go +++ b/internal/wrappers/client.go @@ -517,7 +517,10 @@ func getNewToken(credentialsPayload, authServerURI string) (string, error) { func getCredentialsPayload(accessKeyID, accessKeySecret string) string { logger.PrintIfVerbose("Using Client ID and secret credentials.") - return fmt.Sprintf("grant_type=client_credentials&client_id=%s&client_secret=%s", accessKeyID, accessKeySecret) + // escape possible characters such as +,%, etc... + clientID := url.QueryEscape(accessKeyID) + clientSecret := url.QueryEscape(accessKeySecret) + return fmt.Sprintf("grant_type=client_credentials&client_id=%s&client_secret=%s", clientID, clientSecret) } func getAPIKeyPayload(astToken string) string { @@ -527,9 +530,14 @@ func getAPIKeyPayload(astToken string) string { func getPasswordCredentialsPayload(username, password, adminClientID, adminClientSecret string) string { logger.PrintIfVerbose("Using username and password credentials.") + // escape possible characters such as +,%, etc... + encodedUsername := url.QueryEscape(username) + encodedAdminClientID := url.QueryEscape(adminClientID) + encodedPassword := url.QueryEscape(password) + encodedAdminClientSecret := url.QueryEscape(adminClientSecret) return fmt.Sprintf( "scope=openid&grant_type=password&username=%s&password=%s"+ - "&client_id=%s&client_secret=%s", username, password, adminClientID, adminClientSecret, + "&client_id=%s&client_secret=%s", encodedUsername, encodedPassword, encodedAdminClientID, encodedAdminClientSecret, ) } diff --git a/internal/wrappers/codebashing-http.go b/internal/wrappers/codebashing-http.go index f125e54b5..48ee8be20 100644 --- a/internal/wrappers/codebashing-http.go +++ b/internal/wrappers/codebashing-http.go @@ -16,7 +16,6 @@ import ( const ( failedToParseCodeBashing = "Failed to parse list results" failedGettingCodeBashingURL = "Authentication failed, not able to retrieve codebashing base link" - limitValue = "10000" limit = "limit" noCodebashingLinkAvailable = "No codebashing link available" licenseNotFoundExitCode = 3 @@ -44,7 +43,11 @@ func (r *CodeBashingHTTPWrapper) GetCodeBashingLinks(params map[string]string, c if err != nil { return nil, nil, err } - defer resp.Body.Close() + defer func() { + if err == nil { + _ = resp.Body.Close() + } + }() decoder := json.NewDecoder(resp.Body) switch resp.StatusCode { case http.StatusBadRequest, http.StatusInternalServerError: diff --git a/internal/wrappers/feature-flags-http.go b/internal/wrappers/feature-flags-http.go index cb037c4a5..3806e79bf 100644 --- a/internal/wrappers/feature-flags-http.go +++ b/internal/wrappers/feature-flags-http.go @@ -42,7 +42,11 @@ func (f FeatureFlagsHTTPWrapper) GetAll() (*FeatureFlagsResponseModel, error) { } decoder := json.NewDecoder(resp.Body) - defer resp.Body.Close() + defer func() { + if err == nil { + _ = resp.Body.Close() + } + }() switch resp.StatusCode { case http.StatusBadRequest, http.StatusInternalServerError: diff --git a/internal/wrappers/feature-flags.go b/internal/wrappers/feature-flags.go index b049fe5b5..5dc19dcd8 100644 --- a/internal/wrappers/feature-flags.go +++ b/internal/wrappers/feature-flags.go @@ -7,6 +7,8 @@ import ( const tenantIDClaimKey = "tenant_id" const PackageEnforcementEnabled = "PACKAGE_ENFORCEMENT_ENABLED" const CVSSV3Enabled = "CVSS_V3_ENABLED" +const MinioEnabled = "MINIO_ENABLED" +const ContainerEngineCLIEnabled = "CONTAINER_ENGINE_CLI_ENABLED" var FeatureFlagsBaseMap = []CommandFlags{ { @@ -16,6 +18,10 @@ var FeatureFlagsBaseMap = []CommandFlags{ Name: PackageEnforcementEnabled, Default: true, }, + { + Name: MinioEnabled, + Default: true, + }, }, }, { @@ -27,6 +33,9 @@ var FeatureFlagsBaseMap = []CommandFlags{ }, }, }, + { + CommandName: "cx project create", + }, } var FeatureFlags = map[string]bool{} diff --git a/internal/wrappers/github-http.go b/internal/wrappers/github-http.go index 11dc17e24..6ba2d31e0 100644 --- a/internal/wrappers/github-http.go +++ b/internal/wrappers/github-http.go @@ -163,7 +163,11 @@ func (g *GitHubHTTPWrapper) getTemplates() error { func (g *GitHubHTTPWrapper) get(url string, target interface{}) error { resp, err := get(g.client, url, target, map[string]string{}) if err != nil { - defer resp.Body.Close() + defer func() { + if err == nil { + _ = resp.Body.Close() + } + }() } return err } @@ -205,7 +209,11 @@ func collectPage( return "", err } - defer resp.Body.Close() + defer func() { + if err == nil { + _ = resp.Body.Close() + } + }() *pageCollection = append(*pageCollection, holder...) next := getNextPageLink(resp) @@ -240,7 +248,11 @@ func get(client *http.Client, url string, target interface{}, queryParams map[st if err != nil { return nil, err } - defer resp.Body.Close() + defer func() { + if err == nil { + _ = resp.Body.Close() + } + }() logger.PrintResponse(resp, true) switch resp.StatusCode { diff --git a/internal/wrappers/gitlab-http.go b/internal/wrappers/gitlab-http.go index 7ce87fc3a..6b4b53d66 100644 --- a/internal/wrappers/gitlab-http.go +++ b/internal/wrappers/gitlab-http.go @@ -141,7 +141,11 @@ func getFromGitLab( if err != nil { return nil, err } - defer resp.Body.Close() + defer func() { + if err == nil { + _ = resp.Body.Close() + } + }() logger.PrintResponse(resp, true) @@ -198,7 +202,11 @@ func collectPageForGitLab( if err != nil { return "", err } - defer resp.Body.Close() + defer func() { + if err == nil { + _ = resp.Body.Close() + } + }() *pageCollection = append(*pageCollection, holder...) nextPageURL := getNextPage(resp) diff --git a/internal/wrappers/groups-http.go b/internal/wrappers/groups-http.go index a7bdfbdc2..ed2ffa83a 100644 --- a/internal/wrappers/groups-http.go +++ b/internal/wrappers/groups-http.go @@ -37,7 +37,11 @@ func (g *GroupsHTTPWrapper) Get(groupName string) ([]Group, error) { if err != nil { return nil, err } - defer resp.Body.Close() + defer func() { + if err == nil { + _ = resp.Body.Close() + } + }() decoder := json.NewDecoder(resp.Body) switch resp.StatusCode { case http.StatusBadRequest, http.StatusInternalServerError: diff --git a/internal/wrappers/mock/access-management-mock.go b/internal/wrappers/mock/access-management-mock.go new file mode 100644 index 000000000..5e695e1ae --- /dev/null +++ b/internal/wrappers/mock/access-management-mock.go @@ -0,0 +1,19 @@ +package mock + +import ( + "fmt" + + "github.com/checkmarx/ast-cli/internal/wrappers" +) + +type AccessManagementMockWrapper struct{} + +func (a AccessManagementMockWrapper) CreateGroupsAssignment(projectID, projectName string, groups []*wrappers.Group) error { + fmt.Println("Called CreateGroupsAssignment in AccessManagementMockWrapper") + return nil +} + +func (a AccessManagementMockWrapper) GetGroups(projectID string) ([]*wrappers.Group, error) { + fmt.Println("Called GetGroups in AccessManagementMockWrapper") + return nil, nil +} diff --git a/internal/wrappers/mock/application-mock.go b/internal/wrappers/mock/application-mock.go new file mode 100644 index 000000000..dce914bc7 --- /dev/null +++ b/internal/wrappers/mock/application-mock.go @@ -0,0 +1,41 @@ +package mock + +import ( + "time" + + applicationErrors "github.com/checkmarx/ast-cli/internal/errors" + "github.com/checkmarx/ast-cli/internal/wrappers" + "github.com/pkg/errors" +) + +type ApplicationsMockWrapper struct{} + +func (a ApplicationsMockWrapper) Get(params map[string]string) (*wrappers.ApplicationsResponseModel, error) { + if params["name"] == NoPermissionApp { + return nil, errors.Errorf(applicationErrors.ApplicationDoesntExistOrNoPermission) + } + if params["name"] == ApplicationDoesntExist { + return nil, errors.Errorf(applicationErrors.ApplicationDoesntExistOrNoPermission) + } + if params["name"] == FakeHTTPStatusBadRequest { + return nil, errors.Errorf(applicationErrors.FailedToGetApplication) + } + if params["name"] == FakeHTTPStatusInternalServerError { + return nil, errors.Errorf(applicationErrors.FailedToGetApplication) + } + mockApplication := wrappers.Application{ + ID: "mockID", + Name: "MOCK", + Description: "This is a mock application", + Criticality: 2, + ProjectIds: []string{"ProjectID1", "ProjectID2"}, + CreatedAt: time.Now(), + } + + response := &wrappers.ApplicationsResponseModel{ + TotalCount: 1, + Applications: []wrappers.Application{mockApplication}, + } + + return response, nil +} diff --git a/internal/wrappers/mock/constants.go b/internal/wrappers/mock/constants.go new file mode 100644 index 000000000..96d7d5a63 --- /dev/null +++ b/internal/wrappers/mock/constants.go @@ -0,0 +1,8 @@ +package mock + +const ( + ApplicationDoesntExist = "application-doesnt-exist" + NoPermissionApp = "NoPermissionApp" + FakeHTTPStatusBadRequest = "fake-http-status-bad-request" + FakeHTTPStatusInternalServerError = "fake-http-status-internal-server-error" +) diff --git a/internal/wrappers/mock/feature-flags-mock.go b/internal/wrappers/mock/feature-flags-mock.go index 7ca3db375..4d19e83aa 100644 --- a/internal/wrappers/mock/feature-flags-mock.go +++ b/internal/wrappers/mock/feature-flags-mock.go @@ -1,11 +1,17 @@ package mock import ( + "fmt" + "github.com/checkmarx/ast-cli/internal/wrappers" ) -type FeatureFlagsMockWrapper struct{} +var Flags wrappers.FeatureFlagsResponseModel + +type FeatureFlagsMockWrapper struct { +} func (f FeatureFlagsMockWrapper) GetAll() (*wrappers.FeatureFlagsResponseModel, error) { - return &wrappers.FeatureFlagsResponseModel{}, nil + fmt.Println("Called GetAll in FeatureFlagsMockWrapper") + return &Flags, nil } diff --git a/internal/wrappers/mock/groups-mock.go b/internal/wrappers/mock/groups-mock.go index 4c3c39e45..721b820e6 100644 --- a/internal/wrappers/mock/groups-mock.go +++ b/internal/wrappers/mock/groups-mock.go @@ -6,5 +6,5 @@ type GroupsMockWrapper struct { } func (g *GroupsMockWrapper) Get(_ string) ([]wrappers.Group, error) { - return nil, nil + return []wrappers.Group{{ID: "1", Name: "group"}}, nil } diff --git a/internal/wrappers/mock/policy-mock.go b/internal/wrappers/mock/policy-mock.go index 5316680ec..c39a8e688 100644 --- a/internal/wrappers/mock/policy-mock.go +++ b/internal/wrappers/mock/policy-mock.go @@ -23,7 +23,7 @@ func (r *PolicyMockWrapper) EvaluatePolicy(params map[string]string) ( var policies []wrappers.Policy policies = append(policies, policy) - policyResponseModel.Polices = policies + policyResponseModel.Policies = policies return &policyResponseModel, nil, nil } diff --git a/internal/wrappers/mock/projects-mock.go b/internal/wrappers/mock/projects-mock.go index a0391a8fc..78be2a59a 100644 --- a/internal/wrappers/mock/projects-mock.go +++ b/internal/wrappers/mock/projects-mock.go @@ -14,7 +14,8 @@ func (p *ProjectsMockWrapper) Create(model *wrappers.Project) ( error) { fmt.Println("Called Create in ProjectsMockWrapper") return &wrappers.ProjectResponseModel{ - Name: model.Name, + Name: model.Name, + ApplicationIds: model.ApplicationIds, }, nil, nil } func (p *ProjectsMockWrapper) Update(projectID string, model *wrappers.Project) error { diff --git a/internal/wrappers/mock/results-mock.go b/internal/wrappers/mock/results-mock.go index d6dbdaa77..0761f3cb2 100644 --- a/internal/wrappers/mock/results-mock.go +++ b/internal/wrappers/mock/results-mock.go @@ -51,22 +51,123 @@ func (r ResultsMockWrapper) GetAllResultsByScanID(_ map[string]string) ( var dependencyPath = wrappers.DependencyPath{ID: mock, Name: mock, Version: mock, IsResolved: true, IsDevelopment: false, Locations: nil} var dependencyArray = [][]wrappers.DependencyPath{{dependencyPath}} return &wrappers.ScanResultsCollection{ - TotalCount: 3, + TotalCount: 7, Results: []*wrappers.ScanResult{ { Type: "sast", + ID: "1", Severity: "high", ScanResultData: wrappers.ScanResultData{ + LanguageName: "JavaScript", + QueryName: "mock-query-name-1", Nodes: []*wrappers.ScanResultNode{ { - FileName: "dummy-file-name", + FileName: "dummy-file-name-1", Line: 10, Column: 10, Length: 20, }, { - FileName: "dummy-file-name", - Line: 0, + FileName: "dummy-file-name-1", + Line: 11, + Column: 3, + Length: 10, + }, + }, + }, + }, + { + Type: "sast", + ID: "2", + Severity: "high", + ScanResultData: wrappers.ScanResultData{ + LanguageName: "Java", + QueryName: "mock-query-name-2", + Nodes: []*wrappers.ScanResultNode{ + { + FileName: "dummy-file-name-2", + Line: 10, + Column: 10, + Length: 20, + }, + { + FileName: "dummy-file-name-2", + Line: 11, + Column: 3, + Length: 10, + }, + }, + }, + }, + { + Type: "sast", + Severity: "high", + ID: "3", + ScanResultData: wrappers.ScanResultData{ + LanguageName: "Java", + QueryName: "mock-query-name-2", + Nodes: []*wrappers.ScanResultNode{ + { + FileName: "dummy-file-name-2", + Line: 10, + Column: 10, + Length: 20, + }, + { + FileName: "dummy-file-name-2", + Line: 11, + Column: 3, + Length: 10, + }, + { + FileName: "dummy-file-name-2", + Line: 12, + Column: 3, + Length: 10, + }, + }, + }, + }, + { + Type: "sast", + ID: "4", + Severity: "high", + ScanResultData: wrappers.ScanResultData{ + LanguageName: "Java", + QueryName: "mock-query-name-3", + Nodes: []*wrappers.ScanResultNode{ + { + FileName: "dummy-file-name-3", + Line: 10, + Column: 10, + Length: 20, + }, + { + FileName: "dummy-file-name-3", + Line: 11, + Column: 3, + Length: 10, + }, + }, + }, + }, + { + Type: "sast", + ID: "5", + Severity: "high", + ScanResultData: wrappers.ScanResultData{ + LanguageName: "Java", + QueryName: "mock-query-name-3", + Nodes: []*wrappers.ScanResultNode{ + { + FileName: "dummy-file-name-4", + Line: 10, + Column: 10, + Length: 20, + }, + { + FileName: "dummy-file-name-4", + Line: 11, Column: 3, Length: 10, }, diff --git a/internal/wrappers/mock/tenant-mock.go b/internal/wrappers/mock/tenant-mock.go index f0fe441a3..3ef26e66a 100644 --- a/internal/wrappers/mock/tenant-mock.go +++ b/internal/wrappers/mock/tenant-mock.go @@ -2,6 +2,8 @@ package mock import "github.com/checkmarx/ast-cli/internal/wrappers" +var TenantConfiguration []*wrappers.TenantConfigurationResponse + type TenantConfigurationMockWrapper struct { } @@ -10,10 +12,17 @@ func (t TenantConfigurationMockWrapper) GetTenantConfiguration() ( *wrappers.WebError, error, ) { - return &[]*wrappers.TenantConfigurationResponse{ - { - Key: "scan.config.plugins.ideScans", - Value: "true", - }, - }, nil, nil + if len(TenantConfiguration) == 0 { + TenantConfiguration = []*wrappers.TenantConfigurationResponse{ + { + Key: "scan.config.plugins.ideScans", + Value: "true", + }, + { + Key: "scan.config.plugins.aiGuidedRemediation", + Value: "true", + }, + } + } + return &TenantConfiguration, nil, nil } diff --git a/internal/wrappers/policy.go b/internal/wrappers/policy.go index 9ce3934df..53470615e 100644 --- a/internal/wrappers/policy.go +++ b/internal/wrappers/policy.go @@ -3,7 +3,7 @@ package wrappers type PolicyResponseModel struct { Status string `json:"status"` BreakBuild bool `json:"breakBuild"` - Polices []Policy `json:"policies"` + Policies []Policy `json:"policies"` } type Policy struct { @@ -15,6 +15,12 @@ type Policy struct { Tags []string `json:"tags"` } +type PrPolicy struct { + Name string `json:"policyName"` + RulesNames []string `json:"rulesNames"` + BreakBuild bool `json:"breakBuild"` +} + type PolicyWrapper interface { EvaluatePolicy(map[string]string) (*PolicyResponseModel, *WebError, error) } diff --git a/internal/wrappers/pr-http.go b/internal/wrappers/pr-http.go index fa70f1bd4..8756d74ac 100644 --- a/internal/wrappers/pr-http.go +++ b/internal/wrappers/pr-http.go @@ -41,7 +41,11 @@ func (r *PRHTTPWrapper) PostPRDecoration(model *PRModel) ( if err != nil { return "", nil, err } - defer resp.Body.Close() + defer func() { + if err == nil { + _ = resp.Body.Close() + } + }() return handlePRResponseWithBody(resp, err) } @@ -59,7 +63,11 @@ func (r *PRHTTPWrapper) PostGitlabPRDecoration(model *GitlabPRModel) ( if err != nil { return "", nil, err } - defer resp.Body.Close() + defer func() { + if err == nil { + _ = resp.Body.Close() + } + }() return handlePRResponseWithBody(resp, err) } diff --git a/internal/wrappers/pr.go b/internal/wrappers/pr.go index 7158fd4da..1a6fb0cbf 100644 --- a/internal/wrappers/pr.go +++ b/internal/wrappers/pr.go @@ -5,20 +5,22 @@ type PRResponseModel struct { } type PRModel struct { - ScanID string `json:"scanId"` - ScmToken string `json:"scmToken"` - Namespace string `json:"namespace"` - RepoName string `json:"repoName"` - PrNumber int `json:"prNumber"` + ScanID string `json:"scanId"` + ScmToken string `json:"scmToken"` + Namespace string `json:"namespace"` + RepoName string `json:"repoName"` + PrNumber int `json:"prNumber"` + Policies []PrPolicy `json:"violatedPolicyList"` } type GitlabPRModel struct { - ScanID string `json:"scanId"` - ScmToken string `json:"scmToken"` - Namespace string `json:"namespace"` - RepoName string `json:"repoName"` - IiD int `json:"iid"` - GitlabProjectID int `json:"gitlabProjectID"` + ScanID string `json:"scanId"` + ScmToken string `json:"scmToken"` + Namespace string `json:"namespace"` + RepoName string `json:"repoName"` + IiD int `json:"iid"` + GitlabProjectID int `json:"gitlabProjectID"` + Policies []PrPolicy `json:"violatedPolicyList"` } type PRWrapper interface { diff --git a/internal/wrappers/predicates-http.go b/internal/wrappers/predicates-http.go index 0b1dc4edd..db1130d68 100644 --- a/internal/wrappers/predicates-http.go +++ b/internal/wrappers/predicates-http.go @@ -51,7 +51,11 @@ func (r *ResultsPredicatesHTTPWrapper) GetAllPredicatesForSimilarityID(similarit if err != nil { return nil, nil, err } - defer resp.Body.Close() + defer func() { + if err == nil { + _ = resp.Body.Close() + } + }() return handleResponseWithBody(resp, err) } diff --git a/internal/wrappers/projects-http.go b/internal/wrappers/projects-http.go index d369bf55c..fe4814a32 100644 --- a/internal/wrappers/projects-http.go +++ b/internal/wrappers/projects-http.go @@ -33,7 +33,11 @@ func (p *ProjectsHTTPWrapper) Create(model *Project) (*ProjectResponseModel, *Er if err != nil { return nil, nil, err } - defer resp.Body.Close() + defer func() { + if err == nil { + _ = resp.Body.Close() + } + }() return handleProjectResponseWithBody(resp, err, http.StatusCreated) } @@ -48,10 +52,16 @@ func (p *ProjectsHTTPWrapper) Update(projectID string, model *Project) error { if err != nil { return err } - defer resp.Body.Close() + defer func() { + if err == nil { + _ = resp.Body.Close() + } + }() switch resp.StatusCode { case http.StatusNoContent: return nil + case http.StatusForbidden: + return errors.Errorf("Failed to update project %s, status - %d, %s:", projectID, resp.StatusCode, "No permission") default: return errors.Errorf("failed to update project %s, status - %d", projectID, resp.StatusCode) } @@ -72,7 +82,11 @@ func (p *ProjectsHTTPWrapper) UpdateConfiguration(projectID string, configuratio if err != nil { return nil, err } - defer resp.Body.Close() + defer func() { + if err == nil { + _ = resp.Body.Close() + } + }() return handleProjectResponseWithNoBody(resp, err, http.StatusNoContent) } @@ -91,7 +105,11 @@ func (p *ProjectsHTTPWrapper) Get(params map[string]string) ( } decoder := json.NewDecoder(resp.Body) - defer resp.Body.Close() + defer func() { + if err == nil { + _ = resp.Body.Close() + } + }() switch resp.StatusCode { case http.StatusBadRequest, http.StatusInternalServerError: errorModel := ErrorModel{} @@ -122,7 +140,11 @@ func (p *ProjectsHTTPWrapper) GetByID(projectID string) ( if err != nil { return nil, nil, err } - defer resp.Body.Close() + defer func() { + if err == nil { + _ = resp.Body.Close() + } + }() return handleProjectResponseWithBody(resp, err, http.StatusOK) } @@ -138,7 +160,11 @@ func (p *ProjectsHTTPWrapper) GetBranchesByID(projectID string, params map[strin } decoder := json.NewDecoder(resp.Body) - defer resp.Body.Close() + defer func() { + if err == nil { + _ = resp.Body.Close() + } + }() switch resp.StatusCode { case http.StatusBadRequest, http.StatusInternalServerError: @@ -167,7 +193,11 @@ func (p *ProjectsHTTPWrapper) Delete(projectID string) (*ErrorModel, error) { if err != nil { return nil, err } - defer resp.Body.Close() + defer func() { + if err == nil { + _ = resp.Body.Close() + } + }() return handleProjectResponseWithNoBody(resp, err, http.StatusNoContent) } @@ -180,7 +210,11 @@ func (p *ProjectsHTTPWrapper) Tags() ( if err != nil { return nil, nil, err } - defer resp.Body.Close() + defer func() { + if err == nil { + _ = resp.Body.Close() + } + }() decoder := json.NewDecoder(resp.Body) diff --git a/internal/wrappers/projects.go b/internal/wrappers/projects.go index 72a8ee1e9..a1337a9ae 100644 --- a/internal/wrappers/projects.go +++ b/internal/wrappers/projects.go @@ -13,6 +13,7 @@ type Project struct { Tags map[string]string `json:"tags,omitempty"` Groups []string `json:"groups,omitempty"` PrivatePackage bool `json:"privatePackage,omitempty"` + ApplicationIds []string `json:"applicationIds,omitempty"` } type ProjectsCollectionResponseModel struct { @@ -22,16 +23,17 @@ type ProjectsCollectionResponseModel struct { } type ProjectResponseModel struct { - ID string `json:"id"` - Name string `json:"name"` - CreatedAt time.Time `json:"createdAt"` - UpdatedAt time.Time `json:"updatedAt"` - Groups []string `json:"groups"` - Tags map[string]string `json:"tags"` - RepoURL string `json:"repoUrl"` - MainBranch string `json:"mainBranch"` - Origin string `json:"origin,omitempty"` - ScmRepoID string `json:"scmRepoId,omitempty"` + ID string `json:"id"` + Name string `json:"name"` + CreatedAt time.Time `json:"createdAt"` + UpdatedAt time.Time `json:"updatedAt"` + Groups []string `json:"groups"` + Tags map[string]string `json:"tags"` + RepoURL string `json:"repoUrl"` + MainBranch string `json:"mainBranch"` + Origin string `json:"origin,omitempty"` + ScmRepoID string `json:"scmRepoId,omitempty"` + ApplicationIds []string `json:"applicationIds"` } type ProjectConfiguration struct { diff --git a/internal/wrappers/response.go b/internal/wrappers/response.go index 30b347a6c..f0212be8d 100644 --- a/internal/wrappers/response.go +++ b/internal/wrappers/response.go @@ -94,7 +94,11 @@ func handleProjectResponseWithBody(resp *http.Response, err error, } decoder := json.NewDecoder(resp.Body) - defer resp.Body.Close() + defer func() { + if err == nil { + _ = resp.Body.Close() + } + }() switch resp.StatusCode { case http.StatusBadRequest, http.StatusInternalServerError: @@ -106,6 +110,8 @@ func handleProjectResponseWithBody(resp *http.Response, err error, return nil, &errorModel, nil case http.StatusNotFound: return nil, nil, errors.Errorf("project not found") + case http.StatusForbidden: + return nil, nil, errors.Errorf("forbidden action") case successStatusCode: model := ProjectResponseModel{} err = decoder.Decode(&model) diff --git a/internal/wrappers/results-http.go b/internal/wrappers/results-http.go index 8c618f888..116beddb7 100644 --- a/internal/wrappers/results-http.go +++ b/internal/wrappers/results-http.go @@ -191,7 +191,11 @@ func (r *ResultsHTTPWrapper) GetAllResultsTypeByScanID(params map[string]string) return nil, nil, err } - defer resp.Body.Close() + defer func() { + if err == nil { + _ = resp.Body.Close() + } + }() decoder := json.NewDecoder(resp.Body) @@ -244,7 +248,11 @@ func (r *ResultsHTTPWrapper) GetScanSummariesByScanIDS(params map[string]string) return nil, nil, err } - defer resp.Body.Close() + defer func() { + if err == nil { + _ = resp.Body.Close() + } + }() decoder := json.NewDecoder(resp.Body) diff --git a/internal/wrappers/results-json.go b/internal/wrappers/results-json.go index ca38fb8c3..2697cd857 100644 --- a/internal/wrappers/results-json.go +++ b/internal/wrappers/results-json.go @@ -93,6 +93,7 @@ type ScanResultData struct { Group string `json:"group,omitempty"` ResultHash string `json:"resultHash,omitempty"` LanguageName string `json:"languageName,omitempty"` + Redundancy string `json:"redundancy,omitempty"` Description string `json:"description,omitempty"` Nodes []*ScanResultNode `json:"nodes,omitempty"` PackageData []*ScanResultPackageData `json:"packageData,omitempty"` diff --git a/internal/wrappers/results-summary.go b/internal/wrappers/results-summary.go index f93e70df7..90b4b92a0 100644 --- a/internal/wrappers/results-summary.go +++ b/internal/wrappers/results-summary.go @@ -33,6 +33,7 @@ type ResultSummary struct { ScanInfoMessage string EnginesEnabled []string Policies *PolicyResponseModel + EnginesResult EnginesResultsSummary } // nolint: govet @@ -41,11 +42,81 @@ type APISecResult struct { TotalRisksCount int `json:"total_risks_count,omitempty"` Risks []int `json:"risks,omitempty"` RiskDistribution []riskDistribution `json:"risk_distribution,omitempty"` + StatusCode int } type riskDistribution struct { Origin string `json:"origin,omitempty"` Total int `json:"total,omitempty"` } +type EngineResultSummary struct { + Critical int + High int + Medium int + Low int + Info int + StatusCode int +} + +type EnginesResultsSummary map[string]*EngineResultSummary + +func (engineSummary *EnginesResultsSummary) GetCriticalIssues() int { + criticalIssues := 0 + for _, v := range *engineSummary { + criticalIssues += v.Critical + } + return criticalIssues +} + +func (engineSummary *EnginesResultsSummary) GetHighIssues() int { + highIssues := 0 + for _, v := range *engineSummary { + highIssues += v.High + } + return highIssues +} + +func (engineSummary *EnginesResultsSummary) GetLowIssues() int { + lowIssues := 0 + for _, v := range *engineSummary { + lowIssues += v.Low + } + return lowIssues +} + +func (engineSummary *EnginesResultsSummary) GetMediumIssues() int { + mediumIssues := 0 + for _, v := range *engineSummary { + mediumIssues += v.Medium + } + return mediumIssues +} + +func (engineSummary *EnginesResultsSummary) GetInfoIssues() int { + infoIssues := 0 + for _, v := range *engineSummary { + infoIssues += v.Info + } + return infoIssues +} + +func (engineSummary *EngineResultSummary) Increment(level string) { + switch level { + case "critical": + engineSummary.Critical++ + case "high": + engineSummary.High++ + case "medium": + engineSummary.Medium++ + case "low": + engineSummary.Low++ + case "info": + engineSummary.Info++ + } +} + +func (summary *ResultSummary) UpdateEngineResultSummary(engineType, severity string) { + summary.EnginesResult[engineType].Increment(severity) +} func (r *ResultSummary) HasEngine(engine string) bool { for _, v := range r.EnginesEnabled { @@ -81,11 +152,11 @@ func (r *ResultSummary) GetAPISecurityDocumentationTotal() int { if riskAPIDocumentation != nil { return riskAPIDocumentation.Total } - return -1 + return 0 } func (r *ResultSummary) HasPolicies() bool { - return r.Policies != nil && len(r.Policies.Polices) > 0 + return r.Policies != nil && len(r.Policies.Policies) > 0 } func (r *ResultSummary) GeneratePolicyHTML() string { @@ -112,20 +183,20 @@ func (r *ResultSummary) GeneratePolicyHTML() string { ` - for _, police := range r.Policies.Polices { + for _, policy := range r.Policies.Policies { html += ` ` + - police.Name + + policy.Name + ` ` + ` - ` + strings.Join(police.RulesViolated, ",") + + ` + strings.Join(policy.RulesViolated, ",") + ` ` + `` + - strconv.FormatBool(police.BreakBuild) + + strconv.FormatBool(policy.BreakBuild) + ` @@ -144,8 +215,8 @@ func (r *ResultSummary) GeneratePolicyMarkdown() string { markdown += "### Policy Management Violation\n" } markdown += "| Policy | Rule | Break Build |\n|:----------:|:------------:|:---------:|\n" - for _, police := range r.Policies.Polices { - markdown += "|" + police.Name + "|" + strings.Join(police.RulesViolated, ",") + "|" + strconv.FormatBool(police.BreakBuild) + "|\n" + for _, policy := range r.Policies.Policies { + markdown += "|" + policy.Name + "|" + strings.Join(policy.RulesViolated, ",") + "|" + strconv.FormatBool(policy.BreakBuild) + "|\n" } return markdown } diff --git a/internal/wrappers/risks-overview-http.go b/internal/wrappers/risks-overview-http.go index f2e2dfb19..f9cded7b8 100644 --- a/internal/wrappers/risks-overview-http.go +++ b/internal/wrappers/risks-overview-http.go @@ -32,7 +32,11 @@ func (r *RisksOverviewHTTPWrapper) GetAllAPISecRisksByScanID(scanID string) ( return nil, nil, err } - defer resp.Body.Close() + defer func() { + if err == nil { + _ = resp.Body.Close() + } + }() decoder := json.NewDecoder(resp.Body) switch resp.StatusCode { diff --git a/internal/wrappers/sast-metadata-http.go b/internal/wrappers/sast-metadata-http.go index f36d8861a..0d437f690 100644 --- a/internal/wrappers/sast-metadata-http.go +++ b/internal/wrappers/sast-metadata-http.go @@ -30,7 +30,11 @@ func (s *SastIncrementalHTTPWrapper) GetSastMetadataByIDs(params map[string]stri } decoder := json.NewDecoder(resp.Body) - defer resp.Body.Close() + defer func() { + if err == nil { + _ = resp.Body.Close() + } + }() switch resp.StatusCode { case http.StatusBadRequest, http.StatusInternalServerError: diff --git a/internal/wrappers/scans-http.go b/internal/wrappers/scans-http.go index 5fe00946a..fc65bedb1 100644 --- a/internal/wrappers/scans-http.go +++ b/internal/wrappers/scans-http.go @@ -39,7 +39,11 @@ func (s *ScansHTTPWrapper) Create(model *Scan) (*ScanResponseModel, *ErrorModel, if err != nil { return nil, nil, err } - defer resp.Body.Close() + defer func() { + if err == nil { + _ = resp.Body.Close() + } + }() return handleScanResponseWithBody(resp, err, http.StatusCreated) } @@ -51,7 +55,11 @@ func (s *ScansHTTPWrapper) Get(params map[string]string) (*ScansCollectionRespon } decoder := json.NewDecoder(resp.Body) - defer resp.Body.Close() + defer func() { + if err == nil { + _ = resp.Body.Close() + } + }() switch resp.StatusCode { case http.StatusBadRequest, http.StatusInternalServerError: @@ -81,7 +89,11 @@ func (s *ScansHTTPWrapper) GetByID(scanID string) (*ScanResponseModel, *ErrorMod if err != nil { return nil, nil, err } - defer resp.Body.Close() + defer func() { + if err == nil { + _ = resp.Body.Close() + } + }() return handleScanResponseWithBody(resp, err, http.StatusOK) } @@ -92,7 +104,11 @@ func (s *ScansHTTPWrapper) GetWorkflowByID(scanID string) ([]*ScanTaskResponseMo if err != nil { return nil, nil, err } - defer resp.Body.Close() + defer func() { + if err == nil { + _ = resp.Body.Close() + } + }() return handleWorkflowResponseWithBody(resp, err) } @@ -129,7 +145,11 @@ func (s *ScansHTTPWrapper) Delete(scanID string) (*ErrorModel, error) { if err != nil { return nil, err } - defer resp.Body.Close() + defer func() { + if err == nil { + _ = resp.Body.Close() + } + }() return handleScanResponseWithNoBody(resp, err, http.StatusNoContent) } @@ -146,7 +166,11 @@ func (s *ScansHTTPWrapper) Cancel(scanID string) (*ErrorModel, error) { if err != nil { return nil, err } - defer resp.Body.Close() + defer func() { + if err == nil { + _ = resp.Body.Close() + } + }() return handleScanResponseWithNoBody(resp, err, http.StatusNoContent) } @@ -156,7 +180,11 @@ func (s *ScansHTTPWrapper) Tags() (map[string][]string, *ErrorModel, error) { if err != nil { return nil, nil, err } - defer resp.Body.Close() + defer func() { + if err == nil { + _ = resp.Body.Close() + } + }() decoder := json.NewDecoder(resp.Body) switch resp.StatusCode { diff --git a/internal/wrappers/uploads-http.go b/internal/wrappers/uploads-http.go index 3191ead03..10b959bbb 100644 --- a/internal/wrappers/uploads-http.go +++ b/internal/wrappers/uploads-http.go @@ -51,7 +51,8 @@ func (u *UploadsHTTPWrapper) UploadFile(sourcesFile string) (*string, error) { if err != nil { return nil, errors.Errorf("Failed to stat file %s: %s", sourcesFile, err.Error()) } - resp, err := SendHTTPRequestByFullURLContentLength(http.MethodPut, *preSignedURL, file, stat.Size(), true, NoTimeout, accessToken, true) + useAccessToken := FeatureFlags[MinioEnabled] + resp, err := SendHTTPRequestByFullURLContentLength(http.MethodPut, *preSignedURL, file, stat.Size(), useAccessToken, NoTimeout, accessToken, true) if err != nil { return nil, errors.Errorf("Invoking HTTP request to upload file failed - %s", err.Error()) } diff --git a/internal/wrappers/wrapper-constants.go b/internal/wrappers/wrapper-constants.go new file mode 100644 index 000000000..242bebd6c --- /dev/null +++ b/internal/wrappers/wrapper-constants.go @@ -0,0 +1,5 @@ +package wrappers + +const ( + limitValue = "10000" +) diff --git a/test/integration/data/positive1.tf b/test/integration/data/positive1.tf index 532c41524..1a0fcc2a6 100644 --- a/test/integration/data/positive1.tf +++ b/test/integration/data/positive1.tf @@ -17,4 +17,5 @@ resource "aws_lb" "test3" { load_balancer_type = "application" subnets = [aws_subnet.subnet1.id, aws_subnet.subnet2.id] internal = true + drop_invalid_header_fields = true } diff --git a/test/integration/data/sources.zip b/test/integration/data/sources.zip index 9327d23e4..b8335f64e 100644 Binary files a/test/integration/data/sources.zip and b/test/integration/data/sources.zip differ diff --git a/test/integration/pr_test.go b/test/integration/pr_test.go index c742d4a84..155b797ef 100644 --- a/test/integration/pr_test.go +++ b/test/integration/pr_test.go @@ -23,8 +23,7 @@ const ( ) func TestPRGithubDecorationSuccessCase(t *testing.T) { - scanID, _ := getRootScan(t) - + scanID, _ := getRootScan(t, params.SastType) args := []string{ "utils", "pr", @@ -65,7 +64,7 @@ func TestPRGithubDecorationFailure(t *testing.T) { } func TestPRGitlabDecorationSuccessCase(t *testing.T) { - scanID, _ := getRootScan(t) + scanID, _ := getRootScan(t, params.SastType) args := []string{ "utils", diff --git a/test/integration/predicate_test.go b/test/integration/predicate_test.go index bd2b72709..1038b3303 100644 --- a/test/integration/predicate_test.go +++ b/test/integration/predicate_test.go @@ -98,8 +98,6 @@ func TestSastUpdateAndGetPredicatesForSimilarityId(t *testing.T) { assert.Assert(t, (len(predicateResult)) >= 1, "Should have at least 1 predicate as the result.") - deleteScanAndProject() - } func TestGetAndUpdatePredicateWithInvalidScannerType(t *testing.T) { diff --git a/test/integration/project_test.go b/test/integration/project_test.go index bae5c8a65..c78c75995 100644 --- a/test/integration/project_test.go +++ b/test/integration/project_test.go @@ -14,6 +14,7 @@ import ( "github.com/google/uuid" "github.com/checkmarx/ast-cli/internal/commands/util/printer" + applicationErrors "github.com/checkmarx/ast-cli/internal/errors" "github.com/checkmarx/ast-cli/internal/params" "github.com/checkmarx/ast-cli/internal/wrappers" "github.com/spf13/viper" @@ -33,7 +34,7 @@ const SSHKeyFilePath = "ssh-key-file.txt" // - Delete the created project // - Get and assert the project was deleted func TestProjectsE2E(t *testing.T) { - projectID, _ := createProject(t, Tags) + projectID, _ := createProject(t, Tags, Groups) response := listProjectByID(t, projectID) @@ -43,7 +44,7 @@ func TestProjectsE2E(t *testing.T) { project := showProject(t, projectID) assert.Equal(t, project.ID, projectID, "Project ID should match the created project") - assertTags(t, project) + assertTagsAndGroups(t, project, Groups) deleteProject(t, projectID) @@ -53,7 +54,7 @@ func TestProjectsE2E(t *testing.T) { } // Assert project contains created tags and groups -func assertTags(t *testing.T, project wrappers.ProjectResponseModel) { +func assertTagsAndGroups(t *testing.T, project wrappers.ProjectResponseModel, groups []string) { allTags := getAllTags(t, "project") @@ -65,6 +66,8 @@ func assertTags(t *testing.T, project wrappers.ProjectResponseModel) { assert.Assert(t, ok, "Project should contain all created tags. Missing %s", key) assert.Equal(t, val, Tags[key], "Tag value should be equal") } + + assert.Assert(t, len(project.Groups) >= len(groups), "The project must contain at least %d groups", len(groups)) } // Create a project with empty project name should fail @@ -90,6 +93,32 @@ func TestCreateAlreadyExistingProject(t *testing.T) { assertError(t, err, "Failed creating a project: CODE: 208, Failed to create a project, project name") } +func TestProjectCreate_ApplicationDoesntExist_FailAndReturnErrorMessage(t *testing.T) { + + err, _ := executeCommand( + t, "project", "create", flag(params.FormatFlag), + printer.FormatJSON, flag(params.ProjectName), projectNameRandom, + flag(params.ApplicationName), "application-that-doesnt-exist", + ) + + assertError(t, err, applicationErrors.ApplicationDoesntExistOrNoPermission) +} + +func TestProjectCreate_ApplicationExists_CreateProjectSuccessfully(t *testing.T) { + + err, outBuffer := executeCommand( + t, "project", "create", flag(params.FormatFlag), + printer.FormatJSON, flag(params.ProjectName), projectNameRandom, + flag(params.ApplicationName), "my-application", + ) + createdProject := wrappers.ProjectResponseModel{} + unmarshall(t, outBuffer, &createdProject, "Reading project create response JSON should pass") + defer deleteProject(t, createdProject.ID) + assert.NilError(t, err) + assert.Assert(t, createdProject.ID != "", "Project ID should not be empty") + assert.Assert(t, len(createdProject.ApplicationIds) == 1, "The project must be connected to the application") +} + func TestCreateWithInvalidGroup(t *testing.T) { err, _ := executeCommand( t, "project", "create", flag(params.FormatFlag), @@ -117,9 +146,10 @@ func TestProjectBranches(t *testing.T) { assert.Assert(t, strings.Contains(string(result), "[]")) } -func createProject(t *testing.T, tags map[string]string) (string, string) { +func createProject(t *testing.T, tags map[string]string, groups []string) (string, string) { projectName := getProjectNameForTest() + "_for_project" tagsStr := formatTags(tags) + groupsStr := formatGroups(groups) fmt.Printf("Creating project : %s \n", projectName) outBuffer := executeCmdNilAssertion( @@ -129,6 +159,7 @@ func createProject(t *testing.T, tags map[string]string) (string, string) { flag(params.ProjectName), projectName, flag(params.BranchFlag), "master", flag(params.TagList), tagsStr, + flag(params.GroupList), groupsStr, ) createdProject := wrappers.ProjectResponseModel{} diff --git a/test/integration/result_test.go b/test/integration/result_test.go index b9fbbbd03..b3711da84 100644 --- a/test/integration/result_test.go +++ b/test/integration/result_test.go @@ -48,6 +48,7 @@ func TestResultListJson(t *testing.T) { flag(params.TargetFlag), fileName, flag(params.ScanIDFlag), scanID, flag(params.TargetPathFlag), resultsDirectory, + flag(params.SastRedundancyFlag), ) result := wrappers.ScanResultsCollection{} diff --git a/test/integration/root_test.go b/test/integration/root_test.go index 29c1a08f6..feeec9567 100644 --- a/test/integration/root_test.go +++ b/test/integration/root_test.go @@ -6,6 +6,7 @@ import ( "fmt" "log" "os" + "strings" "testing" "github.com/spf13/viper" @@ -28,9 +29,16 @@ var Tags = map[string]string{ "Integration": "Tests", } +var Groups = []string{ + "it_test_group_1", + "it_test_group_2", +} + var testInstance *testing.T var rootScanId string +var rootEnginesScanId string var rootScanProjectId string +var rootEnginesScanProjectId string var rootProjectId string var rootProjectName string @@ -44,7 +52,7 @@ func TestMain(m *testing.M) { } // Create or return a scan to be shared between tests -func getRootScan(t *testing.T) (string, string) { +func getRootScan(t *testing.T, scanTypes ...string) (string, string) { testInstance = t if len(rootScanId) > 0 { @@ -52,10 +60,13 @@ func getRootScan(t *testing.T) (string, string) { log.Println("Using the projectID: ", rootScanProjectId) return rootScanId, rootScanProjectId } - - rootScanId, rootScanProjectId = createScan(testInstance, Zip, Tags) - - return rootScanId, rootScanProjectId + if len(scanTypes) == 0 { + rootScanId, rootScanProjectId = createScan(testInstance, Zip, Tags) + return rootScanId, rootScanProjectId + } else { + rootEnginesScanId, rootEnginesScanProjectId = createScanWithEngines(testInstance, Zip, Tags, strings.Join(scanTypes, ",")) + return rootEnginesScanId, rootEnginesScanProjectId + } } // Delete scan and projects @@ -85,7 +96,7 @@ func getRootProject(t *testing.T) (string, string) { return rootProjectId, rootProjectName } - rootProjectId, rootProjectName = createProject(t, Tags) + rootProjectId, rootProjectName = createProject(t, Tags, Groups) return rootProjectId, rootProjectName } diff --git a/test/integration/scan_test.go b/test/integration/scan_test.go index baa5b91c1..c8b6055de 100644 --- a/test/integration/scan_test.go +++ b/test/integration/scan_test.go @@ -22,6 +22,7 @@ import ( realtime "github.com/checkmarx/ast-cli/internal/commands/scarealtime" "github.com/checkmarx/ast-cli/internal/commands/util" "github.com/checkmarx/ast-cli/internal/commands/util/printer" + applicationErrors "github.com/checkmarx/ast-cli/internal/errors" "github.com/checkmarx/ast-cli/internal/params" "github.com/checkmarx/ast-cli/internal/wrappers" "github.com/spf13/viper" @@ -67,9 +68,68 @@ func TestScanCreateEmptyProjectName(t *testing.T) { assertError(t, err, "Project name is required") // Creating a scan with empty project name should fail } +func TestScanCreate_ExistingApplicationAndExistingProject_CreateScanSuccessfully(t *testing.T) { + args := []string{ + "scan", "create", + flag(params.ApplicationName), "my-application", + flag(params.ProjectName), "my-project", + flag(params.SourcesFlag), ".", + flag(params.ScanTypes), "sast", + flag(params.BranchFlag), "dummy_branch", + } + + err, _ := executeCommand(t, args...) + assert.NilError(t, err) +} + +func TestScanCreate_ExistingApplicationAndNotExistingProject_CreatingNewProjectAndCreateScanSuccessfully(t *testing.T) { + args := []string{ + "scan", "create", + flag(params.ApplicationName), "my-application", + flag(params.ProjectName), projectNameRandom, + flag(params.SourcesFlag), ".", + flag(params.ScanTypes), "sast", + flag(params.BranchFlag), "dummy_branch", + flag(params.ScanInfoFormatFlag), printer.FormatJSON, + } + scanID, projectID := executeCreateScan(t, args) + defer deleteProject(t, projectID) + assert.Assert(t, scanID != "", "Scan ID should not be empty") + assert.Assert(t, projectID != "", "Project ID should not be empty") +} + +func TestScanCreate_ApplicationDoesntExist_FailScanWithError(t *testing.T) { + args := []string{ + "scan", "create", + flag(params.ApplicationName), "application-that-doesnt-exist", + flag(params.ProjectName), "my-project", + flag(params.SourcesFlag), ".", + flag(params.ScanTypes), "sast", + flag(params.BranchFlag), "dummy_branch", + } + + err, _ := executeCommand(t, args...) + assertError(t, err, applicationErrors.ApplicationDoesntExistOrNoPermission) +} + // Create scans from current dir, zip and url and perform assertions in executeScanAssertions func TestScansE2E(t *testing.T) { - scanID, projectID := executeCreateScan(t, getCreateArgs(Zip, Tags, "sast,iac-security,sca")) + scanID, projectID := executeCreateScan(t, getCreateArgsWithGroups(Zip, Tags, Groups, "sast,iac-security,sca")) + defer deleteProject(t, projectID) + + executeScanAssertions(t, projectID, scanID, Tags) + glob, err := filepath.Glob(filepath.Join(os.TempDir(), "cx*.zip")) + if err != nil { + + return + } + assert.Equal(t, len(glob), 0, "Zip file not removed") +} + +func TestScansUpdateProjectGroups(t *testing.T) { + scanID, projectID := executeCreateScan(t, getCreateArgs(Zip, Tags, "sast")) + response := listScanByID(t, scanID) + scanID, projectID = executeCreateScan(t, getCreateArgsWithNameAndGroups(Zip, Tags, Groups, response[0].ProjectName, "sast")) defer deleteProject(t, projectID) executeScanAssertions(t, projectID, scanID, Tags) @@ -395,7 +455,6 @@ func executeScanAssertions(t *testing.T, projectID, scanID string, tags map[stri assert.Assert(t, ok, "Scan should contain all created tags. Missing %s", key) assert.Equal(t, val, Tags[key], "Tag value should be equal") } - deleteScan(t, scanID) response = listScanByID(t, scanID) @@ -414,6 +473,9 @@ func createScanNoWait(t *testing.T, source string, tags map[string]string) (stri func createScanSastNoWait(t *testing.T, source string, tags map[string]string) (string, string) { return executeCreateScan(t, append(getCreateArgs(source, tags, "sast,sca"), flag(params.AsyncFlag))) } +func createScanWithEngines(t *testing.T, source string, tags map[string]string, scanTypes string) (string, string) { + return executeCreateScan(t, append(getCreateArgs(source, tags, scanTypes), flag(params.AsyncFlag))) +} // Create sca scan with resolver func createScanScaWithResolver( @@ -445,11 +507,17 @@ func getProjectNameForScanTests() string { } func getCreateArgs(source string, tags map[string]string, scanTypes string) []string { + return getCreateArgsWithGroups(source, tags, nil, scanTypes) +} +func getCreateArgsWithGroups(source string, tags map[string]string, groups []string, scanTypes string) []string { projectName := getProjectNameForScanTests() - return getCreateArgsWithName(source, tags, projectName, scanTypes) + return getCreateArgsWithNameAndGroups(source, tags, groups, projectName, scanTypes) } func getCreateArgsWithName(source string, tags map[string]string, projectName, scanTypes string) []string { + return getCreateArgsWithNameAndGroups(source, tags, nil, projectName, scanTypes) +} +func getCreateArgsWithNameAndGroups(source string, tags map[string]string, groups []string, projectName, scanTypes string) []string { args := []string{ "scan", "create", flag(params.ProjectName), projectName, @@ -458,6 +526,7 @@ func getCreateArgsWithName(source string, tags map[string]string, projectName, s flag(params.ScanInfoFormatFlag), printer.FormatJSON, flag(params.TagList), formatTags(tags), flag(params.BranchFlag), SlowRepoBranch, + flag(params.ProjectGroupList), formatGroups(groups), } return args } @@ -1174,3 +1243,13 @@ func TestScanWithPolicyTimeout(t *testing.T) { err, _ := executeCommand(t, args...) assert.Error(t, err, "--policy-timeout should be equal or higher than 0") } + +func TestScanListWithFilters(t *testing.T) { + args := []string{ + "scan", "list", + flag(params.FilterFlag), "limit=100", + } + + err, _ := executeCommand(t, args...) + assert.NilError(t, err, "") +} diff --git a/test/integration/util.go b/test/integration/util.go index 8f0c0efca..2f9ceae08 100644 --- a/test/integration/util.go +++ b/test/integration/util.go @@ -26,6 +26,15 @@ func formatTags(tags map[string]string) string { tagsStr = strings.TrimRight(tagsStr, ",") return tagsStr } +func formatGroups(groups []string) string { + var groupsStr string + for _, group := range groups { + groupsStr += group + groupsStr += "," + } + groupsStr = strings.TrimRight(groupsStr, ",") + return groupsStr +} func getAllTags(t *testing.T, baseCmd string) map[string][]string { tagsCommand, buffer := createRedirectedTestCommand(t) diff --git a/test/integration/util_command.go b/test/integration/util_command.go index 233b32bf6..0ecec9c58 100644 --- a/test/integration/util_command.go +++ b/test/integration/util_command.go @@ -60,6 +60,7 @@ func createASTIntegrationTestCommand(t *testing.T) *cobra.Command { viper.AutomaticEnv() viper.Set("CX_TOKEN_EXPIRY_SECONDS", 2) scans := viper.GetString(params.ScansPathKey) + applications := viper.GetString(params.ApplicationsPathKey) groups := viper.GetString(params.GroupsPathKey) projects := viper.GetString(params.ProjectsPathKey) results := viper.GetString(params.ResultsPathKey) @@ -80,8 +81,10 @@ func createASTIntegrationTestCommand(t *testing.T) *cobra.Command { featureFlagsPath := viper.GetString(params.FeatureFlagsKey) policyEvaluationPath := viper.GetString(params.PolicyEvaluationPathKey) sastIncrementalPath := viper.GetString(params.SastMetadataPathKey) + accessManagementPath := viper.GetString(params.AccessManagementPathKey) scansWrapper := wrappers.NewHTTPScansWrapper(scans) + applicationsWrapper := wrappers.NewApplicationsHTTPWrapper(applications) resultsPdfReportsWrapper := wrappers.NewResultsPdfReportsHTTPWrapper(resultsPdfPath) resultsSbomReportsWrapper := wrappers.NewResultsSbomReportsHTTPWrapper(resultsSbomPath, resultsSbomProxyPath) @@ -108,8 +111,10 @@ func createASTIntegrationTestCommand(t *testing.T) *cobra.Command { featureFlagsWrapper := wrappers.NewFeatureFlagsHTTPWrapper(featureFlagsPath) policyWrapper := wrappers.NewHTTPPolicyWrapper(policyEvaluationPath) sastMetadataWrapper := wrappers.NewSastIncrementalHTTPWrapper(sastIncrementalPath) + accessManagementWrapper := wrappers.NewAccessManagementHTTPWrapper(accessManagementPath) astCli := commands.NewAstCLI( + applicationsWrapper, scansWrapper, resultsSbomReportsWrapper, resultsPdfReportsWrapper, @@ -137,6 +142,7 @@ func createASTIntegrationTestCommand(t *testing.T) *cobra.Command { featureFlagsWrapper, policyWrapper, sastMetadataWrapper, + accessManagementWrapper, ) return astCli }