diff --git a/CHANGELOG.md b/CHANGELOG.md index 0ec0b4f..d2a76d7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,14 @@ how a consumer would use the library or CLI tool (e.g. adding unit tests, updating documentation, etc) are not captured here. +## 2.10.0 - 2026-01-27 + +### Added +- Added support for filtering agents by agent health state modification date. + - Added the `agent_health_modified_in_last_days` parameter in the SDK's agent get methods to filter agents by health state modification date. + - Added the `--agent-health-modified-within-days` option to the CLI's `incydr agents list` command to filter agents by health state modification date. + - Added the `agent_health_modification_date` field to the Agent response model. + ## 2.9.0 - 2026-01-22 ### Added diff --git a/src/_incydr_cli/cmds/agents.py b/src/_incydr_cli/cmds/agents.py index ed59541..a3bf91e 100644 --- a/src/_incydr_cli/cmds/agents.py +++ b/src/_incydr_cli/cmds/agents.py @@ -60,6 +60,12 @@ def agents(): "of the given health issue type(s). Health issue types include the following: NOT_CONNECTING, NOT_SENDING_SECURITY_EVENTS, SECURITY_INGEST_REJECTED, MISSING_MACOS_PERMISSION_FULL_DISK_ACCESS, MISSING_MACOS_PERMISSION_ACCESSIBILITY.", cls=incompatible_with("healthy"), ) +@click.option( + "--agent-health-modified-within-days", + type=int, + default=None, + help="Filter agents that have had agent health modified in the last N days (starting from midnight this morning), where N is the value of the parameter.", +) @table_format_option @columns_option @logging_options @@ -67,6 +73,7 @@ def list_( active: bool = None, healthy: bool = None, unhealthy: str = None, + agent_health_modified_within_days: int = None, format_: TableFormat = None, columns: str = None, ): @@ -90,6 +97,7 @@ def list_( active=active, agent_healthy=agent_healthy, agent_health_issue_types=health_issues, + agent_health_modified_in_last_days=agent_health_modified_within_days, ) if format_ == TableFormat.table: diff --git a/src/_incydr_sdk/__version__.py b/src/_incydr_sdk/__version__.py index ab75905..90cecc1 100644 --- a/src/_incydr_sdk/__version__.py +++ b/src/_incydr_sdk/__version__.py @@ -1,4 +1,4 @@ # SPDX-FileCopyrightText: 2022-present Code42 Software # # SPDX-License-Identifier: MIT -__version__ = "2.9.0" +__version__ = "2.10.0" diff --git a/src/_incydr_sdk/agents/client.py b/src/_incydr_sdk/agents/client.py index 9f557f2..430ff25 100644 --- a/src/_incydr_sdk/agents/client.py +++ b/src/_incydr_sdk/agents/client.py @@ -39,6 +39,7 @@ def get_page( page_size: int = 500, agent_healthy: bool = None, agent_health_issue_types: Union[List[str], str] = None, + agent_health_modified_in_last_days: Optional[int] = None, user_id: str = None, ) -> AgentsPage: """ @@ -56,6 +57,7 @@ def get_page( * **sort_key**: [`SortKeys`][agents-sort-keys] - Values on which the response will be sorted. Defaults to agent name. * **agent_healthy**: `bool | None` - Optionally retrieve agents with this health status. Agents that have no health issue types are considered healthy. * **agent_health_issue_types**: `List[str] | str` - Optionally retrieve agents that have (at least) any of the given issue type(s). Health issue types include the following: `NOT_CONNECTING`, `NOT_SENDING_SECURITY_EVENTS`, `SECURITY_INGEST_REJECTED`, `MISSING_MACOS_PERMISSION_FULL_DISK_ACCESS`, `MISSING_MACOS_PERMISSION_ACCESSIBILITY`. + * **agent_health_modified_in_last_days**: `int | None` - Optionally retrieve agents that have had their agent health modified in the last N days. * **user_id**: `str` - Optionally retrieve only agents associated with this user ID. **Returns**: An [`AgentsPage`][agentspage-model] object. @@ -67,6 +69,7 @@ def get_page( anyOfAgentHealthIssueTypes=[agent_health_issue_types] if isinstance(agent_health_issue_types, str) else agent_health_issue_types, + agentHealthModifiedInLastDays=agent_health_modified_in_last_days, srtDir=sort_dir, srtKey=sort_key, pageSize=page_size, @@ -85,6 +88,7 @@ def iter_all( page_size: int = 500, agent_healthy: bool = None, agent_health_issue_types: List[str] = None, + agent_health_modified_in_last_days: Optional[int] = None, user_id: str = None, ) -> Iterator[Agent]: """ @@ -100,6 +104,7 @@ def iter_all( agent_type=agent_type, agent_healthy=agent_healthy, agent_health_issue_types=agent_health_issue_types, + agent_health_modified_in_last_days=agent_health_modified_in_last_days, sort_dir=sort_dir, sort_key=sort_key, page_num=page_num, diff --git a/src/_incydr_sdk/agents/models.py b/src/_incydr_sdk/agents/models.py index e34cf74..d4137cd 100644 --- a/src/_incydr_sdk/agents/models.py +++ b/src/_incydr_sdk/agents/models.py @@ -48,6 +48,7 @@ class Agent(ResponseModel): * **active**: `bool` If the agent status is active. * **agent_type**: [`AgentType`][agent-type] The type of agent. * **agent_health_issue_types: `List[str]` List of health issues with the agent. Health issue types include the following: `NOT_CONNECTING`, `NOT_SENDING_SECURITY_EVENTS`, `SECURITY_INGEST_REJECTED`, `MISSING_MACOS_PERMISSION_FULL_DISK_ACCESS`, `MISSING_MACOS_PERMISSION_ACCESSIBILITY`. + * **agent_health_modification_date**: `datetime` The time the agent's health state was last modified. * **app_version**: `str` The app version of the agent. * **product_version**: `str` The product version of the agent. * **last_connected**: `datetime` The time the agent last connected to a Code42 Authority server. @@ -66,6 +67,9 @@ class Agent(ResponseModel): active: Optional[bool] agent_type: Optional[Union[AgentType, str]] = Field(alias="agentType") agent_health_issue_types: Optional[List[str]] = Field(alias="agentHealthIssueTypes") + agent_health_modification_date: Optional[datetime] = Field( + alias="agentHealthModificationDate" + ) app_version: Optional[str] = Field(alias="appVersion") product_version: Optional[str] = Field(alias="productVersion") last_connected: Optional[datetime] = Field(alias="lastConnected") @@ -102,6 +106,7 @@ class QueryAgentsRequest(BaseModel): agentType: Optional[Union[AgentType, str]] = None agentHealthy: Optional[bool] = None anyOfAgentHealthIssueTypes: Optional[List[str]] = None + agentHealthModifiedInLastDays: Optional[int] = None srtKey: Optional[Union[SortKeys, str]] = None srtDir: Optional[str] = None pageSize: Optional[int] = None diff --git a/tests/test_agents.py b/tests/test_agents.py index f62b3a0..dc542f3 100644 --- a/tests/test_agents.py +++ b/tests/test_agents.py @@ -26,6 +26,7 @@ "active": True, "agentType": "COMBINED", "agentHealthIssueTypes": ["NOT_CONNECTING"], + "agentHealthModificationDate": "2022-07-14T17:03:22.123000Z", "appVersion": "1.0", "productVersion": "2.0", "lastConnected": "2022-07-14T17:05:44.524000Z", @@ -46,6 +47,7 @@ "active": True, "agentType": "COMBINED", "agentHealthIssueTypes": [], + "agentHealthModificationDate": "2022-07-14T17:02:15.456000Z", "appVersion": "1.0", "productVersion": "2.0", "lastConnected": "2022-07-14T17:05:44.524000Z", @@ -65,6 +67,7 @@ "active": True, "agentType": "COMBINED", "agentHealthIssueTypes": [], + "agentHealthModificationDate": "2022-07-14T17:01:30.789000Z", "appVersion": "1.0", "productVersion": "2.0", "lastConnected": "2022-07-14T17:05:44.524000Z", @@ -117,6 +120,9 @@ def test_get_agent_returns_expected_data(mock_get_agent): assert agent.modification_date == datetime.fromisoformat( TEST_AGENT_1["modificationDate"].replace("Z", "+00:00") ) + assert agent.agent_health_modification_date == datetime.fromisoformat( + TEST_AGENT_1["agentHealthModificationDate"].replace("Z", "+00:00") + ) def test_get_page_when_default_query_params_returns_expected_data( @@ -165,6 +171,35 @@ def test_get_page_when_custom_query_params_returns_expected_data( assert page.total_count == len(page.agents) == 2 +def test_get_page_when_agent_health_modified_in_last_days_passed_makes_expected_call( + httpserver_auth: HTTPServer, +): + query = { + "agentHealthModifiedInLastDays": 7, + "srtKey": "NAME", + "srtDir": "ASC", + "pageSize": 500, + "page": 1, + } + + agents_data = { + "agents": [TEST_AGENT_1, TEST_AGENT_2], + "totalCount": 2, + "pageSize": 500, + "page": 1, + } + httpserver_auth.expect_request( + uri="/v1/agents", method="GET", query_string=urlencode(query) + ).respond_with_json(agents_data) + + client = Client() + page = client.agents.v1.get_page(agent_health_modified_in_last_days=7) + assert isinstance(page, AgentsPage) + assert page.agents[0].json() == json.dumps(TEST_AGENT_1, separators=(",", ":")) + assert page.agents[1].json() == json.dumps(TEST_AGENT_2, separators=(",", ":")) + assert page.total_count == len(page.agents) == 2 + + def test_iter_all_when_default_params_returns_expected_data( httpserver_auth: HTTPServer, ): @@ -346,6 +381,34 @@ def test_cli_list_when_unhealthy_option_passed_with_string_parses_issue_types_co assert result.exit_code == 0 +def test_cli_list_when_health_modified_days_option_passed_makes_expected_call( + httpserver_auth: HTTPServer, runner +): + query = { + "agentHealthModifiedInLastDays": 7, + "srtKey": "NAME", + "srtDir": "ASC", + "pageSize": 500, + "page": 1, + } + + agents_data = { + "agents": [TEST_AGENT_1, TEST_AGENT_2], + "totalCount": 2, + "pageSize": 500, + "page": 1, + } + httpserver_auth.expect_request( + uri="/v1/agents", method="GET", query_string=urlencode(query) + ).respond_with_json(agents_data) + + result = runner.invoke( + incydr, ["agents", "list", "--agent-health-modified-within-days", "7"] + ) + httpserver_auth.check() + assert result.exit_code == 0 + + def test_cli_show_when_custom_params_makes_expected_call( httpserver_auth: HTTPServer, runner, mock_get_agent ): diff --git a/tests/test_users.py b/tests/test_users.py index 0e47659..a4253b8 100644 --- a/tests/test_users.py +++ b/tests/test_users.py @@ -34,6 +34,7 @@ "active": True, "agentType": "COMBINED", "agentHealthIssueTypes": ["NOT_CONNECTING"], + "agentHealthModificationDate": "2022-07-14T17:03:22.123000Z", "appVersion": "1.0", "productVersion": "2.0", "lastConnected": "2022-07-14T17:05:44.524000Z", @@ -54,6 +55,7 @@ "active": True, "agentType": "COMBINED", "agentHealthIssueTypes": [], + "agentHealthModificationDate": "2022-07-14T17:02:15.456000Z", "appVersion": "1.0", "productVersion": "2.0", "lastConnected": "2022-07-14T17:05:44.524000Z", @@ -73,6 +75,7 @@ "active": True, "agentType": "COMBINED", "agentHealthIssueTypes": [], + "agentHealthModificationDate": "2022-07-14T17:01:30.789000Z", "appVersion": "1.0", "productVersion": "2.0", "lastConnected": "2022-07-14T17:05:44.524000Z",