Skip to content

Commit 13a9202

Browse files
Merge pull request #156 from code42/INTEG-2957/search-subgroups
add subgroups for search filters
2 parents 8295d32 + c708c80 commit 13a9202

14 files changed

Lines changed: 554 additions & 21 deletions

File tree

CHANGELOG.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,23 @@
88
The intended audience of this file is for `incydr` SDK and CLI consumers -- as such, changes that don't affect
99
how a consumer would use the library or CLI tool (e.g. adding unit tests, updating documentation, etc) are not captured
1010
here.
11+
12+
## 2.6.0 - 2025-07-23
13+
14+
### Added
15+
- Support for subgroups in file event queries and saved searches. See [this documentation](https://support.code42.com/hc/en-us/articles/14827671672087-Forensic-Search-reference#h_01JKEF6ESSMTEGFG28WZM6TNDR) for more details about this type of query.
16+
- New methods for EventQuery() to enable more flexible filtering:
17+
- `is_any`
18+
- `is_none`
19+
- `date_range`
20+
- `subquery`
21+
- New methods to download files by XFC content ID.
22+
- `sdk.files.download_file_by_xfc_content_id` and `sdk.files.stream_file_by_xfc_content_id`
23+
- `incydr files download-by-xfc-id`
24+
25+
### Fixed
26+
- An issue where in some cases saved searches could not be retrieved.
27+
1128
## 2.5.0 - 2025-06-06
1229

1330
### Added

docs/sdk/clients/file_event_queries.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ Use the `EventQuery` class to create a query for searching and filtering file ev
66

77
::: _incydr_sdk.queries.file_events.EventQuery
88
:docstring:
9-
:members: equals not_equals exists does_not_exist greater_than less_than matches_any
9+
:members: equals not_equals exists does_not_exist greater_than less_than matches_any is_any is_none date_range subquery
1010

1111
## Query Building
1212

src/_incydr_cli/cmds/files.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,3 +31,17 @@ def download(sha256: str, path: str):
3131
"""
3232
client = Client()
3333
client.files.v1.download_file_by_sha256(sha256, path)
34+
35+
36+
@files.command(cls=IncydrCommand)
37+
@click.argument("XFC_ID")
38+
@path_option
39+
@logging_options
40+
def download_by_xfc_id(xfc_id: str, path: str):
41+
"""
42+
Download the file matching the given XFC content ID hash to the target path.
43+
"""
44+
client = Client()
45+
client.files.v1.download_file_by_xfc_content_id(
46+
xfc_content_id=xfc_id, target_path=path
47+
)

src/_incydr_sdk/__version__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
# SPDX-FileCopyrightText: 2022-present Code42 Software <integrations@code42.com>
22
#
33
# SPDX-License-Identifier: MIT
4-
__version__ = "2.5.0"
4+
__version__ = "2.6.0"

src/_incydr_sdk/core/settings.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,8 +50,7 @@ class IncydrSettings(BaseSettings):
5050
Usage:
5151
5252
>> import incydr
53-
>>> client = incydr.Client()
54-
>>> client.settings.page_size = 10
53+
>>> client = incydr.Client(page_size = 10)
5554
5655
Settings can also be loaded from shell environment variables or .env files. Just prefix a setting's attribute name
5756
with `INCYDR_` when configuring via enviroment vars.

src/_incydr_sdk/enums/file_events.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ class Operator(_Enum):
55
# all valid filter operators for querying file events
66
IS = "IS"
77
IS_NOT = "IS_NOT"
8+
IS_ANY = "IS_ANY"
9+
IS_NONE = "IS_NONE"
810
EXISTS = "EXISTS"
911
DOES_NOT_EXIST = "DOES_NOT_EXIST"
1012
GREATER_THAN = "GREATER_THAN"

src/_incydr_sdk/file_events/client.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,16 @@
1414
class InvalidQueryException(IncydrException):
1515
"""Raised when the file events search endpoint returns a 400."""
1616

17-
def __init__(self, query=None):
17+
def __init__(self, query=None, exception=None):
1818
self.query = query
1919
self.message = (
2020
"400 Response Error: Invalid query. Please double check your query filters are valid. "
2121
"\nTip: Make sure you're specifying your filter fields in dot notation. "
2222
"\nFor example, filter by 'file.archiveId' to filter by the archiveId field within the file object.)"
2323
)
24+
if "problems" in exception.response.json().keys():
25+
self.message += f"\nRaw problem data from the response: {exception.response.json()['problems']}"
26+
self.original_exception = exception
2427
super().__init__(self.message)
2528

2629

@@ -65,7 +68,7 @@ def search(self, query: EventQuery) -> FileEventsPage:
6568
response = self._parent.session.post("/v2/file-events", json=query.dict())
6669
except HTTPError as err:
6770
if err.response.status_code == 400:
68-
raise InvalidQueryException(query)
71+
raise InvalidQueryException(query=query, exception=err)
6972
raise err
7073
page = FileEventsPage.parse_response(response)
7174
query.page_token = page.next_pg_token

src/_incydr_sdk/file_events/models/event.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -709,6 +709,11 @@ class Event(Model):
709709
example="GIT_PUSH",
710710
title="The method of file movement. For example: UPLOADED, DOWNLOADED, EMAILED.",
711711
)
712+
xfc_event_id: Optional[str] = Field(
713+
None,
714+
alias="xfcEventId",
715+
description="The identifier for the exfiltrated file collection data associated with this event.",
716+
)
712717

713718

714719
class Git(Model):

src/_incydr_sdk/file_events/models/response.py

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
from __future__ import annotations
2+
13
from datetime import datetime
24
from typing import List
35
from typing import Optional
@@ -32,6 +34,17 @@ class SearchFilterGroup(ResponseModel):
3234
)
3335

3436

37+
class SearchFilterGroupV2(ResponseModel):
38+
subgroup_clause: Optional[str] = Field(
39+
alias="subgroupClause",
40+
description="Grouping clause for subgroups.",
41+
example="AND",
42+
)
43+
subgroups: Optional[List[Union[SearchFilterGroup, SearchFilterGroupV2]]] = Field(
44+
description="One or more FilterGroups to be combined in a query, or a FilterSubgroupV2"
45+
)
46+
47+
3548
class QueryProblem(ResponseModel):
3649
"""
3750
A model containing data on a query problem.
@@ -99,7 +112,7 @@ class SavedSearch(ResponseModel):
99112
* **created_by_username**: `str` - The username of the user who created the saved search.
100113
* **creation_timestamp**: `datetime` - The time at which the saved search was created.
101114
* **group_clause**: `GroupClause` - `AND` or `OR`. Grouping clause for any specified groups. Defaults to `AND`.
102-
* **groups**: `List[SearchFilterGroup]` - One or more FilterGroups to be combined in a query.
115+
* **groups**: `List[Union[SearchFilterGroup, SearchFilterGroupV2]]` - One or more FilterGroups or FilterGroupV2s to be combined in a query.
103116
* **id**: `str` - The ID for the saved search.
104117
* **modified_by_uid**: `str` - The ID of the user who last modified the saved search.
105118
* **modified_by_username**: `str` - The username of the user who last modified the saved search.
@@ -139,7 +152,7 @@ class SavedSearch(ResponseModel):
139152
description="Grouping clause for any specified groups.",
140153
example="OR",
141154
)
142-
groups: Optional[List[SearchFilterGroup]] = Field(
155+
groups: Optional[List[Union[SearchFilterGroup, SearchFilterGroupV2]]] = Field(
143156
description="One or more FilterGroups to be combined in a query."
144157
)
145158
id: Optional[str] = Field(

src/_incydr_sdk/files/client.py

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,3 +65,46 @@ def stream_file_by_sha256(self, sha256: str):
6565
return self._parent.session.get(
6666
f"/v1/files/get-file-by-sha256/{sha256}", stream=True
6767
)
68+
69+
def download_file_by_xfc_content_id(
70+
self, xfc_content_id: str, target_path: Path
71+
) -> Path:
72+
"""Download a file that matches the given XFC content ID.
73+
74+
**Parameters:**
75+
76+
* **xfc_content_id**: `str` (required) The XFC content ID for file you wish to download.
77+
* **target_path**: `Path | str` a string or `pathlib.Path` object that represents the target file path and
78+
name to which the file will be saved to.
79+
80+
**Returns**: A `pathlib.Path` object representing the location of the downloaded file.
81+
"""
82+
target = Path(
83+
target_path
84+
) # ensure that target is a path even if we're given a string
85+
response = self._parent.session.get(
86+
f"/v1/files/get-file-by-xfc-content-id/{xfc_content_id}"
87+
)
88+
target.write_bytes(response.content)
89+
return target
90+
91+
def stream_file_by_xfc_content_id(self, xfc_content_id: str):
92+
"""Stream a file that matches the given XFC content ID.
93+
94+
**Example usage:**
95+
```
96+
>>> with sdk.files.v1.stream_file_by_xfc_content_id("content_id_example") as response:
97+
>>> with open("./testfile.zip", "wb") as file:
98+
>>> for chunk in response.iter_content(chunk_size=128):
99+
>>> file.write(chunk)
100+
```
101+
102+
**Parameters:**
103+
104+
* **xfc_content_id**: `str` (required) The XFC content ID for file you wish to download.
105+
106+
**Returns**: A `requests.Response` object with a stream of the requested file.
107+
"""
108+
return self._parent.session.get(
109+
f"/v1/files/get-file-by-xfc-content-id/{xfc_content_id}", stream=True
110+
)

0 commit comments

Comments
 (0)