Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
83 changes: 70 additions & 13 deletions internal/commands/scan_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -193,8 +193,9 @@ func TestScanCreate_ApplicationNameIsNotExactMatch_FailedToCreateScan(t *testing
assert.Assert(t, err.Error() == errorConstants.ApplicationDoesntExistOrNoPermission)
}

func TestScanCreate_ExistingProjectAndApplicationWithNoPermission_ShouldCreateScan(t *testing.T) {
execCmdNilAssertion(t, "scan", "create", "--project-name", "MOCK", "--application-name", mock.ApplicationDoesntExist, "-s", dummyRepo, "-b", "dummy_branch")
func TestScanCreate_ExistingProjectAndApplicationWithNoPermission_ShouldFailScan(t *testing.T) {
err := execCmdNotNilAssertion(t, "scan", "create", "--project-name", "MOCK", "--application-name", mock.NoPermissionApp, "-s", dummyRepo, "-b", "dummy_branch")
assert.Assert(t, strings.Contains(err.Error(), errorConstants.FailedToGetApplication), err.Error())
}

func TestScanCreate_ExistingApplicationWithNoPermission_FailedToCreateScan(t *testing.T) {
Expand Down Expand Up @@ -712,18 +713,13 @@ func TestCreateScan_WhenProjectExists_ShouldIgnoreGroups(t *testing.T) {
assert.Equal(t, strings.Contains(stdoutString, noUpdatesForExistingProject), true, "Expected output: %s", noUpdatesForExistingProject)
}

func TestCreateScan_WhenProjectExists_ShouldIgnoreApplication(t *testing.T) {
file := createOutputFile(t, outputFileName)
defer deleteOutputFile(file)
defer logger.SetOutput(os.Stdout)
// Now as we give the ability to assign existing projects to applications , there is validation if application exists

func TestCreateScan_WhenProjectExists_GetApplication_Fails500Err_Failed(t *testing.T) {
baseArgs := []string{scanCommand, "create", "--project-name", "MOCK", "-s", dummyRepo, "-b", "dummy_branch",
"--debug", "--application-name", "anyApplication"}
execCmdNilAssertion(t, baseArgs...)
stdoutString, err := util.ReadFileAsString(file.Name())
if err != nil {
t.Fatalf("Failed to read log file: %v", err)
}
assert.Equal(t, strings.Contains(stdoutString, noUpdatesForExistingProject), true, "Expected output: %s", noUpdatesForExistingProject)
"--debug", "--application-name", mock.FakeInternalServerError500}
err := execCmdNotNilAssertion(t, baseArgs...)
assert.ErrorContains(t, err, errorConstants.FailedToGetApplication, err.Error())
}
func TestScanCreateLastSastScanTimeWithInvalidValue(t *testing.T) {
baseArgs := []string{"scan", "create", "--project-name", "MOCK", "-s", dummyRepo, "-b", "dummy_branch", "--sca-exploitable-path", "true", "--sca-last-sast-scan-time", "notaniteger"}
Expand Down Expand Up @@ -3623,3 +3619,64 @@ func Test_CreateScanWithIgnorePolicyFlag(t *testing.T) {
"scan", "create", "--project-name", "MOCK", "-s", "data/sources.zip", "--branch", "dummy_branch", "--ignore-policy",
)
}

func Test_CreateScanWithExistingProjectAndAssign_Application(t *testing.T) {
file := createOutputFile(t, outputFileName)
defer deleteOutputFile(file)
defer logger.SetOutput(os.Stdout)

baseArgs := []string{"scan", "create", "--project-name", "MOCK", "-s", ".", "--branch", "main", "--application-name", mock.ExistingApplication, "--debug"}
execCmdNilAssertion(
t,
baseArgs...,
)
stdoutString, err := util.ReadFileAsString(file.Name())
if err != nil {
t.Fatalf("Failed to read log file: %v", err)
}
assert.Equal(t, strings.Contains(stdoutString, "Successfully updated the application"), true, "Expected output: %s", "Successfully updated the application")
}

func Test_CreateScanWithExistingProjectAndAssign_FailedNoApplication_NameProvided(t *testing.T) {
file := createOutputFile(t, outputFileName)
defer deleteOutputFile(file)
defer logger.SetOutput(os.Stdout)

baseArgs := []string{"scan", "create", "--project-name", "MOCK", "-s", ".", "--branch", "main", "--debug"}
execCmdNilAssertion(
t,
baseArgs...,
)
stdoutString, err := util.ReadFileAsString(file.Name())
if err != nil {
t.Fatalf("Failed to read log file: %v", err)
}
assert.Equal(t, strings.Contains(stdoutString, "No application name provided. Skipping application update"), true, "Expected output: %s", "No application name provided. Skipping application update")
}

func Test_CreateScanWithExistingProjectAndAssign_FailedApplication_DoesNot_Exist(t *testing.T) {
baseArgs := []string{"scan", "create", "--project-name", "MOCK", "-s", ".", "--branch", "main", "--debug", "--application-name", "NoPermissionApp"}
err := execCmdNotNilAssertion(
t,
baseArgs...,
)
assert.ErrorContains(t, err, errorConstants.FailedToGetApplication, err.Error())
}

func Test_CreateScanWithExistingProjectAssign_to_Application_FF_DirectAssociationEnabledShouldPass(t *testing.T) {
file := createOutputFile(t, outputFileName)
defer deleteOutputFile(file)
defer logger.SetOutput(os.Stdout)

mock.Flag = wrappers.FeatureFlagResponseModel{Name: wrappers.DirectAssociationEnabled, Status: true}
baseArgs := []string{"scan", "create", "--project-name", "MOCK", "-s", ".", "--branch", "main", "--debug", "--application-name", mock.ExistingApplication}
execCmdNilAssertion(
t,
baseArgs...,
)
stdoutString, err := util.ReadFileAsString(file.Name())
if err != nil {
t.Fatalf("Failed to read log file: %v", err)
}
assert.Equal(t, strings.Contains(stdoutString, "Successfully updated the application"), true, "Expected output: %s", "Successfully updated the application")
}
3 changes: 3 additions & 0 deletions internal/constants/errors/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@ const (
NoASCALicense = "User doesn't have \"AI Protection\" or \"Checkmarx One Assist\" license"
FailedUploadFileMsgWithDomain = "Unable to upload the file to the pre-signed URL. Try adding the domain: %s to your allow list."
FailedUploadFileMsgWithURL = "Unable to upload the file to the pre-signed URL. Try adding the URL: %s to your allow list."
NoPermissionToUpdateApplication = "you do not have permission to update the application"
FailedToUpdateApplication = "failed to update application"
ApplicationNotFound = "Application not found"

// asca Engine
FileExtensionIsRequired = "file must have an extension"
Expand Down
73 changes: 73 additions & 0 deletions internal/services/applications.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,16 @@ package services

import (
errorConstants "github.com/checkmarx/ast-cli/internal/constants/errors"
"github.com/checkmarx/ast-cli/internal/logger"
"github.com/checkmarx/ast-cli/internal/wrappers"
"github.com/checkmarx/ast-cli/internal/wrappers/utils"
"github.com/pkg/errors"
)

const (
ApplicationRuleType = "project.name.in"
)

func createApplicationIds(applicationID, existingApplicationIds []string) []string {
for _, id := range applicationID {
if !utils.Contains(existingApplicationIds, id) {
Expand Down Expand Up @@ -58,3 +63,71 @@ func verifyApplicationNameExactMatch(applicationName string, resp *wrappers.Appl
}
return application
}

func findApplicationAndUpdate(applicationName string, applicationsWrapper wrappers.ApplicationsWrapper, projectName, projectID string, featureFlagsWrapper wrappers.FeatureFlagsWrapper) error {
if applicationName == "" {
logger.PrintfIfVerbose("No application name provided. Skipping application update")
return nil
}
applicationResp, err := GetApplication(applicationName, applicationsWrapper)
if err != nil {
return errors.Wrapf(err, "%s:%s", errorConstants.FailedToGetApplication, applicationName)
}
if applicationResp == nil {
return errors.Errorf("%s: %s", errorConstants.ApplicationNotFound, applicationName)
}

directAssociationEnabled, _ := wrappers.GetSpecificFeatureFlag(featureFlagsWrapper, wrappers.DirectAssociationEnabled)
if directAssociationEnabled.Status {
err = associateProjectToApplication(applicationResp.ID, projectID, applicationResp.ProjectIds, applicationsWrapper)
if err != nil {
return err
}
return nil
}
var applicationModel wrappers.ApplicationConfiguration
var newApplicationRule wrappers.Rule
var applicationID string

applicationModel.Name = applicationResp.Name
applicationModel.Description = applicationResp.Description
applicationModel.Criticality = applicationResp.Criticality
applicationModel.Type = applicationResp.Type
applicationModel.Tags = applicationResp.Tags
newApplicationRule.Type = ApplicationRuleType
newApplicationRule.Value = projectName
applicationModel.Rules = append(applicationModel.Rules, applicationResp.Rules...)
applicationModel.Rules = append(applicationModel.Rules, newApplicationRule)
applicationID = applicationResp.ID

err = updateApplication(&applicationModel, applicationsWrapper, applicationID)
if err != nil {
return err
}
return nil
}

func updateApplication(applicationModel *wrappers.ApplicationConfiguration, applicationWrapper wrappers.ApplicationsWrapper, applicationID string) error {
errorModel, err := applicationWrapper.Update(applicationID, applicationModel)
return handleApplicationUpdateResponse(errorModel, err)
}

func associateProjectToApplication(applicationID, projectID string, associatedProjectIds []string, applicationsWrapper wrappers.ApplicationsWrapper) error {
associatedProjectIds = append(associatedProjectIds, projectID)
associateProjectsModel := &wrappers.AssociateProjectModel{
ProjectIds: associatedProjectIds,
}
errorModel, err := applicationsWrapper.CreateProjectAssociation(applicationID, associateProjectsModel)
return handleApplicationUpdateResponse(errorModel, err)
}

func handleApplicationUpdateResponse(errorModel *wrappers.ErrorModel, err error) error {
if errorModel != nil {
err = errors.Errorf(ErrorCodeFormat, errorConstants.FailedToUpdateApplication, errorModel.Code, errorModel.Message)
}
if errorModel == nil && err == nil {
logger.PrintIfVerbose("Successfully updated the application")
return nil
}
return err
}
51 changes: 51 additions & 0 deletions internal/services/applications_test.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
package services

import (
errorConstants "github.com/checkmarx/ast-cli/internal/constants/errors"
"github.com/checkmarx/ast-cli/internal/wrappers"
"github.com/checkmarx/ast-cli/internal/wrappers/mock"
"gotest.tools/assert"
"reflect"
"strings"
"testing"
)

Expand Down Expand Up @@ -36,3 +41,49 @@ func Test_createApplicationIds(t *testing.T) {
})
}
}
func Test_ProjectAssociation_ToApplicationDirectly(t *testing.T) {
applicationWrapper := &mock.ApplicationsMockWrapper{}

tests := []struct {
description string
applicationName string
projectName string
error string
}{
{"Project association to Application should fail with 403 forbidden permission error", mock.FakeForbidden403, "random-project", errorConstants.NoPermissionToUpdateApplication},
{"Project association to Application should fail with 401 unauthorized error", mock.FakeUnauthorized401, "random-project", errorConstants.StatusUnauthorized},
{"Project association to Application should fail with 400 BadRequest error", mock.FakeBadRequest400, "random-project", errorConstants.FailedToUpdateApplication},
}

for _, test := range tests {
tt := test
t.Run(tt.description, func(t *testing.T) {
err := associateProjectToApplication(tt.applicationName, tt.projectName, []string{}, applicationWrapper)
assert.Assert(t, strings.Contains(err.Error(), tt.error), err.Error())
})
}
}

func Test_ProjectAssociation_ToApplicationWithoutDirectAssociation(t *testing.T) {
applicationModel := wrappers.ApplicationConfiguration{}
applicationWrapper := &mock.ApplicationsMockWrapper{}

tests := []struct {
description string
applicationID string
projectName string
error string
}{
{"Application update should fail with 403 forbidden permission error", mock.FakeForbidden403, "random-project", errorConstants.NoPermissionToUpdateApplication},
{"Application update should fail with 401 unauthorized error", mock.FakeUnauthorized401, "random-project", errorConstants.StatusUnauthorized},
{"Application update should fail with 400 BadRequest error", mock.FakeBadRequest400, "random-project", errorConstants.FailedToUpdateApplication},
}

for _, test := range tests {
tt := test
t.Run(tt.description, func(t *testing.T) {
err := updateApplication(&applicationModel, applicationWrapper, tt.applicationID)
assert.Assert(t, strings.Contains(err.Error(), tt.error), err.Error())
})
}
}
7 changes: 5 additions & 2 deletions internal/services/projects.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,9 +40,14 @@ func FindProject(
}
branchName := strings.TrimSpace(viper.GetString(commonParams.BranchKey))
isBranchPrimary, _ = cmd.Flags().GetBool(commonParams.BranchPrimaryFlag)
applicationName, _ := cmd.Flags().GetString(commonParams.ApplicationName)
for i := 0; i < len(resp.Projects); i++ {
project := resp.Projects[i]
if project.Name == projectName {
err = findApplicationAndUpdate(applicationName, applicationWrapper, projectName, project.ID, featureFlagsWrapper)
if err != nil {
return "", err
}
projectTags, _ := cmd.Flags().GetString(commonParams.ProjectTagList)
projectPrivatePackage, _ := cmd.Flags().GetString(commonParams.ProjecPrivatePackageFlag)
return updateProject(&project, projectsWrapper, projectTags, projectPrivatePackage, isBranchPrimary, branchName)
Expand All @@ -51,8 +56,6 @@ func FindProject(

projectGroups, _ := cmd.Flags().GetString(commonParams.ProjectGroupList)
projectPrivatePackage, _ := cmd.Flags().GetString(commonParams.ProjecPrivatePackageFlag)

applicationName, _ := cmd.Flags().GetString(commonParams.ApplicationName)
applicationID, appErr := getApplicationID(applicationName, applicationWrapper)
if appErr != nil {
return "", appErr
Expand Down
77 changes: 77 additions & 0 deletions internal/wrappers/application-http.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
package wrappers

import (
"bytes"
"encoding/json"
"fmt"
"net/http"

errorConstants "github.com/checkmarx/ast-cli/internal/constants/errors"
Expand All @@ -20,6 +22,81 @@ func NewApplicationsHTTPWrapper(path string) ApplicationsWrapper {
}
}

func (a *ApplicationsHTTPWrapper) CreateProjectAssociation(applicationID string, projectAssociationModel *AssociateProjectModel) (*ErrorModel, error) {
clientTimeout := viper.GetUint(commonParams.ClientTimeoutKey)
jsonBytes, err := json.Marshal(*projectAssociationModel)
if err != nil {
return nil, err
}
associationPath := fmt.Sprintf("%s/%s/%s", a.path, applicationID, "projects")
resp, err := SendHTTPRequest(http.MethodPost, associationPath, bytes.NewBuffer(jsonBytes), true, clientTimeout)
if err != nil {
return nil, err
}
decoder := json.NewDecoder(resp.Body)
defer func() {
_ = resp.Body.Close()
}()
switch resp.StatusCode {
case http.StatusBadRequest:
errorModel := ErrorModel{}
err = decoder.Decode(&errorModel)
if err != nil {
return nil, errors.Errorf("failed to parse application response for project updation: %s ", err)
}
return &errorModel, nil

case http.StatusCreated:
return nil, nil

case http.StatusForbidden:
return nil, errors.New(errorConstants.NoPermissionToUpdateApplication)

case http.StatusUnauthorized:
return nil, errors.New(errorConstants.StatusUnauthorized)
default:
return nil, errors.Errorf("response status code %d", resp.StatusCode)
}
}

func (a *ApplicationsHTTPWrapper) Update(applicationID string, applicationBody *ApplicationConfiguration) (*ErrorModel, error) {
clientTimeout := viper.GetUint(commonParams.ClientTimeoutKey)
jsonBytes, err := json.Marshal(applicationBody)
updatePath := fmt.Sprintf("%s/%s", a.path, applicationID)
if err != nil {
return nil, err
}
resp, err := SendHTTPRequest(http.MethodPut, updatePath, bytes.NewBuffer(jsonBytes), true, clientTimeout)
if err != nil {
return nil, err
}
decoder := json.NewDecoder(resp.Body)
defer func() {
_ = resp.Body.Close()
}()

switch resp.StatusCode {
case http.StatusBadRequest:
errorModel := ErrorModel{}
err = decoder.Decode(&errorModel)
if err != nil {
return nil, errors.Errorf("failed to parse application response: %s ", err)
}
return &errorModel, nil

case http.StatusNoContent:
return nil, nil

case http.StatusForbidden:
return nil, errors.New(errorConstants.NoPermissionToUpdateApplication)

case http.StatusUnauthorized:
return nil, errors.New(errorConstants.StatusUnauthorized)
default:
return nil, errors.Errorf("response status code %d", resp.StatusCode)
}
}

func (a *ApplicationsHTTPWrapper) Get(params map[string]string) (*ApplicationsResponseModel, error) {
if _, ok := params[limit]; !ok {
params[limit] = limitValue
Expand Down
Loading
Loading