Skip to content

Commit 615a684

Browse files
authored
Terraform configuration capability for Account Takeover (#305)
* Adding terraform support for ATO site allowlist * Adding tests for ATO allowlist client * Adding tests for ATO allowlist resource * Adding edge cases check for ATO allowlist configuration * Assign description to ATO allowlist * Assign description to ATO allowlist * Changing errors in ATO to match the global imperva error format * Adding ATO mitigation configuration client * Adding ATO mitigation configuration - Additionally making changes to ATO allowlist log messages for better diagnostics * Removing updatedAt test for allowlist as it is not deterministic * Update tests to not depend on updatedAt timestamp for ATO allowlist * Adding documentation for ATO terraform usage * Updating status code requirements for ATO * Update ATO site mitigation configuration documentation * Update ATO mitigation client to be deterministic in the order of endpoints. We solve this by using each endpoint mitigation configuration to have its own ID * Update ATO mitigation client to be deterministic in the order of endpoints. We solve this by using each endpoint mitigation configuration to have its own ID * Refactor site level mitigation to allow endpoint level mitigation and update docs * Make account id optional for ato mitigation configuration * Adding documentation changes - Updating ATO resources to handle site doesn't exist error
1 parent 8b4f95a commit 615a684

13 files changed

+1573
-0
lines changed

examples/example.tf

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -753,4 +753,25 @@ resource "incapsula_api_security_endpoint_config" "example-api-security-endpoint
753753
method = "GET"
754754
invalid_param_value_violation_action = "BLOCK_IP"
755755
missing_param_violation_action = "BLOCK_IP"
756+
}
757+
758+
####################################################################
759+
# ATO Allowlist
760+
####################################################################
761+
762+
resource "incapsula_ato_site_allowlist" "example-ato-site-allowlist-test" {
763+
site_id = incapsula_site.example-site.id
764+
allowlist = [ { "ip": "192.10.20.0", "mask": "24", "desc": "Test IP 1"}, { "ip": "192.10.20.1", "mask": "8", "desc": "Test IP 2" } ]
765+
}
766+
767+
####################################################################
768+
# ATO Mitigation configuration
769+
####################################################################
770+
771+
resource "incapsula_ato_endpoint_mitigation_configuration" "example-ato-endpoint-1-mitigation-configuration" {
772+
site_id = incapsula_site.example-site.id
773+
endpoint_id = "5000"
774+
low_action = "NONE"
775+
medium_action = "NONE"
776+
high_action = "NONE"
756777
}

incapsula/client_ato_allowlist.go

Lines changed: 266 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,266 @@
1+
package incapsula
2+
3+
import (
4+
"encoding/json"
5+
"fmt"
6+
"io"
7+
"log"
8+
"net/http"
9+
"time"
10+
)
11+
12+
const endpointATOSiteBase = "/ato/v2/sites"
13+
const endpointAtoAllowlist = "/allowlist"
14+
15+
type AtoAllowlistItem struct {
16+
Ip string `json:"ip"`
17+
Mask string `json:"mask"`
18+
Desc string `json:"desc"`
19+
}
20+
21+
type ATOAllowlistDTO struct {
22+
AccountId int `json:"accountId"`
23+
SiteId int `json:"siteId"`
24+
Allowlist []AtoAllowlistItem `json:"allowlist"`
25+
}
26+
27+
func formEmptyAllowlistDTO(accountId, siteId int) *ATOAllowlistDTO {
28+
return &ATOAllowlistDTO{
29+
AccountId: accountId,
30+
SiteId: siteId,
31+
Allowlist: make([]AtoAllowlistItem, 0),
32+
}
33+
}
34+
35+
func (atoAllowlistDTO *ATOAllowlistDTO) toMap() (map[string]interface{}, error) {
36+
37+
// Initialize the data map that terraform uses
38+
var atoAllowlistMap = make(map[string]interface{})
39+
40+
// Set site id
41+
atoAllowlistMap["site_id"] = atoAllowlistDTO.SiteId
42+
atoAllowlistMap["accountId"] = atoAllowlistDTO.AccountId
43+
44+
// Assign the allowlist if present to the terraform compatible map
45+
if atoAllowlistDTO.Allowlist != nil {
46+
47+
atoAllowlistMap["allowlist"] = make([]map[string]interface{}, len(atoAllowlistDTO.Allowlist))
48+
49+
for i, allowlistItem := range atoAllowlistDTO.Allowlist {
50+
51+
atoAllowlistMap["allowlist"].([]map[string]interface{})[i] = map[string]interface{}{
52+
"ip": allowlistItem.Ip,
53+
"mask": allowlistItem.Mask,
54+
"desc": allowlistItem.Desc,
55+
}
56+
atoAllowlistMap["allowlist"].([]map[string]interface{})[i] = atoAllowlistMap["allowlist"].([]map[string]interface{})[i]
57+
}
58+
59+
} else {
60+
atoAllowlistMap["allowlist"] = make([]interface{}, 0)
61+
}
62+
63+
return atoAllowlistMap, nil
64+
}
65+
66+
func formAtoAllowlistDTOFromMap(atoAllowlistMap map[string]interface{}) (*ATOAllowlistDTO, error) {
67+
68+
atoAllowlistDTO := ATOAllowlistDTO{}
69+
70+
// Validate site_id
71+
switch atoAllowlistMap["site_id"].(type) {
72+
case int:
73+
break
74+
default:
75+
return nil, fmt.Errorf("site_id should be of type int")
76+
}
77+
78+
// validate account_id
79+
switch atoAllowlistMap["account_id"].(type) {
80+
case int:
81+
break
82+
default:
83+
return nil, fmt.Errorf("account_id should be of type int")
84+
}
85+
86+
// Assign site ID
87+
atoAllowlistDTO.SiteId = atoAllowlistMap["site_id"].(int)
88+
89+
// Assign account ID
90+
atoAllowlistDTO.AccountId = atoAllowlistMap["account_id"].(int)
91+
92+
// Assign the allowlist
93+
if atoAllowlistMap["allowlist"] == nil {
94+
atoAllowlistDTO.Allowlist = make([]AtoAllowlistItem, 0)
95+
}
96+
97+
// Verify that the allowlist is an array
98+
if _, ok := atoAllowlistMap["allowlist"].([]interface{}); !ok {
99+
return nil, fmt.Errorf("allowlist should have type array")
100+
}
101+
102+
allowlistItemsInMap := atoAllowlistMap["allowlist"].([]interface{})
103+
atoAllowlistDTO.Allowlist = make([]AtoAllowlistItem, len(allowlistItemsInMap))
104+
105+
// Convert each allowlist entry in the map to the allowlist item for the DTO
106+
for i, allowlistItemMap := range allowlistItemsInMap {
107+
allowListItemMap := allowlistItemMap.(map[string]interface{})
108+
109+
// Initialize allowlist item
110+
allowlistItem := AtoAllowlistItem{}
111+
112+
// Check that IP is not empty
113+
if allowListItemMap["ip"] == nil {
114+
return nil, fmt.Errorf("IP cannot be empty in allowlist items")
115+
}
116+
117+
allowlistItem.Ip = allowListItemMap["ip"].(string)
118+
119+
// Extract description
120+
if allowListItemMap["desc"] != nil {
121+
allowlistItem.Desc = allowListItemMap["desc"].(string)
122+
}
123+
124+
// Extract subnet from map
125+
if allowListItemMap["mask"] != nil {
126+
allowlistItem.Mask = allowListItemMap["mask"].(string)
127+
}
128+
129+
atoAllowlistDTO.Allowlist[i] = allowlistItem
130+
}
131+
132+
return &atoAllowlistDTO, nil
133+
}
134+
135+
func (c *Client) GetAtoSiteAllowlistWithRetries(accountId, siteId int) (*ATOAllowlistDTO, int, error) {
136+
// Since the newly created site can take upto 30 seconds to be fully configured, we per.si a simple backoff
137+
var backoffSchedule = []time.Duration{
138+
5 * time.Second,
139+
15 * time.Second,
140+
30 * time.Second,
141+
60 * time.Second,
142+
75 * time.Second,
143+
}
144+
var lastError error
145+
146+
for _, backoff := range backoffSchedule {
147+
atoAllowlistDTO, status, err := c.GetAtoSiteAllowlist(accountId, siteId)
148+
if err == nil {
149+
return atoAllowlistDTO, status, nil
150+
}
151+
lastError = err
152+
time.Sleep(backoff)
153+
}
154+
return nil, 0, lastError
155+
}
156+
157+
func (c *Client) GetAtoSiteAllowlist(accountId, siteId int) (*ATOAllowlistDTO, int, error) {
158+
log.Printf("[INFO] Getting IP allowlist for (Site Id: %d)\n", siteId)
159+
160+
// Get request to ATO
161+
var reqURL string
162+
if accountId == 0 {
163+
reqURL = fmt.Sprintf("%s%s/%d%s", c.config.BaseURLAPI, endpointATOSiteBase, siteId, endpointAtoAllowlist)
164+
} else {
165+
reqURL = fmt.Sprintf("%s%s/%d%s?caid=%d", c.config.BaseURLAPI, endpointATOSiteBase, siteId, endpointAtoAllowlist, accountId)
166+
}
167+
resp, err := c.DoJsonRequestWithHeaders(http.MethodGet, reqURL, nil, ReadATOSiteAllowlistOperation)
168+
if err != nil {
169+
return nil, 0, fmt.Errorf("[Error] Error executing get ATO allowlist request for site with id %d: %s", siteId, err)
170+
}
171+
172+
// Read the body
173+
defer resp.Body.Close()
174+
responseBody, err := io.ReadAll(resp.Body)
175+
176+
// Dump JSON
177+
log.Printf("[DEBUG] ATO allowlist JSON response: %s\n", string(responseBody))
178+
179+
// Parse the JSON
180+
var atoAllowlistItems []AtoAllowlistItem
181+
var atoAllowlistDTO ATOAllowlistDTO
182+
err = json.Unmarshal(responseBody, &atoAllowlistItems)
183+
atoAllowlistDTO.SiteId = siteId
184+
atoAllowlistDTO.AccountId = accountId
185+
atoAllowlistDTO.Allowlist = atoAllowlistItems
186+
if err != nil {
187+
return nil, resp.StatusCode, fmt.Errorf("[Error] Q Error parsing ATO allowlist response for site with ID: %d %s\nresponse: %s", siteId, err, string(responseBody))
188+
}
189+
190+
return &atoAllowlistDTO, resp.StatusCode, nil
191+
}
192+
193+
func (c *Client) UpdateATOSiteAllowlistWithRetries(atoSiteAllowlistDTO *ATOAllowlistDTO) error {
194+
// Since the newly created site can take upto 30 seconds to be fully configured, we perform a simple backoff
195+
var backoffSchedule = []time.Duration{
196+
5 * time.Second,
197+
15 * time.Second,
198+
30 * time.Second,
199+
60 * time.Second,
200+
75 * time.Second,
201+
}
202+
var lastError error
203+
204+
for _, backoff := range backoffSchedule {
205+
err := c.UpdateATOSiteAllowlist(atoSiteAllowlistDTO)
206+
if err == nil {
207+
return nil
208+
}
209+
lastError = err
210+
time.Sleep(backoff)
211+
}
212+
return lastError
213+
}
214+
215+
func (c *Client) UpdateATOSiteAllowlist(atoSiteAllowlistDTO *ATOAllowlistDTO) error {
216+
217+
log.Printf("[INFO] Updating ATO IP allowlist for (Site Id: %d)\n", atoSiteAllowlistDTO.SiteId)
218+
219+
// Form the request body
220+
atoAllowlistJSON, err := json.Marshal(atoSiteAllowlistDTO.Allowlist)
221+
222+
// verify site ID and account ID are not the default value for int type
223+
if atoSiteAllowlistDTO.SiteId == 0 {
224+
return fmt.Errorf("site_id is not specified in updating ATO allowlist")
225+
}
226+
var reqURL string
227+
if atoSiteAllowlistDTO.AccountId == 0 {
228+
reqURL = fmt.Sprintf("%s%s/%d%s", c.config.BaseURLAPI, endpointATOSiteBase, atoSiteAllowlistDTO.SiteId, endpointAtoAllowlist)
229+
} else {
230+
reqURL = fmt.Sprintf("%s%s/%d%s?caid=%d", c.config.BaseURLAPI, endpointATOSiteBase, atoSiteAllowlistDTO.SiteId, endpointAtoAllowlist, atoSiteAllowlistDTO.AccountId)
231+
}
232+
233+
// Update request to ATO
234+
response, err := c.DoJsonRequestWithHeaders(http.MethodPut, reqURL, atoAllowlistJSON, UpdateATOSiteAllowlistOperation)
235+
236+
// Read the body
237+
defer response.Body.Close()
238+
responseBody, err := io.ReadAll(response.Body)
239+
240+
log.Printf("Updated ATO allowlist with response : %s", responseBody)
241+
242+
// Handle request error
243+
if err != nil {
244+
return fmt.Errorf("[Error] Error executing update ATO allowlist request for site with id %d: %s", atoSiteAllowlistDTO.SiteId, err)
245+
}
246+
247+
if response.StatusCode >= http.StatusBadRequest {
248+
return fmt.Errorf("[Error] Error executing update ATO allowlist request for site with status %d: %d", response.StatusCode, atoSiteAllowlistDTO.SiteId)
249+
}
250+
251+
return nil
252+
253+
}
254+
255+
func (c *Client) DeleteATOSiteAllowlist(accountId, siteId int) error {
256+
log.Printf("[INFO] Deleting IP allowlist for (Site Id: %d)\n", siteId)
257+
258+
err := c.UpdateATOSiteAllowlist(formEmptyAllowlistDTO(accountId, siteId))
259+
260+
// Handle request error
261+
if err != nil {
262+
return fmt.Errorf("[Error] Error executing delete ATO allowlist request for site with id %d: %s", siteId, err)
263+
}
264+
265+
return nil
266+
}

0 commit comments

Comments
 (0)