Skip to content

Commit 1509239

Browse files
feat(asset): add asset categorization support (#298) (#297)
1 parent 8283bfe commit 1509239

4 files changed

Lines changed: 209 additions & 6 deletions

File tree

pyoaev/__init__.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
# -*- coding: utf-8 -*-
2-
__version__ = "2.260622.0"
2+
__version__ = "2.260627.0"
33

44
from pyoaev._version import ( # noqa: F401
55
__author__,
@@ -8,6 +8,12 @@
88
__license__,
99
__title__,
1010
)
11+
from pyoaev.asset_types import ( # noqa: F401
12+
AssetCategory,
13+
AssetCriticality,
14+
AssetSubCategory,
15+
CloudProvider,
16+
)
1117
from pyoaev.client import OpenAEV # noqa: F401
1218
from pyoaev.configuration import * # noqa: F401,F403,F405
1319
from pyoaev.contracts import * # noqa: F401,F403,F405
@@ -22,5 +28,9 @@
2228
"__title__",
2329
"__version__",
2430
"OpenAEV",
31+
"AssetCategory",
32+
"AssetSubCategory",
33+
"CloudProvider",
34+
"AssetCriticality",
2535
]
2636
__all__.extend(exceptions.__all__) # noqa: F405

pyoaev/apis/endpoint.py

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,16 +13,30 @@ class Endpoint(RESTObject):
1313
class EndpointManager(RESTManager):
1414
_path = "/endpoints"
1515
_obj_cls = Endpoint
16+
# asset_name is the only required attribute. Everything else - including endpoint_hostname,
17+
# endpoint_platform and endpoint_arch - is optional: agents and collectors typically provide
18+
# the endpoint fields, while category-driven assets (web app, cloud, network, ...) may omit
19+
# them and the backend defaults endpoint_platform / endpoint_arch to "Unknown".
1620
_create_attrs = RequiredOptional(
17-
required=(
21+
required=("asset_name",),
22+
optional=(
1823
"endpoint_hostname",
1924
"endpoint_platform",
2025
"endpoint_arch",
21-
),
22-
optional=(
23-
"endpoint_mac_addresses",
2426
"endpoint_ips",
27+
"endpoint_mac_addresses",
28+
"endpoint_url",
29+
"asset_description",
2530
"asset_external_reference",
31+
"asset_tags",
32+
"asset_category",
33+
"asset_subcategory",
34+
"asset_criticality",
35+
"asset_internet_facing",
36+
"asset_cloud_provider",
37+
"asset_cloud_native_type",
38+
"asset_cloud_region",
39+
"asset_metadata",
2640
),
2741
)
2842

pyoaev/asset_types.py

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
# -*- coding: utf-8 -*-
2+
"""Canonical asset taxonomy for OpenAEV (mirrors the backend enums).
3+
4+
These give producers (collectors, custom integrations) a single, typed vocabulary for the
5+
two-level asset categorization (category + subcategory), criticality and cloud provider, instead
6+
of free-form strings. They are plain ``str`` enums so they can be dropped straight into the dict
7+
payloads sent to the API.
8+
"""
9+
10+
from enum import Enum
11+
12+
13+
class AssetCategory(str, Enum):
14+
HOST = "HOST"
15+
CONTAINER_WORKLOAD = "CONTAINER_WORKLOAD"
16+
CLOUD_RESOURCE = "CLOUD_RESOURCE"
17+
WEB_APPLICATION = "WEB_APPLICATION"
18+
NETWORK_DEVICE = "NETWORK_DEVICE"
19+
MOBILE_DEVICE = "MOBILE_DEVICE"
20+
IOT_OT_DEVICE = "IOT_OT_DEVICE"
21+
IDENTITY = "IDENTITY"
22+
SAAS_APPLICATION = "SAAS_APPLICATION"
23+
AI_TARGET = "AI_TARGET"
24+
SECURITY_PLATFORM = "SECURITY_PLATFORM"
25+
GENERIC_ASSET = "GENERIC_ASSET"
26+
27+
28+
class AssetSubCategory(str, Enum):
29+
# HOST
30+
SERVER = "SERVER"
31+
WORKSTATION = "WORKSTATION"
32+
LAPTOP = "LAPTOP"
33+
VIRTUAL_MACHINE = "VIRTUAL_MACHINE"
34+
HYPERVISOR = "HYPERVISOR"
35+
MAINFRAME = "MAINFRAME"
36+
THIN_CLIENT = "THIN_CLIENT"
37+
# CONTAINER_WORKLOAD
38+
CONTAINER = "CONTAINER"
39+
CONTAINER_IMAGE = "CONTAINER_IMAGE"
40+
KUBERNETES_POD = "KUBERNETES_POD"
41+
KUBERNETES_CLUSTER = "KUBERNETES_CLUSTER"
42+
KUBERNETES_NODE = "KUBERNETES_NODE"
43+
SERVERLESS_FUNCTION = "SERVERLESS_FUNCTION"
44+
# CLOUD_RESOURCE
45+
COMPUTE = "COMPUTE"
46+
STORAGE = "STORAGE"
47+
DATABASE = "DATABASE"
48+
NETWORKING = "NETWORKING"
49+
SERVERLESS = "SERVERLESS"
50+
CONTAINER_REGISTRY = "CONTAINER_REGISTRY"
51+
KUBERNETES = "KUBERNETES"
52+
IAM_PRINCIPAL = "IAM_PRINCIPAL"
53+
SECRETS_KEY_MGMT = "SECRETS_KEY_MGMT"
54+
MESSAGING_QUEUE = "MESSAGING_QUEUE"
55+
ANALYTICS_DATA = "ANALYTICS_DATA"
56+
AI_ML_SERVICE = "AI_ML_SERVICE"
57+
IAC_TEMPLATE = "IAC_TEMPLATE"
58+
CLOUD_OTHER = "CLOUD_OTHER"
59+
# WEB_APPLICATION
60+
WEBSITE = "WEBSITE"
61+
WEB_API = "WEB_API"
62+
SINGLE_PAGE_APP = "SINGLE_PAGE_APP"
63+
GRAPHQL_API = "GRAPHQL_API"
64+
WEB_SERVICE = "WEB_SERVICE"
65+
MICROSERVICE = "MICROSERVICE"
66+
# NETWORK_DEVICE
67+
ROUTER = "ROUTER"
68+
SWITCH = "SWITCH"
69+
FIREWALL = "FIREWALL"
70+
LOAD_BALANCER = "LOAD_BALANCER"
71+
VPN_GATEWAY = "VPN_GATEWAY"
72+
WIRELESS_AP = "WIRELESS_AP"
73+
PROXY = "PROXY"
74+
DNS_SERVER = "DNS_SERVER"
75+
DHCP_SERVER = "DHCP_SERVER"
76+
SAN_NAS = "SAN_NAS"
77+
NETWORK_OTHER = "NETWORK_OTHER"
78+
# MOBILE_DEVICE
79+
SMARTPHONE = "SMARTPHONE"
80+
TABLET = "TABLET"
81+
# IOT_OT_DEVICE
82+
IOT_SENSOR = "IOT_SENSOR"
83+
IP_CAMERA = "IP_CAMERA"
84+
GATEWAY = "GATEWAY"
85+
POINT_OF_SALE = "POINT_OF_SALE"
86+
MEDIA_DEVICE = "MEDIA_DEVICE"
87+
PLC = "PLC"
88+
RTU = "RTU"
89+
HMI = "HMI"
90+
SCADA_HISTORIAN = "SCADA_HISTORIAN"
91+
MEDICAL_DEVICE = "MEDICAL_DEVICE"
92+
PRINTER_PERIPHERAL = "PRINTER_PERIPHERAL"
93+
BUILDING_MGMT = "BUILDING_MGMT"
94+
# IDENTITY
95+
USER_ACCOUNT = "USER_ACCOUNT"
96+
SERVICE_ACCOUNT = "SERVICE_ACCOUNT"
97+
GROUP = "GROUP"
98+
ROLE = "ROLE"
99+
SHARED_MAILBOX = "SHARED_MAILBOX"
100+
NON_HUMAN_IDENTITY = "NON_HUMAN_IDENTITY"
101+
# SAAS_APPLICATION
102+
SAAS_APP = "SAAS_APP"
103+
SAAS_TENANT = "SAAS_TENANT"
104+
# AI_TARGET
105+
LLM_MODEL = "LLM_MODEL"
106+
AI_AGENT = "AI_AGENT"
107+
MCP_SERVER = "MCP_SERVER"
108+
RAG_PIPELINE = "RAG_PIPELINE"
109+
# SECURITY_PLATFORM
110+
EDR = "EDR"
111+
XDR = "XDR"
112+
SIEM = "SIEM"
113+
SOAR = "SOAR"
114+
NDR = "NDR"
115+
ISPM = "ISPM"
116+
LLM_FIREWALL = "LLM_FIREWALL"
117+
AI_GATEWAY = "AI_GATEWAY"
118+
119+
120+
class CloudProvider(str, Enum):
121+
AWS = "AWS"
122+
AZURE = "AZURE"
123+
GCP = "GCP"
124+
OCI = "OCI"
125+
ALIBABA = "ALIBABA"
126+
KUBERNETES = "KUBERNETES"
127+
OTHER = "OTHER"
128+
129+
130+
class AssetCriticality(str, Enum):
131+
VERY_HIGH = "VERY_HIGH"
132+
HIGH = "HIGH"
133+
MEDIUM = "MEDIUM"
134+
LOW = "LOW"
135+
UNKNOWN = "UNKNOWN"

test/apis/endpoint/test_endpoint.py

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
from unittest import TestCase, main, mock
22
from unittest.mock import ANY
33

4-
from pyoaev import OpenAEV
4+
from pyoaev import AssetCategory, AssetSubCategory, CloudProvider, OpenAEV
55
from pyoaev.apis.inputs.search import Filter, FilterGroup, SearchPaginationInput
66

77

@@ -53,5 +53,49 @@ def test_search_input_correctly_serialised(self, mock_request):
5353
)
5454

5555

56+
class TestEndpointCategorization(TestCase):
57+
@mock.patch("requests.Session.request", side_effect=mock_response)
58+
def test_upsert_web_application_without_platform(self, mock_request):
59+
api_client = OpenAEV("url", "token")
60+
data = {
61+
"asset_name": "Filigran website",
62+
"asset_category": AssetCategory.WEB_APPLICATION,
63+
"asset_subcategory": AssetSubCategory.WEBSITE,
64+
"endpoint_url": "https://filigran.io",
65+
"asset_internet_facing": True,
66+
}
67+
68+
api_client.endpoint.upsert(data)
69+
70+
mock_request.assert_called_once()
71+
_, kwargs = mock_request.call_args
72+
self.assertEqual(kwargs["method"], "post")
73+
self.assertEqual(kwargs["url"], "url/api/endpoints/agentless/upsert")
74+
self.assertEqual(kwargs["json"]["asset_category"], "WEB_APPLICATION")
75+
# A category-driven asset can be upserted without platform / arch.
76+
self.assertNotIn("endpoint_platform", kwargs["json"])
77+
self.assertNotIn("endpoint_arch", kwargs["json"])
78+
79+
@mock.patch("requests.Session.request", side_effect=mock_response)
80+
def test_upsert_cloud_resource(self, mock_request):
81+
api_client = OpenAEV("url", "token")
82+
data = {
83+
"asset_name": "prod-bucket",
84+
"asset_category": AssetCategory.CLOUD_RESOURCE,
85+
"asset_subcategory": AssetSubCategory.STORAGE,
86+
"asset_cloud_provider": CloudProvider.AWS,
87+
"asset_cloud_native_type": "s3_bucket",
88+
"asset_cloud_region": "eu-west-1",
89+
}
90+
91+
api_client.endpoint.upsert(data)
92+
93+
mock_request.assert_called_once()
94+
_, kwargs = mock_request.call_args
95+
self.assertEqual(kwargs["url"], "url/api/endpoints/agentless/upsert")
96+
self.assertEqual(kwargs["json"]["asset_cloud_provider"], "AWS")
97+
self.assertEqual(kwargs["json"]["asset_cloud_native_type"], "s3_bucket")
98+
99+
56100
if __name__ == "__main__":
57101
main()

0 commit comments

Comments
 (0)