NodeZero
Integrates with the NodeZero autonomous penetration testing platform to fetch weaknesses discovered during pentest operations. Automatically ingests HIGH and CRITICAL severity weaknesses as incidents for tracking and remediation.
Vulnerability Management · NodeZero
Details
| ID | NodeZero |
|---|---|
| Provider | Horizon3.ai |
| Category | Vulnerability Management |
| From Version | 6.1.0 |
| Docker Image | demisto/py3-tools:1.0.0.10895515 |
| Supported Modules | Agentix XSIAM |
README
NodeZero
Integrates with the NodeZero autonomous penetration testing platform to fetch weaknesses discovered during pentest operations. Automatically ingests HIGH and CRITICAL severity weaknesses as incidents for tracking and remediation.
Configure NodeZero on Cortex XSOAR
- Navigate to Settings > Integrations > Servers & Services.
- Search for NodeZero.
- Click Add instance to create and configure a new integration instance.
| Parameter | Description | Required |
|---|---|---|
| Server URL | The NodeZero API server URL. | True |
| API Key | The API Key required to authenticate to the NodeZero service. | True |
| Trust any certificate (not secure) | When selected, certificates are not checked. | False |
| Use system proxy settings | Runs the integration instance using the proxy server (HTTP or HTTPS) that you defined in the server configuration. | False |
| Fetch incidents | When selected, the integration fetches incidents. | False |
| Incident type | The incident type to create for fetched incidents. | False |
| Maximum number of weaknesses to fetch | Maximum number of incidents to fetch per run. Default is 200. | False |
| First fetch time | How far back to fetch on first run (e.g., “7 days”, “3 days”). Default is 7 days. | False |
| Incidents Fetch Interval | How often to fetch new incidents (in minutes). Default is 10080 (7 days). | False |
- Click Test to validate the URLs, token, and connection.
Fetch Incidents
The integration fetches HIGH and CRITICAL severity weaknesses from NodeZero pentest operations as incidents. Each weakness is converted to an XSOAR incident with the following mappings:
| NodeZero Field | XSOAR Incident Field |
|---|---|
| uuid | dbotMirrorId |
| created_at | occurred |
| severity | severity (CRITICAL=4, HIGH=3) |
| vuln_id | externalid |
| affected_asset_display_name | sourcehostname |
| ip | sourceip |
| score | nodezeroweaknessscore |
| vuln_category | nodezeroweaknesscategory |
| has_proof | nodezeroweaknessproven |
| attack_paths_count | nodezeroattackpathscount |
| vuln_cisa_kev | nodezerocisakov |
| vuln_known_ransomware_campaign_use | nodezeroransomwareuse |
| op_id | nodezeroopid |
Commands
You can execute these commands from the Cortex XSOAR CLI, as part of an automation, or in a playbook.
After you successfully execute a command, a DBot message is displayed in the War Room with the command details.
nodezero-get-weaknesses
Retrieves HIGH and CRITICAL weaknesses discovered by NodeZero pentests.
Base Command
nodezero-get-weaknesses
Input
| Argument Name | Description | Required |
|---|---|---|
| since_date | Fetch weaknesses created on or after this date (ISO 8601, e.g. 2024-01-01T00:00:00). Defaults to 7 days ago. | Optional |
| limit | Maximum number of weaknesses to return (1–1000). Default is 50. | Optional |
Context Output
| Path | Type | Description |
|---|---|---|
| NodeZero.Weakness.uuid | String | Unique identifier of the weakness. |
| NodeZero.Weakness.created_at | Date | Timestamp when the weakness was first discovered. |
| NodeZero.Weakness.vuln_id | String | Vulnerability identifier (e.g. CVE ID). |
| NodeZero.Weakness.vuln_name | String | Full vulnerability name. |
| NodeZero.Weakness.vuln_short_name | String | Short vulnerability name. |
| NodeZero.Weakness.vuln_category | String | Vulnerability category. |
| NodeZero.Weakness.vuln_cisa_kev | Boolean | Whether the vulnerability is in the CISA Known Exploited Vulnerabilities catalog. |
| NodeZero.Weakness.vuln_known_ransomware_campaign_use | Boolean | Whether the vulnerability is known to be used in ransomware campaigns. |
| NodeZero.Weakness.ip | String | IP address of the affected asset. |
| NodeZero.Weakness.has_proof | Boolean | Whether NodeZero has proof of exploitability. |
| NodeZero.Weakness.score | Number | Weakness severity score. |
| NodeZero.Weakness.severity | String | Weakness severity level (HIGH or CRITICAL). |
| NodeZero.Weakness.affected_asset_uuid | String | UUID of the affected asset. |
| NodeZero.Weakness.affected_asset_display_name | String | Display name of the affected asset. |
| NodeZero.Weakness.attack_paths_count | Number | Number of attack paths through this weakness. |
| NodeZero.Weakness.op_id | String | ID of the pentest operation that discovered this weakness. |
Command example
!nodezero-get-weaknesses limit=5
Human Readable Output
NodeZero Weaknesses
uuid severity vuln_name ip score has_proof abc-123 CRITICAL Example Vuln 10.0.0.1 9.8 true
Deduplication
The integration uses ID-based deduplication to prevent duplicate incidents:
- On first run, weaknesses from the last N days (configured via “First fetch time”) are fetched.
- On subsequent runs, the integration queries weaknesses since the most recent
created_attimestamp from the previous fetch. - UUIDs of weaknesses at the latest timestamp are tracked to avoid re-fetching them if they appear in the next query window.
This ensures that even if multiple weaknesses share the same timestamp, they are only ingested once.
Known Limitations
- Only HIGH and CRITICAL severity weaknesses are fetched.
- The integration uses a GraphQL API with JWT-based authentication.
- JWT tokens are cached and automatically refreshed before expiration.
Troubleshooting
If you encounter authentication errors, verify that:
- The API Key is correct and has not expired.
- The Server URL is accessible from the XSOAR server.
- SSL certificates are valid (or “Trust any certificate” is enabled for testing).
Configuration parameters
url— Server URL (required)credentials— API Key (required)insecure— Trust any certificate (not secure)proxy— Use system proxy settingsisFetch— Fetch incidentsincidentType— Incident typemax_fetch— Maximum number of weaknesses to fetchfirst_fetch— First fetch timeincidentFetchInterval— Incidents Fetch Interval
Commands (1)
-
nodezero-get-weaknessesRetrieves HIGH and CRITICAL weaknesses discovered by NodeZero pentests.
from dataclasses import dataclass from datetime import datetime, timedelta, UTC import time from typing import Any, Literal, TypedDict import demistomock as demisto import requests import urllib3 from CommonServerPython import * from CommonServerUserPython import * from pydantic import BaseModel, Field from pydantic_core import ValidationError urllib3.disable_warnings() XSOAR_DATE_FORMAT = "%Y-%m-%dT%H:%M:%SZ" NZ_DATE_FORMAT = "%Y-%m-%dT%H:%M:%S" MAX_PAGE_SIZE = 100 DEFAULT_MAX_FETCH = 200 DEFAULT_FIRST_FETCH = "7 days" AUTH_ENDPOINT = "v1/auth" GRAPHQL_ENDPOINT = "v1/graphql" HELLO_WORLD_QUERY = "query HelloWorld { hello }" WEAKNESSES_PAGE_QUERY = """ query WeaknessesPage($page_input: PageInput!) { weaknesses_page(page_input: $page_input) { weaknesses { uuid created_at vuln_id vuln_name vuln_short_name vuln_category vuln_cisa_kev vuln_known_ransomware_campaign_use ip has_proof score severity affected_asset_uuid affected_asset_display_name attack_paths_count op_id } page_info { page_num page_size } } } """ class Credentials(BaseModel): password: str class Params(BaseModel): url: str credentials: Credentials insecure: bool = False proxy: bool = False max_fetch: int = DEFAULT_MAX_FETCH first_fetch: str = DEFAULT_FIRST_FETCH class Token(BaseModel): token: str class Incident(TypedDict): name: str occurred: str rawJSON: str dbotMirrorId: str class Weakness(BaseModel): uuid: str created_at: str vuln_id: str vuln_name: str | None vuln_short_name: str | None vuln_category: str | None vuln_cisa_kev: bool | None vuln_known_ransomware_campaign_use: bool | None ip: str | None has_proof: bool | None score: float | None severity: str | None affected_asset_uuid: str | None affected_asset_display_name: str | None attack_paths_count: int op_id: str | None def to_incident(self) -> Incident: """Convert a Weakness to an XSOAR Incident.""" asset_name = self.affected_asset_display_name or "Unknown Asset" vuln_name = self.vuln_short_name or self.vuln_name or self.vuln_id data = self.model_dump() data["incident_type"] = "NodeZero Weakness" return Incident( name=f"{vuln_name} on {asset_name}", occurred=self.created_at if self.created_at.endswith("Z") else f"{self.created_at}Z", dbotMirrorId=self.uuid, rawJSON=json.dumps(data), ) class PageInfo(BaseModel): page_num: int page_size: int class WeaknessesPage(BaseModel): weaknesses: list[Weakness] page_info: PageInfo @classmethod def from_graphql_response(cls, response: dict) -> "WeaknessesPage": data = response.get("data", {}).get("weaknesses_page", {}) return cls( weaknesses=[Weakness.model_validate(w) for w in data.get("weaknesses", [])], page_info=PageInfo.model_validate(data.get("page_info", {})), ) def _default_since_date() -> str: return (datetime.now(UTC) - timedelta(days=7)).strftime(NZ_DATE_FORMAT) class GetWeaknessesArgs(BaseModel): """Parsed arguments for the nodezero-get-weaknesses command.""" since_date: str = Field(default_factory=_default_since_date) limit: int = Field(default=50, le=1000) class LastRun(BaseModel): """State persisted between fetch-incidents runs.""" last_fetch_date: str | None = None # ISO 8601 format last_ids: list[str] = [] # UUIDs of incidents at last_fetch_date timestamp @dataclass(frozen=True) class UnauthenticatedError(Exception): message: str = "Authentication failed: invalid or expired API key" @dataclass(frozen=True) class TokenExpiredError(Exception): pass @dataclass(frozen=True) class ResponseValidationError(Exception): validation_error: ValidationError @dataclass(frozen=True) class ParametersValidationError(Exception): validation_error: ValidationError class Client(BaseClient): def __init__( self, base_url: str, api_key: str, verify: bool, proxy: bool, ): super().__init__(base_url=base_url, verify=verify, proxy=proxy) self._base_url: str = base_url self._verify: bool = verify self._api_key: str = api_key self._jwt: str | None = None self._expiry: int = 0 self.load_integration_context() def load_integration_context(self) -> None: context = get_integration_context() if context: self._jwt = context["jwt"] self._expiry = context["expiry"] def is_authenticated(self) -> bool: return self._jwt is not None and self._expiry > int(time.time()) def authenticate(self) -> None: url = f"{self._base_url}/{AUTH_ENDPOINT}" json = {"key": self._api_key} now = int(time.time()) response = self._session.post(url, json=json) self._raise_for_status(response) content = Token.model_validate_json(response.content) self._jwt = content.token self._expiry = now + 3600 - 60 # 1 hr minus 60s buffer to prevent race conditions _ = set_integration_context({"jwt": self._jwt, "expiry": self._expiry}) def _graphql_request(self, query: str, variables: dict[str, Any]) -> requests.Response: url = f"{self._base_url}/{GRAPHQL_ENDPOINT}" headers = {"Authorization": f"Bearer {self._jwt}"} json = {"query": query, "variables": variables} resp = self._session.post(url, json=json, headers=headers) self._raise_for_status(resp) return resp def _raise_for_status(self, response: requests.Response) -> None: if response.status_code == 401: raise UnauthenticatedError response.raise_for_status() def hello_world(self) -> None: """Send hello_world request to check availability. If no error is raised, the call was successful """ _ = self._graphql_request(HELLO_WORLD_QUERY, {}) def query_weaknesses_page( self, page_num: int, page_size: int, since_date: str, ) -> WeaknessesPage: """Fetch a page of HIGH/CRITICAL OPEN weaknesses since the given date. Args: page_num: 1-indexed page number page_size: Number of results per page since_date: Fetch weaknesses first seen on or after this date (YYYY-MM-DD) Returns: WeaknessesPage with weaknesses list and page_info """ variables = { "page_input": { "page_num": page_num, "page_size": page_size, "filter_by_inputs": [ {"field_name": "severity", "values": ["HIGH", "CRITICAL"]}, {"field_name": "created_at", "greater_than_or_equal": since_date}, ], "order_by": "created_at", "sort_order": "ASC", } } resp = self._graphql_request(WEAKNESSES_PAGE_QUERY, variables) try: return WeaknessesPage.from_graphql_response(resp.json()) except ValidationError as e: raise ResponseValidationError(e) def authenticate(client: Client): if not client.is_authenticated(): client.authenticate() def fetch_all_weaknesses_pages( client: Client, since_date: str, max_fetch: int = DEFAULT_MAX_FETCH, ) -> list[Weakness]: """Fetch all HIGH/CRITICAL OPEN weaknesses since the given date, paginating as needed. Args: client: NodeZero API client since_date: Fetch weaknesses first seen on or after this date (YYYY-MM-DD) max_fetch: Maximum number of weaknesses to fetch Returns: List of WeaknessesPages, up to max_fetch items """ all_weaknesses: list[Weakness] = [] page_num = 1 page_size = min(MAX_PAGE_SIZE, max_fetch) while len(all_weaknesses) < max_fetch: page = client.query_weaknesses_page( page_num=page_num, page_size=page_size, since_date=since_date, ) all_weaknesses.extend(page.weaknesses) if len(page.weaknesses) < page_size: break page_num += 1 return all_weaknesses[:max_fetch] """ COMMAND FUNCTIONS """ def get_weaknesses_command( client: Client, args: GetWeaknessesArgs, ) -> CommandResults: """Return a table of HIGH/CRITICAL weaknesses from NodeZero. Args: client: NodeZero API client. args: Parsed command arguments. Returns: CommandResults with human-readable table and context data. """ authenticate(client) weaknesses = fetch_all_weaknesses_pages( client=client, since_date=args.since_date, max_fetch=args.limit, ) outputs = [w.model_dump() for w in weaknesses] headers = [ "uuid", "severity", "vuln_id", "vuln_short_name", "ip", "affected_asset_display_name", "score", "created_at", ] return CommandResults( readable_output=tableToMarkdown( "NodeZero Weaknesses", outputs, headers=headers, removeNull=True, ), outputs_prefix="NodeZero.Weakness", outputs_key_field="uuid", outputs=outputs, ) def _test_module(client: Client) -> Literal["ok"]: """Tests API connectivity and authentication. Returning 'ok' indicates that the integration works like it is supposed to. Connection to the service is successful. Raises: exceptions if something goes wrong. Args: client: client to use Returns: 'ok' if test passed, anything else will fail the test. """ authenticate(client) client.hello_world() return "ok" def dedup_by_ids(weaknesses: list[Weakness], ids_to_skip: list[str]) -> list[Weakness]: """Filter out weaknesses that were already fetched in a previous run. Args: weaknesses: List of weaknesses to filter ids_to_skip: UUIDs of weaknesses to exclude Returns: List of weaknesses not in ids_to_skip """ if not ids_to_skip: return weaknesses skip_set = set(ids_to_skip) return [w for w in weaknesses if w.uuid not in skip_set] def fetch_incidents( client: Client, max_fetch: int = DEFAULT_MAX_FETCH, first_fetch_time: datetime | None = None, ) -> tuple[list[Incident], LastRun]: """Fetch HIGH/CRITICAL OPEN weaknesses as incidents. On first run, fetches weaknesses from `first_fetch_time`. On subsequent runs, fetches weaknesses since the last fetch date. Args: client: NodeZero API client max_fetch: Maximum number of incidents to fetch first_fetch_time: Datetime to fetch from on first run Returns: Tuple of (incidents list, new last_run state) """ authenticate(client) last_run = LastRun.model_validate(demisto.getLastRun()) if last_run.last_fetch_date: since_date = last_run.last_fetch_date elif first_fetch_time: since_date = first_fetch_time.strftime(NZ_DATE_FORMAT) else: since_date = (datetime.now(UTC) - timedelta(days=7)).strftime(NZ_DATE_FORMAT) demisto.debug(f"Fetching weaknesses since {since_date}") weaknesses = fetch_all_weaknesses_pages( client=client, since_date=since_date, max_fetch=max_fetch, ) pre_dedup_count = len(weaknesses) weaknesses = dedup_by_ids(weaknesses, last_run.last_ids) demisto.debug(f"After deduplication: {len(weaknesses)} weaknesses (filtered {pre_dedup_count - len(weaknesses)})") if not weaknesses: if pre_dedup_count > 0 and last_run.last_fetch_date: # All results were duplicates — advance timestamp by 1s to prevent infinite loop advanced = datetime.strptime(last_run.last_fetch_date, NZ_DATE_FORMAT) + timedelta(seconds=1) next_run = LastRun(last_fetch_date=advanced.strftime(NZ_DATE_FORMAT), last_ids=[]) demisto.debug(f"All results deduped; advancing fetch date to {next_run.last_fetch_date}") return [], next_run return [], last_run incidents = [w.to_incident() for w in weaknesses] latest_timestamp = max(w.created_at for w in weaknesses) last_ids = [w.uuid for w in weaknesses if w.created_at == latest_timestamp] next_run = LastRun(last_fetch_date=latest_timestamp, last_ids=last_ids) demisto.debug(f"Fetched {len(incidents)} incidents, next fetch from {latest_timestamp}") return incidents, next_run def params() -> Params: try: return Params.model_validate(demisto.params()) except ValidationError as e: raise ParametersValidationError(e) def main(): _args = demisto.args() command = demisto.command() try: params = Params.model_validate(demisto.params()) demisto.debug(f"Command being called is {command}") client = Client( base_url=params.url, api_key=params.credentials.password, verify=not params.insecure, proxy=params.proxy, ) if command == "test-module": return_results(_test_module(client)) elif command == "nodezero-get-weaknesses": weakness_args = GetWeaknessesArgs.model_validate(_args) return_results(get_weaknesses_command(client, weakness_args)) elif command == "fetch-incidents": first_fetch_time = arg_to_datetime( arg=params.first_fetch, arg_name="First fetch time", required=True, ) incidents, next_run = fetch_incidents( client, max_fetch=params.max_fetch, first_fetch_time=first_fetch_time, ) demisto.setLastRun(next_run.model_dump()) demisto.incidents(incidents) # type: ignore[arg-type] else: raise NotImplementedError(f"Command {command} is not implemented") except Exception as e: return_error(f"Failed to execute {command} command.\nError:\n{str(e)}") if __name__ in ("__main__", "__builtin__", "builtins"): # pragma: no cover main()