From 5a1525bf1a290eb4ebcc5bad336ceca2c631928f Mon Sep 17 00:00:00 2001 From: Harshal Pawar Date: Mon, 16 Oct 2023 11:24:27 +0000 Subject: [PATCH 1/3] BOTMAN-11961 add custom code api to botman --- CHANGELOG.md | 7 ++ pkg/botman/botman.go | 1 + pkg/botman/custom_code.go | 112 ++++++++++++++++++ pkg/botman/custom_code_test.go | 205 +++++++++++++++++++++++++++++++++ pkg/botman/mocks.go | 16 +++ 5 files changed, 341 insertions(+) create mode 100644 pkg/botman/custom_code.go create mode 100644 pkg/botman/custom_code_test.go diff --git a/CHANGELOG.md b/CHANGELOG.md index a9ce30c0..d72b5e06 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # EDGEGRID GOLANG RELEASE NOTES +## X.X.X (November xx, 2023) + +#### FEATURES/ENHANCEMENTS: + +* BOTMAN + * Added API support for Custom Code - read and update + ## 7.4.0 (October 24, 2023) #### FEATURES/ENHANCEMENTS: diff --git a/pkg/botman/botman.go b/pkg/botman/botman.go index 679eb0a0..50dff4f1 100644 --- a/pkg/botman/botman.go +++ b/pkg/botman/botman.go @@ -37,6 +37,7 @@ type ( CustomClientSequence CustomDefinedBot CustomDenyAction + CustomCode JavascriptInjection RecategorizedAkamaiDefinedBot ResponseAction diff --git a/pkg/botman/custom_code.go b/pkg/botman/custom_code.go new file mode 100644 index 00000000..02dfa46f --- /dev/null +++ b/pkg/botman/custom_code.go @@ -0,0 +1,112 @@ +package botman + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + + validation "github.com/go-ozzo/ozzo-validation/v4" +) + +type ( + // The CustomCode interface supports retrieving and updating custom code + CustomCode interface { + GetCustomCode(ctx context.Context, params GetCustomCodeRequest) (map[string]interface{}, error) + + UpdateCustomCode(ctx context.Context, params UpdateCustomCodeRequest) (map[string]interface{}, error) + } + + // GetCustomCodeRequest is used to retrieve custom code + GetCustomCodeRequest struct { + ConfigID int64 + Version int64 + } + + // UpdateCustomCodeRequest is used to modify custom code + UpdateCustomCodeRequest struct { + ConfigID int64 + Version int64 + JsonPayload json.RawMessage + } +) + +// Validate validates a GetCustomCodeRequest. +func (v GetCustomCodeRequest) Validate() error { + return validation.Errors{ + "ConfigID": validation.Validate(v.ConfigID, validation.Required), + "Version": validation.Validate(v.Version, validation.Required), + }.Filter() +} + +// Validate validates an UpdateCustomCodeRequest. +func (v UpdateCustomCodeRequest) Validate() error { + return validation.Errors{ + "ConfigID": validation.Validate(v.ConfigID, validation.Required), + "Version": validation.Validate(v.Version, validation.Required), + "JsonPayload": validation.Validate(v.JsonPayload, validation.Required), + }.Filter() +} + +func (b *botman) GetCustomCode(ctx context.Context, params GetCustomCodeRequest) (map[string]interface{}, error) { + logger := b.Log(ctx) + logger.Debug("GetCustomCode") + + if err := params.Validate(); err != nil { + return nil, fmt.Errorf("%w: %s", ErrStructValidation, err.Error()) + } + + uri := fmt.Sprintf( + "/appsec/v1/configs/%d/versions/%d/advanced-settings/transactional-endpoint-protection/custom-code", + params.ConfigID, + params.Version) + + req, err := http.NewRequestWithContext(ctx, http.MethodGet, uri, nil) + if err != nil { + return nil, fmt.Errorf("failed to create GetCustomCode request: %w", err) + } + + var result map[string]interface{} + resp, err := b.Exec(req, &result) + if err != nil { + return nil, fmt.Errorf("GetCustomCode request failed: %w", err) + } + + if resp.StatusCode != http.StatusOK { + return nil, b.Error(resp) + } + + return result, nil +} + +func (b *botman) UpdateCustomCode(ctx context.Context, params UpdateCustomCodeRequest) (map[string]interface{}, error) { + logger := b.Log(ctx) + logger.Debug("UpdateCustomCode") + + if err := params.Validate(); err != nil { + return nil, fmt.Errorf("%w: %s", ErrStructValidation, err.Error()) + } + + putURL := fmt.Sprintf( + "/appsec/v1/configs/%d/versions/%d/advanced-settings/transactional-endpoint-protection/custom-code", + params.ConfigID, + params.Version, + ) + + req, err := http.NewRequestWithContext(ctx, http.MethodPut, putURL, nil) + if err != nil { + return nil, fmt.Errorf("failed to create UpdateCustomCode request: %w", err) + } + + var result map[string]interface{} + resp, err := b.Exec(req, &result, params.JsonPayload) + if err != nil { + return nil, fmt.Errorf("UpdateCustomCode request failed: %w", err) + } + + if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusCreated && resp.StatusCode != http.StatusNoContent { + return nil, b.Error(resp) + } + + return result, nil +} diff --git a/pkg/botman/custom_code_test.go b/pkg/botman/custom_code_test.go new file mode 100644 index 00000000..31c44fd9 --- /dev/null +++ b/pkg/botman/custom_code_test.go @@ -0,0 +1,205 @@ +package botman + +import ( + "context" + "encoding/json" + "errors" + "net/http" + "net/http/httptest" + "testing" + + "github.com/akamai/AkamaiOPEN-edgegrid-golang/v7/pkg/session" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// Test Get CustomCode +func TestBotman_GetCustomCode(t *testing.T) { + tests := map[string]struct { + params GetCustomCodeRequest + responseStatus int + responseBody string + expectedPath string + expectedResponse map[string]interface{} + withError func(*testing.T, error) + }{ + "200 OK": { + params: GetCustomCodeRequest{ + ConfigID: 43253, + Version: 15, + }, + responseStatus: http.StatusOK, + responseBody: `{"testKey":"testValue3"}`, + expectedPath: "/appsec/v1/configs/43253/versions/15/advanced-settings/transactional-endpoint-protection/custom-code", + expectedResponse: map[string]interface{}{"testKey": "testValue3"}, + }, + "500 internal server error": { + params: GetCustomCodeRequest{ + ConfigID: 43253, + Version: 15, + }, + responseStatus: http.StatusInternalServerError, + responseBody: ` + { + "type": "internal_error", + "title": "Internal Server Error", + "detail": "Error fetching data" + }`, + expectedPath: "/appsec/v1/configs/43253/versions/15/advanced-settings/transactional-endpoint-protection/custom-code", + withError: func(t *testing.T, err error) { + want := &Error{ + Type: "internal_error", + Title: "Internal Server Error", + Detail: "Error fetching data", + StatusCode: http.StatusInternalServerError, + } + assert.True(t, errors.Is(err, want), "want: %s; got: %s", want, err) + }, + }, + "Missing ConfigID": { + params: GetCustomCodeRequest{ + Version: 15, + }, + withError: func(t *testing.T, err error) { + want := ErrStructValidation + assert.True(t, errors.Is(err, want), "want: %s; got: %s", want, err) + assert.Contains(t, err.Error(), "ConfigID") + }, + }, + "Missing Version": { + params: GetCustomCodeRequest{ + ConfigID: 43253, + }, + withError: func(t *testing.T, err error) { + want := ErrStructValidation + assert.True(t, errors.Is(err, want), "want: %s; got: %s", want, err) + assert.Contains(t, err.Error(), "Version") + }, + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + mockServer := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, test.expectedPath, r.URL.String()) + assert.Equal(t, http.MethodGet, r.Method) + w.WriteHeader(test.responseStatus) + _, err := w.Write([]byte(test.responseBody)) + assert.NoError(t, err) + })) + client := mockAPIClient(t, mockServer) + result, err := client.GetCustomCode(context.Background(), test.params) + if test.withError != nil { + test.withError(t, err) + return + } + require.NoError(t, err) + assert.Equal(t, test.expectedResponse, result) + }) + } +} + +// Test Update CustomCode. +func TestBotman_UpdateCustomCode(t *testing.T) { + tests := map[string]struct { + params UpdateCustomCodeRequest + responseStatus int + responseBody string + expectedPath string + expectedResponse map[string]interface{} + withError func(*testing.T, error) + }{ + "200 Success": { + params: UpdateCustomCodeRequest{ + ConfigID: 43253, + Version: 15, + JsonPayload: json.RawMessage(`{"testKey":"testValue3"}`), + }, + responseStatus: http.StatusOK, + responseBody: `{"testKey":"testValue3"}`, + expectedResponse: map[string]interface{}{"testKey": "testValue3"}, + expectedPath: "/appsec/v1/configs/43253/versions/15/advanced-settings/transactional-endpoint-protection/custom-code", + }, + "500 internal server error": { + params: UpdateCustomCodeRequest{ + ConfigID: 43253, + Version: 15, + JsonPayload: json.RawMessage(`{"testKey":"testValue3"}`), + }, + responseStatus: http.StatusInternalServerError, + responseBody: ` + { + "type": "internal_error", + "title": "Internal Server Error", + "detail": "Error updating data" + }`, + expectedPath: "/appsec/v1/configs/43253/versions/15/advanced-settings/transactional-endpoint-protection/custom-code", + withError: func(t *testing.T, err error) { + want := &Error{ + Type: "internal_error", + Title: "Internal Server Error", + Detail: "Error updating data", + StatusCode: http.StatusInternalServerError, + } + assert.True(t, errors.Is(err, want), "want: %s; got: %s", want, err) + }, + }, + "Missing ConfigID": { + params: UpdateCustomCodeRequest{ + Version: 15, + JsonPayload: json.RawMessage(`{"testKey":"testValue3"}`), + }, + withError: func(t *testing.T, err error) { + want := ErrStructValidation + assert.True(t, errors.Is(err, want), "want: %s; got: %s", want, err) + assert.Contains(t, err.Error(), "ConfigID") + }, + }, + "Missing Version": { + params: UpdateCustomCodeRequest{ + ConfigID: 43253, + JsonPayload: json.RawMessage(`{"testKey":"testValue3"}`), + }, + withError: func(t *testing.T, err error) { + want := ErrStructValidation + assert.True(t, errors.Is(err, want), "want: %s; got: %s", want, err) + assert.Contains(t, err.Error(), "Version") + }, + }, + "Missing JsonPayload": { + params: UpdateCustomCodeRequest{ + ConfigID: 43253, + Version: 15, + }, + withError: func(t *testing.T, err error) { + want := ErrStructValidation + assert.True(t, errors.Is(err, want), "want: %s; got: %s", want, err) + assert.Contains(t, err.Error(), "JsonPayload") + }, + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + mockServer := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, test.expectedPath, r.URL.String()) + assert.Equal(t, http.MethodPut, r.Method) + w.WriteHeader(test.responseStatus) + if len(test.responseBody) > 0 { + _, err := w.Write([]byte(test.responseBody)) + assert.NoError(t, err) + } + })) + client := mockAPIClient(t, mockServer) + result, err := client.UpdateCustomCode( + session.ContextWithOptions( + context.Background()), test.params) + if test.withError != nil { + test.withError(t, err) + return + } + require.NoError(t, err) + assert.Equal(t, test.expectedResponse, result) + }) + } +} diff --git a/pkg/botman/mocks.go b/pkg/botman/mocks.go index 4181b8e0..4cfa2d63 100644 --- a/pkg/botman/mocks.go +++ b/pkg/botman/mocks.go @@ -564,3 +564,19 @@ func (p *Mock) UpdateCustomClientSequence(ctx context.Context, params UpdateCust } return args.Get(0).(*CustomClientSequenceResponse), nil } + +func (p *Mock) GetCustomCode(ctx context.Context, params GetCustomCodeRequest) (map[string]interface{}, error) { + args := p.Called(ctx, params) + if args.Error(1) != nil { + return nil, args.Error(1) + } + return args.Get(0).(map[string]interface{}), nil +} + +func (p *Mock) UpdateCustomCode(ctx context.Context, params UpdateCustomCodeRequest) (map[string]interface{}, error) { + args := p.Called(ctx, params) + if args.Error(1) != nil { + return nil, args.Error(1) + } + return args.Get(0).(map[string]interface{}), nil +} From 8553af474189ba6a4770ca579fa99ef104d005fe Mon Sep 17 00:00:00 2001 From: Hamza Bentebbaa Date: Tue, 14 Nov 2023 15:47:13 +0000 Subject: [PATCH 2/3] SECKSD-23178 update ip geo to support asn --- CHANGELOG.md | 5 +- pkg/appsec/ip_geo.go | 9 +- pkg/appsec/ip_geo_test.go | 144 ++++++++++++++++++----- pkg/appsec/testdata/TestIPGeo/IPGeo.json | 25 ---- 4 files changed, 128 insertions(+), 55 deletions(-) delete mode 100644 pkg/appsec/testdata/TestIPGeo/IPGeo.json diff --git a/CHANGELOG.md b/CHANGELOG.md index d72b5e06..45b4b0db 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,9 @@ #### FEATURES/ENHANCEMENTS: +* APPSEC + * Added `ASNControls` field to `UpdateIPGeoRequest` and `IPGeoFirewall` structs to support firewall blocking by ASN client lists + * BOTMAN * Added API support for Custom Code - read and update @@ -19,7 +22,7 @@ * IAM * Phone number is no longer required for IAM user for `CreateUser` and `UpdateUserInfo` methods - + ## 7.3.0 (September 19, 2023) #### FEATURES/ENHANCEMENTS: diff --git a/pkg/appsec/ip_geo.go b/pkg/appsec/ip_geo.go index 1b0bf97f..1dc33a7c 100644 --- a/pkg/appsec/ip_geo.go +++ b/pkg/appsec/ip_geo.go @@ -40,7 +40,12 @@ type ( BlockedIPNetworkLists *IPGeoNetworkLists `json:"blockedIPNetworkLists,omitempty"` } - // IPGeoIPControls is used to specify IP or GEO network lists to be blocked or allowed. + // IPGeoASNControls is used to specify ASN network lists to be blocked. + IPGeoASNControls struct { + BlockedIPNetworkLists *IPGeoNetworkLists `json:"blockedIPNetworkLists,omitempty"` + } + + // IPGeoIPControls is used to specify IP, GEO or ASN network lists to be blocked or allowed. IPGeoIPControls struct { AllowedIPNetworkLists *IPGeoNetworkLists `json:"allowedIPNetworkLists,omitempty"` BlockedIPNetworkLists *IPGeoNetworkLists `json:"blockedIPNetworkLists,omitempty"` @@ -59,6 +64,7 @@ type ( Block string `json:"block"` GeoControls *IPGeoGeoControls `json:"geoControls,omitempty"` IPControls *IPGeoIPControls `json:"ipControls,omitempty"` + ASNControls *IPGeoASNControls `json:"asnControls,omitempty"` UkraineGeoControls *UkraineGeoControl `json:"ukraineGeoControl,omitempty"` } @@ -67,6 +73,7 @@ type ( Block string `json:"block"` GeoControls *IPGeoGeoControls `json:"geoControls,omitempty"` IPControls *IPGeoIPControls `json:"ipControls,omitempty"` + ASNControls *IPGeoASNControls `json:"asnControls,omitempty"` UkraineGeoControls *UkraineGeoControl `json:"ukraineGeoControl,omitempty"` } diff --git a/pkg/appsec/ip_geo_test.go b/pkg/appsec/ip_geo_test.go index 577a655c..a24ae984 100644 --- a/pkg/appsec/ip_geo_test.go +++ b/pkg/appsec/ip_geo_test.go @@ -2,7 +2,6 @@ package appsec import ( "context" - "encoding/json" "errors" "net/http" "net/http/httptest" @@ -15,13 +14,6 @@ import ( // Test IPGeo func TestAppSec_GetIPGeo(t *testing.T) { - - result := GetIPGeoResponse{} - - respData := compactJSON(loadFixtureBytes("testdata/TestIPGeo/IPGeo.json")) - err := json.Unmarshal([]byte(respData), &result) - require.NoError(t, err) - tests := map[string]struct { params GetIPGeoRequest responseStatus int @@ -36,10 +28,64 @@ func TestAppSec_GetIPGeo(t *testing.T) { Version: 15, PolicyID: "AAAA_81230", }, - responseStatus: http.StatusOK, - responseBody: respData, - expectedPath: "/appsec/v1/configs/43253/versions/15/security-policies/AAAA_81230/ip-geo-firewall", - expectedResponse: &result, + responseStatus: http.StatusOK, + responseBody: `{ + "block": "blockSpecificIPGeo", + "asnControls": { + "blockedIPNetworkLists": { + "networkList": [ + "12345_ASNTEST" + ] + } + }, + "geoControls": { + "blockedIPNetworkLists": { + "networkList": [ + "72138_TEST1" + ] + } + }, + "ipControls": { + "allowedIPNetworkLists": { + "networkList": [ + "56921_TEST" + ] + }, + "blockedIPNetworkLists": { + "networkList": [ + "53712_TESTLIST123" + ] + } + }, + "ukraineGeoControl": { + "action": "alert" + } + }`, + expectedPath: "/appsec/v1/configs/43253/versions/15/security-policies/AAAA_81230/ip-geo-firewall", + expectedResponse: &GetIPGeoResponse{ + Block: "blockSpecificIPGeo", + GeoControls: &IPGeoGeoControls{ + BlockedIPNetworkLists: &IPGeoNetworkLists{ + NetworkList: []string{"72138_TEST1"}, + }, + }, + IPControls: &IPGeoIPControls{ + AllowedIPNetworkLists: &IPGeoNetworkLists{ + NetworkList: []string{"56921_TEST"}, + }, + BlockedIPNetworkLists: &IPGeoNetworkLists{ + NetworkList: []string{"53712_TESTLIST123"}, + }, + }, + ASNControls: &IPGeoASNControls{ + BlockedIPNetworkLists: &IPGeoNetworkLists{ + NetworkList: []string{"12345_ASNTEST"}, + }, + }, + UkraineGeoControls: &UkraineGeoControl{ + Action: "alert", + }, + }, }, "500 internal server error": { params: GetIPGeoRequest{ @@ -87,18 +133,6 @@ func TestAppSec_GetIPGeo(t *testing.T) { // Test Update IPGeo. func TestAppSec_UpdateIPGeo(t *testing.T) { - result := UpdateIPGeoResponse{} - - respData := compactJSON(loadFixtureBytes("testdata/TestIPGeo/IPGeo.json")) - err := json.Unmarshal([]byte(respData), &result) - require.NoError(t, err) - - req := UpdateIPGeoRequest{} - - reqData := compactJSON(loadFixtureBytes("testdata/TestIPGeo/IPGeo.json")) - err = json.Unmarshal([]byte(reqData), &req) - require.NoError(t, err) - tests := map[string]struct { params UpdateIPGeoRequest responseStatus int @@ -117,10 +151,64 @@ func TestAppSec_UpdateIPGeo(t *testing.T) { headers: http.Header{ "Content-Type": []string{"application/json;charset=UTF-8"}, }, - responseStatus: http.StatusCreated, - responseBody: respData, - expectedResponse: &result, - expectedPath: "/appsec/v1/configs/43253/versions/15/security-policies/AAAA_81230/ip-geo-firewall", + responseStatus: http.StatusCreated, + responseBody: `{ + "block": "blockSpecificIPGeo", + "asnControls": { + "blockedIPNetworkLists": { + "networkList": [ + "12345_ASNTEST" + ] + } + }, + "geoControls": { + "blockedIPNetworkLists": { + "networkList": [ + "72138_TEST1" + ] + } + }, + "ipControls": { + "allowedIPNetworkLists": { + "networkList": [ + "56921_TEST" + ] + }, + "blockedIPNetworkLists": { + "networkList": [ + "53712_TESTLIST123" + ] + } + }, + "ukraineGeoControl": { + "action": "alert" + } + }`, + expectedResponse: &UpdateIPGeoResponse{ + Block: "blockSpecificIPGeo", + GeoControls: &IPGeoGeoControls{ + BlockedIPNetworkLists: &IPGeoNetworkLists{ + NetworkList: []string{"72138_TEST1"}, + }, + }, + IPControls: &IPGeoIPControls{ + AllowedIPNetworkLists: &IPGeoNetworkLists{ + NetworkList: []string{"56921_TEST"}, + }, + BlockedIPNetworkLists: &IPGeoNetworkLists{ + NetworkList: []string{"53712_TESTLIST123"}, + }, + }, + ASNControls: &IPGeoASNControls{ + BlockedIPNetworkLists: &IPGeoNetworkLists{ + NetworkList: []string{"12345_ASNTEST"}, + }, + }, + UkraineGeoControls: &UkraineGeoControl{ + Action: "alert", + }, + }, + expectedPath: "/appsec/v1/configs/43253/versions/15/security-policies/AAAA_81230/ip-geo-firewall", }, "500 internal server error": { params: UpdateIPGeoRequest{ diff --git a/pkg/appsec/testdata/TestIPGeo/IPGeo.json b/pkg/appsec/testdata/TestIPGeo/IPGeo.json deleted file mode 100644 index a3dc08d3..00000000 --- a/pkg/appsec/testdata/TestIPGeo/IPGeo.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "block": "blockSpecificIPGeo", - "geoControls": { - "blockedIPNetworkLists": { - "networkList": [ - "72138_TEST1" - ] - } - }, - "ipControls": { - "allowedIPNetworkLists": { - "networkList": [ - "56921_TEST" - ] - }, - "blockedIPNetworkLists": { - "networkList": [ - "53712_TESTLIST123" - ] - } - }, - "ukraineGeoControl": { - "action": "alert" - } -} \ No newline at end of file From 696bec84aed86c24dd57830756f3554d82d2946b Mon Sep 17 00:00:00 2001 From: "Dzhafarov, Dawid" Date: Wed, 22 Nov 2023 16:32:16 +0100 Subject: [PATCH 3/3] DXE-3169 Add changelog entry for 7.5.0 release --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 45b4b0db..f7a20e2f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # EDGEGRID GOLANG RELEASE NOTES -## X.X.X (November xx, 2023) +## 7.5.0 (November 28, 2023) #### FEATURES/ENHANCEMENTS: