From 8c4cc406bb03a5118eb45b253b8ef24c5667ea61 Mon Sep 17 00:00:00 2001 From: Shai Aizinbud Date: Mon, 15 Jan 2024 12:50:08 +0200 Subject: [PATCH 1/4] bugfix: pyTenable crashes if srcInterface or dstInterface not null Addressed a bug causing a crash in when events->srcInterface or events->dstInterface was not null upon query completion. The issue was traced back to incorrect usage of the marshmallow package. Note: In Tenable.OT version 3.17, a bug existed where events->srcInterface and dstInterface consistently returned null. Due to this behavior, the current bug did not manifest itself during that version Additionally, added the retrieval of required nodes from dstInterface, ensuring that all necessary nodes are now properly obtained. --- tenable/ot/graphql/query.py | 15 +++++++++++++++ tenable/ot/graphql/schema/events.py | 25 +++++++++++++++++++++---- tenable/ot/schema/assets.py | 20 ++++++++++++++++---- 3 files changed, 52 insertions(+), 8 deletions(-) diff --git a/tenable/ot/graphql/query.py b/tenable/ot/graphql/query.py index eba2a21d3..e3126d018 100644 --- a/tenable/ot/graphql/query.py +++ b/tenable/ot/graphql/query.py @@ -423,6 +423,21 @@ } dstInterface{ id + lastSeen + firstSeen + mac + ips { + nodes{ + ip + } + } + dnsNames{ + nodes + } + family + directAsset { + id + } } dstNames{ nodes diff --git a/tenable/ot/graphql/schema/events.py b/tenable/ot/graphql/schema/events.py index cdfd164f5..c10e32c26 100644 --- a/tenable/ot/graphql/schema/events.py +++ b/tenable/ot/graphql/schema/events.py @@ -2,7 +2,7 @@ from tenable.ot.graphql.definitions import NodesSchema, SchemaBase from tenable.ot.graphql.schema.assets import Asset -from tenable.ot.schema.assets import NetworkInterface +from tenable.ot.schema.assets import NetworkInterface, IP, IPList from tenable.ot.schema.base import NodesList, AssetInfoList, AssetInfo from tenable.ot.schema.events import ( Event, @@ -32,6 +32,16 @@ class IDListSchema(SchemaBase): nodes = fields.Nested(IDSchema, required=True, many=True) +class IPSchema(SchemaBase): + dataclass = IP + ip = fields.IPv4(required=True) + + +class IPListSchema(SchemaBase): + dataclass = IPList + nodes = fields.Nested(IPSchema, required=True, many=True) + + class ActionSchema(SchemaBase): dataclass = Action aid = fields.UUID(required=True) @@ -187,16 +197,23 @@ class PolicySchema(SchemaBase): ) +class DnsListSchema(SchemaBase): + dataclass = NodesList + + nodes = fields.List(fields.String(required=True), required=True) + + class NetworkInterfaceSchema(SchemaBase): dataclass = NetworkInterface + id = fields.UUID(required=True) last_seen = fields.DateTime(allow_none=True, data_key="lastSeen") first_seen = fields.DateTime(allow_none=True, data_key="firstSeen") mac = fields.String(allow_none=True) - - ips = fields.IPv4(required=True) + ips = fields.Nested(IPListSchema, required=True) family = fields.String(required=True) - direct_asset = fields.Nested(Asset, allow_none=True, data_key="directAsset") + direct_asset = fields.Nested(IDSchema, allow_none=True, data_key="directAsset") + dns_names = fields.Nested(DnsListSchema, required=True, data_key="dnsNames") class AssetInfoSchema(SchemaBase): diff --git a/tenable/ot/schema/assets.py b/tenable/ot/schema/assets.py index 254d09059..0f9d80f0c 100644 --- a/tenable/ot/schema/assets.py +++ b/tenable/ot/schema/assets.py @@ -1,4 +1,5 @@ import datetime +import ipaddress import typing import uuid from typing import List, Optional @@ -144,16 +145,27 @@ class Assets(NodesList): nodes: List[Asset] +@dataclass +class IP: + ip: ipaddress.IPv4Address + + +@dataclass +class IPList: + nodes: List[IP] + + @dataclass class NetworkInterface: """ This class holds a network interface's information. """ - direct_asset: Asset - family: str - first_seen: datetime.datetime id: uuid.UUID - ips: str last_seen: datetime.datetime + first_seen: datetime.datetime mac: str + family: str + direct_asset: Asset + ips: IPList + dns_names: NodesList From 2869ed10bb07926c1d563d0972aa1e16ddd6005e Mon Sep 17 00:00:00 2001 From: Shai Aizinbud Date: Wed, 31 Jan 2024 19:26:27 +0200 Subject: [PATCH 2/4] move IP and IPList from schema/assets.py to schema/base.py --- tenable/ot/graphql/schema/events.py | 4 ++-- tenable/ot/schema/assets.py | 12 +----------- tenable/ot/schema/base.py | 11 +++++++++++ 3 files changed, 14 insertions(+), 13 deletions(-) diff --git a/tenable/ot/graphql/schema/events.py b/tenable/ot/graphql/schema/events.py index c10e32c26..9a4bbd319 100644 --- a/tenable/ot/graphql/schema/events.py +++ b/tenable/ot/graphql/schema/events.py @@ -2,8 +2,8 @@ from tenable.ot.graphql.definitions import NodesSchema, SchemaBase from tenable.ot.graphql.schema.assets import Asset -from tenable.ot.schema.assets import NetworkInterface, IP, IPList -from tenable.ot.schema.base import NodesList, AssetInfoList, AssetInfo +from tenable.ot.schema.assets import NetworkInterface +from tenable.ot.schema.base import NodesList, AssetInfoList, AssetInfo,IP, IPList from tenable.ot.schema.events import ( Event, EventTypeDetails, diff --git a/tenable/ot/schema/assets.py b/tenable/ot/schema/assets.py index 0f9d80f0c..36519b3d0 100644 --- a/tenable/ot/schema/assets.py +++ b/tenable/ot/schema/assets.py @@ -6,7 +6,7 @@ from dataclasses import dataclass -from tenable.ot.schema.base import NodesList +from tenable.ot.schema.base import NodesList, IPList from tenable.ot.schema.plugins import Plugin, Plugins @@ -145,16 +145,6 @@ class Assets(NodesList): nodes: List[Asset] -@dataclass -class IP: - ip: ipaddress.IPv4Address - - -@dataclass -class IPList: - nodes: List[IP] - - @dataclass class NetworkInterface: """ diff --git a/tenable/ot/schema/base.py b/tenable/ot/schema/base.py index b6927ec57..283245e15 100644 --- a/tenable/ot/schema/base.py +++ b/tenable/ot/schema/base.py @@ -1,3 +1,4 @@ +import ipaddress import uuid from dataclasses import dataclass from typing import List @@ -32,3 +33,13 @@ class AssetInfo: @dataclass class AssetInfoList(NodesList): nodes: List[AssetInfo] + + +@dataclass +class IP: + ip: ipaddress.IPv4Address + + +@dataclass +class IPList: + nodes: List[IP] From d358b09c565f8528f83cbd94badbb7f50cf6beee Mon Sep 17 00:00:00 2001 From: Shai Aizinbud Date: Mon, 5 Feb 2024 11:55:45 +0200 Subject: [PATCH 3/4] replace ipaddress.IPv4Address with IP in Events --- tenable/ot/schema/events.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tenable/ot/schema/events.py b/tenable/ot/schema/events.py index aa34b2916..8394763be 100644 --- a/tenable/ot/schema/events.py +++ b/tenable/ot/schema/events.py @@ -6,7 +6,7 @@ from typing import List, Optional from tenable.ot.schema.assets import NetworkInterface -from tenable.ot.schema.base import NodesList, AssetInfoList +from tenable.ot.schema.base import NodesList, AssetInfoList, IP @dataclass @@ -121,7 +121,7 @@ class Event: completion: str continuous: bool dst_assets: AssetInfoList - dst_ip: ipaddress.IPv4Address + dst_ip: IP dst_names: NodesList event_type: EventTypeDetails has_details: bool @@ -136,7 +136,7 @@ class Event: resolved: bool severity: str src_assets: AssetInfoList - src_ip: ipaddress.IPv4Address + src_ip: IP src_names: NodesList time: datetime.datetime type: str From 924afd015c9b58e1b15b8fe31a4bd0a7c385b0c7 Mon Sep 17 00:00:00 2001 From: Shai Aizinbud Date: Mon, 12 Feb 2024 16:51:37 +0200 Subject: [PATCH 4/4] add fields to tests, revert last commit, and minor fixes * Add srcInterface and dstInterface fields to OT test * Revert change of last commit - dst_ip, src_ip should remain IPv4Address and cannot be converted to IP class * Change directAsset field of NetworkInterface to ID class * Move ID, IDList classes to base.py * Remove Lists from src_interface and dst_interface - These fields cannot return lists --- tenable/ot/graphql/schema/events.py | 13 ++-- tenable/ot/schema/assets.py | 4 +- tenable/ot/schema/base.py | 10 +++ tenable/ot/schema/events.py | 24 +++---- tests/ot/test_events.py | 105 ++++++++++++++++++++++++++-- 5 files changed, 129 insertions(+), 27 deletions(-) diff --git a/tenable/ot/graphql/schema/events.py b/tenable/ot/graphql/schema/events.py index 9a4bbd319..16cd82616 100644 --- a/tenable/ot/graphql/schema/events.py +++ b/tenable/ot/graphql/schema/events.py @@ -1,9 +1,16 @@ from marshmallow import fields from tenable.ot.graphql.definitions import NodesSchema, SchemaBase -from tenable.ot.graphql.schema.assets import Asset from tenable.ot.schema.assets import NetworkInterface -from tenable.ot.schema.base import NodesList, AssetInfoList, AssetInfo,IP, IPList +from tenable.ot.schema.base import ( + NodesList, + AssetInfoList, + AssetInfo, + IP, + IPList, + ID, + IDList +) from tenable.ot.schema.events import ( Event, EventTypeDetails, @@ -17,8 +24,6 @@ RackSlot, ActionList, Action, - ID, - IDList, ) diff --git a/tenable/ot/schema/assets.py b/tenable/ot/schema/assets.py index 36519b3d0..20262186c 100644 --- a/tenable/ot/schema/assets.py +++ b/tenable/ot/schema/assets.py @@ -6,7 +6,7 @@ from dataclasses import dataclass -from tenable.ot.schema.base import NodesList, IPList +from tenable.ot.schema.base import NodesList, IPList, ID from tenable.ot.schema.plugins import Plugin, Plugins @@ -156,6 +156,6 @@ class NetworkInterface: first_seen: datetime.datetime mac: str family: str - direct_asset: Asset + direct_asset: ID ips: IPList dns_names: NodesList diff --git a/tenable/ot/schema/base.py b/tenable/ot/schema/base.py index 283245e15..7e2660022 100644 --- a/tenable/ot/schema/base.py +++ b/tenable/ot/schema/base.py @@ -43,3 +43,13 @@ class IP: @dataclass class IPList: nodes: List[IP] + + +@dataclass +class ID: + id: uuid.UUID + + +@dataclass +class IDList: + nodes: List[ID] diff --git a/tenable/ot/schema/events.py b/tenable/ot/schema/events.py index 8394763be..9e99e4937 100644 --- a/tenable/ot/schema/events.py +++ b/tenable/ot/schema/events.py @@ -6,17 +6,11 @@ from typing import List, Optional from tenable.ot.schema.assets import NetworkInterface -from tenable.ot.schema.base import NodesList, AssetInfoList, IP - - -@dataclass -class ID: - id: uuid.UUID - - -@dataclass -class IDList: - nodes: List[ID] +from tenable.ot.schema.base import ( + NodesList, + AssetInfoList, + IDList +) @dataclass @@ -121,7 +115,7 @@ class Event: completion: str continuous: bool dst_assets: AssetInfoList - dst_ip: IP + dst_ip: ipaddress.IPv4Address dst_names: NodesList event_type: EventTypeDetails has_details: bool @@ -136,17 +130,17 @@ class Event: resolved: bool severity: str src_assets: AssetInfoList - src_ip: IP + src_ip: ipaddress.IPv4Address src_names: NodesList time: datetime.datetime type: str comment: Optional[str] = None - dst_interface: Optional[List[NetworkInterface]] = None + dst_interface: Optional[NetworkInterface] = None dst_mac: Optional[str] = None protocol_nice_name: Optional[str] = None resolved_ts: Optional[datetime.datetime] = None resolved_user: Optional[str] = None - src_interface: Optional[List[NetworkInterface]] = None + src_interface: Optional[NetworkInterface] = None src_mac: Optional[str] = None diff --git a/tests/ot/test_events.py b/tests/ot/test_events.py index a03a48f0f..5022b36f3 100644 --- a/tests/ot/test_events.py +++ b/tests/ot/test_events.py @@ -7,9 +7,18 @@ import pytest import responses +from tenable.ot.schema.assets import NetworkInterface from tenable.ot.graphql.iterators import OTGraphIterator -from tenable.ot.schema.base import AssetInfo, NodesList, AssetInfoList +from tenable.ot.schema.base import ( + AssetInfo, + NodesList, + AssetInfoList, + IPList, + IP, + IDList, + ID, +) from tenable.ot.schema.events import ( Event, EventTypeDetails, @@ -17,7 +26,7 @@ GroupMember, Group, EventCount, - ActionList, IDList, + ActionList ) @@ -150,7 +159,26 @@ def test_list(fixture_ot): } ] }, - "srcInterface": None, + "srcInterface": { + "id": "71d1edd9-bdce-4a59-96f7-7f041baa7680", + "lastSeen": "2024-02-12T12:29:11.75946Z", + "firstSeen": "2024-02-05T16:36:32.946517Z", + "mac": "00:50:56:83:6c:23", + "ips": { + "nodes": [ + { + "ip": "10.100.20.17" + } + ] + }, + "dnsNames": { + "nodes": [] + }, + "family": "VMware", + "directAsset": { + "id": "b1158857-b9fe-41ac-ba50-db17ecfd5746" + } + }, "srcNames": {"nodes": ["Eng. Station #100"]}, "dstAssets": { "nodes": [ @@ -160,7 +188,26 @@ def test_list(fixture_ot): } ] }, - "dstInterface": None, + "dstInterface": { + "id": "56044b78-d307-467d-a5ba-eb1c522b73af", + "lastSeen": "2024-02-12T12:29:09.568506Z", + "firstSeen": "2024-02-05T16:36:32.747096Z", + "mac": "00:09:91:06:4f:9a", + "ips": { + "nodes": [ + { + "ip": "10.100.103.20" + } + ] + }, + "dnsNames": { + "nodes": [] + }, + "family": "GE", + "directAsset": { + "id": "1ab1220a-479e-4ce8-ba2a-e1b2b63d8345" + } + }, "dstNames": {"nodes": ["CPU 412-2 PN/DP"]}, "hasDetails": False, "payloadSize": 0, @@ -299,7 +346,30 @@ def test_list(fixture_ot): ) ] ), - src_interface=None, + src_interface=NetworkInterface( + id=uuid.UUID('71d1edd9-bdce-4a59-96f7-7f041baa7680'), + last_seen=datetime.datetime( + 2024, 2, 12, 12, 29, 11, 759460, tzinfo=datetime.timezone.utc + ), + first_seen=datetime.datetime( + 2024, 2, 5, 16, 36, 32, 946517, tzinfo=datetime.timezone.utc + ), + mac='00:50:56:83:6c:23', + family='VMware', + direct_asset=ID( + id=uuid.UUID('b1158857-b9fe-41ac-ba50-db17ecfd5746') + ), + ips=IPList( + nodes=[ + IP( + ip=IPv4Address('10.100.20.17') + ) + ] + ), + dns_names=NodesList( + nodes=[] + ) + ), src_names=NodesList(nodes=["Eng. Station #100"]), dst_assets=AssetInfoList( nodes=[ @@ -309,7 +379,30 @@ def test_list(fixture_ot): ) ] ), - dst_interface=None, + dst_interface=NetworkInterface( + id=uuid.UUID('56044b78-d307-467d-a5ba-eb1c522b73af'), + last_seen=datetime.datetime( + 2024, 2, 12, 12, 29, 9, 568506, tzinfo=datetime.timezone.utc + ), + first_seen=datetime.datetime( + 2024, 2, 5, 16, 36, 32, 747096, tzinfo=datetime.timezone.utc + ), + mac='00:09:91:06:4f:9a', + family='GE', + direct_asset=ID( + id=uuid.UUID('1ab1220a-479e-4ce8-ba2a-e1b2b63d8345') + ), + ips=IPList( + nodes=[ + IP( + ip=IPv4Address('10.100.103.20') + ) + ] + ), + dns_names=NodesList( + nodes=[] + ) + ), dst_names=NodesList(nodes=["CPU 412-2 PN/DP"]), has_details=False, payload_size=0,