Skip to content

Commit 3d08fa0

Browse files
authored
Merge pull request #469 from TheHive-Project/444-review-procedure-endpoints
#444 - Review procedure endpoints
2 parents e2a980e + 264028c commit 3d08fa0

File tree

3 files changed

+217
-33
lines changed

3 files changed

+217
-33
lines changed

tests/test_procedure_endpoint.py

Lines changed: 83 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,16 @@
11
import pytest
2+
23
from thehive4py.client import TheHiveApi
34
from thehive4py.errors import TheHiveError
45
from thehive4py.helpers import now_to_ts
6+
from thehive4py.query.filters import In
57
from thehive4py.types.alert import OutputAlert
68
from thehive4py.types.case import OutputCase
7-
from thehive4py.types.procedure import InputUpdateProcedure, OutputProcedure
9+
from thehive4py.types.procedure import (
10+
InputProcedure,
11+
InputUpdateProcedure,
12+
OutputProcedure,
13+
)
814

915

1016
class TestProcedureEndpoint:
@@ -24,6 +30,42 @@ def test_create_in_alert_and_get(
2430
fetched_procedure = thehive.procedure.get(procedure_id=created_procedure["_id"])
2531
assert created_procedure == fetched_procedure
2632

33+
def test_bulk_create_in_alert_and_find(
34+
self, thehive: TheHiveApi, test_alert: OutputAlert
35+
):
36+
37+
procedures: list[InputProcedure] = [
38+
{
39+
"occurDate": now_to_ts(),
40+
"patternId": "T1059.006",
41+
"tactic": "execution",
42+
"description": "...",
43+
},
44+
{
45+
"occurDate": now_to_ts(),
46+
"patternId": "T1059.007",
47+
"tactic": "execution",
48+
"description": "...",
49+
},
50+
]
51+
created_procedures = thehive.procedure.bulk_create_in_alert(
52+
alert_id=test_alert["_id"], procedures=procedures
53+
)
54+
55+
fetched_procedures = thehive.procedure.find(
56+
filters=In(
57+
field="_id",
58+
values=[procedure["_id"] for procedure in created_procedures],
59+
)
60+
)
61+
assert sorted(
62+
created_procedures,
63+
key=lambda procedure: procedure["_id"],
64+
) == sorted(
65+
fetched_procedures,
66+
key=lambda procedure: procedure["_id"],
67+
)
68+
2769
def test_create_in_case_and_get(self, thehive: TheHiveApi, test_case: OutputCase):
2870
created_procedure = thehive.procedure.create_in_case(
2971
case_id=test_case["_id"],
@@ -38,6 +80,40 @@ def test_create_in_case_and_get(self, thehive: TheHiveApi, test_case: OutputCase
3880
fetched_procedure = thehive.procedure.get(procedure_id=created_procedure["_id"])
3981
assert created_procedure == fetched_procedure
4082

83+
def test_bulk_create_in_case_and_find(
84+
self, thehive: TheHiveApi, test_case: OutputCase
85+
):
86+
procedures: list[InputProcedure] = [
87+
{
88+
"occurDate": now_to_ts(),
89+
"patternId": "T1059.006",
90+
"tactic": "execution",
91+
"description": "...",
92+
},
93+
{
94+
"occurDate": now_to_ts(),
95+
"patternId": "T1059.007",
96+
"tactic": "execution",
97+
"description": "...",
98+
},
99+
]
100+
created_procedures = thehive.procedure.bulk_create_in_case(
101+
case_id=test_case["_id"], procedures=procedures
102+
)
103+
fetched_procedures = thehive.procedure.find(
104+
filters=In(
105+
field="_id",
106+
values=[procedure["_id"] for procedure in created_procedures],
107+
)
108+
)
109+
assert sorted(
110+
created_procedures,
111+
key=lambda procedure: procedure["_id"],
112+
) == sorted(
113+
fetched_procedures,
114+
key=lambda procedure: procedure["_id"],
115+
)
116+
41117
def test_delete(self, thehive: TheHiveApi, test_procedure: OutputProcedure):
42118
procedure_id = test_procedure["_id"]
43119
thehive.procedure.delete(procedure_id=procedure_id)
@@ -57,3 +133,9 @@ def test_update(self, thehive: TheHiveApi, test_procedure: OutputProcedure):
57133

58134
for key, value in update_fields.items():
59135
assert updated_procedure.get(key) == value
136+
137+
def test_bulk_delete(self, thehive: TheHiveApi, test_procedure: OutputProcedure):
138+
procedure_id = test_procedure["_id"]
139+
thehive.procedure.bulk_delete(procedure_ids=[procedure_id])
140+
with pytest.raises(TheHiveError):
141+
thehive.procedure.get(procedure_id=procedure_id)

thehive4py/endpoints/procedure.py

Lines changed: 119 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -14,48 +14,132 @@
1414

1515

1616
class ProcedureEndpoint(EndpointBase):
17-
def create_in_alert(
18-
self, alert_id: str, procedure: InputProcedure
17+
def create_in_case(
18+
self, case_id: str, procedure: InputProcedure
1919
) -> OutputProcedure:
20+
"""Create a procedure in a case.
21+
22+
Args:
23+
case_id: The id of the case.
24+
procedure: The fields of the procedure to create.
25+
26+
Returns:
27+
The created procedure.
28+
"""
2029
return self._session.make_request(
21-
"POST", path=f"/api/v1/alert/{alert_id}/procedure", json=procedure
30+
"POST", path=f"/api/v1/case/{case_id}/procedure", json=procedure
2231
)
2332

24-
def create_in_case(
25-
self, case_id: str, procedure: InputProcedure
33+
def bulk_create_in_case(
34+
self, case_id: str, procedures: List[InputProcedure]
35+
) -> List[OutputProcedure]:
36+
"""Create several procedures in a case.
37+
38+
Args:
39+
case_id: The id of the case.
40+
procedures: The list of procedures to create.
41+
42+
Returns:
43+
The list of created procedures.
44+
"""
45+
return self._session.make_request(
46+
"POST",
47+
path=f"/api/v1/case/{case_id}/procedures",
48+
json={"procedures": procedures},
49+
)
50+
51+
def create_in_alert(
52+
self, alert_id: str, procedure: InputProcedure
2653
) -> OutputProcedure:
54+
"""Create a procedure in an alert.
55+
56+
Args:
57+
alert_id: The id of the alert.
58+
procedure: The fields of the procedure to create.
59+
60+
Returns:
61+
The created procedure.
62+
"""
2763
return self._session.make_request(
28-
"POST", path=f"/api/v1/case/{case_id}/procedure", json=procedure
64+
"POST", path=f"/api/v1/alert/{alert_id}/procedure", json=procedure
2965
)
3066

31-
def get(self, procedure_id: str) -> OutputProcedure:
32-
# TODO: temp implementation until a dedicated get endpoint
33-
procedures = self._session.make_request(
67+
def bulk_create_in_alert(
68+
self, alert_id: str, procedures: List[InputProcedure]
69+
) -> List[OutputProcedure]:
70+
"""Create multiple procedures in an alert.
71+
72+
Args:
73+
alert_id: The id of the alert.
74+
procedures: The list of procedures to create.
75+
76+
Returns:
77+
The list of created procedures.
78+
"""
79+
return self._session.make_request(
3480
"POST",
35-
path="/api/v1/query",
36-
json={"query": [{"_name": "getProcedure", "idOrName": procedure_id}]},
81+
path=f"/api/v1/alert/{alert_id}/procedures",
82+
json={"procedures": procedures},
3783
)
38-
try:
39-
return procedures[0]
40-
except IndexError:
41-
raise TheHiveError("404 - Procedure not found")
4284

4385
def delete(self, procedure_id: str) -> None:
86+
"""Delete a procedure.
87+
88+
Args:
89+
procedure_id: The id of the procedure.
90+
91+
Returns:
92+
N/A
93+
"""
4494
return self._session.make_request(
4595
"DELETE", path=f"/api/v1/procedure/{procedure_id}"
4696
)
4797

4898
def update(self, procedure_id: str, fields: InputUpdateProcedure) -> None:
99+
"""Update a procedure.
100+
101+
Args:
102+
procedure_id: The id of the procedure.
103+
fields: The fields of the procedure to update.
104+
105+
Returns:
106+
N/A
107+
"""
49108
return self._session.make_request(
50109
"PATCH", path=f"/api/v1/procedure/{procedure_id}", json=fields
51110
)
52111

112+
def bulk_delete(self, procedure_ids: List[str]) -> None:
113+
"""Delete multiple procedures.
114+
115+
Args:
116+
procedure_ids: The list of procedure ids to delete.
117+
118+
Returns:
119+
N/A
120+
"""
121+
return self._session.make_request(
122+
"POST",
123+
path="/api/v1/procedure/delete/_bulk",
124+
json={"ids": procedure_ids},
125+
)
126+
53127
def find(
54128
self,
55129
filters: Optional[FilterExpr] = None,
56130
sortby: Optional[SortExpr] = None,
57131
paginate: Optional[Paginate] = None,
58132
) -> List[OutputProcedure]:
133+
"""Find multiple procedures.
134+
135+
Args:
136+
filters: The filter expressions to apply in the query.
137+
sortby: The sort expressions to apply in the query.
138+
paginate: The pagination expression to apply in the query.
139+
140+
Returns:
141+
The list of procedures matched by the query or an empty list.
142+
"""
59143
query: QueryExpr = [
60144
{"_name": "listProcedure"},
61145
*self._build_subquery(filters=filters, sortby=sortby, paginate=paginate),
@@ -67,3 +151,23 @@ def find(
67151
params={"name": "procedures"},
68152
json={"query": query},
69153
)
154+
155+
def get(self, procedure_id: str) -> OutputProcedure:
156+
"""Get a procedure by id.
157+
158+
Args:
159+
procedure_id: The id of the procedure.
160+
161+
Returns:
162+
The procedure specified by the id.
163+
"""
164+
# TODO: temp implementation until a dedicated get endpoint
165+
procedures = self._session.make_request(
166+
"POST",
167+
path="/api/v1/query",
168+
json={"query": [{"_name": "getProcedure", "idOrName": procedure_id}]},
169+
)
170+
try:
171+
return procedures[0]
172+
except IndexError:
173+
raise TheHiveError("404 - Procedure not found")

thehive4py/types/procedure.py

Lines changed: 15 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,32 @@
11
from typing import TypedDict
22

3+
from typing_extensions import NotRequired
34

4-
class InputProcedureRequired(TypedDict):
5+
6+
class InputProcedure(TypedDict):
57
occurDate: int
68
patternId: str
9+
tactic: NotRequired[str]
10+
description: NotRequired[str]
711

812

9-
class InputProcedure(InputProcedureRequired, total=False):
10-
tactic: str
11-
description: str
12-
13-
14-
class OutputProcedureRequired(TypedDict):
13+
class OutputProcedure(TypedDict):
1514
_id: str
1615
_createdAt: int
1716
_createdBy: str
17+
_updatedAt: NotRequired[int]
18+
_updatedBy: NotRequired[str]
19+
description: NotRequired[str]
1820
occurDate: int
19-
tactic: str
20-
tacticLabel: str
21+
patternId: NotRequired[str]
22+
patternName: NotRequired[str]
23+
tactic: NotRequired[str]
24+
tacticLabel: NotRequired[str]
2125
extraData: dict
2226

2327

24-
class OutputProcedure(OutputProcedureRequired, total=False):
25-
_updatedAt: int
26-
_updatedBy: str
27-
description: str
28-
patternId: str
29-
patternName: str
30-
31-
3228
class InputUpdateProcedure(TypedDict, total=False):
3329
description: str
3430
occurDate: int
31+
patternId: str
32+
tactic: str

0 commit comments

Comments
 (0)