Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,10 @@ If you need help setting up a custom integration, you can create an [issue](http
- [Ubiquiti Unifi Network](https://github.com/runZeroInc/runzero-custom-integrations/blob/main/ubiquiti-unifi-network/)
## Export from runZero
- [Audit Log to Webhook](https://github.com/runZeroInc/runzero-custom-integrations/blob/main/audit-events-to-webhook/)
- [runZero Vunerability Workflow](https://github.com/runZeroInc/runzero-custom-integrations/blob/main/vulnerability-workflow/)
- [Sumo Logic](https://github.com/runZeroInc/runzero-custom-integrations/blob/main/sumo-logic/)
## Internal Integrations
- [Scan Passive Assets](https://github.com/runZeroInc/runzero-custom-integrations/blob/main/scan-passive-assets/)
- [Vunerability Workflow](https://github.com/runZeroInc/runzero-custom-integrations/blob/main/vulnerability-workflow/)
## The boilerplate folder has examples to follow

1. Sample [README.md](./boilerplate/README.md) for contributing
Expand Down
14 changes: 10 additions & 4 deletions docs/integrations.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"lastUpdated": "2026-01-15T15:30:47.444549Z",
"totalIntegrations": 31,
"lastUpdated": "2026-01-15T15:32:00.895289Z",
"totalIntegrations": 32,
"integrationDetails": [
{
"name": "Moysle",
Expand Down Expand Up @@ -39,8 +39,8 @@
"integration": "https://github.com/runZeroInc/runzero-custom-integrations/blob/main/tanium/custom-integration-tanium.star"
},
{
"name": "runZero Vunerability Workflow",
"type": "outbound",
"name": "Vunerability Workflow",
"type": "internal",
"readme": "https://github.com/runZeroInc/runzero-custom-integrations/blob/main/vulnerability-workflow/README.md",
"integration": "https://github.com/runZeroInc/runzero-custom-integrations/blob/main/vulnerability-workflow/custom-integration-vulnerability-workflow.star"
},
Expand Down Expand Up @@ -116,6 +116,12 @@
"readme": "https://github.com/runZeroInc/runzero-custom-integrations/blob/main/audit-events-to-webhook/README.md",
"integration": "https://github.com/runZeroInc/runzero-custom-integrations/blob/main/audit-events-to-webhook/custom-integration-audit-events.star"
},
{
"name": "Scan Passive Assets",
"type": "internal",
"readme": "https://github.com/runZeroInc/runzero-custom-integrations/blob/main/scan-passive-assets/README.md",
"integration": "https://github.com/runZeroInc/runzero-custom-integrations/blob/main/scan-passive-assets/custom-integration-scan-passive-assets.star"
},
{
"name": "NinjaOne",
"type": "inbound",
Expand Down
55 changes: 55 additions & 0 deletions scan-passive-assets/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
# Custom Integration: Scan Passive Assets

This custom integration finds assets discovered only by passive sources, creates targeted scans from the last-seen agent, and can optionally delete the original passive assets after the scans are scheduled.

## runZero requirements

- Superuser access to the [Custom Integrations configuration](https://console.runzero.com/custom-integrations) in runZero.
- A runZero Organization API token.

## Scan Passive Assets requirements

- A runZero **Site ID** to target for scans (set in `SITE_ID`).
- A CIDR allow list to scope targets (set in `ALLOW_LIST`).
- A decision on whether to delete passive assets after scan creation (set in `DELETE_ASSETS`).

## Steps

### Script configuration

1. Open `scan-passive-assets/custom-integrastion-scan-passive-assets.star`.
2. Update the global configuration values:
- `SITE_ID`: runZero site ID where scans should run.
- `ALLOW_LIST`: list of allowed IPv4 CIDR ranges.
- `DELETE_ASSETS`: set to `False` to keep passive assets after scans are created.
3. (Optional) Adjust the search filter in the export request if you want to include more than `source:sample source_count:1`.

### runZero configuration

1. [Create the Credential for the Custom Integration](https://console.runzero.com/credentials).
- Select the type `Custom Integration Script Secrets`.
- Set `access_secret` to your runZero API token.
- Set `access_key` to a placeholder value like `foo` (unused).
2. [Create the Custom Integration](https://console.runzero.com/custom-integrations/new).
- Add a Name and Icon (e.g., `scan-passive-assets`).
- Toggle `Enable custom integration script` to input the finalized script.
- Click `Validate` to ensure it has valid syntax.
- Click `Save` to create the Custom Integration.
3. [Create the Custom Integration task](https://console.runzero.com/ingest/custom/).
- Select the Credential and Custom Integration created above.
- Update the task schedule to recur at the desired timeframes.
- Select the Explorer you'd like the Custom Integration to run from.
- Click `Save` to kick off the first task.

### What's next?

- The task exports passive assets matching the search filter and groups allowed IPv4 addresses by `last_agent_id`.
- The script creates one scan per agent with the matching targets.
- If `DELETE_ASSETS` is enabled, the matching passive assets are removed after scan creation.
- You can review task activity on the [tasks](https://console.runzero.com/tasks) page.

### Notes

- Only IPv4 addresses are considered; IPv6 addresses are skipped.
- The allow list applies before scans are created, so verify `ALLOW_LIST` matches your internal ranges.
- Disabling `DELETE_ASSETS` is recommended for initial testing.
1 change: 1 addition & 0 deletions scan-passive-assets/config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{ "name": "Scan Passive Assets", "type": "internal" }
116 changes: 116 additions & 0 deletions scan-passive-assets/custom-integration-scan-passive-assets.star
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
load('requests', 'Session')
load('json', json_encode='encode', json_decode='decode')
load('net', 'ip_address')
load('http', 'url_encode')

# -------------------------
# Global Configuration
# -------------------------
SITE_ID = "UPDATE_ME"
DELETE_ASSETS = True
ALLOW_LIST = ["10.0.0.0/8", "192.168.0.0/16"]

# -------------------------
# IP Filtering Functions
# -------------------------
def ip_to_int(ip):
parts = ip.split('.')
return (int(parts[0]) << 24) + (int(parts[1]) << 16) + (int(parts[2]) << 8) + int(parts[3])

def cidr_to_netmask(bits):
return ~((1 << (32 - bits)) - 1) & 0xFFFFFFFF

def ip_in_cidr(ip_str, cidr):
ip_int = ip_to_int(ip_str)
base, mask_bits = cidr.split('/')
base_int = ip_to_int(base)
mask = cidr_to_netmask(int(mask_bits))
return (ip_int & mask) == (base_int & mask)

def is_ip_allowed(ip_str, allow_list):
ip_obj = ip_address(ip_str)
if ip_obj.version != 4:
return False
for cidr in allow_list:
if ip_in_cidr(ip_str, cidr):
return True
return False

# -------------------------
# Entrypoint
# -------------------------
def main(*args, **kwargs):
org_token = kwargs["access_secret"]

session = Session()
session.headers.set("Authorization", "Bearer {}".format(org_token))
session.headers.set("Content-Type", "application/json")

# Step 1: Export assets
params = {"search": "source:sample source_count:1", "fields": "id,addresses,last_agent_id"}
asset_url = "https://console.runzero.com/api/v1.0/export/org/assets.json?{}".format(url_encode(params))
response = session.get(asset_url, timeout=3600)

if not response or response.status_code != 200:
print("Failed to fetch assets")
return []

data = json_decode(response.body)

# Step 2: Filter assets and group IPs by agent
agent_ip_map = {} # {agent_id: [ip, ip, ...]}
asset_ids = []

for asset in data:
agent_id = asset.get("last_agent_id")
if not agent_id:
continue
for ip in asset.get("addresses", []):
print("Evaluating IP: {}".format(ip))
if is_ip_allowed(ip, ALLOW_LIST):
if not agent_ip_map.get(agent_id):
agent_ip_map[agent_id] = []
agent_ip_map[agent_id].append(ip)
if asset["id"] not in asset_ids:
asset_ids.append(asset["id"])


# Step 3: Create scan task per explorer/agent
for agent_id, ips in agent_ip_map.items():
scan_url = "https://console.runzero.com/api/v1.0/org/sites/{}/scan".format(SITE_ID)
scan_payload = {
"targets": "\n".join(ips),
"scan-name": "Auto Scan Sample Only Assets",
"scan-description": "This scan was automatically created to scan assets discovered by the 'sample' source only.",
"scan-frequency": "once",
"scan-start": "0",
"scan-tags": "type=AUTOMATED",
"scan-grace-period": "0",
"agent": agent_id,
"rate": "1000",
"max-host-rate": "20",
"passes": "3",
"max-attempts": "3",
"max-sockets": "500",
"max-group-size": "4096",
"max-ttl": "255",
"screenshots": "true",
}
print(scan_payload)
post = session.put(scan_url, body=bytes(json_encode(scan_payload)))
if post and post.status_code == 200:
print("Scan created for agent {}".format(agent_id))
else:
print("Scan failed for agent {}".format(agent_id))

# Step 4: Optional asset deletion
if DELETE_ASSETS and len(asset_ids) > 0:
delete_url = "https://console.runzero.com/api/v1.0/org/assets/bulk/delete"
delete_payload = {"asset_ids": asset_ids}
del_resp = session.post(delete_url, body=bytes(json_encode(delete_payload)))
if del_resp and del_resp.status_code == 204:
print("Deleted {} assets".format(len(asset_ids)))
else:
print("Asset deletion {} failed".format(del_resp.body))

return []
28 changes: 24 additions & 4 deletions scripts/generate_integration_json.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,16 @@
except Exception as e:
print(f"⚠️ Failed to read config.json in {entry}: {e}")

if not integration_type:
integration_type = "inbound"
integration_type = str(integration_type).lower()

if integration_type not in {"inbound", "outbound", "internal"}:
print(
f"⚠️ Unknown integration type '{integration_type}' in {entry}, defaulting to inbound."
)
integration_type = "inbound"

integration_details.append(
{
"name": friendly_name,
Expand Down Expand Up @@ -75,18 +85,21 @@
new_lines = []
in_inbound_section = False
in_outbound_section = False
in_section = None
in_internal_section = False

# Prepare the new sections
inbound_links = []
outbound_links = []
internal_links = []

for integration in sorted(integration_details, key=lambda x: x["name"].lower()):
link = (
f"- [{integration['name']}]({integration['readme'].replace('/README.md', '/')})"
)
if integration["type"] == "outbound":
outbound_links.append(link)
elif integration["type"] == "internal":
internal_links.append(link)
else:
inbound_links.append(link)

Expand All @@ -103,10 +116,17 @@
new_lines.extend([f"{link}\n" for link in outbound_links])
in_outbound_section = True
continue
elif stripped.startswith("## ") and (in_inbound_section or in_outbound_section):
in_inbound_section = in_outbound_section = False
elif stripped == "## Internal Integrations":
new_lines.append(line)
new_lines.extend([f"{link}\n" for link in internal_links])
in_internal_section = True
continue
elif stripped.startswith("## ") and (
in_inbound_section or in_outbound_section or in_internal_section
):
in_inbound_section = in_outbound_section = in_internal_section = False

if not in_inbound_section and not in_outbound_section:
if not in_inbound_section and not in_outbound_section and not in_internal_section:
new_lines.append(line)

with open(readme_path, "w") as f:
Expand Down
2 changes: 1 addition & 1 deletion vulnerability-workflow/config.json
Original file line number Diff line number Diff line change
@@ -1 +1 @@
{ "name": "runZero Vunerability Workflow", "type": "outbound" }
{ "name": "Vunerability Workflow", "type": "internal" }