Skip to content

Commit c199543

Browse files
Existing project Association to Existing Application (AST-108181) (#1312)
Associate existing project to existing application via CLI
1 parent a7bd119 commit c199543

11 files changed

Lines changed: 415 additions & 62 deletions

File tree

internal/commands/scan_test.go

Lines changed: 70 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -193,8 +193,9 @@ func TestScanCreate_ApplicationNameIsNotExactMatch_FailedToCreateScan(t *testing
193193
assert.Assert(t, err.Error() == errorConstants.ApplicationDoesntExistOrNoPermission)
194194
}
195195

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

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

715-
func TestCreateScan_WhenProjectExists_ShouldIgnoreApplication(t *testing.T) {
716-
file := createOutputFile(t, outputFileName)
717-
defer deleteOutputFile(file)
718-
defer logger.SetOutput(os.Stdout)
716+
// Now as we give the ability to assign existing projects to applications , there is validation if application exists
717+
718+
func TestCreateScan_WhenProjectExists_GetApplication_Fails500Err_Failed(t *testing.T) {
719719
baseArgs := []string{scanCommand, "create", "--project-name", "MOCK", "-s", dummyRepo, "-b", "dummy_branch",
720-
"--debug", "--application-name", "anyApplication"}
721-
execCmdNilAssertion(t, baseArgs...)
722-
stdoutString, err := util.ReadFileAsString(file.Name())
723-
if err != nil {
724-
t.Fatalf("Failed to read log file: %v", err)
725-
}
726-
assert.Equal(t, strings.Contains(stdoutString, noUpdatesForExistingProject), true, "Expected output: %s", noUpdatesForExistingProject)
720+
"--debug", "--application-name", mock.FakeInternalServerError500}
721+
err := execCmdNotNilAssertion(t, baseArgs...)
722+
assert.ErrorContains(t, err, errorConstants.FailedToGetApplication, err.Error())
727723
}
728724
func TestScanCreateLastSastScanTimeWithInvalidValue(t *testing.T) {
729725
baseArgs := []string{"scan", "create", "--project-name", "MOCK", "-s", dummyRepo, "-b", "dummy_branch", "--sca-exploitable-path", "true", "--sca-last-sast-scan-time", "notaniteger"}
@@ -3623,3 +3619,64 @@ func Test_CreateScanWithIgnorePolicyFlag(t *testing.T) {
36233619
"scan", "create", "--project-name", "MOCK", "-s", "data/sources.zip", "--branch", "dummy_branch", "--ignore-policy",
36243620
)
36253621
}
3622+
3623+
func Test_CreateScanWithExistingProjectAndAssign_Application(t *testing.T) {
3624+
file := createOutputFile(t, outputFileName)
3625+
defer deleteOutputFile(file)
3626+
defer logger.SetOutput(os.Stdout)
3627+
3628+
baseArgs := []string{"scan", "create", "--project-name", "MOCK", "-s", ".", "--branch", "main", "--application-name", mock.ExistingApplication, "--debug"}
3629+
execCmdNilAssertion(
3630+
t,
3631+
baseArgs...,
3632+
)
3633+
stdoutString, err := util.ReadFileAsString(file.Name())
3634+
if err != nil {
3635+
t.Fatalf("Failed to read log file: %v", err)
3636+
}
3637+
assert.Equal(t, strings.Contains(stdoutString, "Successfully updated the application"), true, "Expected output: %s", "Successfully updated the application")
3638+
}
3639+
3640+
func Test_CreateScanWithExistingProjectAndAssign_FailedNoApplication_NameProvided(t *testing.T) {
3641+
file := createOutputFile(t, outputFileName)
3642+
defer deleteOutputFile(file)
3643+
defer logger.SetOutput(os.Stdout)
3644+
3645+
baseArgs := []string{"scan", "create", "--project-name", "MOCK", "-s", ".", "--branch", "main", "--debug"}
3646+
execCmdNilAssertion(
3647+
t,
3648+
baseArgs...,
3649+
)
3650+
stdoutString, err := util.ReadFileAsString(file.Name())
3651+
if err != nil {
3652+
t.Fatalf("Failed to read log file: %v", err)
3653+
}
3654+
assert.Equal(t, strings.Contains(stdoutString, "No application name provided. Skipping application update"), true, "Expected output: %s", "No application name provided. Skipping application update")
3655+
}
3656+
3657+
func Test_CreateScanWithExistingProjectAndAssign_FailedApplication_DoesNot_Exist(t *testing.T) {
3658+
baseArgs := []string{"scan", "create", "--project-name", "MOCK", "-s", ".", "--branch", "main", "--debug", "--application-name", "NoPermissionApp"}
3659+
err := execCmdNotNilAssertion(
3660+
t,
3661+
baseArgs...,
3662+
)
3663+
assert.ErrorContains(t, err, errorConstants.FailedToGetApplication, err.Error())
3664+
}
3665+
3666+
func Test_CreateScanWithExistingProjectAssign_to_Application_FF_DirectAssociationEnabledShouldPass(t *testing.T) {
3667+
file := createOutputFile(t, outputFileName)
3668+
defer deleteOutputFile(file)
3669+
defer logger.SetOutput(os.Stdout)
3670+
3671+
mock.Flag = wrappers.FeatureFlagResponseModel{Name: wrappers.DirectAssociationEnabled, Status: true}
3672+
baseArgs := []string{"scan", "create", "--project-name", "MOCK", "-s", ".", "--branch", "main", "--debug", "--application-name", mock.ExistingApplication}
3673+
execCmdNilAssertion(
3674+
t,
3675+
baseArgs...,
3676+
)
3677+
stdoutString, err := util.ReadFileAsString(file.Name())
3678+
if err != nil {
3679+
t.Fatalf("Failed to read log file: %v", err)
3680+
}
3681+
assert.Equal(t, strings.Contains(stdoutString, "Successfully updated the application"), true, "Expected output: %s", "Successfully updated the application")
3682+
}

internal/constants/errors/errors.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,9 @@ const (
2424
NoASCALicense = "User doesn't have \"AI Protection\" or \"Checkmarx One Assist\" license"
2525
FailedUploadFileMsgWithDomain = "Unable to upload the file to the pre-signed URL. Try adding the domain: %s to your allow list."
2626
FailedUploadFileMsgWithURL = "Unable to upload the file to the pre-signed URL. Try adding the URL: %s to your allow list."
27+
NoPermissionToUpdateApplication = "you do not have permission to update the application"
28+
FailedToUpdateApplication = "failed to update application"
29+
ApplicationNotFound = "Application not found"
2730

2831
// asca Engine
2932
FileExtensionIsRequired = "file must have an extension"

internal/services/applications.go

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,16 @@ package services
22

33
import (
44
errorConstants "github.com/checkmarx/ast-cli/internal/constants/errors"
5+
"github.com/checkmarx/ast-cli/internal/logger"
56
"github.com/checkmarx/ast-cli/internal/wrappers"
67
"github.com/checkmarx/ast-cli/internal/wrappers/utils"
78
"github.com/pkg/errors"
89
)
910

11+
const (
12+
ApplicationRuleType = "project.name.in"
13+
)
14+
1015
func createApplicationIds(applicationID, existingApplicationIds []string) []string {
1116
for _, id := range applicationID {
1217
if !utils.Contains(existingApplicationIds, id) {
@@ -58,3 +63,71 @@ func verifyApplicationNameExactMatch(applicationName string, resp *wrappers.Appl
5863
}
5964
return application
6065
}
66+
67+
func findApplicationAndUpdate(applicationName string, applicationsWrapper wrappers.ApplicationsWrapper, projectName, projectID string, featureFlagsWrapper wrappers.FeatureFlagsWrapper) error {
68+
if applicationName == "" {
69+
logger.PrintfIfVerbose("No application name provided. Skipping application update")
70+
return nil
71+
}
72+
applicationResp, err := GetApplication(applicationName, applicationsWrapper)
73+
if err != nil {
74+
return errors.Wrapf(err, "%s:%s", errorConstants.FailedToGetApplication, applicationName)
75+
}
76+
if applicationResp == nil {
77+
return errors.Errorf("%s: %s", errorConstants.ApplicationNotFound, applicationName)
78+
}
79+
80+
directAssociationEnabled, _ := wrappers.GetSpecificFeatureFlag(featureFlagsWrapper, wrappers.DirectAssociationEnabled)
81+
if directAssociationEnabled.Status {
82+
err = associateProjectToApplication(applicationResp.ID, projectID, applicationResp.ProjectIds, applicationsWrapper)
83+
if err != nil {
84+
return err
85+
}
86+
return nil
87+
}
88+
var applicationModel wrappers.ApplicationConfiguration
89+
var newApplicationRule wrappers.Rule
90+
var applicationID string
91+
92+
applicationModel.Name = applicationResp.Name
93+
applicationModel.Description = applicationResp.Description
94+
applicationModel.Criticality = applicationResp.Criticality
95+
applicationModel.Type = applicationResp.Type
96+
applicationModel.Tags = applicationResp.Tags
97+
newApplicationRule.Type = ApplicationRuleType
98+
newApplicationRule.Value = projectName
99+
applicationModel.Rules = append(applicationModel.Rules, applicationResp.Rules...)
100+
applicationModel.Rules = append(applicationModel.Rules, newApplicationRule)
101+
applicationID = applicationResp.ID
102+
103+
err = updateApplication(&applicationModel, applicationsWrapper, applicationID)
104+
if err != nil {
105+
return err
106+
}
107+
return nil
108+
}
109+
110+
func updateApplication(applicationModel *wrappers.ApplicationConfiguration, applicationWrapper wrappers.ApplicationsWrapper, applicationID string) error {
111+
errorModel, err := applicationWrapper.Update(applicationID, applicationModel)
112+
return handleApplicationUpdateResponse(errorModel, err)
113+
}
114+
115+
func associateProjectToApplication(applicationID, projectID string, associatedProjectIds []string, applicationsWrapper wrappers.ApplicationsWrapper) error {
116+
associatedProjectIds = append(associatedProjectIds, projectID)
117+
associateProjectsModel := &wrappers.AssociateProjectModel{
118+
ProjectIds: associatedProjectIds,
119+
}
120+
errorModel, err := applicationsWrapper.CreateProjectAssociation(applicationID, associateProjectsModel)
121+
return handleApplicationUpdateResponse(errorModel, err)
122+
}
123+
124+
func handleApplicationUpdateResponse(errorModel *wrappers.ErrorModel, err error) error {
125+
if errorModel != nil {
126+
err = errors.Errorf(ErrorCodeFormat, errorConstants.FailedToUpdateApplication, errorModel.Code, errorModel.Message)
127+
}
128+
if errorModel == nil && err == nil {
129+
logger.PrintIfVerbose("Successfully updated the application")
130+
return nil
131+
}
132+
return err
133+
}

internal/services/applications_test.go

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,12 @@
11
package services
22

33
import (
4+
errorConstants "github.com/checkmarx/ast-cli/internal/constants/errors"
5+
"github.com/checkmarx/ast-cli/internal/wrappers"
6+
"github.com/checkmarx/ast-cli/internal/wrappers/mock"
7+
"gotest.tools/assert"
48
"reflect"
9+
"strings"
510
"testing"
611
)
712

@@ -36,3 +41,49 @@ func Test_createApplicationIds(t *testing.T) {
3641
})
3742
}
3843
}
44+
func Test_ProjectAssociation_ToApplicationDirectly(t *testing.T) {
45+
applicationWrapper := &mock.ApplicationsMockWrapper{}
46+
47+
tests := []struct {
48+
description string
49+
applicationName string
50+
projectName string
51+
error string
52+
}{
53+
{"Project association to Application should fail with 403 forbidden permission error", mock.FakeForbidden403, "random-project", errorConstants.NoPermissionToUpdateApplication},
54+
{"Project association to Application should fail with 401 unauthorized error", mock.FakeUnauthorized401, "random-project", errorConstants.StatusUnauthorized},
55+
{"Project association to Application should fail with 400 BadRequest error", mock.FakeBadRequest400, "random-project", errorConstants.FailedToUpdateApplication},
56+
}
57+
58+
for _, test := range tests {
59+
tt := test
60+
t.Run(tt.description, func(t *testing.T) {
61+
err := associateProjectToApplication(tt.applicationName, tt.projectName, []string{}, applicationWrapper)
62+
assert.Assert(t, strings.Contains(err.Error(), tt.error), err.Error())
63+
})
64+
}
65+
}
66+
67+
func Test_ProjectAssociation_ToApplicationWithoutDirectAssociation(t *testing.T) {
68+
applicationModel := wrappers.ApplicationConfiguration{}
69+
applicationWrapper := &mock.ApplicationsMockWrapper{}
70+
71+
tests := []struct {
72+
description string
73+
applicationID string
74+
projectName string
75+
error string
76+
}{
77+
{"Application update should fail with 403 forbidden permission error", mock.FakeForbidden403, "random-project", errorConstants.NoPermissionToUpdateApplication},
78+
{"Application update should fail with 401 unauthorized error", mock.FakeUnauthorized401, "random-project", errorConstants.StatusUnauthorized},
79+
{"Application update should fail with 400 BadRequest error", mock.FakeBadRequest400, "random-project", errorConstants.FailedToUpdateApplication},
80+
}
81+
82+
for _, test := range tests {
83+
tt := test
84+
t.Run(tt.description, func(t *testing.T) {
85+
err := updateApplication(&applicationModel, applicationWrapper, tt.applicationID)
86+
assert.Assert(t, strings.Contains(err.Error(), tt.error), err.Error())
87+
})
88+
}
89+
}

internal/services/projects.go

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,9 +40,14 @@ func FindProject(
4040
}
4141
branchName := strings.TrimSpace(viper.GetString(commonParams.BranchKey))
4242
isBranchPrimary, _ = cmd.Flags().GetBool(commonParams.BranchPrimaryFlag)
43+
applicationName, _ := cmd.Flags().GetString(commonParams.ApplicationName)
4344
for i := 0; i < len(resp.Projects); i++ {
4445
project := resp.Projects[i]
4546
if project.Name == projectName {
47+
err = findApplicationAndUpdate(applicationName, applicationWrapper, projectName, project.ID, featureFlagsWrapper)
48+
if err != nil {
49+
return "", err
50+
}
4651
projectTags, _ := cmd.Flags().GetString(commonParams.ProjectTagList)
4752
projectPrivatePackage, _ := cmd.Flags().GetString(commonParams.ProjecPrivatePackageFlag)
4853
return updateProject(&project, projectsWrapper, projectTags, projectPrivatePackage, isBranchPrimary, branchName)
@@ -51,8 +56,6 @@ func FindProject(
5156

5257
projectGroups, _ := cmd.Flags().GetString(commonParams.ProjectGroupList)
5358
projectPrivatePackage, _ := cmd.Flags().GetString(commonParams.ProjecPrivatePackageFlag)
54-
55-
applicationName, _ := cmd.Flags().GetString(commonParams.ApplicationName)
5659
applicationID, appErr := getApplicationID(applicationName, applicationWrapper)
5760
if appErr != nil {
5861
return "", appErr

internal/wrappers/application-http.go

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
package wrappers
22

33
import (
4+
"bytes"
45
"encoding/json"
6+
"fmt"
57
"net/http"
68

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

25+
func (a *ApplicationsHTTPWrapper) CreateProjectAssociation(applicationID string, projectAssociationModel *AssociateProjectModel) (*ErrorModel, error) {
26+
clientTimeout := viper.GetUint(commonParams.ClientTimeoutKey)
27+
jsonBytes, err := json.Marshal(*projectAssociationModel)
28+
if err != nil {
29+
return nil, err
30+
}
31+
associationPath := fmt.Sprintf("%s/%s/%s", a.path, applicationID, "projects")
32+
resp, err := SendHTTPRequest(http.MethodPost, associationPath, bytes.NewBuffer(jsonBytes), true, clientTimeout)
33+
if err != nil {
34+
return nil, err
35+
}
36+
decoder := json.NewDecoder(resp.Body)
37+
defer func() {
38+
_ = resp.Body.Close()
39+
}()
40+
switch resp.StatusCode {
41+
case http.StatusBadRequest:
42+
errorModel := ErrorModel{}
43+
err = decoder.Decode(&errorModel)
44+
if err != nil {
45+
return nil, errors.Errorf("failed to parse application response for project updation: %s ", err)
46+
}
47+
return &errorModel, nil
48+
49+
case http.StatusCreated:
50+
return nil, nil
51+
52+
case http.StatusForbidden:
53+
return nil, errors.New(errorConstants.NoPermissionToUpdateApplication)
54+
55+
case http.StatusUnauthorized:
56+
return nil, errors.New(errorConstants.StatusUnauthorized)
57+
default:
58+
return nil, errors.Errorf("response status code %d", resp.StatusCode)
59+
}
60+
}
61+
62+
func (a *ApplicationsHTTPWrapper) Update(applicationID string, applicationBody *ApplicationConfiguration) (*ErrorModel, error) {
63+
clientTimeout := viper.GetUint(commonParams.ClientTimeoutKey)
64+
jsonBytes, err := json.Marshal(applicationBody)
65+
updatePath := fmt.Sprintf("%s/%s", a.path, applicationID)
66+
if err != nil {
67+
return nil, err
68+
}
69+
resp, err := SendHTTPRequest(http.MethodPut, updatePath, bytes.NewBuffer(jsonBytes), true, clientTimeout)
70+
if err != nil {
71+
return nil, err
72+
}
73+
decoder := json.NewDecoder(resp.Body)
74+
defer func() {
75+
_ = resp.Body.Close()
76+
}()
77+
78+
switch resp.StatusCode {
79+
case http.StatusBadRequest:
80+
errorModel := ErrorModel{}
81+
err = decoder.Decode(&errorModel)
82+
if err != nil {
83+
return nil, errors.Errorf("failed to parse application response: %s ", err)
84+
}
85+
return &errorModel, nil
86+
87+
case http.StatusNoContent:
88+
return nil, nil
89+
90+
case http.StatusForbidden:
91+
return nil, errors.New(errorConstants.NoPermissionToUpdateApplication)
92+
93+
case http.StatusUnauthorized:
94+
return nil, errors.New(errorConstants.StatusUnauthorized)
95+
default:
96+
return nil, errors.Errorf("response status code %d", resp.StatusCode)
97+
}
98+
}
99+
23100
func (a *ApplicationsHTTPWrapper) Get(params map[string]string) (*ApplicationsResponseModel, error) {
24101
if _, ok := params[limit]; !ok {
25102
params[limit] = limitValue

0 commit comments

Comments
 (0)