Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ def get_access_token(self) -> str:
raise DemistoException(response.get("error"))
access_token = response["access_token"]
new_ctx = {
"refresh_token": ctx.get("refresh_token") or response.get("refresh_token"),
"refresh_token": response.get("refresh_token") or ctx.get("refresh_token"),
"access_token": access_token,
"expire_date": (now + timedelta(seconds=int(response.get("expires_in", 0)) - 60)).isoformat(),
}
Expand Down Expand Up @@ -150,7 +150,7 @@ def search_events(self, start_time: str, end_time: str, limit: int) -> List[Dict
}

params = {"startTime": start_time, "endTime": end_time, "pageLimit": PAGE_LIMIT_DEFAULT}
demisto.debug(f"Time intervarl: {start_time} to {end_time}.")
demisto.debug(f"Time interval: {start_time} to {end_time}.")

while True:
params["page"] = str(page)
Expand All @@ -166,7 +166,7 @@ def search_events(self, start_time: str, end_time: str, limit: int) -> List[Dict

events_page = response.get("messageResponse", [])
status = response.get("status")
demisto.debug(f"Successfully fetched {events_page} on page {page}")
demisto.debug(f"Successfully fetched {len(events_page)} events on page {page}")
if status != "success":
demisto.debug(f"API returned status='{status}', stopping pagination.")
break
Expand Down Expand Up @@ -214,8 +214,8 @@ def get_events(client: Client, args: dict) -> CommandResults:

now_ts = int(time.time() * 1000)

start_date = date_to_timestamp(start_date_str, DATE_FORMAT) if start_date_str else now_ts
end_date = date_to_timestamp(end_date_str, DATE_FORMAT) if end_date_str else (now_ts - 60 * 1000)
start_date = date_to_timestamp(start_date_str, DATE_FORMAT) if start_date_str else (now_ts - 60 * 1000)
end_date = date_to_timestamp(end_date_str, DATE_FORMAT) if end_date_str else now_ts

events = client.search_events(str(start_date), str(end_date), limit)
add_time_to_events(events)
Expand Down Expand Up @@ -311,7 +311,7 @@ def main() -> None: # pragma: no cover
)
if command == "test-module":
raise Exception("Please use !manage-engine-test instead")
if command == "manage-engine-test":
elif command == "manage-engine-test":
return_results(test_module(client))
elif command == "manage-engine-get-events":
return_results(get_events(client, demisto.args()))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ script:
description: Gets events from ManageEngine.
execution: false
name: manage-engine-get-events
dockerimage: demisto/python3:3.12.12.5490952
dockerimage: demisto/python3:3.12.13.7444307
isfetchevents: true
runonce: false
script: '-'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,41 @@ def test_get_access_token_code(client: Client, mocker):
assert stored["refresh_token"] == "code_refresh"


def test_get_access_token_refresh_updates_rotating_token(client: Client, mocker):
"""
Given:
- An integration context with an expired access token and an old refresh token ("old_rtoken").
- The Zoho OAuth server returns a NEW refresh token ("new_rtoken") alongside the new access token
(Zoho uses rotating refresh tokens — each refresh invalidates the previous token).
When:
- Calling `get_access_token()`.
Then:
- The NEW refresh token from the API response ("new_rtoken") is stored in the integration context,
NOT the old one from context ("old_rtoken").
- Failing to update would cause the integration to stop ingesting events after Zoho invalidates
the old token (typically within a few days).
"""
past = "2000-01-01T00:00:00"
ctx = {"access_token": "old_token", "refresh_token": "old_rtoken", "expire_date": past}
mocker.patch("ManageEngineEventCollector.demisto.getIntegrationContext", return_value=ctx)
stored = {}
mocker.patch("ManageEngineEventCollector.demisto.setIntegrationContext", side_effect=lambda x: stored.update(x))

mocker.patch.object(
client,
"_http_request",
return_value={"access_token": "new_token", "refresh_token": "new_rtoken", "expires_in": "3600"},
)

token = client.get_access_token()

assert token == "new_token"
# The NEW refresh token from the response must be stored, not the old one from context
assert (
stored["refresh_token"] == "new_rtoken"
), "Rotating refresh token from API response must overwrite the old token in context"


# ─────── Tests for test_module ────────────────────────────────────────────────


Expand Down Expand Up @@ -204,6 +239,41 @@ def test_get_events_no_push(client: Client, mocker):
assert results.readable_output == result_markdown


def test_get_events_default_dates_start_before_end(mocker):
"""
Given:
- No start_date or end_date are provided in args.
When:
- Calling `get_events()`.
Then:
- The start_date passed to search_events is BEFORE end_date (start = now-60s, end = now).
- Previously the defaults were swapped (start=now, end=now-60s), causing start > end
and the API returning no events.
"""
from ManageEngineEventCollector import get_events

captured = {}

def fake_search_events(start_time, end_time, limit):
captured["start_time"] = int(start_time)
captured["end_time"] = int(end_time)
return []

mock_client = mocker.Mock(spec=Client)
mock_client.search_events.side_effect = fake_search_events

mocker.patch("ManageEngineEventCollector.send_events_to_xsiam")

get_events(mock_client, {"should_push_events": "false"})

assert captured["start_time"] < captured["end_time"], (
"start_time must be less than end_time when no dates are provided; "
"previously the defaults were swapped causing no events to be returned"
)
# start should be approximately 60 seconds (60000ms) before end
assert captured["end_time"] - captured["start_time"] == pytest.approx(60 * 1000, abs=500)


def test_fetch_events_all_new_events_updates_to_max(client, mocker):
"""
Given:
Expand Down
7 changes: 7 additions & 0 deletions Packs/ManageEngine/ReleaseNotes/1_0_6.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
#### Integrations

##### ManageEngine

- Updated the Docker image to: *demisto/python3:3.12.13.7444307*.
- Fixed an issue where the Zoho OAuth rotating refresh token was not updated in the integration context, causing the integration to stop ingesting events after a few days.
- Fixed an issue where the *start_date* and *end_date* default values were swapped in the **manage-engine-get-events** command, causing no events to be returned when no dates were provided.
7 changes: 5 additions & 2 deletions Packs/ManageEngine/pack_metadata.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,11 @@
"name": "ManageEngine",
"description": "ManageEngine Endpoint Central is a Unified Endpoint Management solution that helps in managing thousands of servers, desktops, laptops and mobile devices from a single console.",
"support": "xsoar",
"currentVersion": "1.0.5",
"currentVersion": "1.0.6",
"author": "Cortex XSOAR",
"url": "https://www.paloaltonetworks.com/cortex",
"email": "",
"created": "2025-07-01T00:00:00Z",
"categories": [
"Analytics & SIEM"
],
Expand All @@ -17,7 +18,9 @@
],
"keywords": [
"Desktop Central",
"Endpoint Central"
"Endpoint Central",
"ManageEngine",
"Zoho"
],
"marketplaces": [
"marketplacev2",
Expand Down
Loading