Skip to content

Commit 4a2f3b4

Browse files
authored
Merge pull request #13 from aquasecurity/offboard_and_filter
Support offboarding and filtered onboarding
2 parents abbb0dd + 366f41c commit 4a2f3b4

File tree

7 files changed

+182
-75
lines changed

7 files changed

+182
-75
lines changed

examples/organization-dedicated-project/main.tf

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,7 @@ module "aqua_gcp_projects_attachment" {
115115
project_id = each.value
116116
dedicated_project = local.dedicated
117117
labels = local.aqua_custom_labels
118+
onboarding_organization_projects = local.projects_list
118119
onboarding_create_role_id = module.aqua_gcp_onboarding.create_role_id # Referencing outputs from the onboarding module
119120
onboarding_cspm_service_account_key = module.aqua_gcp_onboarding.cspm_service_account_key # Referencing outputs from the onboarding module
120121
onboarding_service_account_email = module.aqua_gcp_onboarding.service_account_email # Referencing outputs from the onboarding module

examples/organization-same-project-list/main.tf

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,7 @@ module "aqua_gcp_projects_attachment" {
9090
project_id = each.value
9191
dedicated_project = local.dedicated
9292
labels = local.aqua_custom_labels
93+
onboarding_organization_projects = local.projects_list
9394
onboarding_create_role_id = module.aqua_gcp_onboarding[each.value].create_role_id # Referencing outputs from the onboarding module
9495
onboarding_service_account_email = module.aqua_gcp_onboarding[each.value].service_account_email # Referencing outputs from the onboarding module
9596
onboarding_cspm_service_account_key = module.aqua_gcp_cspm_iam.cspm_service_account_key # Referencing outputs from the cspm_iam module

examples/organization-same-project/main.tf

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,7 @@ module "aqua_gcp_projects_attachment" {
106106
project_id = each.value
107107
dedicated_project = local.dedicated
108108
labels = local.aqua_custom_labels
109+
onboarding_organization_projects = local.projects_list
109110
onboarding_create_role_id = module.aqua_gcp_onboarding[each.value].create_role_id # Referencing outputs from the onboarding module
110111
onboarding_service_account_email = module.aqua_gcp_onboarding[each.value].service_account_email # Referencing outputs from the onboarding module
111112
onboarding_cspm_service_account_key = module.aqua_gcp_cspm_iam.cspm_service_account_key # Referencing outputs from the cspm_iam module

modules/project_attachment/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ No modules.
6060
| <a name="input_labels"></a> [labels](#input\_labels) | Additional resource labels to will be send to the Autoconnect API | `map(string)` | `{}` | no |
6161
| <a name="input_onboarding_create_role_id"></a> [onboarding\_create\_role\_id](#input\_onboarding\_create\_role\_id) | ID of the create role that has been created in the root module. This should be referenced from the root onboarding module. | `string` | n/a | yes |
6262
| <a name="input_onboarding_cspm_service_account_key"></a> [onboarding\_cspm\_service\_account\_key](#input\_onboarding\_cspm\_service\_account\_key) | The base64 encoded Key of the CSPM service account that has been created or supplied (in case that var.create\_service\_account is false) in the root module. This should be referenced from the root onboarding module only for organization dedicated onboarding. | `string` | `""` | no |
63+
| <a name="input_onboarding_organization_projects"></a> [onboarding\_organization\_projects](#input\_onboarding\_organization\_projects) | List of all organization IDs (This should be provided only if type of onboarding is 'organization') | `list(string)` | `[]` | no |
6364
| <a name="input_onboarding_project_id"></a> [onboarding\_project\_id](#input\_onboarding\_project\_id) | Google Cloud Project ID has been created in the root module. This should be referenced from the root onboarding module. | `string` | n/a | yes |
6465
| <a name="input_onboarding_project_number"></a> [onboarding\_project\_number](#input\_onboarding\_project\_number) | Google Cloud Project Number has been created in the root module. This should be referenced from the root onboarding module. | `string` | n/a | yes |
6566
| <a name="input_onboarding_service_account_email"></a> [onboarding\_service\_account\_email](#input\_onboarding\_service\_account\_email) | Email of the service account that has been created or fetched (in case that var.create\_service\_account is false) in the root module. This should be referenced from the root onboarding module. | `string` | n/a | yes |

modules/project_attachment/trigger-gcp.py

Lines changed: 170 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -6,83 +6,179 @@
66
import http.client
77
import ssl
88

9-
timestamp = str(int(time.time() * 1000))
10-
119
# Retrieve the query parameters
12-
query = json.loads(sys.stdin.read())
13-
api_key = query.get("api_key", "")
14-
api_secret = query.get("api_secret", "")
15-
autoconnect_url = query.get("autoconnect_url", "")
16-
service_account_key = query.get("service_account_key", "")
17-
client_config = query.get("client_config", "")
18-
cspm_group_id = query.get("cspm_group_id", "")
19-
configuration_id = query.get("configuration_id", "")
20-
scan_mode = query.get("scan_mode", "")
21-
project_id = query.get("project_id", "")
22-
organization_id = query.get("organization_id", "")
23-
organization_onboarding_str = query.get("organization_onboarding", "")
24-
additional_resource_tags = query.get("additional_resource_tags", "")
25-
26-
if organization_onboarding_str.lower() == "true":
27-
organization_onboarding = True
28-
elif organization_onboarding_str.lower() == "false":
29-
organization_onboarding = False
30-
31-
parsed_json = json.loads(service_account_key)
32-
type = parsed_json["type"]
33-
client_email = parsed_json["client_email"]
34-
private_key = parsed_json["private_key"].replace("\n", "\\n")
35-
name = f'{project_id}:{client_email}'
36-
37-
def get_signature(aqua_secret, tstmp, path, method, body=''):
38-
enc = tstmp + method + path + body
10+
query = json.loads(sys.stdin.read())
11+
aqua_api_key = query.get("api_key", "")
12+
aqua_api_secret = query.get("api_secret", "")
13+
aqua_autoconnect_url = query.get("autoconnect_url", "").split("//")[1]
14+
service_account_key = query.get("service_account_key", "")
15+
client_config = query.get("client_config", "")
16+
cspm_group_id = query.get("cspm_group_id", "")
17+
configuration_id = query.get("configuration_id", "")
18+
scan_mode = query.get("scan_mode", "")
19+
project_id = query.get("project_id", "")
20+
organization_onboarding = query.get("organization_onboarding", "") == "True"
21+
organization_id = query.get("organization_id", "")
22+
organization_projects = query.get("organization_projects", "").split(",")
23+
additional_resource_tags = query.get("additional_resource_tags", "")
24+
25+
26+
def get_signature(api_secret, timestamp, path, method, body=''):
27+
enc = timestamp + method + path + body
3928
enc_b = bytes(enc, 'utf-8')
40-
secret = bytes(aqua_secret, 'utf-8')
29+
secret = bytes(api_secret, 'utf-8')
4130
sig = hmac.new(secret, enc_b, hashlib.sha256).hexdigest()
4231
return sig
4332

44-
internal_signature = get_signature(api_secret, timestamp, "/v2/internal_apikeys", "GET")
45-
46-
body_cspm = '{"autoconnect":true,"cloud":"google","connection":{"google":{"client_email":"' + client_email + '","private_key":"' + private_key + '","project_id":"' + project_id + '","type":"' + type + '"}},"group_id":' + str(int(cspm_group_id)) + ',"name":"' + name + '"}'
47-
48-
49-
body = json.dumps({
50-
"configuration_id": configuration_id,
51-
"cspm_group_id": int(cspm_group_id),
52-
"organization_id": organization_id,
53-
"project_to_onboard": project_id,
54-
"payload": service_account_key,
55-
"payload_vm": client_config,
56-
"scan_mode": scan_mode,
57-
"organization_onboarding": organization_onboarding,
58-
"additional_resource_tags": additional_resource_tags,
59-
"project_to_onboard": project_id,
60-
"deployment_method": "Terraform"
61-
})
62-
63-
signature_cspm_keys = get_signature(api_secret, timestamp, "/v2/keys", "POST", body_cspm)
64-
65-
headers = {
66-
"X-API-Key": api_key,
67-
"X-Authenticate-Api-Key-Signature": internal_signature,
68-
"X-Register-New-Cspm-Signature": signature_cspm_keys,
69-
"X-Timestamp": timestamp,
70-
"Content-Type": "application/json"
71-
}
72-
73-
conn = http.client.HTTPSConnection(autoconnect_url.split("//")[1], context = ssl._create_unverified_context())
74-
path = "/discover/google"
75-
method = "POST"
76-
77-
conn.request(method, path, body=body, headers=headers)
78-
response = conn.getresponse()
79-
onboarding_status = 'received response: status {}, body: {}'.format(response.status, response.read().decode("utf-8"))
80-
81-
conn.close()
82-
83-
84-
output = {
85-
"status": onboarding_status
86-
}
8733

88-
print(json.dumps(output))
34+
def get_headers(cspm_key, cspm_method='GET', body_cspm=''):
35+
timestamp = str(int(time.time() * 1000))
36+
internal_signature = get_signature(aqua_api_secret, timestamp, "/v2/internal_apikeys", method="GET")
37+
38+
cspm_path = "/v2/keys"
39+
if cspm_key is not None:
40+
cspm_path += "/" + cspm_key
41+
42+
signature_cspm_keys = get_signature(
43+
api_secret=aqua_api_secret,
44+
timestamp =timestamp,
45+
path =cspm_path,
46+
method =cspm_method,
47+
body =body_cspm
48+
)
49+
50+
headers = {
51+
"X-API-Key": aqua_api_key,
52+
"X-Authenticate-Api-Key-Signature": internal_signature,
53+
"X-Register-New-Cspm-Signature": signature_cspm_keys,
54+
"X-Timestamp": timestamp,
55+
"Content-Type": "application/json"
56+
}
57+
58+
return headers
59+
60+
61+
def http_request(url, path, method, headers, body):
62+
connection = http.client.HTTPSConnection(url, context=ssl._create_unverified_context())
63+
connection.request(method, path, body, headers)
64+
response = connection.getresponse()
65+
data = {"status": response.status, "body": response.read().decode('utf-8'), "reason": response.reason}
66+
connection.close()
67+
68+
return data
69+
70+
71+
def trigger_discovery():
72+
parsed_json = json.loads(service_account_key)
73+
project_type = parsed_json["type"]
74+
client_email = parsed_json["client_email"]
75+
private_key = parsed_json["private_key"].replace("\n", "\\n")
76+
name = f'{project_id}:{client_email}'
77+
body_cspm = ('{"autoconnect":true,"cloud":"google","connection":{"google":{"client_email":"' +
78+
client_email + '","private_key":"' + private_key + '","project_id":"' + project_id +
79+
'","type":"' + project_type + '"}},"group_id":' + str(int(cspm_group_id)) + ',"name":"' + name + '"}')
80+
headers = get_headers(None, "POST", body_cspm)
81+
82+
body = json.dumps({
83+
"cloud": "google",
84+
"project_to_onboard": project_id,
85+
"configuration_id": configuration_id,
86+
"scan_mode": scan_mode,
87+
"deployment_method": "Terraform",
88+
"cspm_group_id": int(cspm_group_id),
89+
"organization_onboarding": organization_onboarding,
90+
"additional_resource_tags": additional_resource_tags,
91+
"organization_id": organization_id,
92+
"payload": service_account_key,
93+
"payload_vm": client_config,
94+
})
95+
96+
response = http_request(aqua_autoconnect_url, "/discover/google", "POST", headers, body)
97+
result = f'received response: status {response["status"]}, body: {response["body"]}'
98+
99+
return result
100+
101+
102+
def get_organization_projects_aqua(_organization_id):
103+
path = (f"/connected-accounts/provider?"
104+
f"cloud_provider=google&"
105+
f"integration_type=auto_discovery&"
106+
f"organization_id={_organization_id}")
107+
108+
response = http_request(aqua_autoconnect_url, path, "GET", get_headers(cspm_key=None), None)
109+
110+
result = []
111+
err = None
112+
if response["status"] == 200:
113+
try:
114+
data_dict = json.loads(response["body"])
115+
result = [
116+
{"id": account["CSPMData"]["role_arn"].split(":")[0], "cspm_key": account["CSPMData"]["id"]}
117+
for account in data_dict["cloud_accounts_data"]
118+
]
119+
except json.JSONDecodeError:
120+
err = "get_organization_projects_aqua:: Response content is not valid JSON"
121+
else:
122+
err = "get_organization_projects_aqua:: Error: " + str(response["status"]) + " " + response["reason"]
123+
124+
return result, err
125+
126+
127+
def offboard_project(project):
128+
129+
method = "DELETE"
130+
body = json.dumps({
131+
"cloud_accounts": [
132+
{
133+
"cloud_account_id": project["id"],
134+
"cspm_key_id": str(project["cspm_key"])
135+
}
136+
]
137+
})
138+
cspm_key = str(project["cspm_key"])
139+
140+
response = http_request(aqua_autoconnect_url, "/discover", method, get_headers(cspm_key, cspm_method=method), body)
141+
142+
err = None
143+
result = None
144+
if response["status"] == 200:
145+
try:
146+
result = json.loads(response["body"])
147+
except json.JSONDecodeError:
148+
err = "offboard_project:: Response content is not valid JSON"
149+
else:
150+
err = "offboard_project:: Error: " + str(response["status"]) + " " + response["reason"]
151+
152+
return result, err
153+
154+
155+
def main():
156+
aqua_connected_accounts, err = get_organization_projects_aqua(organization_id)
157+
if err:
158+
onboarding_status = err
159+
else:
160+
if organization_onboarding:
161+
offboard_projects = []
162+
offboard_project_results = []
163+
if not err:
164+
for project in aqua_connected_accounts:
165+
if project["id"] not in organization_projects:
166+
offboard_projects.append(project)
167+
168+
for project in offboard_projects:
169+
offboard_project_result, err = offboard_project(project)
170+
offboard_project_results.append([offboard_project_result, err])
171+
172+
if project_id not in [project["id"] for project in aqua_connected_accounts]:
173+
onboarding_status = trigger_discovery()
174+
else:
175+
onboarding_status = "Project already connected"
176+
177+
output = {
178+
"status": onboarding_status
179+
}
180+
print(json.dumps(output))
181+
182+
183+
if __name__ == "__main__":
184+
main()

modules/project_attachment/trigger.tf

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,10 @@ data "external" "gcp_onboarding" {
1616
organization_onboarding = var.type == "organization" ? "True" : "False"
1717
scan_mode = var.dedicated_project ? "Dedicated-Project" : "Same-Project"
1818
organization_id = var.type == "single" && !var.dedicated_project ? "" : var.org_name
19+
organization_projects = join(",", var.onboarding_organization_projects)
1920
additional_resource_tags = join(",", [for key, value in var.labels : "${key}:${value}"])
2021
}
21-
depends_on = [local.service_account_key, local.client_config_rendered, google_project_iam_member.project_iam_member_create_role, google_project_iam_member.project_iam_member_cspm_role]
22+
depends_on = [local.service_account_key, local.client_config_rendered, google_project_iam_member.project_iam_member_create_role, google_project_iam_member.project_iam_member_cspm_role, google_project_service.project_api_services]
2223
}
2324

2425

modules/project_attachment/variables.tf

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,12 @@ variable "org_name" {
130130
type = string
131131
}
132132

133+
variable "onboarding_organization_projects" {
134+
type = list(string)
135+
description = "List of all organization IDs (This should be provided only if type of onboarding is 'organization')"
136+
default = []
137+
}
138+
133139
variable "labels" {
134140
description = "Additional resource labels to will be send to the Autoconnect API"
135141
type = map(string)

0 commit comments

Comments
 (0)