Compare commits

..

3 Commits

Author SHA1 Message Date
Guillaume BOURGEOIS 5501297984 feat(gcp-security): new Google Cloud cloud-containment integration
Compute Engine + Security Command Center, 7 commands: list/create/delete VPC
firewall rules (deny to isolate), list/stop instances, list SCC findings.
Service-account RS256 JWT auth (remote engine, PyJWT + cryptography).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-11 23:54:02 +02:00
Guillaume BOURGEOIS 31d0a5938a feat(azure-security): new Microsoft Azure cloud-containment integration
Azure Resource Manager (Defender for Cloud + Network), 9 commands: list/get
security alerts, update alert state, secure score, list/get NSGs, create/delete
NSG security rules (deny inbound to isolate). OAuth2 client-credentials, stdlib-only.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-11 23:54:01 +02:00
Guillaume BOURGEOIS 62a44aea28 feat(aws): new AWS cloud-containment integration
EC2 + IAM + STS via AWS Query protocol (Signature V4), 9 commands: describe
instances/security-groups, authorize/revoke security-group ingress, modify
instance security groups (isolate), stop instances, list/update IAM access keys
(deactivate compromised key), STS caller identity. Stdlib-only.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-11 23:54:00 +02:00
28 changed files with 2893 additions and 0 deletions
+126
View File
@@ -0,0 +1,126 @@
id: aws
name: AWS
version: 1.0.0
description: "Amazon Web Services (EC2, IAM, STS) — cloud containment: describe instances and security groups, authorize/revoke security-group ingress rules, change an instance's security groups (isolate), stop instances, and deactivate a compromised IAM access key. AWS Signature V4 authentication; stdlib-only, no extra Python dependencies."
changelog: "1.0.0 — Initial release: EC2 describe instances/security-groups, authorize/revoke ingress, modify instance security groups, stop instances; IAM list/update access keys; STS caller identity."
category: cloud
# Per-instance configuration. Requests are signed with AWS Signature V4.
# Use an IAM user/role access key with EC2 + IAM permissions. session_token is
# only needed for temporary (STS) credentials.
config_schema:
properties:
access_key_id:
type: string
description: "AWS access key ID"
secret_access_key:
type: string
description: "AWS secret access key"
x-soar-sensitive: true
region:
type: string
description: "Default AWS region (e.g. eu-west-1)"
default: "us-east-1"
session_token:
type: string
description: "Optional STS session token (for temporary credentials)"
x-soar-sensitive: true
required:
- access_key_id
- secret_access_key
commands:
- id: describe_instances
name: aws-describe-instances
description: "Describe EC2 instances (optionally a single instance by ID)."
risk: read
inputs_schema:
properties:
instance_id: { type: string, description: "Optional instance ID to fetch a single instance" }
region: { type: string, description: "Region override" }
required: []
outputs_schema: { properties: {} }
- id: describe_security_groups
name: aws-describe-security-groups
description: "Describe EC2 security groups (optionally a single group by ID)."
risk: read
inputs_schema:
properties:
group_id: { type: string, description: "Optional security group ID" }
region: { type: string, description: "Region override" }
required: []
outputs_schema: { properties: {} }
- id: authorize_security_group_ingress
name: aws-authorize-security-group-ingress
description: "Add an inbound rule to a security group."
inputs_schema:
properties:
group_id: { type: string, description: "Security group ID" }
protocol: { type: string, description: "IP protocol (tcp, udp, icmp, or -1 for all)" }
from_port: { type: number, description: "Start port" }
to_port: { type: number, description: "End port" }
cidr: { type: string, description: "Source CIDR (e.g. 203.0.113.0/24)" }
region: { type: string, description: "Region override" }
required: [group_id, protocol, cidr]
outputs_schema: { properties: {} }
- id: revoke_security_group_ingress
name: aws-revoke-security-group-ingress
description: "Remove an inbound rule from a security group (containment)."
inputs_schema:
properties:
group_id: { type: string, description: "Security group ID" }
protocol: { type: string, description: "IP protocol (tcp, udp, icmp, or -1 for all)" }
from_port: { type: number, description: "Start port" }
to_port: { type: number, description: "End port" }
cidr: { type: string, description: "Source CIDR to revoke" }
region: { type: string, description: "Region override" }
required: [group_id, protocol, cidr]
outputs_schema: { properties: {} }
- id: modify_instance_security_groups
name: aws-modify-instance-security-groups
description: "Replace the security groups attached to an instance (e.g. move it to an isolation group)."
inputs_schema:
properties:
instance_id: { type: string, description: "Instance ID" }
group_ids: { type: string, description: "Comma-separated security group IDs to set" }
region: { type: string, description: "Region override" }
required: [instance_id, group_ids]
outputs_schema: { properties: {} }
- id: stop_instances
name: aws-stop-instances
description: "Stop one or more EC2 instances."
inputs_schema:
properties:
instance_ids: { type: string, description: "Comma-separated instance IDs" }
force: { type: boolean, description: "Force stop (default false)" }
region: { type: string, description: "Region override" }
required: [instance_ids]
outputs_schema: { properties: {} }
- id: list_access_keys
name: aws-list-access-keys
description: "List a user's IAM access keys."
risk: read
inputs_schema:
properties:
user_name: { type: string, description: "IAM user name (omit to use the calling user)" }
required: []
outputs_schema: { properties: {} }
- id: update_access_key
name: aws-update-access-key
description: "Activate or deactivate an IAM access key (deactivate to contain a compromised key)."
inputs_schema:
properties:
access_key_id: { type: string, description: "The access key ID to update" }
status: { type: string, description: "Active or Inactive" }
user_name: { type: string, description: "IAM user name (omit to use the calling user)" }
required: [access_key_id, status]
outputs_schema: { properties: {} }
- id: test_connection
name: aws-test-connection
description: "Verify credentials via STS GetCallerIdentity (used by the Test button)."
risk: read
inputs_schema:
properties: {}
required: []
outputs_schema: { properties: {} }
@@ -0,0 +1,151 @@
import json, os, sys, hmac, hashlib, datetime
import urllib.parse, urllib.request, urllib.error
import xml.etree.ElementTree as ET
def _cfg():
return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
def _inputs():
return json.loads(os.environ.get("INTEGRATION_INPUTS", "{}"))
def _sign_key(key, date_stamp, region, service):
def _h(k, m):
return hmac.new(k, m.encode("utf-8"), hashlib.sha256).digest()
k_date = _h(("AWS4" + key).encode("utf-8"), date_stamp)
k_region = _h(k_date, region)
k_service = _h(k_region, service)
return _h(k_service, "aws4_request")
def _strip_ns(tag):
return tag.split("}", 1)[1] if "}" in tag else tag
def _xml_to_dict(elem):
d = {}
children = list(elem)
if not children:
return (elem.text or "").strip()
for c in children:
tag = _strip_ns(c.tag)
val = _xml_to_dict(c)
if tag in d:
if not isinstance(d[tag], list):
d[tag] = [d[tag]]
d[tag].append(val)
else:
d[tag] = val
return d
def aws_query(service, host, region, action, version, params, cfg):
# params: dict of extra query params for this Action
body_params = {"Action": action, "Version": version}
body_params.update({k: str(v) for k, v in params.items() if v is not None})
body = urllib.parse.urlencode(sorted(body_params.items()))
access_key = str(cfg.get("access_key_id", ""))
secret_key = str(cfg.get("secret_access_key", ""))
session_token = cfg.get("session_token") or ""
now = datetime.datetime.utcnow()
amz_date = now.strftime("%Y%m%dT%H%M%SZ")
date_stamp = now.strftime("%Y%m%d")
method = "POST"
canonical_uri = "/"
canonical_querystring = ""
payload_hash = hashlib.sha256(body.encode("utf-8")).hexdigest()
canonical_headers = "content-type:application/x-www-form-urlencoded; charset=utf-8\n" \
"host:" + host + "\n" \
"x-amz-date:" + amz_date + "\n"
signed_headers = "content-type;host;x-amz-date"
if session_token:
canonical_headers += "x-amz-security-token:" + session_token + "\n"
signed_headers = "content-type;host;x-amz-date;x-amz-security-token"
canonical_request = "\n".join([method, canonical_uri, canonical_querystring,
canonical_headers, signed_headers, payload_hash])
algorithm = "AWS4-HMAC-SHA256"
credential_scope = date_stamp + "/" + region + "/" + service + "/aws4_request"
string_to_sign = "\n".join([algorithm, amz_date, credential_scope,
hashlib.sha256(canonical_request.encode("utf-8")).hexdigest()])
signing_key = _sign_key(secret_key, date_stamp, region, service)
signature = hmac.new(signing_key, string_to_sign.encode("utf-8"), hashlib.sha256).hexdigest()
authorization = (algorithm + " Credential=" + access_key + "/" + credential_scope +
", SignedHeaders=" + signed_headers + ", Signature=" + signature)
headers = {
"Content-Type": "application/x-www-form-urlencoded; charset=utf-8",
"X-Amz-Date": amz_date,
"Authorization": authorization,
"Accept": "application/json",
}
if session_token:
headers["X-Amz-Security-Token"] = session_token
req = urllib.request.Request("https://" + host + "/", data=body.encode("utf-8"), headers=headers, method="POST")
with urllib.request.urlopen(req, timeout=60) as r:
raw = r.read()
root = ET.fromstring(raw)
return {_strip_ns(root.tag): _xml_to_dict(root)}
def _region(cfg, inputs):
return (inputs.get("region") if inputs else None) or cfg.get("region") or "us-east-1"
def ec2(action, params, cfg, inputs):
region = _region(cfg, inputs)
return aws_query("ec2", "ec2." + region + ".amazonaws.com", region, action, "2016-11-15", params, cfg)
def iam(action, params, cfg):
return aws_query("iam", "iam.amazonaws.com", "us-east-1", action, "2010-05-08", params, cfg)
def sts(action, params, cfg):
return aws_query("sts", "sts.amazonaws.com", "us-east-1", action, "2011-06-15", params, cfg)
def _run(fn):
try:
print(json.dumps(fn(_cfg(), _inputs())))
except urllib.error.HTTPError as e:
print(json.dumps({"error": "HTTP " + str(e.code), "detail": e.read().decode("utf-8", "replace")}))
sys.exit(1)
except Exception as e:
print(json.dumps({"error": str(e)}))
sys.exit(1)
def main(cfg, inputs):
group_id = inputs.get("group_id")
if not group_id:
raise Exception("group_id is required")
protocol = inputs.get("protocol")
if not protocol:
raise Exception("protocol is required")
cidr = inputs.get("cidr")
if not cidr:
raise Exception("cidr is required")
params = {
"GroupId": group_id,
"IpPermissions.1.IpProtocol": protocol,
"IpPermissions.1.IpRanges.1.CidrIp": cidr,
}
from_port = inputs.get("from_port")
if from_port is not None and str(from_port).strip() != "":
params["IpPermissions.1.FromPort"] = int(from_port)
to_port = inputs.get("to_port")
if to_port is not None and str(to_port).strip() != "":
params["IpPermissions.1.ToPort"] = int(to_port)
return ec2("AuthorizeSecurityGroupIngress", params, cfg, inputs)
_run(main)
@@ -0,0 +1,131 @@
import json, os, sys, hmac, hashlib, datetime
import urllib.parse, urllib.request, urllib.error
import xml.etree.ElementTree as ET
def _cfg():
return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
def _inputs():
return json.loads(os.environ.get("INTEGRATION_INPUTS", "{}"))
def _sign_key(key, date_stamp, region, service):
def _h(k, m):
return hmac.new(k, m.encode("utf-8"), hashlib.sha256).digest()
k_date = _h(("AWS4" + key).encode("utf-8"), date_stamp)
k_region = _h(k_date, region)
k_service = _h(k_region, service)
return _h(k_service, "aws4_request")
def _strip_ns(tag):
return tag.split("}", 1)[1] if "}" in tag else tag
def _xml_to_dict(elem):
d = {}
children = list(elem)
if not children:
return (elem.text or "").strip()
for c in children:
tag = _strip_ns(c.tag)
val = _xml_to_dict(c)
if tag in d:
if not isinstance(d[tag], list):
d[tag] = [d[tag]]
d[tag].append(val)
else:
d[tag] = val
return d
def aws_query(service, host, region, action, version, params, cfg):
# params: dict of extra query params for this Action
body_params = {"Action": action, "Version": version}
body_params.update({k: str(v) for k, v in params.items() if v is not None})
body = urllib.parse.urlencode(sorted(body_params.items()))
access_key = str(cfg.get("access_key_id", ""))
secret_key = str(cfg.get("secret_access_key", ""))
session_token = cfg.get("session_token") or ""
now = datetime.datetime.utcnow()
amz_date = now.strftime("%Y%m%dT%H%M%SZ")
date_stamp = now.strftime("%Y%m%d")
method = "POST"
canonical_uri = "/"
canonical_querystring = ""
payload_hash = hashlib.sha256(body.encode("utf-8")).hexdigest()
canonical_headers = "content-type:application/x-www-form-urlencoded; charset=utf-8\n" \
"host:" + host + "\n" \
"x-amz-date:" + amz_date + "\n"
signed_headers = "content-type;host;x-amz-date"
if session_token:
canonical_headers += "x-amz-security-token:" + session_token + "\n"
signed_headers = "content-type;host;x-amz-date;x-amz-security-token"
canonical_request = "\n".join([method, canonical_uri, canonical_querystring,
canonical_headers, signed_headers, payload_hash])
algorithm = "AWS4-HMAC-SHA256"
credential_scope = date_stamp + "/" + region + "/" + service + "/aws4_request"
string_to_sign = "\n".join([algorithm, amz_date, credential_scope,
hashlib.sha256(canonical_request.encode("utf-8")).hexdigest()])
signing_key = _sign_key(secret_key, date_stamp, region, service)
signature = hmac.new(signing_key, string_to_sign.encode("utf-8"), hashlib.sha256).hexdigest()
authorization = (algorithm + " Credential=" + access_key + "/" + credential_scope +
", SignedHeaders=" + signed_headers + ", Signature=" + signature)
headers = {
"Content-Type": "application/x-www-form-urlencoded; charset=utf-8",
"X-Amz-Date": amz_date,
"Authorization": authorization,
"Accept": "application/json",
}
if session_token:
headers["X-Amz-Security-Token"] = session_token
req = urllib.request.Request("https://" + host + "/", data=body.encode("utf-8"), headers=headers, method="POST")
with urllib.request.urlopen(req, timeout=60) as r:
raw = r.read()
root = ET.fromstring(raw)
return {_strip_ns(root.tag): _xml_to_dict(root)}
def _region(cfg, inputs):
return (inputs.get("region") if inputs else None) or cfg.get("region") or "us-east-1"
def ec2(action, params, cfg, inputs):
region = _region(cfg, inputs)
return aws_query("ec2", "ec2." + region + ".amazonaws.com", region, action, "2016-11-15", params, cfg)
def iam(action, params, cfg):
return aws_query("iam", "iam.amazonaws.com", "us-east-1", action, "2010-05-08", params, cfg)
def sts(action, params, cfg):
return aws_query("sts", "sts.amazonaws.com", "us-east-1", action, "2011-06-15", params, cfg)
def _run(fn):
try:
print(json.dumps(fn(_cfg(), _inputs())))
except urllib.error.HTTPError as e:
print(json.dumps({"error": "HTTP " + str(e.code), "detail": e.read().decode("utf-8", "replace")}))
sys.exit(1)
except Exception as e:
print(json.dumps({"error": str(e)}))
sys.exit(1)
def main(cfg, inputs):
instance_id = inputs.get("instance_id")
params = {"InstanceId.1": instance_id} if instance_id else {}
return ec2("DescribeInstances", params, cfg, inputs)
_run(main)
@@ -0,0 +1,131 @@
import json, os, sys, hmac, hashlib, datetime
import urllib.parse, urllib.request, urllib.error
import xml.etree.ElementTree as ET
def _cfg():
return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
def _inputs():
return json.loads(os.environ.get("INTEGRATION_INPUTS", "{}"))
def _sign_key(key, date_stamp, region, service):
def _h(k, m):
return hmac.new(k, m.encode("utf-8"), hashlib.sha256).digest()
k_date = _h(("AWS4" + key).encode("utf-8"), date_stamp)
k_region = _h(k_date, region)
k_service = _h(k_region, service)
return _h(k_service, "aws4_request")
def _strip_ns(tag):
return tag.split("}", 1)[1] if "}" in tag else tag
def _xml_to_dict(elem):
d = {}
children = list(elem)
if not children:
return (elem.text or "").strip()
for c in children:
tag = _strip_ns(c.tag)
val = _xml_to_dict(c)
if tag in d:
if not isinstance(d[tag], list):
d[tag] = [d[tag]]
d[tag].append(val)
else:
d[tag] = val
return d
def aws_query(service, host, region, action, version, params, cfg):
# params: dict of extra query params for this Action
body_params = {"Action": action, "Version": version}
body_params.update({k: str(v) for k, v in params.items() if v is not None})
body = urllib.parse.urlencode(sorted(body_params.items()))
access_key = str(cfg.get("access_key_id", ""))
secret_key = str(cfg.get("secret_access_key", ""))
session_token = cfg.get("session_token") or ""
now = datetime.datetime.utcnow()
amz_date = now.strftime("%Y%m%dT%H%M%SZ")
date_stamp = now.strftime("%Y%m%d")
method = "POST"
canonical_uri = "/"
canonical_querystring = ""
payload_hash = hashlib.sha256(body.encode("utf-8")).hexdigest()
canonical_headers = "content-type:application/x-www-form-urlencoded; charset=utf-8\n" \
"host:" + host + "\n" \
"x-amz-date:" + amz_date + "\n"
signed_headers = "content-type;host;x-amz-date"
if session_token:
canonical_headers += "x-amz-security-token:" + session_token + "\n"
signed_headers = "content-type;host;x-amz-date;x-amz-security-token"
canonical_request = "\n".join([method, canonical_uri, canonical_querystring,
canonical_headers, signed_headers, payload_hash])
algorithm = "AWS4-HMAC-SHA256"
credential_scope = date_stamp + "/" + region + "/" + service + "/aws4_request"
string_to_sign = "\n".join([algorithm, amz_date, credential_scope,
hashlib.sha256(canonical_request.encode("utf-8")).hexdigest()])
signing_key = _sign_key(secret_key, date_stamp, region, service)
signature = hmac.new(signing_key, string_to_sign.encode("utf-8"), hashlib.sha256).hexdigest()
authorization = (algorithm + " Credential=" + access_key + "/" + credential_scope +
", SignedHeaders=" + signed_headers + ", Signature=" + signature)
headers = {
"Content-Type": "application/x-www-form-urlencoded; charset=utf-8",
"X-Amz-Date": amz_date,
"Authorization": authorization,
"Accept": "application/json",
}
if session_token:
headers["X-Amz-Security-Token"] = session_token
req = urllib.request.Request("https://" + host + "/", data=body.encode("utf-8"), headers=headers, method="POST")
with urllib.request.urlopen(req, timeout=60) as r:
raw = r.read()
root = ET.fromstring(raw)
return {_strip_ns(root.tag): _xml_to_dict(root)}
def _region(cfg, inputs):
return (inputs.get("region") if inputs else None) or cfg.get("region") or "us-east-1"
def ec2(action, params, cfg, inputs):
region = _region(cfg, inputs)
return aws_query("ec2", "ec2." + region + ".amazonaws.com", region, action, "2016-11-15", params, cfg)
def iam(action, params, cfg):
return aws_query("iam", "iam.amazonaws.com", "us-east-1", action, "2010-05-08", params, cfg)
def sts(action, params, cfg):
return aws_query("sts", "sts.amazonaws.com", "us-east-1", action, "2011-06-15", params, cfg)
def _run(fn):
try:
print(json.dumps(fn(_cfg(), _inputs())))
except urllib.error.HTTPError as e:
print(json.dumps({"error": "HTTP " + str(e.code), "detail": e.read().decode("utf-8", "replace")}))
sys.exit(1)
except Exception as e:
print(json.dumps({"error": str(e)}))
sys.exit(1)
def main(cfg, inputs):
group_id = inputs.get("group_id")
params = {"GroupId.1": group_id} if group_id else {}
return ec2("DescribeSecurityGroups", params, cfg, inputs)
_run(main)
@@ -0,0 +1,131 @@
import json, os, sys, hmac, hashlib, datetime
import urllib.parse, urllib.request, urllib.error
import xml.etree.ElementTree as ET
def _cfg():
return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
def _inputs():
return json.loads(os.environ.get("INTEGRATION_INPUTS", "{}"))
def _sign_key(key, date_stamp, region, service):
def _h(k, m):
return hmac.new(k, m.encode("utf-8"), hashlib.sha256).digest()
k_date = _h(("AWS4" + key).encode("utf-8"), date_stamp)
k_region = _h(k_date, region)
k_service = _h(k_region, service)
return _h(k_service, "aws4_request")
def _strip_ns(tag):
return tag.split("}", 1)[1] if "}" in tag else tag
def _xml_to_dict(elem):
d = {}
children = list(elem)
if not children:
return (elem.text or "").strip()
for c in children:
tag = _strip_ns(c.tag)
val = _xml_to_dict(c)
if tag in d:
if not isinstance(d[tag], list):
d[tag] = [d[tag]]
d[tag].append(val)
else:
d[tag] = val
return d
def aws_query(service, host, region, action, version, params, cfg):
# params: dict of extra query params for this Action
body_params = {"Action": action, "Version": version}
body_params.update({k: str(v) for k, v in params.items() if v is not None})
body = urllib.parse.urlencode(sorted(body_params.items()))
access_key = str(cfg.get("access_key_id", ""))
secret_key = str(cfg.get("secret_access_key", ""))
session_token = cfg.get("session_token") or ""
now = datetime.datetime.utcnow()
amz_date = now.strftime("%Y%m%dT%H%M%SZ")
date_stamp = now.strftime("%Y%m%d")
method = "POST"
canonical_uri = "/"
canonical_querystring = ""
payload_hash = hashlib.sha256(body.encode("utf-8")).hexdigest()
canonical_headers = "content-type:application/x-www-form-urlencoded; charset=utf-8\n" \
"host:" + host + "\n" \
"x-amz-date:" + amz_date + "\n"
signed_headers = "content-type;host;x-amz-date"
if session_token:
canonical_headers += "x-amz-security-token:" + session_token + "\n"
signed_headers = "content-type;host;x-amz-date;x-amz-security-token"
canonical_request = "\n".join([method, canonical_uri, canonical_querystring,
canonical_headers, signed_headers, payload_hash])
algorithm = "AWS4-HMAC-SHA256"
credential_scope = date_stamp + "/" + region + "/" + service + "/aws4_request"
string_to_sign = "\n".join([algorithm, amz_date, credential_scope,
hashlib.sha256(canonical_request.encode("utf-8")).hexdigest()])
signing_key = _sign_key(secret_key, date_stamp, region, service)
signature = hmac.new(signing_key, string_to_sign.encode("utf-8"), hashlib.sha256).hexdigest()
authorization = (algorithm + " Credential=" + access_key + "/" + credential_scope +
", SignedHeaders=" + signed_headers + ", Signature=" + signature)
headers = {
"Content-Type": "application/x-www-form-urlencoded; charset=utf-8",
"X-Amz-Date": amz_date,
"Authorization": authorization,
"Accept": "application/json",
}
if session_token:
headers["X-Amz-Security-Token"] = session_token
req = urllib.request.Request("https://" + host + "/", data=body.encode("utf-8"), headers=headers, method="POST")
with urllib.request.urlopen(req, timeout=60) as r:
raw = r.read()
root = ET.fromstring(raw)
return {_strip_ns(root.tag): _xml_to_dict(root)}
def _region(cfg, inputs):
return (inputs.get("region") if inputs else None) or cfg.get("region") or "us-east-1"
def ec2(action, params, cfg, inputs):
region = _region(cfg, inputs)
return aws_query("ec2", "ec2." + region + ".amazonaws.com", region, action, "2016-11-15", params, cfg)
def iam(action, params, cfg):
return aws_query("iam", "iam.amazonaws.com", "us-east-1", action, "2010-05-08", params, cfg)
def sts(action, params, cfg):
return aws_query("sts", "sts.amazonaws.com", "us-east-1", action, "2011-06-15", params, cfg)
def _run(fn):
try:
print(json.dumps(fn(_cfg(), _inputs())))
except urllib.error.HTTPError as e:
print(json.dumps({"error": "HTTP " + str(e.code), "detail": e.read().decode("utf-8", "replace")}))
sys.exit(1)
except Exception as e:
print(json.dumps({"error": str(e)}))
sys.exit(1)
def main(cfg, inputs):
user_name = inputs.get("user_name")
params = {"UserName": user_name} if user_name else {}
return iam("ListAccessKeys", params, cfg)
_run(main)
@@ -0,0 +1,143 @@
import json, os, sys, hmac, hashlib, datetime
import urllib.parse, urllib.request, urllib.error
import xml.etree.ElementTree as ET
def _cfg():
return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
def _inputs():
return json.loads(os.environ.get("INTEGRATION_INPUTS", "{}"))
def _sign_key(key, date_stamp, region, service):
def _h(k, m):
return hmac.new(k, m.encode("utf-8"), hashlib.sha256).digest()
k_date = _h(("AWS4" + key).encode("utf-8"), date_stamp)
k_region = _h(k_date, region)
k_service = _h(k_region, service)
return _h(k_service, "aws4_request")
def _strip_ns(tag):
return tag.split("}", 1)[1] if "}" in tag else tag
def _xml_to_dict(elem):
d = {}
children = list(elem)
if not children:
return (elem.text or "").strip()
for c in children:
tag = _strip_ns(c.tag)
val = _xml_to_dict(c)
if tag in d:
if not isinstance(d[tag], list):
d[tag] = [d[tag]]
d[tag].append(val)
else:
d[tag] = val
return d
def aws_query(service, host, region, action, version, params, cfg):
# params: dict of extra query params for this Action
body_params = {"Action": action, "Version": version}
body_params.update({k: str(v) for k, v in params.items() if v is not None})
body = urllib.parse.urlencode(sorted(body_params.items()))
access_key = str(cfg.get("access_key_id", ""))
secret_key = str(cfg.get("secret_access_key", ""))
session_token = cfg.get("session_token") or ""
now = datetime.datetime.utcnow()
amz_date = now.strftime("%Y%m%dT%H%M%SZ")
date_stamp = now.strftime("%Y%m%d")
method = "POST"
canonical_uri = "/"
canonical_querystring = ""
payload_hash = hashlib.sha256(body.encode("utf-8")).hexdigest()
canonical_headers = "content-type:application/x-www-form-urlencoded; charset=utf-8\n" \
"host:" + host + "\n" \
"x-amz-date:" + amz_date + "\n"
signed_headers = "content-type;host;x-amz-date"
if session_token:
canonical_headers += "x-amz-security-token:" + session_token + "\n"
signed_headers = "content-type;host;x-amz-date;x-amz-security-token"
canonical_request = "\n".join([method, canonical_uri, canonical_querystring,
canonical_headers, signed_headers, payload_hash])
algorithm = "AWS4-HMAC-SHA256"
credential_scope = date_stamp + "/" + region + "/" + service + "/aws4_request"
string_to_sign = "\n".join([algorithm, amz_date, credential_scope,
hashlib.sha256(canonical_request.encode("utf-8")).hexdigest()])
signing_key = _sign_key(secret_key, date_stamp, region, service)
signature = hmac.new(signing_key, string_to_sign.encode("utf-8"), hashlib.sha256).hexdigest()
authorization = (algorithm + " Credential=" + access_key + "/" + credential_scope +
", SignedHeaders=" + signed_headers + ", Signature=" + signature)
headers = {
"Content-Type": "application/x-www-form-urlencoded; charset=utf-8",
"X-Amz-Date": amz_date,
"Authorization": authorization,
"Accept": "application/json",
}
if session_token:
headers["X-Amz-Security-Token"] = session_token
req = urllib.request.Request("https://" + host + "/", data=body.encode("utf-8"), headers=headers, method="POST")
with urllib.request.urlopen(req, timeout=60) as r:
raw = r.read()
root = ET.fromstring(raw)
return {_strip_ns(root.tag): _xml_to_dict(root)}
def _region(cfg, inputs):
return (inputs.get("region") if inputs else None) or cfg.get("region") or "us-east-1"
def ec2(action, params, cfg, inputs):
region = _region(cfg, inputs)
return aws_query("ec2", "ec2." + region + ".amazonaws.com", region, action, "2016-11-15", params, cfg)
def iam(action, params, cfg):
return aws_query("iam", "iam.amazonaws.com", "us-east-1", action, "2010-05-08", params, cfg)
def sts(action, params, cfg):
return aws_query("sts", "sts.amazonaws.com", "us-east-1", action, "2011-06-15", params, cfg)
def _run(fn):
try:
print(json.dumps(fn(_cfg(), _inputs())))
except urllib.error.HTTPError as e:
print(json.dumps({"error": "HTTP " + str(e.code), "detail": e.read().decode("utf-8", "replace")}))
sys.exit(1)
except Exception as e:
print(json.dumps({"error": str(e)}))
sys.exit(1)
def main(cfg, inputs):
instance_id = inputs.get("instance_id")
if not instance_id:
raise Exception("instance_id is required")
group_ids_raw = inputs.get("group_ids")
if not group_ids_raw or not str(group_ids_raw).strip():
raise Exception("group_ids is required")
group_ids = [s.strip() for s in str(group_ids_raw).split(",") if s.strip()]
if not group_ids:
raise Exception("group_ids is required")
params = {"InstanceId": instance_id}
for i, gid in enumerate(group_ids, start=1):
params["GroupId.%d" % i] = gid
return ec2("ModifyInstanceAttribute", params, cfg, inputs)
_run(main)
@@ -0,0 +1,151 @@
import json, os, sys, hmac, hashlib, datetime
import urllib.parse, urllib.request, urllib.error
import xml.etree.ElementTree as ET
def _cfg():
return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
def _inputs():
return json.loads(os.environ.get("INTEGRATION_INPUTS", "{}"))
def _sign_key(key, date_stamp, region, service):
def _h(k, m):
return hmac.new(k, m.encode("utf-8"), hashlib.sha256).digest()
k_date = _h(("AWS4" + key).encode("utf-8"), date_stamp)
k_region = _h(k_date, region)
k_service = _h(k_region, service)
return _h(k_service, "aws4_request")
def _strip_ns(tag):
return tag.split("}", 1)[1] if "}" in tag else tag
def _xml_to_dict(elem):
d = {}
children = list(elem)
if not children:
return (elem.text or "").strip()
for c in children:
tag = _strip_ns(c.tag)
val = _xml_to_dict(c)
if tag in d:
if not isinstance(d[tag], list):
d[tag] = [d[tag]]
d[tag].append(val)
else:
d[tag] = val
return d
def aws_query(service, host, region, action, version, params, cfg):
# params: dict of extra query params for this Action
body_params = {"Action": action, "Version": version}
body_params.update({k: str(v) for k, v in params.items() if v is not None})
body = urllib.parse.urlencode(sorted(body_params.items()))
access_key = str(cfg.get("access_key_id", ""))
secret_key = str(cfg.get("secret_access_key", ""))
session_token = cfg.get("session_token") or ""
now = datetime.datetime.utcnow()
amz_date = now.strftime("%Y%m%dT%H%M%SZ")
date_stamp = now.strftime("%Y%m%d")
method = "POST"
canonical_uri = "/"
canonical_querystring = ""
payload_hash = hashlib.sha256(body.encode("utf-8")).hexdigest()
canonical_headers = "content-type:application/x-www-form-urlencoded; charset=utf-8\n" \
"host:" + host + "\n" \
"x-amz-date:" + amz_date + "\n"
signed_headers = "content-type;host;x-amz-date"
if session_token:
canonical_headers += "x-amz-security-token:" + session_token + "\n"
signed_headers = "content-type;host;x-amz-date;x-amz-security-token"
canonical_request = "\n".join([method, canonical_uri, canonical_querystring,
canonical_headers, signed_headers, payload_hash])
algorithm = "AWS4-HMAC-SHA256"
credential_scope = date_stamp + "/" + region + "/" + service + "/aws4_request"
string_to_sign = "\n".join([algorithm, amz_date, credential_scope,
hashlib.sha256(canonical_request.encode("utf-8")).hexdigest()])
signing_key = _sign_key(secret_key, date_stamp, region, service)
signature = hmac.new(signing_key, string_to_sign.encode("utf-8"), hashlib.sha256).hexdigest()
authorization = (algorithm + " Credential=" + access_key + "/" + credential_scope +
", SignedHeaders=" + signed_headers + ", Signature=" + signature)
headers = {
"Content-Type": "application/x-www-form-urlencoded; charset=utf-8",
"X-Amz-Date": amz_date,
"Authorization": authorization,
"Accept": "application/json",
}
if session_token:
headers["X-Amz-Security-Token"] = session_token
req = urllib.request.Request("https://" + host + "/", data=body.encode("utf-8"), headers=headers, method="POST")
with urllib.request.urlopen(req, timeout=60) as r:
raw = r.read()
root = ET.fromstring(raw)
return {_strip_ns(root.tag): _xml_to_dict(root)}
def _region(cfg, inputs):
return (inputs.get("region") if inputs else None) or cfg.get("region") or "us-east-1"
def ec2(action, params, cfg, inputs):
region = _region(cfg, inputs)
return aws_query("ec2", "ec2." + region + ".amazonaws.com", region, action, "2016-11-15", params, cfg)
def iam(action, params, cfg):
return aws_query("iam", "iam.amazonaws.com", "us-east-1", action, "2010-05-08", params, cfg)
def sts(action, params, cfg):
return aws_query("sts", "sts.amazonaws.com", "us-east-1", action, "2011-06-15", params, cfg)
def _run(fn):
try:
print(json.dumps(fn(_cfg(), _inputs())))
except urllib.error.HTTPError as e:
print(json.dumps({"error": "HTTP " + str(e.code), "detail": e.read().decode("utf-8", "replace")}))
sys.exit(1)
except Exception as e:
print(json.dumps({"error": str(e)}))
sys.exit(1)
def main(cfg, inputs):
group_id = inputs.get("group_id")
if not group_id:
raise Exception("group_id is required")
protocol = inputs.get("protocol")
if not protocol:
raise Exception("protocol is required")
cidr = inputs.get("cidr")
if not cidr:
raise Exception("cidr is required")
params = {
"GroupId": group_id,
"IpPermissions.1.IpProtocol": protocol,
"IpPermissions.1.IpRanges.1.CidrIp": cidr,
}
from_port = inputs.get("from_port")
if from_port is not None and str(from_port).strip() != "":
params["IpPermissions.1.FromPort"] = int(from_port)
to_port = inputs.get("to_port")
if to_port is not None and str(to_port).strip() != "":
params["IpPermissions.1.ToPort"] = int(to_port)
return ec2("RevokeSecurityGroupIngress", params, cfg, inputs)
_run(main)
+148
View File
@@ -0,0 +1,148 @@
import json, os, sys, hmac, hashlib, datetime
import urllib.parse, urllib.request, urllib.error
import xml.etree.ElementTree as ET
def _cfg():
return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
def _inputs():
return json.loads(os.environ.get("INTEGRATION_INPUTS", "{}"))
def _sign_key(key, date_stamp, region, service):
def _h(k, m):
return hmac.new(k, m.encode("utf-8"), hashlib.sha256).digest()
k_date = _h(("AWS4" + key).encode("utf-8"), date_stamp)
k_region = _h(k_date, region)
k_service = _h(k_region, service)
return _h(k_service, "aws4_request")
def _strip_ns(tag):
return tag.split("}", 1)[1] if "}" in tag else tag
def _xml_to_dict(elem):
d = {}
children = list(elem)
if not children:
return (elem.text or "").strip()
for c in children:
tag = _strip_ns(c.tag)
val = _xml_to_dict(c)
if tag in d:
if not isinstance(d[tag], list):
d[tag] = [d[tag]]
d[tag].append(val)
else:
d[tag] = val
return d
def aws_query(service, host, region, action, version, params, cfg):
# params: dict of extra query params for this Action
body_params = {"Action": action, "Version": version}
body_params.update({k: str(v) for k, v in params.items() if v is not None})
body = urllib.parse.urlencode(sorted(body_params.items()))
access_key = str(cfg.get("access_key_id", ""))
secret_key = str(cfg.get("secret_access_key", ""))
session_token = cfg.get("session_token") or ""
now = datetime.datetime.utcnow()
amz_date = now.strftime("%Y%m%dT%H%M%SZ")
date_stamp = now.strftime("%Y%m%d")
method = "POST"
canonical_uri = "/"
canonical_querystring = ""
payload_hash = hashlib.sha256(body.encode("utf-8")).hexdigest()
canonical_headers = "content-type:application/x-www-form-urlencoded; charset=utf-8\n" \
"host:" + host + "\n" \
"x-amz-date:" + amz_date + "\n"
signed_headers = "content-type;host;x-amz-date"
if session_token:
canonical_headers += "x-amz-security-token:" + session_token + "\n"
signed_headers = "content-type;host;x-amz-date;x-amz-security-token"
canonical_request = "\n".join([method, canonical_uri, canonical_querystring,
canonical_headers, signed_headers, payload_hash])
algorithm = "AWS4-HMAC-SHA256"
credential_scope = date_stamp + "/" + region + "/" + service + "/aws4_request"
string_to_sign = "\n".join([algorithm, amz_date, credential_scope,
hashlib.sha256(canonical_request.encode("utf-8")).hexdigest()])
signing_key = _sign_key(secret_key, date_stamp, region, service)
signature = hmac.new(signing_key, string_to_sign.encode("utf-8"), hashlib.sha256).hexdigest()
authorization = (algorithm + " Credential=" + access_key + "/" + credential_scope +
", SignedHeaders=" + signed_headers + ", Signature=" + signature)
headers = {
"Content-Type": "application/x-www-form-urlencoded; charset=utf-8",
"X-Amz-Date": amz_date,
"Authorization": authorization,
"Accept": "application/json",
}
if session_token:
headers["X-Amz-Security-Token"] = session_token
req = urllib.request.Request("https://" + host + "/", data=body.encode("utf-8"), headers=headers, method="POST")
with urllib.request.urlopen(req, timeout=60) as r:
raw = r.read()
root = ET.fromstring(raw)
return {_strip_ns(root.tag): _xml_to_dict(root)}
def _region(cfg, inputs):
return (inputs.get("region") if inputs else None) or cfg.get("region") or "us-east-1"
def ec2(action, params, cfg, inputs):
region = _region(cfg, inputs)
return aws_query("ec2", "ec2." + region + ".amazonaws.com", region, action, "2016-11-15", params, cfg)
def iam(action, params, cfg):
return aws_query("iam", "iam.amazonaws.com", "us-east-1", action, "2010-05-08", params, cfg)
def sts(action, params, cfg):
return aws_query("sts", "sts.amazonaws.com", "us-east-1", action, "2011-06-15", params, cfg)
def _run(fn):
try:
print(json.dumps(fn(_cfg(), _inputs())))
except urllib.error.HTTPError as e:
print(json.dumps({"error": "HTTP " + str(e.code), "detail": e.read().decode("utf-8", "replace")}))
sys.exit(1)
except Exception as e:
print(json.dumps({"error": str(e)}))
sys.exit(1)
def main(cfg, inputs):
instance_ids_raw = inputs.get("instance_ids")
if not instance_ids_raw or not str(instance_ids_raw).strip():
raise Exception("instance_ids is required")
instance_ids = [s.strip() for s in str(instance_ids_raw).split(",") if s.strip()]
if not instance_ids:
raise Exception("instance_ids is required")
force = inputs.get("force", False)
if isinstance(force, str):
force = force.strip().lower() in ("true", "1", "yes")
else:
force = bool(force)
params = {}
for i, iid in enumerate(instance_ids, start=1):
params["InstanceId.%d" % i] = iid
if force:
params["Force"] = "true"
return ec2("StopInstances", params, cfg, inputs)
_run(main)
+130
View File
@@ -0,0 +1,130 @@
import json, os, sys, hmac, hashlib, datetime
import urllib.parse, urllib.request, urllib.error
import xml.etree.ElementTree as ET
def _cfg():
return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
def _inputs():
return json.loads(os.environ.get("INTEGRATION_INPUTS", "{}"))
def _sign_key(key, date_stamp, region, service):
def _h(k, m):
return hmac.new(k, m.encode("utf-8"), hashlib.sha256).digest()
k_date = _h(("AWS4" + key).encode("utf-8"), date_stamp)
k_region = _h(k_date, region)
k_service = _h(k_region, service)
return _h(k_service, "aws4_request")
def _strip_ns(tag):
return tag.split("}", 1)[1] if "}" in tag else tag
def _xml_to_dict(elem):
d = {}
children = list(elem)
if not children:
return (elem.text or "").strip()
for c in children:
tag = _strip_ns(c.tag)
val = _xml_to_dict(c)
if tag in d:
if not isinstance(d[tag], list):
d[tag] = [d[tag]]
d[tag].append(val)
else:
d[tag] = val
return d
def aws_query(service, host, region, action, version, params, cfg):
# params: dict of extra query params for this Action
body_params = {"Action": action, "Version": version}
body_params.update({k: str(v) for k, v in params.items() if v is not None})
body = urllib.parse.urlencode(sorted(body_params.items()))
access_key = str(cfg.get("access_key_id", ""))
secret_key = str(cfg.get("secret_access_key", ""))
session_token = cfg.get("session_token") or ""
now = datetime.datetime.utcnow()
amz_date = now.strftime("%Y%m%dT%H%M%SZ")
date_stamp = now.strftime("%Y%m%d")
method = "POST"
canonical_uri = "/"
canonical_querystring = ""
payload_hash = hashlib.sha256(body.encode("utf-8")).hexdigest()
canonical_headers = "content-type:application/x-www-form-urlencoded; charset=utf-8\n" \
"host:" + host + "\n" \
"x-amz-date:" + amz_date + "\n"
signed_headers = "content-type;host;x-amz-date"
if session_token:
canonical_headers += "x-amz-security-token:" + session_token + "\n"
signed_headers = "content-type;host;x-amz-date;x-amz-security-token"
canonical_request = "\n".join([method, canonical_uri, canonical_querystring,
canonical_headers, signed_headers, payload_hash])
algorithm = "AWS4-HMAC-SHA256"
credential_scope = date_stamp + "/" + region + "/" + service + "/aws4_request"
string_to_sign = "\n".join([algorithm, amz_date, credential_scope,
hashlib.sha256(canonical_request.encode("utf-8")).hexdigest()])
signing_key = _sign_key(secret_key, date_stamp, region, service)
signature = hmac.new(signing_key, string_to_sign.encode("utf-8"), hashlib.sha256).hexdigest()
authorization = (algorithm + " Credential=" + access_key + "/" + credential_scope +
", SignedHeaders=" + signed_headers + ", Signature=" + signature)
headers = {
"Content-Type": "application/x-www-form-urlencoded; charset=utf-8",
"X-Amz-Date": amz_date,
"Authorization": authorization,
"Accept": "application/json",
}
if session_token:
headers["X-Amz-Security-Token"] = session_token
req = urllib.request.Request("https://" + host + "/", data=body.encode("utf-8"), headers=headers, method="POST")
with urllib.request.urlopen(req, timeout=60) as r:
raw = r.read()
root = ET.fromstring(raw)
return {_strip_ns(root.tag): _xml_to_dict(root)}
def _region(cfg, inputs):
return (inputs.get("region") if inputs else None) or cfg.get("region") or "us-east-1"
def ec2(action, params, cfg, inputs):
region = _region(cfg, inputs)
return aws_query("ec2", "ec2." + region + ".amazonaws.com", region, action, "2016-11-15", params, cfg)
def iam(action, params, cfg):
return aws_query("iam", "iam.amazonaws.com", "us-east-1", action, "2010-05-08", params, cfg)
def sts(action, params, cfg):
return aws_query("sts", "sts.amazonaws.com", "us-east-1", action, "2011-06-15", params, cfg)
def _run(fn):
try:
print(json.dumps(fn(_cfg(), _inputs())))
except urllib.error.HTTPError as e:
print(json.dumps({"error": "HTTP " + str(e.code), "detail": e.read().decode("utf-8", "replace")}))
sys.exit(1)
except Exception as e:
print(json.dumps({"error": str(e)}))
sys.exit(1)
def main(cfg, inputs):
identity = sts("GetCallerIdentity", {}, cfg)
return {"ok": True, "identity": identity}
_run(main)
@@ -0,0 +1,143 @@
import json, os, sys, hmac, hashlib, datetime
import urllib.parse, urllib.request, urllib.error
import xml.etree.ElementTree as ET
def _cfg():
return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
def _inputs():
return json.loads(os.environ.get("INTEGRATION_INPUTS", "{}"))
def _sign_key(key, date_stamp, region, service):
def _h(k, m):
return hmac.new(k, m.encode("utf-8"), hashlib.sha256).digest()
k_date = _h(("AWS4" + key).encode("utf-8"), date_stamp)
k_region = _h(k_date, region)
k_service = _h(k_region, service)
return _h(k_service, "aws4_request")
def _strip_ns(tag):
return tag.split("}", 1)[1] if "}" in tag else tag
def _xml_to_dict(elem):
d = {}
children = list(elem)
if not children:
return (elem.text or "").strip()
for c in children:
tag = _strip_ns(c.tag)
val = _xml_to_dict(c)
if tag in d:
if not isinstance(d[tag], list):
d[tag] = [d[tag]]
d[tag].append(val)
else:
d[tag] = val
return d
def aws_query(service, host, region, action, version, params, cfg):
# params: dict of extra query params for this Action
body_params = {"Action": action, "Version": version}
body_params.update({k: str(v) for k, v in params.items() if v is not None})
body = urllib.parse.urlencode(sorted(body_params.items()))
access_key = str(cfg.get("access_key_id", ""))
secret_key = str(cfg.get("secret_access_key", ""))
session_token = cfg.get("session_token") or ""
now = datetime.datetime.utcnow()
amz_date = now.strftime("%Y%m%dT%H%M%SZ")
date_stamp = now.strftime("%Y%m%d")
method = "POST"
canonical_uri = "/"
canonical_querystring = ""
payload_hash = hashlib.sha256(body.encode("utf-8")).hexdigest()
canonical_headers = "content-type:application/x-www-form-urlencoded; charset=utf-8\n" \
"host:" + host + "\n" \
"x-amz-date:" + amz_date + "\n"
signed_headers = "content-type;host;x-amz-date"
if session_token:
canonical_headers += "x-amz-security-token:" + session_token + "\n"
signed_headers = "content-type;host;x-amz-date;x-amz-security-token"
canonical_request = "\n".join([method, canonical_uri, canonical_querystring,
canonical_headers, signed_headers, payload_hash])
algorithm = "AWS4-HMAC-SHA256"
credential_scope = date_stamp + "/" + region + "/" + service + "/aws4_request"
string_to_sign = "\n".join([algorithm, amz_date, credential_scope,
hashlib.sha256(canonical_request.encode("utf-8")).hexdigest()])
signing_key = _sign_key(secret_key, date_stamp, region, service)
signature = hmac.new(signing_key, string_to_sign.encode("utf-8"), hashlib.sha256).hexdigest()
authorization = (algorithm + " Credential=" + access_key + "/" + credential_scope +
", SignedHeaders=" + signed_headers + ", Signature=" + signature)
headers = {
"Content-Type": "application/x-www-form-urlencoded; charset=utf-8",
"X-Amz-Date": amz_date,
"Authorization": authorization,
"Accept": "application/json",
}
if session_token:
headers["X-Amz-Security-Token"] = session_token
req = urllib.request.Request("https://" + host + "/", data=body.encode("utf-8"), headers=headers, method="POST")
with urllib.request.urlopen(req, timeout=60) as r:
raw = r.read()
root = ET.fromstring(raw)
return {_strip_ns(root.tag): _xml_to_dict(root)}
def _region(cfg, inputs):
return (inputs.get("region") if inputs else None) or cfg.get("region") or "us-east-1"
def ec2(action, params, cfg, inputs):
region = _region(cfg, inputs)
return aws_query("ec2", "ec2." + region + ".amazonaws.com", region, action, "2016-11-15", params, cfg)
def iam(action, params, cfg):
return aws_query("iam", "iam.amazonaws.com", "us-east-1", action, "2010-05-08", params, cfg)
def sts(action, params, cfg):
return aws_query("sts", "sts.amazonaws.com", "us-east-1", action, "2011-06-15", params, cfg)
def _run(fn):
try:
print(json.dumps(fn(_cfg(), _inputs())))
except urllib.error.HTTPError as e:
print(json.dumps({"error": "HTTP " + str(e.code), "detail": e.read().decode("utf-8", "replace")}))
sys.exit(1)
except Exception as e:
print(json.dumps({"error": str(e)}))
sys.exit(1)
def main(cfg, inputs):
access_key_id = inputs.get("access_key_id")
if not access_key_id:
raise Exception("access_key_id is required")
status = inputs.get("status")
if not status:
raise Exception("status is required")
if status not in ("Active", "Inactive"):
raise Exception("status must be Active or Inactive")
params = {"AccessKeyId": access_key_id, "Status": status}
user_name = inputs.get("user_name")
if user_name and str(user_name).strip():
params["UserName"] = user_name
return iam("UpdateAccessKey", params, cfg)
_run(main)
+120
View File
@@ -0,0 +1,120 @@
id: azure_security
name: Microsoft Azure
version: 1.0.0
description: "Microsoft Azure (Resource Manager: Defender for Cloud + Network) — cloud containment: list and read Defender for Cloud security alerts and update their state, read the secure score, list/read network security groups (NSGs), and add or delete NSG security rules (deny inbound to isolate). Azure AD OAuth2 client-credentials authentication; stdlib-only, no extra Python dependencies."
changelog: "1.0.0 — Initial release: list/get security alerts, update alert state, secure score, list/get NSGs, create/delete NSG security rules."
category: cloud
# Per-instance configuration. Uses application (client-credentials) OAuth2 with
# a service principal that has Reader + Security Admin + Network Contributor on
# the subscription.
config_schema:
properties:
tenant_id:
type: string
description: "Azure AD tenant ID"
client_id:
type: string
description: "Service principal (client) ID"
client_secret:
type: string
description: "Service principal client secret"
x-soar-sensitive: true
subscription_id:
type: string
description: "Azure subscription ID"
required:
- tenant_id
- client_id
- client_secret
- subscription_id
commands:
- id: list_alerts
name: azure-list-alerts
description: "List Microsoft Defender for Cloud security alerts in the subscription."
risk: read
inputs_schema:
properties: {}
required: []
outputs_schema: { properties: {} }
- id: get_alert
name: azure-get-alert
description: "Get a single security alert by its full ARM resource ID."
risk: read
inputs_schema:
properties:
alert_id: { type: string, description: "Full ARM resource ID of the alert (from azure-list-alerts)" }
required: [alert_id]
outputs_schema: { properties: {} }
- id: update_alert_state
name: azure-update-alert-state
description: "Change a security alert's state (dismiss, resolve, activate, or inProgress)."
inputs_schema:
properties:
alert_id: { type: string, description: "Full ARM resource ID of the alert" }
state: { type: string, description: "dismiss | resolve | activate | inProgress" }
required: [alert_id, state]
outputs_schema: { properties: {} }
- id: get_secure_score
name: azure-get-secure-score
description: "Get the subscription's Defender for Cloud secure score."
risk: read
inputs_schema:
properties: {}
required: []
outputs_schema: { properties: {} }
- id: list_nsgs
name: azure-list-nsgs
description: "List network security groups in the subscription."
risk: read
inputs_schema:
properties: {}
required: []
outputs_schema: { properties: {} }
- id: get_nsg
name: azure-get-nsg
description: "Get a single network security group."
risk: read
inputs_schema:
properties:
resource_group: { type: string, description: "Resource group name" }
nsg_name: { type: string, description: "NSG name" }
required: [resource_group, nsg_name]
outputs_schema: { properties: {} }
- id: create_nsg_rule
name: azure-create-nsg-rule
description: "Create or update an NSG security rule (e.g. a Deny inbound rule to isolate a resource)."
inputs_schema:
properties:
resource_group: { type: string, description: "Resource group name" }
nsg_name: { type: string, description: "NSG name" }
rule_name: { type: string, description: "Security rule name" }
priority: { type: number, description: "Rule priority (100-4096)" }
direction: { type: string, description: "Inbound or Outbound (default Inbound)" }
access: { type: string, description: "Allow or Deny (default Deny)" }
protocol: { type: string, description: "Tcp, Udp, or * (default *)" }
source: { type: string, description: "Source address prefix (CIDR or *, default *)" }
destination: { type: string, description: "Destination address prefix (default *)" }
destination_port: { type: string, description: "Destination port range (default *)" }
required: [resource_group, nsg_name, rule_name, priority]
outputs_schema: { properties: {} }
- id: delete_nsg_rule
name: azure-delete-nsg-rule
description: "Delete an NSG security rule."
inputs_schema:
properties:
resource_group: { type: string, description: "Resource group name" }
nsg_name: { type: string, description: "NSG name" }
rule_name: { type: string, description: "Security rule name" }
required: [resource_group, nsg_name, rule_name]
outputs_schema: { properties: {} }
- id: test_connection
name: azure-test-connection
description: "Verify connectivity and the service-principal credentials (used by the Test button)."
risk: read
inputs_schema:
properties: {}
required: []
outputs_schema: { properties: {} }
@@ -0,0 +1,103 @@
import json, os, sys, urllib.parse, urllib.request, urllib.error
ARM = "https://management.azure.com"
def _cfg():
return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
def _inputs():
return json.loads(os.environ.get("INTEGRATION_INPUTS", "{}"))
def _token(cfg):
url = "https://login.microsoftonline.com/" + str(cfg.get("tenant_id", "")) + "/oauth2/v2.0/token"
form = urllib.parse.urlencode({
"grant_type": "client_credentials",
"client_id": str(cfg.get("client_id", "")),
"client_secret": str(cfg.get("client_secret", "")),
"scope": "https://management.azure.com/.default",
}).encode("utf-8")
req = urllib.request.Request(url, data=form, headers={"Content-Type": "application/x-www-form-urlencoded"}, method="POST")
with urllib.request.urlopen(req, timeout=60) as r:
tok = json.loads(r.read())
if not tok.get("access_token"):
raise Exception("Token request failed: " + json.dumps(tok))
return tok["access_token"]
def arm(method, path, token, api_version, body=None, params=None, full_url=None):
url = (full_url if full_url else ARM + path)
qp = {"api-version": api_version}
if params:
qp.update({k: v for k, v in params.items() if v not in (None, "")})
url += ("&" if "?" in url else "?") + urllib.parse.urlencode(qp)
data = json.dumps(body).encode("utf-8") if body is not None else None
headers = {"Authorization": "Bearer " + token, "Accept": "application/json"}
if data is not None:
headers["Content-Type"] = "application/json"
req = urllib.request.Request(url, data=data, headers=headers, method=method)
with urllib.request.urlopen(req, timeout=90) as r:
raw = r.read()
return json.loads(raw) if raw else {}
def _run(fn):
try:
cfg = _cfg()
inputs = _inputs()
token = _token(cfg)
print(json.dumps(fn(cfg, token, inputs)))
except urllib.error.HTTPError as e:
print(json.dumps({"error": "HTTP " + str(e.code), "detail": e.read().decode("utf-8", "replace")}))
sys.exit(1)
except Exception as e:
print(json.dumps({"error": str(e)}))
sys.exit(1)
sub = lambda cfg: str(cfg.get("subscription_id", ""))
q = lambda v: urllib.parse.quote(str(v), safe="")
def main(cfg, token, inputs):
resource_group = inputs.get("resource_group")
if not resource_group:
raise Exception("resource_group is required")
nsg_name = inputs.get("nsg_name")
if not nsg_name:
raise Exception("nsg_name is required")
rule_name = inputs.get("rule_name")
if not rule_name:
raise Exception("rule_name is required")
priority = inputs.get("priority")
if priority in (None, ""):
raise Exception("priority is required")
direction = inputs.get("direction")
access = inputs.get("access")
protocol = inputs.get("protocol")
source = inputs.get("source")
destination = inputs.get("destination")
destination_port = inputs.get("destination_port")
path = ("/subscriptions/" + sub(cfg) + "/resourceGroups/" + q(resource_group) +
"/providers/Microsoft.Network/networkSecurityGroups/" + q(nsg_name) +
"/securityRules/" + q(rule_name))
body = {
"properties": {
"priority": int(priority),
"direction": direction or "Inbound",
"access": access or "Deny",
"protocol": protocol or "*",
"sourceAddressPrefix": source or "*",
"destinationAddressPrefix": destination or "*",
"sourcePortRange": "*",
"destinationPortRange": destination_port or "*",
}
}
return arm("PUT", path, token, "2023-09-01", body=body)
_run(main)
@@ -0,0 +1,84 @@
import json, os, sys, urllib.parse, urllib.request, urllib.error
ARM = "https://management.azure.com"
def _cfg():
return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
def _inputs():
return json.loads(os.environ.get("INTEGRATION_INPUTS", "{}"))
def _token(cfg):
url = "https://login.microsoftonline.com/" + str(cfg.get("tenant_id", "")) + "/oauth2/v2.0/token"
form = urllib.parse.urlencode({
"grant_type": "client_credentials",
"client_id": str(cfg.get("client_id", "")),
"client_secret": str(cfg.get("client_secret", "")),
"scope": "https://management.azure.com/.default",
}).encode("utf-8")
req = urllib.request.Request(url, data=form, headers={"Content-Type": "application/x-www-form-urlencoded"}, method="POST")
with urllib.request.urlopen(req, timeout=60) as r:
tok = json.loads(r.read())
if not tok.get("access_token"):
raise Exception("Token request failed: " + json.dumps(tok))
return tok["access_token"]
def arm(method, path, token, api_version, body=None, params=None, full_url=None):
url = (full_url if full_url else ARM + path)
qp = {"api-version": api_version}
if params:
qp.update({k: v for k, v in params.items() if v not in (None, "")})
url += ("&" if "?" in url else "?") + urllib.parse.urlencode(qp)
data = json.dumps(body).encode("utf-8") if body is not None else None
headers = {"Authorization": "Bearer " + token, "Accept": "application/json"}
if data is not None:
headers["Content-Type"] = "application/json"
req = urllib.request.Request(url, data=data, headers=headers, method=method)
with urllib.request.urlopen(req, timeout=90) as r:
raw = r.read()
return json.loads(raw) if raw else {}
def _run(fn):
try:
cfg = _cfg()
inputs = _inputs()
token = _token(cfg)
print(json.dumps(fn(cfg, token, inputs)))
except urllib.error.HTTPError as e:
print(json.dumps({"error": "HTTP " + str(e.code), "detail": e.read().decode("utf-8", "replace")}))
sys.exit(1)
except Exception as e:
print(json.dumps({"error": str(e)}))
sys.exit(1)
sub = lambda cfg: str(cfg.get("subscription_id", ""))
q = lambda v: urllib.parse.quote(str(v), safe="")
def main(cfg, token, inputs):
resource_group = inputs.get("resource_group")
if not resource_group:
raise Exception("resource_group is required")
nsg_name = inputs.get("nsg_name")
if not nsg_name:
raise Exception("nsg_name is required")
rule_name = inputs.get("rule_name")
if not rule_name:
raise Exception("rule_name is required")
path = ("/subscriptions/" + sub(cfg) + "/resourceGroups/" + q(resource_group) +
"/providers/Microsoft.Network/networkSecurityGroups/" + q(nsg_name) +
"/securityRules/" + q(rule_name))
result = arm("DELETE", path, token, "2023-09-01")
if not result:
return {"ok": True, "deleted": rule_name}
return result
_run(main)
@@ -0,0 +1,67 @@
import json, os, sys, urllib.parse, urllib.request, urllib.error
ARM = "https://management.azure.com"
def _cfg():
return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
def _inputs():
return json.loads(os.environ.get("INTEGRATION_INPUTS", "{}"))
def _token(cfg):
url = "https://login.microsoftonline.com/" + str(cfg.get("tenant_id", "")) + "/oauth2/v2.0/token"
form = urllib.parse.urlencode({
"grant_type": "client_credentials",
"client_id": str(cfg.get("client_id", "")),
"client_secret": str(cfg.get("client_secret", "")),
"scope": "https://management.azure.com/.default",
}).encode("utf-8")
req = urllib.request.Request(url, data=form, headers={"Content-Type": "application/x-www-form-urlencoded"}, method="POST")
with urllib.request.urlopen(req, timeout=60) as r:
tok = json.loads(r.read())
if not tok.get("access_token"):
raise Exception("Token request failed: " + json.dumps(tok))
return tok["access_token"]
def arm(method, path, token, api_version, body=None, params=None, full_url=None):
url = (full_url if full_url else ARM + path)
qp = {"api-version": api_version}
if params:
qp.update({k: v for k, v in params.items() if v not in (None, "")})
url += ("&" if "?" in url else "?") + urllib.parse.urlencode(qp)
data = json.dumps(body).encode("utf-8") if body is not None else None
headers = {"Authorization": "Bearer " + token, "Accept": "application/json"}
if data is not None:
headers["Content-Type"] = "application/json"
req = urllib.request.Request(url, data=data, headers=headers, method=method)
with urllib.request.urlopen(req, timeout=90) as r:
raw = r.read()
return json.loads(raw) if raw else {}
def _run(fn):
try:
cfg = _cfg()
inputs = _inputs()
token = _token(cfg)
print(json.dumps(fn(cfg, token, inputs)))
except urllib.error.HTTPError as e:
print(json.dumps({"error": "HTTP " + str(e.code), "detail": e.read().decode("utf-8", "replace")}))
sys.exit(1)
except Exception as e:
print(json.dumps({"error": str(e)}))
sys.exit(1)
def main(cfg, token, inputs):
alert_id = inputs.get("alert_id")
if not alert_id:
raise Exception("alert_id is required")
return arm("GET", None, token, "2022-01-01", full_url=ARM + str(alert_id))
_run(main)
@@ -0,0 +1,76 @@
import json, os, sys, urllib.parse, urllib.request, urllib.error
ARM = "https://management.azure.com"
def _cfg():
return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
def _inputs():
return json.loads(os.environ.get("INTEGRATION_INPUTS", "{}"))
def _token(cfg):
url = "https://login.microsoftonline.com/" + str(cfg.get("tenant_id", "")) + "/oauth2/v2.0/token"
form = urllib.parse.urlencode({
"grant_type": "client_credentials",
"client_id": str(cfg.get("client_id", "")),
"client_secret": str(cfg.get("client_secret", "")),
"scope": "https://management.azure.com/.default",
}).encode("utf-8")
req = urllib.request.Request(url, data=form, headers={"Content-Type": "application/x-www-form-urlencoded"}, method="POST")
with urllib.request.urlopen(req, timeout=60) as r:
tok = json.loads(r.read())
if not tok.get("access_token"):
raise Exception("Token request failed: " + json.dumps(tok))
return tok["access_token"]
def arm(method, path, token, api_version, body=None, params=None, full_url=None):
url = (full_url if full_url else ARM + path)
qp = {"api-version": api_version}
if params:
qp.update({k: v for k, v in params.items() if v not in (None, "")})
url += ("&" if "?" in url else "?") + urllib.parse.urlencode(qp)
data = json.dumps(body).encode("utf-8") if body is not None else None
headers = {"Authorization": "Bearer " + token, "Accept": "application/json"}
if data is not None:
headers["Content-Type"] = "application/json"
req = urllib.request.Request(url, data=data, headers=headers, method=method)
with urllib.request.urlopen(req, timeout=90) as r:
raw = r.read()
return json.loads(raw) if raw else {}
def _run(fn):
try:
cfg = _cfg()
inputs = _inputs()
token = _token(cfg)
print(json.dumps(fn(cfg, token, inputs)))
except urllib.error.HTTPError as e:
print(json.dumps({"error": "HTTP " + str(e.code), "detail": e.read().decode("utf-8", "replace")}))
sys.exit(1)
except Exception as e:
print(json.dumps({"error": str(e)}))
sys.exit(1)
sub = lambda cfg: str(cfg.get("subscription_id", ""))
q = lambda v: urllib.parse.quote(str(v), safe="")
def main(cfg, token, inputs):
resource_group = inputs.get("resource_group")
if not resource_group:
raise Exception("resource_group is required")
nsg_name = inputs.get("nsg_name")
if not nsg_name:
raise Exception("nsg_name is required")
path = ("/subscriptions/" + sub(cfg) + "/resourceGroups/" + q(resource_group) +
"/providers/Microsoft.Network/networkSecurityGroups/" + q(nsg_name))
return arm("GET", path, token, "2023-09-01")
_run(main)
@@ -0,0 +1,68 @@
import json, os, sys, urllib.parse, urllib.request, urllib.error
ARM = "https://management.azure.com"
def _cfg():
return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
def _inputs():
return json.loads(os.environ.get("INTEGRATION_INPUTS", "{}"))
def _token(cfg):
url = "https://login.microsoftonline.com/" + str(cfg.get("tenant_id", "")) + "/oauth2/v2.0/token"
form = urllib.parse.urlencode({
"grant_type": "client_credentials",
"client_id": str(cfg.get("client_id", "")),
"client_secret": str(cfg.get("client_secret", "")),
"scope": "https://management.azure.com/.default",
}).encode("utf-8")
req = urllib.request.Request(url, data=form, headers={"Content-Type": "application/x-www-form-urlencoded"}, method="POST")
with urllib.request.urlopen(req, timeout=60) as r:
tok = json.loads(r.read())
if not tok.get("access_token"):
raise Exception("Token request failed: " + json.dumps(tok))
return tok["access_token"]
def arm(method, path, token, api_version, body=None, params=None, full_url=None):
url = (full_url if full_url else ARM + path)
qp = {"api-version": api_version}
if params:
qp.update({k: v for k, v in params.items() if v not in (None, "")})
url += ("&" if "?" in url else "?") + urllib.parse.urlencode(qp)
data = json.dumps(body).encode("utf-8") if body is not None else None
headers = {"Authorization": "Bearer " + token, "Accept": "application/json"}
if data is not None:
headers["Content-Type"] = "application/json"
req = urllib.request.Request(url, data=data, headers=headers, method=method)
with urllib.request.urlopen(req, timeout=90) as r:
raw = r.read()
return json.loads(raw) if raw else {}
def _run(fn):
try:
cfg = _cfg()
inputs = _inputs()
token = _token(cfg)
print(json.dumps(fn(cfg, token, inputs)))
except urllib.error.HTTPError as e:
print(json.dumps({"error": "HTTP " + str(e.code), "detail": e.read().decode("utf-8", "replace")}))
sys.exit(1)
except Exception as e:
print(json.dumps({"error": str(e)}))
sys.exit(1)
sub = lambda cfg: str(cfg.get("subscription_id", ""))
def main(cfg, token, inputs):
path = "/subscriptions/" + sub(cfg) + "/providers/Microsoft.Security/secureScores/ascScore"
return arm("GET", path, token, "2020-01-01")
_run(main)
@@ -0,0 +1,68 @@
import json, os, sys, urllib.parse, urllib.request, urllib.error
ARM = "https://management.azure.com"
def _cfg():
return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
def _inputs():
return json.loads(os.environ.get("INTEGRATION_INPUTS", "{}"))
def _token(cfg):
url = "https://login.microsoftonline.com/" + str(cfg.get("tenant_id", "")) + "/oauth2/v2.0/token"
form = urllib.parse.urlencode({
"grant_type": "client_credentials",
"client_id": str(cfg.get("client_id", "")),
"client_secret": str(cfg.get("client_secret", "")),
"scope": "https://management.azure.com/.default",
}).encode("utf-8")
req = urllib.request.Request(url, data=form, headers={"Content-Type": "application/x-www-form-urlencoded"}, method="POST")
with urllib.request.urlopen(req, timeout=60) as r:
tok = json.loads(r.read())
if not tok.get("access_token"):
raise Exception("Token request failed: " + json.dumps(tok))
return tok["access_token"]
def arm(method, path, token, api_version, body=None, params=None, full_url=None):
url = (full_url if full_url else ARM + path)
qp = {"api-version": api_version}
if params:
qp.update({k: v for k, v in params.items() if v not in (None, "")})
url += ("&" if "?" in url else "?") + urllib.parse.urlencode(qp)
data = json.dumps(body).encode("utf-8") if body is not None else None
headers = {"Authorization": "Bearer " + token, "Accept": "application/json"}
if data is not None:
headers["Content-Type"] = "application/json"
req = urllib.request.Request(url, data=data, headers=headers, method=method)
with urllib.request.urlopen(req, timeout=90) as r:
raw = r.read()
return json.loads(raw) if raw else {}
def _run(fn):
try:
cfg = _cfg()
inputs = _inputs()
token = _token(cfg)
print(json.dumps(fn(cfg, token, inputs)))
except urllib.error.HTTPError as e:
print(json.dumps({"error": "HTTP " + str(e.code), "detail": e.read().decode("utf-8", "replace")}))
sys.exit(1)
except Exception as e:
print(json.dumps({"error": str(e)}))
sys.exit(1)
sub = lambda cfg: str(cfg.get("subscription_id", ""))
def main(cfg, token, inputs):
path = "/subscriptions/" + sub(cfg) + "/providers/Microsoft.Security/alerts"
return arm("GET", path, token, "2022-01-01")
_run(main)
@@ -0,0 +1,68 @@
import json, os, sys, urllib.parse, urllib.request, urllib.error
ARM = "https://management.azure.com"
def _cfg():
return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
def _inputs():
return json.loads(os.environ.get("INTEGRATION_INPUTS", "{}"))
def _token(cfg):
url = "https://login.microsoftonline.com/" + str(cfg.get("tenant_id", "")) + "/oauth2/v2.0/token"
form = urllib.parse.urlencode({
"grant_type": "client_credentials",
"client_id": str(cfg.get("client_id", "")),
"client_secret": str(cfg.get("client_secret", "")),
"scope": "https://management.azure.com/.default",
}).encode("utf-8")
req = urllib.request.Request(url, data=form, headers={"Content-Type": "application/x-www-form-urlencoded"}, method="POST")
with urllib.request.urlopen(req, timeout=60) as r:
tok = json.loads(r.read())
if not tok.get("access_token"):
raise Exception("Token request failed: " + json.dumps(tok))
return tok["access_token"]
def arm(method, path, token, api_version, body=None, params=None, full_url=None):
url = (full_url if full_url else ARM + path)
qp = {"api-version": api_version}
if params:
qp.update({k: v for k, v in params.items() if v not in (None, "")})
url += ("&" if "?" in url else "?") + urllib.parse.urlencode(qp)
data = json.dumps(body).encode("utf-8") if body is not None else None
headers = {"Authorization": "Bearer " + token, "Accept": "application/json"}
if data is not None:
headers["Content-Type"] = "application/json"
req = urllib.request.Request(url, data=data, headers=headers, method=method)
with urllib.request.urlopen(req, timeout=90) as r:
raw = r.read()
return json.loads(raw) if raw else {}
def _run(fn):
try:
cfg = _cfg()
inputs = _inputs()
token = _token(cfg)
print(json.dumps(fn(cfg, token, inputs)))
except urllib.error.HTTPError as e:
print(json.dumps({"error": "HTTP " + str(e.code), "detail": e.read().decode("utf-8", "replace")}))
sys.exit(1)
except Exception as e:
print(json.dumps({"error": str(e)}))
sys.exit(1)
sub = lambda cfg: str(cfg.get("subscription_id", ""))
def main(cfg, token, inputs):
path = "/subscriptions/" + sub(cfg) + "/providers/Microsoft.Network/networkSecurityGroups"
return arm("GET", path, token, "2023-09-01")
_run(main)
@@ -0,0 +1,69 @@
import json, os, sys, urllib.parse, urllib.request, urllib.error
ARM = "https://management.azure.com"
def _cfg():
return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
def _inputs():
return json.loads(os.environ.get("INTEGRATION_INPUTS", "{}"))
def _token(cfg):
url = "https://login.microsoftonline.com/" + str(cfg.get("tenant_id", "")) + "/oauth2/v2.0/token"
form = urllib.parse.urlencode({
"grant_type": "client_credentials",
"client_id": str(cfg.get("client_id", "")),
"client_secret": str(cfg.get("client_secret", "")),
"scope": "https://management.azure.com/.default",
}).encode("utf-8")
req = urllib.request.Request(url, data=form, headers={"Content-Type": "application/x-www-form-urlencoded"}, method="POST")
with urllib.request.urlopen(req, timeout=60) as r:
tok = json.loads(r.read())
if not tok.get("access_token"):
raise Exception("Token request failed: " + json.dumps(tok))
return tok["access_token"]
def arm(method, path, token, api_version, body=None, params=None, full_url=None):
url = (full_url if full_url else ARM + path)
qp = {"api-version": api_version}
if params:
qp.update({k: v for k, v in params.items() if v not in (None, "")})
url += ("&" if "?" in url else "?") + urllib.parse.urlencode(qp)
data = json.dumps(body).encode("utf-8") if body is not None else None
headers = {"Authorization": "Bearer " + token, "Accept": "application/json"}
if data is not None:
headers["Content-Type"] = "application/json"
req = urllib.request.Request(url, data=data, headers=headers, method=method)
with urllib.request.urlopen(req, timeout=90) as r:
raw = r.read()
return json.loads(raw) if raw else {}
def _run(fn):
try:
cfg = _cfg()
inputs = _inputs()
token = _token(cfg)
print(json.dumps(fn(cfg, token, inputs)))
except urllib.error.HTTPError as e:
print(json.dumps({"error": "HTTP " + str(e.code), "detail": e.read().decode("utf-8", "replace")}))
sys.exit(1)
except Exception as e:
print(json.dumps({"error": str(e)}))
sys.exit(1)
sub = lambda cfg: str(cfg.get("subscription_id", ""))
def main(cfg, token, inputs):
path = "/subscriptions/" + sub(cfg)
arm("GET", path, token, "2022-12-01")
return {"ok": True}
_run(main)
@@ -0,0 +1,78 @@
import json, os, sys, urllib.parse, urllib.request, urllib.error
ARM = "https://management.azure.com"
def _cfg():
return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
def _inputs():
return json.loads(os.environ.get("INTEGRATION_INPUTS", "{}"))
def _token(cfg):
url = "https://login.microsoftonline.com/" + str(cfg.get("tenant_id", "")) + "/oauth2/v2.0/token"
form = urllib.parse.urlencode({
"grant_type": "client_credentials",
"client_id": str(cfg.get("client_id", "")),
"client_secret": str(cfg.get("client_secret", "")),
"scope": "https://management.azure.com/.default",
}).encode("utf-8")
req = urllib.request.Request(url, data=form, headers={"Content-Type": "application/x-www-form-urlencoded"}, method="POST")
with urllib.request.urlopen(req, timeout=60) as r:
tok = json.loads(r.read())
if not tok.get("access_token"):
raise Exception("Token request failed: " + json.dumps(tok))
return tok["access_token"]
def arm(method, path, token, api_version, body=None, params=None, full_url=None):
url = (full_url if full_url else ARM + path)
qp = {"api-version": api_version}
if params:
qp.update({k: v for k, v in params.items() if v not in (None, "")})
url += ("&" if "?" in url else "?") + urllib.parse.urlencode(qp)
data = json.dumps(body).encode("utf-8") if body is not None else None
headers = {"Authorization": "Bearer " + token, "Accept": "application/json"}
if data is not None:
headers["Content-Type"] = "application/json"
req = urllib.request.Request(url, data=data, headers=headers, method=method)
with urllib.request.urlopen(req, timeout=90) as r:
raw = r.read()
return json.loads(raw) if raw else {}
def _run(fn):
try:
cfg = _cfg()
inputs = _inputs()
token = _token(cfg)
print(json.dumps(fn(cfg, token, inputs)))
except urllib.error.HTTPError as e:
print(json.dumps({"error": "HTTP " + str(e.code), "detail": e.read().decode("utf-8", "replace")}))
sys.exit(1)
except Exception as e:
print(json.dumps({"error": str(e)}))
sys.exit(1)
VALID_STATES = ("dismiss", "resolve", "activate", "inProgress")
def main(cfg, token, inputs):
alert_id = inputs.get("alert_id")
if not alert_id:
raise Exception("alert_id is required")
state = inputs.get("state")
if not state:
raise Exception("state is required")
if state not in VALID_STATES:
raise Exception("state must be one of: " + ", ".join(VALID_STATES))
result = arm("POST", None, token, "2022-01-01", body=None, full_url=ARM + str(alert_id) + "/" + state)
if not result:
return {"ok": True, "state": state}
return result
_run(main)
+97
View File
@@ -0,0 +1,97 @@
id: gcp_security
name: Google Cloud
version: 1.0.0
description: "Google Cloud Platform (Compute Engine + Security Command Center) — cloud containment: list/create/delete VPC firewall rules (deny to isolate), list and stop Compute Engine instances, and list Security Command Center findings. Authenticates with a Google service account (RS256 JWT bearer flow). Runs on a remote engine. Requires the Python 'PyJWT' and 'cryptography' libraries on the engine host (pip install pyjwt cryptography)."
changelog: "1.0.0 — Initial release: list/create/delete firewall rules, list/stop instances, list Security Command Center findings."
category: cloud
# Per-instance configuration. The scripts build a signed RS256 assertion from the
# service account's private_key/client_email and exchange it for an access token
# (scope cloud-platform). The service account needs Compute and Security Center
# roles. organization_id is only used by gcp-list-scc-findings.
config_schema:
properties:
service_account_json:
type: string
description: "Full service account key JSON (must contain client_email and private_key)"
x-soar-sensitive: true
project_id:
type: string
description: "GCP project ID"
organization_id:
type: string
description: "GCP organization ID (only required for Security Command Center findings)"
required:
- service_account_json
- project_id
commands:
- id: list_firewalls
name: gcp-list-firewalls
description: "List VPC firewall rules in the project."
risk: read
inputs_schema:
properties: {}
required: []
outputs_schema: { properties: {} }
- id: create_firewall
name: gcp-create-firewall
description: "Create a VPC firewall rule (e.g. a deny rule to isolate targets)."
inputs_schema:
properties:
name: { type: string, description: "Firewall rule name" }
network: { type: string, description: "Network name (default 'default')" }
direction: { type: string, description: "INGRESS or EGRESS (default INGRESS)" }
action: { type: string, description: "allow or deny (default deny)" }
protocol: { type: string, description: "Protocol (tcp, udp, all — default all)" }
ports: { type: string, description: "Comma-separated ports (optional; omit for all)" }
ranges: { type: string, description: "Comma-separated source/destination CIDR ranges (default 0.0.0.0/0)" }
priority: { type: number, description: "Rule priority 0-65535 (default 1000)" }
target_tags: { type: string, description: "Comma-separated target network tags (optional)" }
required: [name]
outputs_schema: { properties: {} }
- id: delete_firewall
name: gcp-delete-firewall
description: "Delete a VPC firewall rule by name."
inputs_schema:
properties:
name: { type: string, description: "Firewall rule name" }
required: [name]
outputs_schema: { properties: {} }
- id: list_instances
name: gcp-list-instances
description: "List Compute Engine instances in a zone."
risk: read
inputs_schema:
properties:
zone: { type: string, description: "Zone (e.g. europe-west1-b)" }
required: [zone]
outputs_schema: { properties: {} }
- id: stop_instance
name: gcp-stop-instance
description: "Stop a Compute Engine instance (containment)."
inputs_schema:
properties:
zone: { type: string, description: "Zone of the instance" }
instance: { type: string, description: "Instance name" }
required: [zone, instance]
outputs_schema: { properties: {} }
- id: list_scc_findings
name: gcp-list-scc-findings
description: "List Security Command Center findings for the organization."
risk: read
inputs_schema:
properties:
filter: { type: string, description: "Optional SCC filter (e.g. state=\"ACTIVE\")" }
page_size: { type: number, description: "Max findings (default 100)" }
required: []
outputs_schema: { properties: {} }
- id: test_connection
name: gcp-test-connection
description: "Verify the service-account token exchange and project access (used by the Test button)."
risk: read
inputs_schema:
properties: {}
required: []
outputs_schema: { properties: {} }
@@ -0,0 +1,118 @@
import json, os, sys, time, urllib.parse, urllib.request, urllib.error
import jwt
TOKEN_URL = "https://oauth2.googleapis.com/token"
SCOPE = "https://www.googleapis.com/auth/cloud-platform"
COMPUTE = "https://compute.googleapis.com/compute/v1"
def _cfg():
return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
def _inputs():
return json.loads(os.environ.get("INTEGRATION_INPUTS", "{}"))
def _token(cfg):
raw = cfg.get("service_account_json", "")
sa = json.loads(raw) if isinstance(raw, str) else raw
if not sa.get("client_email") or not sa.get("private_key"):
raise Exception("service_account_json must contain client_email and private_key")
now = int(time.time())
aud = sa.get("token_uri") or TOKEN_URL
payload = {
"iss": sa["client_email"],
"scope": SCOPE,
"aud": aud,
"iat": now,
"exp": now + 3600,
}
assertion = jwt.encode(payload, sa["private_key"], algorithm="RS256")
data = urllib.parse.urlencode({
"grant_type": "urn:ietf:params:oauth:grant-type:jwt-bearer",
"assertion": assertion,
}).encode("utf-8")
req = urllib.request.Request(aud, data=data,
headers={"Content-Type": "application/x-www-form-urlencoded"}, method="POST")
with urllib.request.urlopen(req, timeout=60) as r:
tok = json.loads(r.read())
if not tok.get("access_token"):
raise Exception("Token request failed: " + json.dumps(tok))
return tok["access_token"]
def api(method, url, cfg, body=None, params=None):
q = {k: str(v) for k, v in (params or {}).items() if v not in (None, "")}
if q:
url += ("&" if "?" in url else "?") + urllib.parse.urlencode(q)
data = json.dumps(body).encode("utf-8") if body is not None else None
headers = {"Accept": "application/json", "Authorization": "Bearer " + _token(cfg)}
if data is not None:
headers["Content-Type"] = "application/json"
req = urllib.request.Request(url, data=data, headers=headers, method=method)
with urllib.request.urlopen(req, timeout=90) as r:
raw = r.read()
return json.loads(raw) if raw else {}
def _run(fn):
try:
print(json.dumps(fn(_cfg(), _inputs())))
except urllib.error.HTTPError as e:
print(json.dumps({"error": "HTTP " + str(e.code), "detail": e.read().decode("utf-8", "replace")}))
sys.exit(1)
except Exception as e:
print(json.dumps({"error": str(e)}))
sys.exit(1)
proj = lambda cfg: str(cfg.get("project_id", ""))
def _csv(v):
return [s.strip() for s in str(v).split(",") if s.strip()]
def main(cfg, inputs):
name = inputs.get("name")
if not name:
raise Exception("name is required")
network = inputs.get("network") or "default"
direction = inputs.get("direction") or "INGRESS"
action = inputs.get("action") or "deny"
protocol = inputs.get("protocol") or "all"
ports = _csv(inputs.get("ports", ""))
ranges = _csv(inputs.get("ranges", ""))
priority = inputs.get("priority", 1000)
target_tags = _csv(inputs.get("target_tags", ""))
rule_entry = {"IPProtocol": protocol or "all"}
if ports:
rule_entry["ports"] = ports
key = "allowed" if action == "allow" else "denied"
body = {
"name": name,
"network": "global/networks/" + (network or "default"),
"direction": direction or "INGRESS",
"priority": int(priority),
key: [rule_entry],
}
ranges_list = ranges or ["0.0.0.0/0"]
if (direction or "INGRESS") == "EGRESS":
body["destinationRanges"] = ranges_list
else:
body["sourceRanges"] = ranges_list
if target_tags:
body["targetTags"] = target_tags
url = COMPUTE + "/projects/{}/global/firewalls".format(proj(cfg))
return api("POST", url, cfg, body=body)
_run(main)
@@ -0,0 +1,83 @@
import json, os, sys, time, urllib.parse, urllib.request, urllib.error
import jwt
TOKEN_URL = "https://oauth2.googleapis.com/token"
SCOPE = "https://www.googleapis.com/auth/cloud-platform"
COMPUTE = "https://compute.googleapis.com/compute/v1"
def _cfg():
return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
def _inputs():
return json.loads(os.environ.get("INTEGRATION_INPUTS", "{}"))
def _token(cfg):
raw = cfg.get("service_account_json", "")
sa = json.loads(raw) if isinstance(raw, str) else raw
if not sa.get("client_email") or not sa.get("private_key"):
raise Exception("service_account_json must contain client_email and private_key")
now = int(time.time())
aud = sa.get("token_uri") or TOKEN_URL
payload = {
"iss": sa["client_email"],
"scope": SCOPE,
"aud": aud,
"iat": now,
"exp": now + 3600,
}
assertion = jwt.encode(payload, sa["private_key"], algorithm="RS256")
data = urllib.parse.urlencode({
"grant_type": "urn:ietf:params:oauth:grant-type:jwt-bearer",
"assertion": assertion,
}).encode("utf-8")
req = urllib.request.Request(aud, data=data,
headers={"Content-Type": "application/x-www-form-urlencoded"}, method="POST")
with urllib.request.urlopen(req, timeout=60) as r:
tok = json.loads(r.read())
if not tok.get("access_token"):
raise Exception("Token request failed: " + json.dumps(tok))
return tok["access_token"]
def api(method, url, cfg, body=None, params=None):
q = {k: str(v) for k, v in (params or {}).items() if v not in (None, "")}
if q:
url += ("&" if "?" in url else "?") + urllib.parse.urlencode(q)
data = json.dumps(body).encode("utf-8") if body is not None else None
headers = {"Accept": "application/json", "Authorization": "Bearer " + _token(cfg)}
if data is not None:
headers["Content-Type"] = "application/json"
req = urllib.request.Request(url, data=data, headers=headers, method=method)
with urllib.request.urlopen(req, timeout=90) as r:
raw = r.read()
return json.loads(raw) if raw else {}
def _run(fn):
try:
print(json.dumps(fn(_cfg(), _inputs())))
except urllib.error.HTTPError as e:
print(json.dumps({"error": "HTTP " + str(e.code), "detail": e.read().decode("utf-8", "replace")}))
sys.exit(1)
except Exception as e:
print(json.dumps({"error": str(e)}))
sys.exit(1)
proj = lambda cfg: str(cfg.get("project_id", ""))
q = lambda v: urllib.parse.quote(str(v), safe="")
def main(cfg, inputs):
name = inputs.get("name")
if not name:
raise Exception("name is required")
url = COMPUTE + "/projects/{}/global/firewalls/{}".format(proj(cfg), q(name))
return api("DELETE", url, cfg)
_run(main)
@@ -0,0 +1,79 @@
import json, os, sys, time, urllib.parse, urllib.request, urllib.error
import jwt
TOKEN_URL = "https://oauth2.googleapis.com/token"
SCOPE = "https://www.googleapis.com/auth/cloud-platform"
COMPUTE = "https://compute.googleapis.com/compute/v1"
def _cfg():
return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
def _inputs():
return json.loads(os.environ.get("INTEGRATION_INPUTS", "{}"))
def _token(cfg):
raw = cfg.get("service_account_json", "")
sa = json.loads(raw) if isinstance(raw, str) else raw
if not sa.get("client_email") or not sa.get("private_key"):
raise Exception("service_account_json must contain client_email and private_key")
now = int(time.time())
aud = sa.get("token_uri") or TOKEN_URL
payload = {
"iss": sa["client_email"],
"scope": SCOPE,
"aud": aud,
"iat": now,
"exp": now + 3600,
}
assertion = jwt.encode(payload, sa["private_key"], algorithm="RS256")
data = urllib.parse.urlencode({
"grant_type": "urn:ietf:params:oauth:grant-type:jwt-bearer",
"assertion": assertion,
}).encode("utf-8")
req = urllib.request.Request(aud, data=data,
headers={"Content-Type": "application/x-www-form-urlencoded"}, method="POST")
with urllib.request.urlopen(req, timeout=60) as r:
tok = json.loads(r.read())
if not tok.get("access_token"):
raise Exception("Token request failed: " + json.dumps(tok))
return tok["access_token"]
def api(method, url, cfg, body=None, params=None):
q = {k: str(v) for k, v in (params or {}).items() if v not in (None, "")}
if q:
url += ("&" if "?" in url else "?") + urllib.parse.urlencode(q)
data = json.dumps(body).encode("utf-8") if body is not None else None
headers = {"Accept": "application/json", "Authorization": "Bearer " + _token(cfg)}
if data is not None:
headers["Content-Type"] = "application/json"
req = urllib.request.Request(url, data=data, headers=headers, method=method)
with urllib.request.urlopen(req, timeout=90) as r:
raw = r.read()
return json.loads(raw) if raw else {}
def _run(fn):
try:
print(json.dumps(fn(_cfg(), _inputs())))
except urllib.error.HTTPError as e:
print(json.dumps({"error": "HTTP " + str(e.code), "detail": e.read().decode("utf-8", "replace")}))
sys.exit(1)
except Exception as e:
print(json.dumps({"error": str(e)}))
sys.exit(1)
proj = lambda cfg: str(cfg.get("project_id", ""))
def main(cfg, inputs):
url = COMPUTE + "/projects/{}/global/firewalls".format(proj(cfg))
return api("GET", url, cfg)
_run(main)
@@ -0,0 +1,83 @@
import json, os, sys, time, urllib.parse, urllib.request, urllib.error
import jwt
TOKEN_URL = "https://oauth2.googleapis.com/token"
SCOPE = "https://www.googleapis.com/auth/cloud-platform"
COMPUTE = "https://compute.googleapis.com/compute/v1"
def _cfg():
return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
def _inputs():
return json.loads(os.environ.get("INTEGRATION_INPUTS", "{}"))
def _token(cfg):
raw = cfg.get("service_account_json", "")
sa = json.loads(raw) if isinstance(raw, str) else raw
if not sa.get("client_email") or not sa.get("private_key"):
raise Exception("service_account_json must contain client_email and private_key")
now = int(time.time())
aud = sa.get("token_uri") or TOKEN_URL
payload = {
"iss": sa["client_email"],
"scope": SCOPE,
"aud": aud,
"iat": now,
"exp": now + 3600,
}
assertion = jwt.encode(payload, sa["private_key"], algorithm="RS256")
data = urllib.parse.urlencode({
"grant_type": "urn:ietf:params:oauth:grant-type:jwt-bearer",
"assertion": assertion,
}).encode("utf-8")
req = urllib.request.Request(aud, data=data,
headers={"Content-Type": "application/x-www-form-urlencoded"}, method="POST")
with urllib.request.urlopen(req, timeout=60) as r:
tok = json.loads(r.read())
if not tok.get("access_token"):
raise Exception("Token request failed: " + json.dumps(tok))
return tok["access_token"]
def api(method, url, cfg, body=None, params=None):
q = {k: str(v) for k, v in (params or {}).items() if v not in (None, "")}
if q:
url += ("&" if "?" in url else "?") + urllib.parse.urlencode(q)
data = json.dumps(body).encode("utf-8") if body is not None else None
headers = {"Accept": "application/json", "Authorization": "Bearer " + _token(cfg)}
if data is not None:
headers["Content-Type"] = "application/json"
req = urllib.request.Request(url, data=data, headers=headers, method=method)
with urllib.request.urlopen(req, timeout=90) as r:
raw = r.read()
return json.loads(raw) if raw else {}
def _run(fn):
try:
print(json.dumps(fn(_cfg(), _inputs())))
except urllib.error.HTTPError as e:
print(json.dumps({"error": "HTTP " + str(e.code), "detail": e.read().decode("utf-8", "replace")}))
sys.exit(1)
except Exception as e:
print(json.dumps({"error": str(e)}))
sys.exit(1)
proj = lambda cfg: str(cfg.get("project_id", ""))
q = lambda v: urllib.parse.quote(str(v), safe="")
def main(cfg, inputs):
zone = inputs.get("zone")
if not zone:
raise Exception("zone is required")
url = COMPUTE + "/projects/{}/zones/{}/instances".format(proj(cfg), q(zone))
return api("GET", url, cfg)
_run(main)
@@ -0,0 +1,81 @@
import json, os, sys, time, urllib.parse, urllib.request, urllib.error
import jwt
TOKEN_URL = "https://oauth2.googleapis.com/token"
SCOPE = "https://www.googleapis.com/auth/cloud-platform"
COMPUTE = "https://compute.googleapis.com/compute/v1"
def _cfg():
return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
def _inputs():
return json.loads(os.environ.get("INTEGRATION_INPUTS", "{}"))
def _token(cfg):
raw = cfg.get("service_account_json", "")
sa = json.loads(raw) if isinstance(raw, str) else raw
if not sa.get("client_email") or not sa.get("private_key"):
raise Exception("service_account_json must contain client_email and private_key")
now = int(time.time())
aud = sa.get("token_uri") or TOKEN_URL
payload = {
"iss": sa["client_email"],
"scope": SCOPE,
"aud": aud,
"iat": now,
"exp": now + 3600,
}
assertion = jwt.encode(payload, sa["private_key"], algorithm="RS256")
data = urllib.parse.urlencode({
"grant_type": "urn:ietf:params:oauth:grant-type:jwt-bearer",
"assertion": assertion,
}).encode("utf-8")
req = urllib.request.Request(aud, data=data,
headers={"Content-Type": "application/x-www-form-urlencoded"}, method="POST")
with urllib.request.urlopen(req, timeout=60) as r:
tok = json.loads(r.read())
if not tok.get("access_token"):
raise Exception("Token request failed: " + json.dumps(tok))
return tok["access_token"]
def api(method, url, cfg, body=None, params=None):
q = {k: str(v) for k, v in (params or {}).items() if v not in (None, "")}
if q:
url += ("&" if "?" in url else "?") + urllib.parse.urlencode(q)
data = json.dumps(body).encode("utf-8") if body is not None else None
headers = {"Accept": "application/json", "Authorization": "Bearer " + _token(cfg)}
if data is not None:
headers["Content-Type"] = "application/json"
req = urllib.request.Request(url, data=data, headers=headers, method=method)
with urllib.request.urlopen(req, timeout=90) as r:
raw = r.read()
return json.loads(raw) if raw else {}
def _run(fn):
try:
print(json.dumps(fn(_cfg(), _inputs())))
except urllib.error.HTTPError as e:
print(json.dumps({"error": "HTTP " + str(e.code), "detail": e.read().decode("utf-8", "replace")}))
sys.exit(1)
except Exception as e:
print(json.dumps({"error": str(e)}))
sys.exit(1)
def main(cfg, inputs):
org = cfg.get("organization_id")
if not org:
raise Exception("organization_id must be configured for SCC findings")
filter_ = inputs.get("filter")
page_size = inputs.get("page_size", 100)
url = "https://securitycenter.googleapis.com/v1/organizations/" + str(org) + "/sources/-/findings"
return api("GET", url, cfg, params={"filter": filter_, "pageSize": int(page_size)})
_run(main)
@@ -0,0 +1,86 @@
import json, os, sys, time, urllib.parse, urllib.request, urllib.error
import jwt
TOKEN_URL = "https://oauth2.googleapis.com/token"
SCOPE = "https://www.googleapis.com/auth/cloud-platform"
COMPUTE = "https://compute.googleapis.com/compute/v1"
def _cfg():
return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
def _inputs():
return json.loads(os.environ.get("INTEGRATION_INPUTS", "{}"))
def _token(cfg):
raw = cfg.get("service_account_json", "")
sa = json.loads(raw) if isinstance(raw, str) else raw
if not sa.get("client_email") or not sa.get("private_key"):
raise Exception("service_account_json must contain client_email and private_key")
now = int(time.time())
aud = sa.get("token_uri") or TOKEN_URL
payload = {
"iss": sa["client_email"],
"scope": SCOPE,
"aud": aud,
"iat": now,
"exp": now + 3600,
}
assertion = jwt.encode(payload, sa["private_key"], algorithm="RS256")
data = urllib.parse.urlencode({
"grant_type": "urn:ietf:params:oauth:grant-type:jwt-bearer",
"assertion": assertion,
}).encode("utf-8")
req = urllib.request.Request(aud, data=data,
headers={"Content-Type": "application/x-www-form-urlencoded"}, method="POST")
with urllib.request.urlopen(req, timeout=60) as r:
tok = json.loads(r.read())
if not tok.get("access_token"):
raise Exception("Token request failed: " + json.dumps(tok))
return tok["access_token"]
def api(method, url, cfg, body=None, params=None):
q = {k: str(v) for k, v in (params or {}).items() if v not in (None, "")}
if q:
url += ("&" if "?" in url else "?") + urllib.parse.urlencode(q)
data = json.dumps(body).encode("utf-8") if body is not None else None
headers = {"Accept": "application/json", "Authorization": "Bearer " + _token(cfg)}
if data is not None:
headers["Content-Type"] = "application/json"
req = urllib.request.Request(url, data=data, headers=headers, method=method)
with urllib.request.urlopen(req, timeout=90) as r:
raw = r.read()
return json.loads(raw) if raw else {}
def _run(fn):
try:
print(json.dumps(fn(_cfg(), _inputs())))
except urllib.error.HTTPError as e:
print(json.dumps({"error": "HTTP " + str(e.code), "detail": e.read().decode("utf-8", "replace")}))
sys.exit(1)
except Exception as e:
print(json.dumps({"error": str(e)}))
sys.exit(1)
proj = lambda cfg: str(cfg.get("project_id", ""))
q = lambda v: urllib.parse.quote(str(v), safe="")
def main(cfg, inputs):
zone = inputs.get("zone")
if not zone:
raise Exception("zone is required")
instance = inputs.get("instance")
if not instance:
raise Exception("instance is required")
url = COMPUTE + "/projects/{}/zones/{}/instances/{}/stop".format(proj(cfg), q(zone), q(instance))
return api("POST", url, cfg)
_run(main)
@@ -0,0 +1,80 @@
import json, os, sys, time, urllib.parse, urllib.request, urllib.error
import jwt
TOKEN_URL = "https://oauth2.googleapis.com/token"
SCOPE = "https://www.googleapis.com/auth/cloud-platform"
COMPUTE = "https://compute.googleapis.com/compute/v1"
def _cfg():
return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
def _inputs():
return json.loads(os.environ.get("INTEGRATION_INPUTS", "{}"))
def _token(cfg):
raw = cfg.get("service_account_json", "")
sa = json.loads(raw) if isinstance(raw, str) else raw
if not sa.get("client_email") or not sa.get("private_key"):
raise Exception("service_account_json must contain client_email and private_key")
now = int(time.time())
aud = sa.get("token_uri") or TOKEN_URL
payload = {
"iss": sa["client_email"],
"scope": SCOPE,
"aud": aud,
"iat": now,
"exp": now + 3600,
}
assertion = jwt.encode(payload, sa["private_key"], algorithm="RS256")
data = urllib.parse.urlencode({
"grant_type": "urn:ietf:params:oauth:grant-type:jwt-bearer",
"assertion": assertion,
}).encode("utf-8")
req = urllib.request.Request(aud, data=data,
headers={"Content-Type": "application/x-www-form-urlencoded"}, method="POST")
with urllib.request.urlopen(req, timeout=60) as r:
tok = json.loads(r.read())
if not tok.get("access_token"):
raise Exception("Token request failed: " + json.dumps(tok))
return tok["access_token"]
def api(method, url, cfg, body=None, params=None):
q = {k: str(v) for k, v in (params or {}).items() if v not in (None, "")}
if q:
url += ("&" if "?" in url else "?") + urllib.parse.urlencode(q)
data = json.dumps(body).encode("utf-8") if body is not None else None
headers = {"Accept": "application/json", "Authorization": "Bearer " + _token(cfg)}
if data is not None:
headers["Content-Type"] = "application/json"
req = urllib.request.Request(url, data=data, headers=headers, method=method)
with urllib.request.urlopen(req, timeout=90) as r:
raw = r.read()
return json.loads(raw) if raw else {}
def _run(fn):
try:
print(json.dumps(fn(_cfg(), _inputs())))
except urllib.error.HTTPError as e:
print(json.dumps({"error": "HTTP " + str(e.code), "detail": e.read().decode("utf-8", "replace")}))
sys.exit(1)
except Exception as e:
print(json.dumps({"error": str(e)}))
sys.exit(1)
proj = lambda cfg: str(cfg.get("project_id", ""))
def main(cfg, inputs):
url = COMPUTE + "/projects/{}".format(proj(cfg))
api("GET", url, cfg)
return {"ok": True}
_run(main)