|
| 1 | +load('requests', 'Session') |
| 2 | +load('json', json_encode='encode', json_decode='decode') |
| 3 | +load('net', 'ip_address') |
| 4 | +load('http', 'url_encode') |
| 5 | + |
| 6 | +# ------------------------- |
| 7 | +# Global Configuration |
| 8 | +# ------------------------- |
| 9 | +SITE_ID = "UPDATE_ME" |
| 10 | +DELETE_ASSETS = True |
| 11 | +ALLOW_LIST = ["10.0.0.0/8", "192.168.0.0/16"] |
| 12 | + |
| 13 | +# ------------------------- |
| 14 | +# IP Filtering Functions |
| 15 | +# ------------------------- |
| 16 | +def ip_to_int(ip): |
| 17 | + parts = ip.split('.') |
| 18 | + return (int(parts[0]) << 24) + (int(parts[1]) << 16) + (int(parts[2]) << 8) + int(parts[3]) |
| 19 | + |
| 20 | +def cidr_to_netmask(bits): |
| 21 | + return ~((1 << (32 - bits)) - 1) & 0xFFFFFFFF |
| 22 | + |
| 23 | +def ip_in_cidr(ip_str, cidr): |
| 24 | + ip_int = ip_to_int(ip_str) |
| 25 | + base, mask_bits = cidr.split('/') |
| 26 | + base_int = ip_to_int(base) |
| 27 | + mask = cidr_to_netmask(int(mask_bits)) |
| 28 | + return (ip_int & mask) == (base_int & mask) |
| 29 | + |
| 30 | +def is_ip_allowed(ip_str, allow_list): |
| 31 | + ip_obj = ip_address(ip_str) |
| 32 | + if ip_obj.version != 4: |
| 33 | + return False |
| 34 | + for cidr in allow_list: |
| 35 | + if ip_in_cidr(ip_str, cidr): |
| 36 | + return True |
| 37 | + return False |
| 38 | + |
| 39 | +# ------------------------- |
| 40 | +# Entrypoint |
| 41 | +# ------------------------- |
| 42 | +def main(*args, **kwargs): |
| 43 | + org_token = kwargs["access_secret"] |
| 44 | + |
| 45 | + session = Session() |
| 46 | + session.headers.set("Authorization", "Bearer {}".format(org_token)) |
| 47 | + session.headers.set("Content-Type", "application/json") |
| 48 | + |
| 49 | + # Step 1: Export assets |
| 50 | + params = {"search": "source:sample source_count:1", "fields": "id,addresses,last_agent_id"} |
| 51 | + asset_url = "https://console.runzero.com/api/v1.0/export/org/assets.json?{}".format(url_encode(params)) |
| 52 | + response = session.get(asset_url, timeout=3600) |
| 53 | + |
| 54 | + if not response or response.status_code != 200: |
| 55 | + print("Failed to fetch assets") |
| 56 | + return [] |
| 57 | + |
| 58 | + data = json_decode(response.body) |
| 59 | + |
| 60 | + # Step 2: Filter assets and group IPs by agent |
| 61 | + agent_ip_map = {} # {agent_id: [ip, ip, ...]} |
| 62 | + asset_ids = [] |
| 63 | + |
| 64 | + for asset in data: |
| 65 | + agent_id = asset.get("last_agent_id") |
| 66 | + if not agent_id: |
| 67 | + continue |
| 68 | + for ip in asset.get("addresses", []): |
| 69 | + print("Evaluating IP: {}".format(ip)) |
| 70 | + if is_ip_allowed(ip, ALLOW_LIST): |
| 71 | + if not agent_ip_map.get(agent_id): |
| 72 | + agent_ip_map[agent_id] = [] |
| 73 | + agent_ip_map[agent_id].append(ip) |
| 74 | + if asset["id"] not in asset_ids: |
| 75 | + asset_ids.append(asset["id"]) |
| 76 | + |
| 77 | + |
| 78 | + # Step 3: Create scan task per explorer/agent |
| 79 | + for agent_id, ips in agent_ip_map.items(): |
| 80 | + scan_url = "https://console.runzero.com/api/v1.0/org/sites/{}/scan".format(SITE_ID) |
| 81 | + scan_payload = { |
| 82 | + "targets": "\n".join(ips), |
| 83 | + "scan-name": "Auto Scan Sample Only Assets", |
| 84 | + "scan-description": "This scan was automatically created to scan assets discovered by the 'sample' source only.", |
| 85 | + "scan-frequency": "once", |
| 86 | + "scan-start": "0", |
| 87 | + "scan-tags": "type=AUTOMATED", |
| 88 | + "scan-grace-period": "0", |
| 89 | + "agent": agent_id, |
| 90 | + "rate": "1000", |
| 91 | + "max-host-rate": "20", |
| 92 | + "passes": "3", |
| 93 | + "max-attempts": "3", |
| 94 | + "max-sockets": "500", |
| 95 | + "max-group-size": "4096", |
| 96 | + "max-ttl": "255", |
| 97 | + "screenshots": "true", |
| 98 | + } |
| 99 | + print(scan_payload) |
| 100 | + post = session.put(scan_url, body=bytes(json_encode(scan_payload))) |
| 101 | + if post and post.status_code == 200: |
| 102 | + print("Scan created for agent {}".format(agent_id)) |
| 103 | + else: |
| 104 | + print("Scan failed for agent {}".format(agent_id)) |
| 105 | + |
| 106 | + # Step 4: Optional asset deletion |
| 107 | + if DELETE_ASSETS and len(asset_ids) > 0: |
| 108 | + delete_url = "https://console.runzero.com/api/v1.0/org/assets/bulk/delete" |
| 109 | + delete_payload = {"asset_ids": asset_ids} |
| 110 | + del_resp = session.post(delete_url, body=bytes(json_encode(delete_payload))) |
| 111 | + if del_resp and del_resp.status_code == 204: |
| 112 | + print("Deleted {} assets".format(len(asset_ids))) |
| 113 | + else: |
| 114 | + print("Asset deletion {} failed".format(del_resp.body)) |
| 115 | + |
| 116 | + return [] |
0 commit comments