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
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,18 @@
how a consumer would use the library or CLI tool (e.g. adding unit tests, updating documentation, etc) are not captured
here.

## 2.9.0 - 2026-01-22

### Added
- The `incydr users list-agents` command to list all agents associated with a user.

### Deprecated
- The `incydr users list-devices` command is now properly marked as deprecated. Use `incydr users list-agents` instead.
- The `sdk.users.v1.get_devices` method is now properly marked as deprecated. Use `sdk.agents.v1.iter_all` instead.

### Fixed
- A bug where `sdk.users.v1.get_devices` would cause an error.

## 2.8.1 - 2026-01-21

### Added
Expand Down
44 changes: 43 additions & 1 deletion src/_incydr_cli/cmds/users.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,12 @@
from _incydr_cli.cmds.options.output_options import table_format_option
from _incydr_cli.cmds.options.output_options import TableFormat
from _incydr_cli.cmds.options.utils import user_lookup_callback
from _incydr_cli.cmds.utils import deprecation_warning
from _incydr_cli.cmds.utils import user_lookup
from _incydr_cli.core import IncydrCommand
from _incydr_cli.core import IncydrGroup
from _incydr_cli.file_readers import AutoDecodedFile
from _incydr_sdk.agents.models import Agent
from _incydr_sdk.core.client import Client
from _incydr_sdk.devices.models import Device
from _incydr_sdk.users.client import RoleNotFoundError
Expand Down Expand Up @@ -136,8 +138,9 @@ def list_devices(
columns: Optional[str],
):
"""
List devices associated with a particular user.
DEPRECATED - use list-agents instead.
"""
deprecation_warning("DEPRECATED. Use list-agents instead.")
client = Client()
devices = client.users.v1.get_devices(user).devices

Expand All @@ -164,6 +167,45 @@ def list_devices(
click.echo(item.json())


@users.command(cls=IncydrCommand)
@user_arg
@table_format_option
@columns_option
@logging_options
def list_agents(
user,
format_: TableFormat,
columns: Optional[str],
):
"""
List agents associated with a particular user.
"""
client = Client()
if "@" in user:
user = client.users.v1.get_user(user).user_id
devices = list(client.agents.v1.iter_all(user_id=user))

if format_ == TableFormat.csv:
render.csv(Agent, devices, columns=columns, flat=True)
elif format_ == TableFormat.table:
columns = columns or [
"agent_id",
"name",
"os_hostname",
"active",
"agent_type",
"agent_health_issue_types",
"last_connected",
]
render.table(Agent, devices, columns=columns, flat=False)
elif format_ == TableFormat.json_pretty:
for item in devices:
console.print_json(item.json())
else:
for item in devices:
click.echo(item.json())


@users.command("list-roles", cls=IncydrCommand)
@user_arg
@table_format_option
Expand Down
2 changes: 1 addition & 1 deletion src/_incydr_sdk/__version__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# SPDX-FileCopyrightText: 2022-present Code42 Software <integrations@code42.com>
#
# SPDX-License-Identifier: MIT
__version__ = "2.8.1"
__version__ = "2.9.0"
5 changes: 5 additions & 0 deletions src/_incydr_sdk/agents/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ def get_page(
page_size: int = 500,
agent_healthy: bool = None,
agent_health_issue_types: Union[List[str], str] = None,
user_id: str = None,
) -> AgentsPage:
"""
Get a page of agents.
Expand All @@ -55,6 +56,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`.
* **user_id**: `str` - Optionally retrieve only agents associated with this user ID.

**Returns**: An [`AgentsPage`][agentspage-model] object.
"""
Expand All @@ -69,6 +71,7 @@ def get_page(
srtKey=sort_key,
pageSize=page_size,
page=page_num,
userId=user_id,
)
response = self._parent.session.get("/v1/agents", params=data.dict())
return AgentsPage.parse_response(response)
Expand All @@ -82,6 +85,7 @@ def iter_all(
page_size: int = 500,
agent_healthy: bool = None,
agent_health_issue_types: List[str] = None,
user_id: str = None,
) -> Iterator[Agent]:
"""
Iterate over all agents.
Expand All @@ -100,6 +104,7 @@ def iter_all(
sort_key=sort_key,
page_num=page_num,
page_size=page_size,
user_id=user_id,
)
yield from page.agents
if len(page.agents) < page_size:
Expand Down
1 change: 1 addition & 0 deletions src/_incydr_sdk/agents/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@ class QueryAgentsRequest(BaseModel):
srtDir: Optional[str] = None
pageSize: Optional[int] = None
page: Optional[int] = None
userId: Optional[str] = None

@field_validator("srtDir")
@classmethod
Expand Down
22 changes: 21 additions & 1 deletion src/_incydr_sdk/devices/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,10 +46,12 @@ class Device(ResponseModel):
"""

device_id: Optional[str] = Field(
None,
alias="deviceId",
description="A globally unique ID (guid) for this device.",
)
legacy_device_id: Optional[str] = Field(
None,
alias="legacyDeviceId",
description="The device ID to use for older console-based APIs that require a device Id.",
)
Expand All @@ -59,38 +61,47 @@ class Device(ResponseModel):
description="Device Hostname according to the device's OS.",
)
status: Optional[str] = Field(
None,
description="Device status. Values: Active, Deactivated, Blocked, Deauthorized (Active/Deactivated followed by optional Blocked and/or Deauthorized)",
)
active: Optional[bool] = Field(
None,
description="True means the device will show up on reports, etc.",
)
blocked: Optional[bool] = Field(
None,
description="True means device continues backing up, but restores and logins are disabled.",
)
alert_state: Optional[int] = Field(
None,
alias="alertState",
description="0=ok, 1=connection warning, 2=connection critical",
)
user_id: Optional[str] = Field(
None,
alias="userId",
description="A globally unique ID for this user.",
)
legacy_user_id: Optional[str] = Field(
None,
alias="legacyUserId",
description='The user ID to use for older console-based APIs that require a user Id.\r\nIf your endpoint domain starts with "console" instead of "api", use this Id for endpoints that require a userId.',
)
org_id: Optional[str] = Field(
None,
alias="orgId",
description="An ID for the Code42 organization of the user owning this device.",
)
legacy_org_id: Optional[str] = Field(
None,
alias="legacyOrgId",
description='The org ID to use for older console-based APIs that require an org Id.\r\nIf your endpoint domain starts with "console" instead of "api", use this Id for endpoints that require an orgId.',
)
org_guid: Optional[str] = Field(
alias="orgGuid", description="The globally unique org ID."
None, alias="orgGuid", description="The globally unique org ID."
)
external_reference: Optional[str] = Field(
None,
alias="externalReferenceInfo",
description="Optional external reference information, such as a serial number, asset tag, employee ID, or help desk issue ID.",
)
Expand All @@ -100,25 +111,31 @@ class Device(ResponseModel):
description="The last day and time this device was connected to the server.",
)
os_name: Optional[str] = Field(
None,
alias="osName",
description="Operating system name. Values: Windows*, Mac OS X, Linux, Android, iOS, SunOS, etc",
)
os_version: Optional[str] = Field(
None,
alias="osVersion",
description="Operating system version. Values: 10.5.1, 6.2, etc",
)
os_arch: Optional[str] = Field(
None,
alias="osArch",
description="Hardware architecture. Values: i386, amd64, sparc, x86, etc",
)
address: Optional[str] = Field(
None,
description="Internal IP address and port. Example: 192.168.42.1:4282",
)
remote_address: Optional[str] = Field(
None,
alias="remoteAddress",
description="External IP address and port. Example: 171.22.110.41:13958",
)
time_zone: Optional[str] = Field(
None,
alias="timeZone",
description="Examples: Australia/Canberra, Asia/Calcutta",
)
Expand All @@ -127,14 +144,17 @@ class Device(ResponseModel):
description="Device build version long number, will only be applicable to CP4/SP devices.",
)
creation_date: Optional[datetime] = Field(
None,
alias="creationDate",
description="Date and time this device was created.",
)
modification_date: Optional[datetime] = Field(
None,
alias="modificationDate",
description="Date and time this device was last modified.",
)
login_date: Optional[datetime] = Field(
None,
alias="loginDate",
description="Date and time this device was last logged in.",
)
Expand Down
47 changes: 47 additions & 0 deletions src/_incydr_sdk/users/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
from typing import Iterator
from typing import List
from typing import Union
from warnings import warn

from pydantic import parse_obj_as
from requests import Response
Expand Down Expand Up @@ -126,6 +127,52 @@ def get_devices(
* **sort_dir**: `SortDirection` - 'asc' or 'desc'. The direction in which to sort the response based on the corresponding key. Defaults to 'asc'.
* **sort_key**: `SortKeys` - One or more values on which the response will be sorted. Defaults to device name.

**Returns**: A [`DevicesPage`][devicespage-model] object.
"""
warn(
"users.v1.get_devices is deprecated and replaced by agents.v1.iter_all.",
DeprecationWarning,
stacklevel=2,
)
page_size = page_size or self._parent.settings.page_size
data = QueryDevicesRequest(
page=page_num,
pageSize=page_size,
sortKey=sort_key,
sortDirection=sort_dir,
active=active,
blocked=blocked,
)
response = self._parent.session.get(
f"/v1/users/{user_id}/devices", params=data.dict()
)
return DevicesPage.parse_response(response)

def get_agents(
self,
user_id: str,
active: bool = None,
blocked: bool = None,
page_num: int = 1,
page_size: int = None,
sort_dir: SortDirection = SortDirection.ASC,
sort_key: SortKeys = SortKeys.NAME,
) -> DevicesPage:
"""
Get a page of agents associated with a specific user.

Filter results by passing the appropriate parameters:

**Parameters**:

* **user_id**: `str` (required) - The unique ID for the user.
* **active**: `bool` - Whether or not the device is active. If true, the device will show up on reports, etc.
* **blocked**: `bool` - Whether or not the device is blocked. If true, restores and logins are disabled.
* **page_num**: `int` - Page number for results. Defaulting to 1.
* **page_size**: `int` - Max number of results to return per page. Defaulting to client's `page_size` settings.
* **sort_dir**: `SortDirection` - 'asc' or 'desc'. The direction in which to sort the response based on the corresponding key. Defaults to 'asc'.
* **sort_key**: `SortKeys` - One or more values on which the response will be sorted. Defaults to device name.

**Returns**: A [`DevicesPage`][devicespage-model] object.
"""
page_size = page_size or self._parent.settings.page_size
Expand Down
Loading