Skip to content

Commit 67ff32e

Browse files
committed
revert API use
1 parent 60500ae commit 67ff32e

2 files changed

Lines changed: 79 additions & 235 deletions

File tree

Lines changed: 36 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -1,70 +1,55 @@
1-
from http.cookiejar import DefaultCookiePolicy
1+
import random
2+
import uuid
3+
from datetime import datetime, timezone
24
from typing import Optional
35

4-
from requests import Session
5-
from requests.adapters import HTTPAdapter
6-
from requests.packages.urllib3.util import Retry
76
from src.models.authentication import Authentication
87
from src.models.incident import XSOARSearchIncidentsResponse
98

10-
REQUESTS_TIMEOUT_SECONDS = 60
11-
129

1310
class PaloAltoCortexXSOARClientAPI:
1411
def __init__(self, auth: Authentication, api_url: str) -> None:
1512
self._auth = auth
1613
self.api_url = api_url
1714

18-
self.session = self._prepare_session()
19-
20-
def _build_url(self, path: str) -> str:
21-
"""Build a full URL from the configured api_url and a path."""
22-
return f"{self.api_url.rstrip('/')}{path}"
23-
24-
def _prepare_session(self) -> Session:
25-
"""Preparing a session with automatic retries (with increasing backoff) and no cookies"""
26-
retries = Retry(
27-
total=5,
28-
allowed_methods=["POST"],
29-
status_forcelist=[429, 500, 502, 503, 504],
30-
backoff_factor=0.5,
31-
backoff_jitter=0.2,
32-
)
33-
s = Session()
34-
s.mount("http://", HTTPAdapter(max_retries=retries))
35-
s.mount("https://", HTTPAdapter(max_retries=retries))
36-
s.cookies.set_policy(DefaultCookiePolicy(allowed_domains=[]))
37-
return s
38-
3915
def search_incidents(
4016
self,
4117
from_date: Optional[str] = None,
4218
to_date: Optional[str] = None,
4319
search_from: int = 0,
4420
search_to: int = 100,
4521
) -> XSOARSearchIncidentsResponse:
46-
url = self._build_url("/xsoar/public/v1/incidents/search")
47-
headers = self._auth.get_headers()
48-
49-
size = search_to - search_from
50-
page = search_from // size if size > 0 else 0
51-
52-
body = {
53-
"filter": {
54-
"page": page,
55-
"size": size,
56-
"sort": [{"field": "created", "asc": True}],
57-
}
58-
}
59-
60-
if from_date:
61-
body["filter"]["fromDate"] = from_date
62-
63-
if to_date:
64-
body["filter"]["toDate"] = to_date
65-
66-
response = self.session.post(
67-
url, headers=headers, json=body, timeout=REQUESTS_TIMEOUT_SECONDS
22+
_ = (from_date, to_date, search_from, search_to)
23+
24+
incident_count = random.randint(1, 3)
25+
detection_timestamp = int(datetime.now(timezone.utc).timestamp() * 1000)
26+
27+
data = []
28+
for _ in range(incident_count):
29+
data.append(
30+
{
31+
"id": str(uuid.uuid4()),
32+
"name": "Dummy XSOAR Incident",
33+
"CustomFields": {
34+
"xdralerts": [
35+
{
36+
"alert_id": str(uuid.uuid4()),
37+
"case_id": random.randint(1, 1000),
38+
"action_pretty": random.choice(
39+
["Detected (Reported)", "Prevented (Blocked)"]
40+
),
41+
"actor_process_command_line": (
42+
f"oaev-implant-{uuid.uuid4()}-agent-{uuid.uuid4()}"
43+
),
44+
"actor_process_image_name": "oaev-implant.exe",
45+
"actor_process_image_path": "C:/Dummy/oaev-implant.exe",
46+
"detection_timestamp": detection_timestamp,
47+
}
48+
]
49+
},
50+
}
51+
)
52+
53+
return XSOARSearchIncidentsResponse.model_validate(
54+
{"total": len(data), "data": data}
6855
)
69-
response.raise_for_status()
70-
return XSOARSearchIncidentsResponse.model_validate(response.json())

palo-alto-cortex-xsoar/tests/test_client_api.py

Lines changed: 43 additions & 184 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,4 @@
1-
from unittest.mock import patch
2-
31
import pytest
4-
import requests
52
from src.models.authentication import Authentication
63
from src.models.incident import XSOARSearchIncidentsResponse
74
from src.services.client_api import PaloAltoCortexXSOARClientAPI
@@ -17,187 +14,49 @@ def api_client(auth):
1714
return PaloAltoCortexXSOARClientAPI(auth=auth, api_url="https://test.xsoar.com")
1815

1916

20-
def test_search_incidents_success(api_client):
21-
mock_response = {
22-
"total": 1,
23-
"data": [
24-
{
25-
"id": "1",
26-
"name": "Test Incident",
27-
"CustomFields": {
28-
"xdralerts": [
29-
{"alert_id": "alert1", "detection_timestamp": 1600000000000}
30-
]
31-
},
32-
}
33-
],
34-
}
35-
36-
with patch("requests.Session.post") as mock_post:
37-
mock_post.return_value.json.return_value = mock_response
38-
mock_post.return_value.status_code = 200
39-
40-
response = api_client.search_incidents(
41-
from_date="2023-01-01T00:00:00Z",
42-
to_date="2023-01-01T23:59:59Z",
43-
search_from=0,
44-
search_to=10,
45-
)
46-
47-
assert isinstance(response, XSOARSearchIncidentsResponse)
48-
assert response.total == 1
49-
assert len(response.data) == 1
50-
assert response.data[0].id == "1"
51-
assert response.data[0].custom_fields.xdralerts[0].alert_id == "alert1"
52-
53-
mock_post.assert_called_once()
54-
args, kwargs = mock_post.call_args
55-
assert args[0] == "https://test.xsoar.com/xsoar/public/v1/incidents/search"
56-
assert kwargs["json"]["filter"]["size"] == 10
57-
assert kwargs["json"]["filter"]["page"] == 0
58-
assert kwargs["json"]["filter"]["fromDate"] == "2023-01-01T00:00:00Z"
59-
assert kwargs["json"]["filter"]["toDate"] == "2023-01-01T23:59:59Z"
60-
61-
62-
def test_search_incidents_http_error(api_client):
63-
with patch("requests.Session.post") as mock_post:
64-
mock_post.return_value.raise_for_status.side_effect = (
65-
requests.exceptions.HTTPError("Error")
66-
)
67-
68-
with pytest.raises(requests.exceptions.HTTPError):
69-
api_client.search_incidents()
70-
71-
72-
def test_search_incidents_pagination(api_client):
73-
with patch("requests.Session.post") as mock_post:
74-
mock_post.return_value.json.return_value = {"total": 0, "data": []}
75-
76-
api_client.search_incidents(search_from=20, search_to=30)
77-
78-
_, kwargs = mock_post.call_args
79-
assert kwargs["json"]["filter"]["size"] == 10
80-
assert kwargs["json"]["filter"]["page"] == 2
81-
82-
83-
# --- New tests ---
84-
85-
86-
def test_search_incidents_no_dates(api_client):
87-
"""When no dates are provided, fromDate and toDate are absent."""
88-
with patch("requests.Session.post") as mock_post:
89-
mock_post.return_value.json.return_value = {"total": 0, "data": []}
90-
api_client.search_incidents()
91-
_, kwargs = mock_post.call_args
92-
assert "fromDate" not in kwargs["json"]["filter"]
93-
assert "toDate" not in kwargs["json"]["filter"]
94-
95-
96-
def test_search_incidents_only_from_date(api_client):
97-
"""When only from_date is provided, toDate is absent."""
98-
with patch("requests.Session.post") as mock_post:
99-
mock_post.return_value.json.return_value = {"total": 0, "data": []}
100-
api_client.search_incidents(from_date="2026-01-01T00:00:00Z")
101-
_, kwargs = mock_post.call_args
102-
assert kwargs["json"]["filter"]["fromDate"] == "2026-01-01T00:00:00Z"
103-
assert "toDate" not in kwargs["json"]["filter"]
104-
105-
106-
def test_search_incidents_only_to_date(api_client):
107-
"""When only to_date is provided, fromDate is absent."""
108-
with patch("requests.Session.post") as mock_post:
109-
mock_post.return_value.json.return_value = {"total": 0, "data": []}
110-
api_client.search_incidents(to_date="2026-12-31T23:59:59Z")
111-
_, kwargs = mock_post.call_args
112-
assert "fromDate" not in kwargs["json"]["filter"]
113-
assert kwargs["json"]["filter"]["toDate"] == "2026-12-31T23:59:59Z"
114-
115-
116-
def test_search_incidents_zero_size(api_client):
117-
"""When search_from == search_to, size is 0 and page is 0."""
118-
with patch("requests.Session.post") as mock_post:
119-
mock_post.return_value.json.return_value = {"total": 0, "data": []}
120-
api_client.search_incidents(search_from=5, search_to=5)
121-
_, kwargs = mock_post.call_args
122-
assert kwargs["json"]["filter"]["size"] == 0
123-
assert kwargs["json"]["filter"]["page"] == 0
124-
125-
126-
def test_search_incidents_default_page_size(api_client):
127-
"""Default search_from=0, search_to=100 gives size=100, page=0."""
128-
with patch("requests.Session.post") as mock_post:
129-
mock_post.return_value.json.return_value = {"total": 0, "data": []}
130-
api_client.search_incidents()
131-
_, kwargs = mock_post.call_args
132-
assert kwargs["json"]["filter"]["size"] == 100
133-
assert kwargs["json"]["filter"]["page"] == 0
134-
135-
136-
def test_search_incidents_headers_sent(api_client, auth):
137-
"""Auth headers are included in the request."""
138-
expected_headers = auth.get_headers()
139-
with patch("requests.Session.post") as mock_post:
140-
mock_post.return_value.json.return_value = {"total": 0, "data": []}
141-
api_client.search_incidents()
142-
_, kwargs = mock_post.call_args
143-
assert kwargs["headers"] == expected_headers
144-
145-
146-
def test_search_incidents_sort_order(api_client):
147-
"""Request body always includes sort by 'created' ascending."""
148-
with patch("requests.Session.post") as mock_post:
149-
mock_post.return_value.json.return_value = {"total": 0, "data": []}
150-
api_client.search_incidents()
151-
_, kwargs = mock_post.call_args
152-
assert kwargs["json"]["filter"]["sort"] == [{"field": "created", "asc": True}]
153-
154-
155-
def test_search_incidents_connection_error(api_client):
156-
"""Connection errors propagate."""
157-
with patch(
158-
"requests.Session.post",
159-
side_effect=requests.exceptions.ConnectionError("no route"),
160-
):
161-
with pytest.raises(requests.exceptions.ConnectionError):
162-
api_client.search_incidents()
163-
164-
165-
def test_search_incidents_timeout(api_client):
166-
"""Timeout errors propagate."""
167-
with patch(
168-
"requests.Session.post", side_effect=requests.exceptions.Timeout("timed out")
169-
):
170-
with pytest.raises(requests.exceptions.Timeout):
171-
api_client.search_incidents()
172-
173-
174-
def test_search_incidents_multiple_incidents(api_client):
175-
"""Response with multiple incidents is parsed correctly."""
176-
mock_response = {
177-
"total": 2,
178-
"data": [
179-
{
180-
"id": "1",
181-
"name": "Inc 1",
182-
"CustomFields": {
183-
"xdralerts": [{"alert_id": "a1", "detection_timestamp": 1000}]
184-
},
185-
},
186-
{
187-
"id": "2",
188-
"name": "Inc 2",
189-
"CustomFields": {
190-
"xdralerts": [{"alert_id": "a2", "detection_timestamp": 2000}]
191-
},
192-
},
193-
],
194-
}
195-
with patch("requests.Session.post") as mock_post:
196-
mock_post.return_value.json.return_value = mock_response
197-
response = api_client.search_incidents()
198-
assert response.total == 2
199-
assert len(response.data) == 2
200-
assert response.data[1].custom_fields.xdralerts[0].alert_id == "a2"
17+
def test_search_incidents_returns_valid_dummy_response(api_client):
18+
response = api_client.search_incidents()
19+
20+
assert isinstance(response, XSOARSearchIncidentsResponse)
21+
assert response.total == len(response.data)
22+
assert response.total >= 1
23+
24+
for incident in response.data:
25+
assert incident.id
26+
assert incident.custom_fields is not None
27+
assert len(incident.custom_fields.xdralerts) == 1
28+
alert = incident.custom_fields.xdralerts[0]
29+
assert alert.alert_id
30+
assert isinstance(alert.detection_timestamp, int)
31+
32+
33+
def test_search_incidents_accepts_filters_without_external_calls(api_client):
34+
response = api_client.search_incidents(
35+
from_date="2026-01-01T00:00:00Z",
36+
to_date="2026-01-01T23:59:59Z",
37+
search_from=10,
38+
search_to=20,
39+
)
40+
41+
assert isinstance(response, XSOARSearchIncidentsResponse)
42+
assert response.total == len(response.data)
43+
44+
45+
def test_search_incidents_can_be_forced_to_multiple_items(api_client, monkeypatch):
46+
monkeypatch.setattr("src.services.client_api.random.randint", lambda a, b: 2)
47+
response = api_client.search_incidents()
48+
assert response.total == 2
49+
assert len(response.data) == 2
50+
51+
52+
def test_search_incidents_generated_ids_are_unique(api_client):
53+
response = api_client.search_incidents()
54+
incident_ids = [incident.id for incident in response.data]
55+
alert_ids = [
56+
incident.custom_fields.xdralerts[0].alert_id for incident in response.data
57+
]
58+
assert len(set(incident_ids)) == len(incident_ids)
59+
assert len(set(alert_ids)) == len(alert_ids)
20160

20261

20362
def test_api_client_stores_api_url():

0 commit comments

Comments
 (0)