From 29b3e808104c163a60cb743ca799a7b2302383f7 Mon Sep 17 00:00:00 2001 From: tyler-maze Date: Tue, 9 Jun 2026 10:53:28 -0500 Subject: [PATCH 1/3] Add Maze Security custom integration Inbound integration that imports vulnerability investigation data from the Maze Security API into runZero. Fetches investigations updated within the last 30 days, groups them by asset, and maps each investigation to a Vulnerability on the corresponding ImportAsset. Includes cursor-based pagination, retry logic for 5xx errors, OS extraction from root cause analysis, and rich metadata from related scanner findings (cloud platform, region, scanner type, asset type). Co-Authored-By: Claude Opus 4.6 --- maze-security/README.md | 38 ++ maze-security/config.json | 4 + .../custom-integration-maze-security.star | 365 ++++++++++++++++++ 3 files changed, 407 insertions(+) create mode 100644 maze-security/README.md create mode 100644 maze-security/config.json create mode 100644 maze-security/custom-integration-maze-security.star diff --git a/maze-security/README.md b/maze-security/README.md new file mode 100644 index 0000000..6fa3b5b --- /dev/null +++ b/maze-security/README.md @@ -0,0 +1,38 @@ +# Maze Security Custom Integration + +## Overview +This integration imports vulnerability investigation data from the Maze Security API into runZero. Each Maze investigation is mapped to a runZero Vulnerability and grouped by affected asset. + +## Configuration + +| Parameter | Value | +|-----------|-------| +| **Access Key** | *(not used)* | +| **Access Secret** | Maze Security API key | + +## How It Works + +1. Fetches all investigations from `POST /v1/investigations/search` with cursor-based pagination +2. Groups investigations by asset (extracted from `scanner_finding_hash` or `related_scanner_findings`) +3. Each investigation becomes a `Vulnerability` on the corresponding `ImportAsset` +4. Maze-specific data (exploitability verdict, root cause analysis, severity reasoning) is stored in custom attributes + +## Asset Mapping + +| Maze Field | runZero Field | +|------------|---------------| +| Asset ID (from scanner_finding_hash) | `ImportAsset.id` | +| Asset name | `ImportAsset.hostnames` | +| CVE ID | `Vulnerability.cve` | +| CVSS base score | `Vulnerability.cvss3BaseScore` | +| Maze severity | `Vulnerability.severityRank` / `severityScore` | +| Exploitability | `Vulnerability.exploitable` | +| Remediation | `Vulnerability.solution` | +| Investigation details | `Vulnerability.customAttributes` | + +## Testing + +```bash +runzero script --filename maze-security/custom-integration-maze-security.star \ + --kwargs access_secret=YOUR_MAZE_API_KEY +``` diff --git a/maze-security/config.json b/maze-security/config.json new file mode 100644 index 0000000..62dc7ce --- /dev/null +++ b/maze-security/config.json @@ -0,0 +1,4 @@ +{ + "name": "Maze Security", + "type": "inbound" +} diff --git a/maze-security/custom-integration-maze-security.star b/maze-security/custom-integration-maze-security.star new file mode 100644 index 0000000..5b1b201 --- /dev/null +++ b/maze-security/custom-integration-maze-security.star @@ -0,0 +1,365 @@ +load('runzero.types', 'ImportAsset', 'NetworkInterface', 'Vulnerability') +load('json', json_encode='encode', json_decode='decode') +load('http', http_post='post') +load('time', 'now', 'parse_duration') + +MAZE_API_URL = "https://api.mazehq.com" +PAGE_LIMIT = 1000 +DEFAULT_DAYS_BACK = 30 +MAX_RETRIES = 3 + +SEVERITY_RANK = { + "CRITICAL": 4, + "HIGH": 3, + "MEDIUM": 2, + "LOW": 1, + "NOT_EXPLOITABLE": 0, +} + +SEVERITY_SCORE = { + "CRITICAL": 10.0, + "HIGH": 7.0, + "MEDIUM": 5.0, + "LOW": 2.0, + "NOT_EXPLOITABLE": 0.0, +} + + +def compute_updated_from(days_back): + """Compute ISO 8601 timestamp for N days ago.""" + duration_str = "-{}h".format(days_back * 24) + cutoff = now() + parse_duration(duration_str) + raw = str(cutoff).split(".")[0] + return raw.replace(" ", "T") + "Z" + + +def search_investigations(api_key): + """Fetch investigations from Maze API updated within DEFAULT_DAYS_BACK.""" + updated_from = compute_updated_from(DEFAULT_DAYS_BACK) + print("Starting Maze API fetch (updated_from: {})...".format(updated_from)) + headers = { + "X-API-Key": api_key, + "Content-Type": "application/json", + "Accept": "application/json", + } + url = "{}/v1/investigations/search".format(MAZE_API_URL) + print("URL: {}".format(url)) + + all_investigations = [] + cursor = None + has_more = True + page = 0 + + while has_more: + page += 1 + body = {"limit": PAGE_LIMIT, "updated_from": updated_from} + if cursor: + body["cursor"] = cursor + + print("Fetching page {}...".format(page)) + response = None + for attempt in range(MAX_RETRIES): + response = http_post(url, headers=headers, body=bytes(json_encode(body)), timeout=600) + print("Response status: {}".format(response.status_code)) + if response.status_code == 200: + break + if response.status_code >= 500 and attempt < MAX_RETRIES - 1: + print("Server error {}, retry {}/{}...".format(response.status_code, attempt + 1, MAX_RETRIES)) + continue + break + + if response.status_code != 200: + print("Maze API error: status {}".format(response.status_code)) + print("Response body: {}".format(response.body[:500])) + break + + data = json_decode(response.body) + investigations = data.get("data", []) + all_investigations.extend(investigations) + print("Page {}: got {} investigations (total: {})".format(page, len(investigations), len(all_investigations))) + + has_more = data.get("has_more", False) + cursor = data.get("next_cursor") + + if not cursor: + has_more = False + + print("Fetched {} total investigations from Maze API".format(len(all_investigations))) + return all_investigations + + +def parse_asset_id(scanner_finding_hash): + """Extract asset identifier from scanner_finding_hash (format: scanner::CVE::asset_id).""" + if not scanner_finding_hash: + return "" + parts = scanner_finding_hash.split("::") + if len(parts) >= 3: + return parts[2] + return scanner_finding_hash + + +def force_string(value): + """Coerce value to string, truncated to 1023 chars for customAttributes.""" + if value == None: + return "" + if type(value) == "list": + return ",".join([str(v) for v in value])[:1023] + if type(value) == "dict": + return json_encode(value)[:1023] + return str(value)[:1023] + + +def build_root_cause_summary(rca_list): + """Summarize vulnerability_root_cause_analysis into a compact string.""" + if not rca_list: + return "" + parts = [] + for rca in rca_list: + title = rca.get("title", "") + status = rca.get("status", "") + reasoning = rca.get("reasoning", "") + parts.append("{}: {} - {}".format(title, status, reasoning)) + return " | ".join(parts)[:1023] + + +def build_vulnerability(investigation): + """Convert a Maze investigation into a runZero Vulnerability object.""" + inv_id = investigation.get("id", "") + cve_id = investigation.get("cve_id", "") + maze_severity = investigation.get("maze_severity", "") + exploitability = investigation.get("exploitability", "") + exploitability_reason = investigation.get("exploitability_reason", "") + + snapshot = investigation.get("snapshot", {}) + if not snapshot: + snapshot = {} + cve_info = snapshot.get("cve", {}) + if not cve_info: + cve_info = {} + cvss_info = snapshot.get("cvss", {}) + if not cvss_info: + cvss_info = {} + + description = cve_info.get("description", "") + cvss_base = cvss_info.get("base_score", 0.0) + if cvss_base == None: + cvss_base = 0.0 + cvss_version = cvss_info.get("version", "") + + rank = SEVERITY_RANK.get(maze_severity, 0) + score = SEVERITY_SCORE.get(maze_severity, 0.0) + + is_exploitable = exploitability == "exploitable" + + severity_details = investigation.get("severity_details", {}) + if not severity_details: + severity_details = {} + severity_reasoning = severity_details.get("reasoning", "") + + rca_list = investigation.get("vulnerability_root_cause_analysis", []) + if not rca_list: + rca_list = [] + rca_summary = build_root_cause_summary(rca_list) + + remediation = investigation.get("remediation", "") + if not remediation: + remediation = "" + + custom_attrs = { + "maze_investigation_id": force_string(inv_id), + "maze_exploitability": force_string(exploitability), + "maze_exploitability_reason": force_string(exploitability_reason), + "maze_severity": force_string(maze_severity), + "maze_severity_reasoning": force_string(severity_reasoning), + "maze_root_cause_analysis": rca_summary, + "maze_snapshot_status": force_string(snapshot.get("status", "")), + "maze_cvss_vector": force_string(cvss_info.get("vector_string", "")), + "maze_cvss_source": force_string(cvss_info.get("source", "")), + "maze_cve_potential_impact": force_string(cve_info.get("potential_impact", "")), + "maze_updated_at": force_string(investigation.get("updated_at", "")), + } + + likelihood = investigation.get("likelihood", "") + if likelihood: + custom_attrs["maze_likelihood"] = force_string(likelihood) + impact = investigation.get("impact", "") + if impact: + custom_attrs["maze_impact"] = force_string(impact) + + vuln_params = { + "id": inv_id, + "name": cve_id, + "description": str(description)[:1024], + "cve": cve_id, + "solution": str(remediation)[:1024], + "severityRank": rank, + "severityScore": float(score), + "riskRank": rank, + "riskScore": float(score), + "exploitable": is_exploitable, + "customAttributes": custom_attrs, + } + + if cvss_version.startswith("3"): + vuln_params["cvss3BaseScore"] = float(cvss_base) + elif cvss_version.startswith("2"): + vuln_params["cvss2BaseScore"] = float(cvss_base) + else: + vuln_params["cvss3BaseScore"] = float(cvss_base) + + return Vulnerability(**vuln_params) + + +def extract_os_from_rca(rca_list): + """Extract OS name and version from vulnerability_root_cause_analysis.""" + os_name = "" + os_version = "" + if not rca_list: + return os_name, os_version + for rca in rca_list: + title = rca.get("title", "").lower() + actual = rca.get("actual_value", "") + if not actual: + continue + if "operating system" in title: + os_name = actual + elif "kernel version" in title or "os version" in title: + os_version = actual + return os_name, os_version + + +def add_to_asset_map(asset_map, asset_id, hostname, finding, inv): + """Add an investigation to the asset map, accumulating metadata and vulns.""" + if asset_id not in asset_map: + asset_map[asset_id] = { + "id": asset_id, + "hostname": hostname, + "asset_type": "", + "cloud_platform": "", + "region": "", + "account_id": "", + "scanner": "", + "asset_full_id": "", + "os": "", + "os_version": "", + "vulnerabilities": [], + } + + entry = asset_map[asset_id] + + if finding: + if not entry["asset_type"]: + entry["asset_type"] = finding.get("asset_type", "") + if not entry["cloud_platform"]: + entry["cloud_platform"] = finding.get("cloud_platform", "") + if not entry["region"]: + entry["region"] = finding.get("region", "") + if not entry["account_id"]: + entry["account_id"] = finding.get("account_id", "") + if not entry["scanner"]: + entry["scanner"] = finding.get("scanner", "") + if not entry["asset_full_id"]: + entry["asset_full_id"] = finding.get("asset_id", "") + + rca_list = inv.get("vulnerability_root_cause_analysis", []) + if not rca_list: + rca_list = [] + os_name, os_version = extract_os_from_rca(rca_list) + if os_name and not entry["os"]: + entry["os"] = os_name + if os_version and not entry["os_version"]: + entry["os_version"] = os_version + + vuln = build_vulnerability(inv) + entry["vulnerabilities"].append(vuln) + + +def build_assets(investigations): + """Group investigations by asset and build ImportAsset objects with Vulnerability lists.""" + asset_map = {} + + for inv in investigations: + scanner_hash = inv.get("scanner_finding_hash", "") + asset_id = parse_asset_id(scanner_hash) + + if not asset_id: + asset_id = inv.get("id", "") + + related = inv.get("related_scanner_findings", []) + if not related: + related = [] + + if related: + for finding in related: + finding_asset = finding.get("asset_name", "") + if not finding_asset: + finding_asset = asset_id + add_to_asset_map(asset_map, finding_asset, finding_asset, finding, inv) + else: + add_to_asset_map(asset_map, asset_id, asset_id, None, inv) + + assets = [] + for asset_id, asset_data in asset_map.items(): + hostname = asset_data["hostname"] + vulns = asset_data["vulnerabilities"] + os_name = asset_data["os"] + os_version = asset_data["os_version"] + + custom_attrs = { + "maze_finding_count": str(len(vulns)), + "source": "maze_security", + } + if asset_data["asset_type"]: + custom_attrs["maze_asset_type"] = force_string(asset_data["asset_type"]) + if asset_data["cloud_platform"]: + custom_attrs["maze_cloud_platform"] = force_string(asset_data["cloud_platform"]) + if asset_data["region"]: + custom_attrs["maze_region"] = force_string(asset_data["region"]) + if asset_data["account_id"]: + custom_attrs["maze_account_id"] = force_string(asset_data["account_id"]) + if asset_data["scanner"]: + custom_attrs["maze_scanner"] = force_string(asset_data["scanner"]) + if asset_data["asset_full_id"]: + custom_attrs["maze_asset_full_id"] = force_string(asset_data["asset_full_id"]) + + asset_params = { + "id": str(asset_id), + "hostnames": [hostname] if hostname else [], + "vulnerabilities": vulns[:999], + "customAttributes": custom_attrs, + } + if os_name: + asset_params["os"] = os_name + if os_version: + asset_params["osVersion"] = os_version + if asset_data["asset_type"]: + asset_params["deviceType"] = asset_data["asset_type"] + + assets.append(ImportAsset(**asset_params)) + + return assets + + +def main(*args, **kwargs): + print("=== Maze Security Integration Starting ===") + print("kwargs keys: {}".format(list(kwargs.keys()))) + + api_key = kwargs.get("access_secret") + + if not api_key: + print("Error: Maze API key not provided in access_secret") + return [] + + print("API key length: {}".format(len(api_key))) + + investigations = search_investigations(api_key) + + if not investigations: + print("No investigations returned from Maze API") + return [] + + assets = build_assets(investigations) + print("Built {} assets from {} investigations".format(len(assets), len(investigations))) + print("=== Maze Security Integration Complete ===") + + return assets From 6357c36d821529792090a787e301a991813ba38f Mon Sep 17 00:00:00 2001 From: tyler-maze Date: Tue, 9 Jun 2026 10:56:52 -0500 Subject: [PATCH 2/3] Rename Maze Security to Maze throughout integration Co-Authored-By: Claude Opus 4.6 --- maze-security/README.md | 6 +++--- maze-security/config.json | 2 +- maze-security/custom-integration-maze-security.star | 6 +++--- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/maze-security/README.md b/maze-security/README.md index 6fa3b5b..95e8f17 100644 --- a/maze-security/README.md +++ b/maze-security/README.md @@ -1,14 +1,14 @@ -# Maze Security Custom Integration +# Maze Custom Integration ## Overview -This integration imports vulnerability investigation data from the Maze Security API into runZero. Each Maze investigation is mapped to a runZero Vulnerability and grouped by affected asset. +This integration imports vulnerability investigation data from the Maze API into runZero. Each Maze investigation is mapped to a runZero Vulnerability and grouped by affected asset. ## Configuration | Parameter | Value | |-----------|-------| | **Access Key** | *(not used)* | -| **Access Secret** | Maze Security API key | +| **Access Secret** | Maze API key | ## How It Works diff --git a/maze-security/config.json b/maze-security/config.json index 62dc7ce..d50201a 100644 --- a/maze-security/config.json +++ b/maze-security/config.json @@ -1,4 +1,4 @@ { - "name": "Maze Security", + "name": "Maze", "type": "inbound" } diff --git a/maze-security/custom-integration-maze-security.star b/maze-security/custom-integration-maze-security.star index 5b1b201..ef24d54 100644 --- a/maze-security/custom-integration-maze-security.star +++ b/maze-security/custom-integration-maze-security.star @@ -307,7 +307,7 @@ def build_assets(investigations): custom_attrs = { "maze_finding_count": str(len(vulns)), - "source": "maze_security", + "source": "maze", } if asset_data["asset_type"]: custom_attrs["maze_asset_type"] = force_string(asset_data["asset_type"]) @@ -341,7 +341,7 @@ def build_assets(investigations): def main(*args, **kwargs): - print("=== Maze Security Integration Starting ===") + print("=== Maze Integration Starting ===") print("kwargs keys: {}".format(list(kwargs.keys()))) api_key = kwargs.get("access_secret") @@ -360,6 +360,6 @@ def main(*args, **kwargs): assets = build_assets(investigations) print("Built {} assets from {} investigations".format(len(assets), len(investigations))) - print("=== Maze Security Integration Complete ===") + print("=== Maze Integration Complete ===") return assets From 7d46c80469fec465ce0f7bbc68f7f2bb092cab9a Mon Sep 17 00:00:00 2001 From: tyler-maze Date: Tue, 9 Jun 2026 10:57:41 -0500 Subject: [PATCH 3/3] Align README to repo conventions and add Maze icon Co-Authored-By: Claude Opus 4.6 --- maze-security/README.md | 72 +++++++++++++++++++++--------------- maze-security/maze-icon.png | Bin 0 -> 1488 bytes 2 files changed, 43 insertions(+), 29 deletions(-) create mode 100644 maze-security/maze-icon.png diff --git a/maze-security/README.md b/maze-security/README.md index 95e8f17..cca3853 100644 --- a/maze-security/README.md +++ b/maze-security/README.md @@ -1,38 +1,52 @@ -# Maze Custom Integration +# Custom Integration: Maze -## Overview -This integration imports vulnerability investigation data from the Maze API into runZero. Each Maze investigation is mapped to a runZero Vulnerability and grouped by affected asset. +Maze -## Configuration +## runZero requirements -| Parameter | Value | -|-----------|-------| -| **Access Key** | *(not used)* | -| **Access Secret** | Maze API key | +- Superuser access to the [Custom Integrations configuration](https://console.runzero.com/custom-integrations) in runZero. -## How It Works +## Maze requirements -1. Fetches all investigations from `POST /v1/investigations/search` with cursor-based pagination -2. Groups investigations by asset (extracted from `scanner_finding_hash` or `related_scanner_findings`) -3. Each investigation becomes a `Vulnerability` on the corresponding `ImportAsset` -4. Maze-specific data (exploitability verdict, root cause analysis, severity reasoning) is stored in custom attributes +- Maze API key with access to the Investigations API. -## Asset Mapping +## Steps -| Maze Field | runZero Field | -|------------|---------------| -| Asset ID (from scanner_finding_hash) | `ImportAsset.id` | -| Asset name | `ImportAsset.hostnames` | -| CVE ID | `Vulnerability.cve` | -| CVSS base score | `Vulnerability.cvss3BaseScore` | -| Maze severity | `Vulnerability.severityRank` / `severityScore` | -| Exploitability | `Vulnerability.exploitable` | -| Remediation | `Vulnerability.solution` | -| Investigation details | `Vulnerability.customAttributes` | +### Maze configuration -## Testing +1. Obtain your **API Key** from the Maze platform. +2. Confirm API access to `https://api.mazehq.com/v1/investigations/search`. -```bash -runzero script --filename maze-security/custom-integration-maze-security.star \ - --kwargs access_secret=YOUR_MAZE_API_KEY -``` +### runZero configuration + +1. (OPTIONAL) - Make any necessary changes to the script to align with your environment. + - Adjust `DEFAULT_DAYS_BACK` to control how far back investigations are fetched (default: 30 days). + - Modify custom attribute mappings as needed. +2. [Create the Credential for the Custom Integration](https://console.runzero.com/credentials). + - Select the type `Custom Integration Script Secrets`. + - For `access_key`, input a placeholder value (unused in this integration). + - Use the `access_secret` field for your **Maze API Key**. +3. [Create the Custom Integration](https://console.runzero.com/custom-integrations/new). + - Add a Name and Icon for the integration (e.g., "maze"). The icon is included in this directory. + - 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. +4. [Create the Custom Integration task](https://console.runzero.com/ingest/custom/). + - Select the Credential and Custom Integration created in steps 2 and 3. + - Update the task schedule to recur at the desired timeframes. + - Select the Explorer you would like the Custom Integration to run from. + - Click `Save` to kick off the first task. + +### What's next? + +- You will see the task kick off on the [tasks](https://console.runzero.com/tasks) page like any other integration. +- The task will update existing assets with vulnerability investigation data pulled from Maze. +- The task will create new assets when there are no existing assets that meet merge criteria (hostname, MAC, etc). +- You can search for assets enriched by this custom integration with the runZero search `custom_integration:maze`. + +### Notes + +- The integration fetches investigations updated within the last 30 days by default. Adjust `DEFAULT_DAYS_BACK` in the script to change this. +- Each investigation is mapped to a **Vulnerability** on the corresponding asset, including CVE, CVSS scores, exploitability verdict, and root cause analysis. +- When `related_scanner_findings` data is available, additional metadata (cloud platform, region, scanner type, account ID) is included as custom attributes. +- The integration includes retry logic for transient API errors (5xx) with up to 3 attempts per request. diff --git a/maze-security/maze-icon.png b/maze-security/maze-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..5d59746a958f4359e8bc3453b850ba608b6e3f38 GIT binary patch literal 1488 zcmbu9`#aMM0LH)D*`$rxY;MKDD3|IW;#=a(SO}SW(wIA8Bh4k29nQ&eNe}Xn`z>)? zPtrx0j%&F+F0slC2gfa!MGIzn9}yKf z?3wIH@DN8sEnLF^0Ezt(2=FLR_E18??ahfm?STB^;eh#@*q8u7LmG0=6Al1yUrRF+ z$0*2Z{%Y6i z!-yOi=Oc59@`SToE*knK2m-EG)Bzr0)Vhtf3g6ZR4a@jf zRc?IcPEMP%>^Jhp6%HwjF~wWh(U*ttMAf0E2LuWregeK%=&PtwL!a&uX=T{}{eo=n zC)&H<>*2z^Jp1jAob#k1ZM}7H@{2a7H56rBoOvUtTtsGe<6Wm9Tt+s)5kOt^)z<=S&?r^t!F-}%BWl!s>Fu0! zA{NrfU^(>jfwQ?Nn;h_rI3;A)H9uaRGeca`FI;dj=)2*y9n}g^0nac}*^ri(9K18E z+DCt!ny4YH&h1v+DJuC_B8r8?#2`Z-aIFJfunRniwt<1q98-&qBHhyQxrlg(Na-|L)p- z97-Oma%aekb{BuAd*g#OEkxP}MLH{!(?5a{6CxBo(FXO)Ypq;Z$)=d{b2WH6u_ySt z*L(Ejm=og{j+37$L|VpP1wCJ#6fk{D_d(xdm~8X<9zUb$mNSLT4i-1|)dwf98=dH# zQ}#1JlfLdWiTcrZgqbyp$nP0PCa~o*L$su-pB_In&BbqH>NB}i^7$$yEBv}mq@{O# z8v9y|$;qdSi7yybY({TcwmV8E#8$^R_O~R*7N|PMea10dYEz-Nskng?SZVbX;x0b* zXDmG$5>?4X)MswbAT}VV6e6rE(UQBSVFc%jg2~I`xjAM%dN;kZO8?-Q3{6llO4XfQ z3I;WRLP;(|`=(l0lIjW9EOu3O1%WrR)J>!e~oow z);>(Yvg|4=G;@}domK1f&|^%_3(G#i^N&lqDb~F(w{upMzZ_?j<1Y1G+~TeJxb9YM zN$gFyYg|qyGe!3NEu>mD-`K0FFrBquh*d;`O2!$sncJJaiUyj zH}6%4^dMef$a)GvB+lPxw8r&;Mz7>Z9-J86D6}NmJZb8xEjjrsT=~upCnxUdy>=}%@me<%zGw)TD$BJ&BXv@p`jr9nPh}r;naAS3EX`0cnO-hKhxr{Ly})zA)QbEZH_u#V^40k0h5j_bjLkW#3pABA zlcFs}+@9RZaKPeR*DKp*Nl#4Hqw&<|2is&=P(xUga9Kc$XFH9|*eO)wpX1JToS9GNJ!B2E&!M7OXz(SV0g+NAVxSB^={ zwmD*Seuat^y=fkYA~eft+qdeD5((|nn09>>;xeH