Compare commits
3 Commits
14e5e102a9
...
5501297984
| Author | SHA1 | Date | |
|---|---|---|---|
| 5501297984 | |||
| 31d0a5938a | |||
| 62a44aea28 |
@@ -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)
|
||||||
@@ -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)
|
||||||
@@ -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)
|
||||||
@@ -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)
|
||||||
@@ -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)
|
||||||
Reference in New Issue
Block a user