Skip to content

Commit 51081ce

Browse files
bjoernricksgreenbonebot
authored andcommitted
Add: Implement Aggregates command
1 parent 5d36c79 commit 51081ce

File tree

5 files changed

+683
-1
lines changed

5 files changed

+683
-1
lines changed

gvm/protocols/gmp/_gmp224.py

Lines changed: 61 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,17 @@
22
#
33
# SPDX-License-Identifier: GPL-3.0-or-later
44

5-
from typing import Optional, Union
5+
from typing import Iterable, Optional, Union
66

77
from .._protocol import GvmProtocol, T
88
from .requests import (
9+
Aggregates,
10+
AggregateStatistic,
911
Authentication,
12+
EntityType,
1013
PortList,
1114
PortRangeType,
15+
SortOrder,
1216
Version,
1317
)
1418

@@ -208,3 +212,59 @@ def modify_port_list(
208212
return self._send_and_transform_command(
209213
PortList.modify_port_list(port_list_id, comment=comment, name=name)
210214
)
215+
216+
def get_aggregates(
217+
self,
218+
resource_type: Union[EntityType, str],
219+
*,
220+
filter_string: Optional[str] = None,
221+
filter_id: Optional[str] = None,
222+
sort_criteria: Optional[
223+
Iterable[dict[str, Union[str, SortOrder, AggregateStatistic]]]
224+
] = None,
225+
data_columns: Optional[Iterable[str]] = None,
226+
group_column: Optional[str] = None,
227+
subgroup_column: Optional[str] = None,
228+
text_columns: Optional[Iterable[str]] = None,
229+
first_group: Optional[int] = None,
230+
max_groups: Optional[int] = None,
231+
mode: Optional[int] = None,
232+
**kwargs,
233+
) -> T:
234+
"""Request aggregated information on a resource / entity type
235+
236+
Additional arguments can be set via the kwargs parameter for backward
237+
compatibility with older versions of python-gvm, but are not validated.
238+
239+
Args:
240+
resource_type: The entity type to gather data from
241+
filter_string: Filter term to use for the query
242+
filter_id: UUID of an existing filter to use for the query
243+
sort_criteria: List of sort criteria (dicts that can contain
244+
a field, stat and order)
245+
data_columns: List of fields to aggregate data from
246+
group_column: The field to group the entities by
247+
subgroup_column: The field to further group the entities
248+
inside groups by
249+
text_columns: List of simple text columns which no statistics
250+
are calculated for
251+
first_group: The index of the first aggregate group to return
252+
max_groups: The maximum number of aggregate groups to return,
253+
-1 for all
254+
mode: Special mode for aggregation
255+
"""
256+
return self._send_and_transform_command(
257+
Aggregates.get_aggregates(
258+
resource_type,
259+
filter_string=filter_string,
260+
filter_id=filter_id,
261+
sort_criteria=sort_criteria,
262+
data_columns=data_columns,
263+
group_column=group_column,
264+
subgroup_column=subgroup_column,
265+
text_columns=text_columns,
266+
first_group=first_group,
267+
max_groups=max_groups,
268+
**kwargs,
269+
)
270+
)

gvm/protocols/gmp/requests/__init__.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,22 @@
22
#
33
# SPDX-License-Identifier: GPL-3.0-or-later
44

5+
from ._aggregates import Aggregates, AggregateStatistic, SortOrder
56
from ._auth import Authentication
7+
from ._entity_type import EntityType
68
from ._port_list import PortList, PortRangeType
79
from ._resource_names import ResourceNames, ResourceType
810
from ._version import Version
911

1012
__all__ = (
13+
"AggregateStatistic",
14+
"Aggregates",
1115
"Authentication",
1216
"PortList",
1317
"PortRangeType",
1418
"Version",
1519
"ResourceNames",
1620
"ResourceType",
21+
"SortOrder",
22+
"EntityType",
1723
)
Lines changed: 199 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,199 @@
1+
# SPDX-FileCopyrightText: 2024 Greenbone AG
2+
#
3+
# SPDX-License-Identifier: GPL-3.0-or-later
4+
5+
from typing import Iterable, Optional, Union
6+
7+
from gvm._enum import Enum
8+
from gvm.errors import InvalidArgumentType, RequiredArgument
9+
from gvm.protocols.core import Request
10+
from gvm.xml import XmlCommand
11+
12+
from ._entity_type import EntityType
13+
14+
15+
class AggregateStatistic(Enum):
16+
"""Enum for aggregate statistic types"""
17+
18+
COUNT = "count" # Number of items
19+
C_COUNT = "c_count" # Cumulative number of items
20+
C_SUM = "c_sum" # Cumulative sum of values
21+
MAX = "max" # Maximum value
22+
MEAN = "mean" # Arithmetic mean of values
23+
MIN = "min" # Minimum value
24+
SUM = "sum" # Sum of values
25+
TEXT = "text" # Text column value
26+
VALUE = "value" # Group or subgroup column value
27+
28+
29+
class SortOrder(Enum):
30+
"""Enum for sort order"""
31+
32+
ASCENDING = "ascending"
33+
DESCENDING = "descending"
34+
35+
36+
class Aggregates:
37+
@classmethod
38+
def get_aggregates(
39+
cls,
40+
resource_type: Union[EntityType, str],
41+
*,
42+
filter_string: Optional[str] = None,
43+
filter_id: Optional[str] = None,
44+
sort_criteria: Optional[
45+
Iterable[dict[str, Union[str, SortOrder, AggregateStatistic]]]
46+
] = None,
47+
data_columns: Optional[Union[Iterable[str], str]] = None,
48+
group_column: Optional[str] = None,
49+
subgroup_column: Optional[str] = None,
50+
text_columns: Optional[Union[Iterable[str], str]] = None,
51+
first_group: Optional[int] = None,
52+
max_groups: Optional[int] = None,
53+
mode: Optional[int] = None,
54+
**kwargs,
55+
) -> Request:
56+
"""Request aggregated information on a resource / entity type
57+
58+
Additional arguments can be set via the kwargs parameter for backward
59+
compatibility with older versions of python-gvm, but are not validated.
60+
61+
Args:
62+
resource_type: The entity type to gather data from
63+
filter_string: Filter term to use for the query
64+
filter_id: UUID of an existing filter to use for the query
65+
sort_criteria: List of sort criteria (dicts that can contain
66+
a field, stat and order)
67+
data_columns: List of fields to aggregate data from
68+
group_column: The field to group the entities by
69+
subgroup_column: The field to further group the entities
70+
inside groups by
71+
text_columns: List of simple text columns which no statistics
72+
are calculated for
73+
first_group: The index of the first aggregate group to return
74+
max_groups: The maximum number of aggregate groups to return,
75+
-1 for all
76+
mode: Special mode for aggregation
77+
"""
78+
if not resource_type:
79+
raise RequiredArgument(
80+
function=cls.get_aggregates.__name__, argument="resource_type"
81+
)
82+
83+
if not isinstance(resource_type, EntityType):
84+
resource_type = EntityType(resource_type)
85+
86+
cmd = XmlCommand("get_aggregates")
87+
88+
_actual_resource_type = resource_type
89+
if resource_type.value == EntityType.AUDIT.value:
90+
_actual_resource_type = EntityType.TASK
91+
cmd.set_attribute("usage_type", "audit")
92+
elif resource_type.value == EntityType.POLICY.value:
93+
_actual_resource_type = EntityType.SCAN_CONFIG
94+
cmd.set_attribute("usage_type", "policy")
95+
elif resource_type.value == EntityType.SCAN_CONFIG.value:
96+
cmd.set_attribute("usage_type", "scan")
97+
elif resource_type.value == EntityType.TASK.value:
98+
cmd.set_attribute("usage_type", "scan")
99+
100+
cmd.set_attribute("type", _actual_resource_type.value)
101+
102+
cmd.add_filter(filter_string, filter_id)
103+
104+
if first_group is not None:
105+
try:
106+
cmd.set_attribute("first_group", str(int(first_group)))
107+
except ValueError:
108+
raise InvalidArgumentType(
109+
argument="first_group",
110+
function=cls.get_aggregates.__name__,
111+
) from None
112+
113+
if max_groups is not None:
114+
try:
115+
cmd.set_attribute("max_groups", str(int(max_groups)))
116+
except ValueError:
117+
raise InvalidArgumentType(
118+
argument="first_group",
119+
function=cls.get_aggregates.__name__,
120+
)
121+
122+
if sort_criteria is not None:
123+
if not isinstance(sort_criteria, Iterable) or isinstance(
124+
sort_criteria, str
125+
):
126+
raise InvalidArgumentType(
127+
argument="sort_criteria",
128+
function=cls.get_aggregates.__name__,
129+
arg_type=Iterable.__name__,
130+
)
131+
for sort in sort_criteria:
132+
if not isinstance(sort, dict):
133+
raise InvalidArgumentType(
134+
argument="sort_criteria",
135+
function=cls.get_aggregates.__name__,
136+
)
137+
138+
sort_elem = cmd.add_element("sort")
139+
if sort.get("field"):
140+
sort_elem.set_attribute("field", sort.get("field")) # type: ignore
141+
142+
if sort.get("stat"):
143+
if isinstance(sort["stat"], AggregateStatistic):
144+
sort_elem.set_attribute("stat", sort["stat"].value)
145+
else:
146+
stat = AggregateStatistic(sort["stat"])
147+
sort_elem.set_attribute("stat", stat.value)
148+
149+
if sort.get("order"):
150+
if isinstance(sort["order"], SortOrder):
151+
sort_elem.set_attribute("order", sort["order"].value)
152+
else:
153+
so = SortOrder(sort["order"])
154+
sort_elem.set_attribute("order", so.value)
155+
156+
if data_columns is not None:
157+
if isinstance(data_columns, str):
158+
data_columns = [data_columns]
159+
elif not isinstance(data_columns, Iterable):
160+
raise InvalidArgumentType(
161+
function=cls.get_aggregates.__name__,
162+
argument="data_columns",
163+
arg_type=Iterable.__name__,
164+
)
165+
for column in data_columns:
166+
cmd.add_element("data_column", column)
167+
168+
if group_column is not None:
169+
cmd.set_attribute("group_column", group_column)
170+
171+
if subgroup_column is not None:
172+
if not group_column:
173+
raise RequiredArgument(
174+
f"{cls.get_aggregates.__name__} requires a group_column"
175+
" argument if subgroup_column is given",
176+
function=cls.get_aggregates.__name__,
177+
argument="subgroup_column",
178+
)
179+
cmd.set_attribute("subgroup_column", subgroup_column)
180+
181+
if text_columns is not None:
182+
if isinstance(text_columns, str):
183+
text_columns = [text_columns]
184+
elif not isinstance(text_columns, Iterable):
185+
raise InvalidArgumentType(
186+
function=cls.get_aggregates.__name__,
187+
argument="text_columns",
188+
arg_type=Iterable.__name__,
189+
)
190+
for column in text_columns:
191+
cmd.add_element("text_column", column)
192+
193+
if mode is not None:
194+
cmd.set_attribute("mode", str(mode))
195+
196+
# Add additional keyword args as attributes for backward compatibility.
197+
cmd.set_attributes(kwargs)
198+
199+
return cmd
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
# SPDX-FileCopyrightText: 2024 Greenbone AG
2+
#
3+
# SPDX-License-Identifier: GPL-3.0-or-later
4+
5+
from typing import Optional
6+
7+
from gvm._enum import Enum
8+
9+
10+
class EntityType(Enum):
11+
"""Enum for entity types"""
12+
13+
ALERT = "alert"
14+
ASSET = "asset"
15+
AUDIT = "audit"
16+
CERT_BUND_ADV = "cert_bund_adv"
17+
CPE = "cpe"
18+
CREDENTIAL = "credential"
19+
CVE = "cve"
20+
DFN_CERT_ADV = "dfn_cert_adv"
21+
FILTER = "filter"
22+
GROUP = "group"
23+
HOST = "host"
24+
INFO = "info"
25+
NOTE = "note"
26+
NVT = "nvt"
27+
OPERATING_SYSTEM = "os"
28+
OVALDEF = "ovaldef"
29+
OVERRIDE = "override"
30+
PERMISSION = "permission"
31+
POLICY = "policy"
32+
PORT_LIST = "port_list"
33+
REPORT = "report"
34+
REPORT_FORMAT = "report_format"
35+
RESULT = "result"
36+
ROLE = "role"
37+
SCAN_CONFIG = "config"
38+
SCANNER = "scanner"
39+
SCHEDULE = "schedule"
40+
TAG = "tag"
41+
TARGET = "target"
42+
TASK = "task"
43+
TICKET = "ticket"
44+
TLS_CERTIFICATE = "tls_certificate"
45+
USER = "user"
46+
VULNERABILITY = "vuln"
47+
48+
@classmethod
49+
def from_string(
50+
cls,
51+
entity_type: Optional[str],
52+
) -> Optional["EntityType"]:
53+
"""Convert a entity type string to an actual EntityType instance
54+
55+
Arguments:
56+
entity_type: Entity type string to convert to a EntityType
57+
"""
58+
if entity_type == "vuln":
59+
return cls.VULNERABILITY
60+
61+
if entity_type == "os":
62+
return cls.OPERATING_SYSTEM
63+
64+
if entity_type == "config":
65+
return cls.SCAN_CONFIG
66+
67+
return super().from_string(entity_type)

0 commit comments

Comments
 (0)