Skip to content

Commit 810cb74

Browse files
add methods for xfc id downloading
1 parent 91a8914 commit 810cb74

File tree

6 files changed

+112
-1
lines changed

6 files changed

+112
-1
lines changed

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@
1717
- `is_none`
1818
- `date_range`
1919
- `subquery`
20+
- New methods to download files by XFC content ID.
21+
- `sdk.files.download_file_by_xfc_content_id` and `sdk.files.stream_file_by_xfc_content_id`
22+
- `incydr files download-by-xfc-id`
2023

2124
### Fixed
2225
- An issue where in some cases saved searches could not be retrieved.

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/file_events/client.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ def __init__(self, query=None, exception=None):
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():
24+
if "problems" in exception.response.json().keys():
2525
self.message += f"\nRaw problem data from the response: {exception.response.json()['problems']}"
2626
self.original_exception = exception
2727
super().__init__(self.message)

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/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+
)

tests/test_files.py

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88

99
TEST_SHA256 = "38acb15d02d5ac0f2a2789602e9df950c380d2799b4bdb59394e4eeabdd3a662"
10+
TEST_XFC_ID = "test"
1011
BAD_SHA256 = "asdf"
1112
TEST_DATA = b"test data"
1213

@@ -19,6 +20,14 @@ def mock_file_download(httpserver_auth: HTTPServer):
1920
return httpserver_auth
2021

2122

23+
@pytest.fixture
24+
def mock_file_download_by_xfc(httpserver_auth: HTTPServer):
25+
httpserver_auth.expect_request(
26+
f"/v1/files/get-file-by-xfc-content-id/{TEST_XFC_ID}"
27+
).respond_with_data(response_data=TEST_DATA, status=200)
28+
return httpserver_auth
29+
30+
2231
@pytest.fixture
2332
def mock_bad_sha256(httpserver_auth: HTTPServer):
2433
httpserver_auth.expect_request(
@@ -35,6 +44,14 @@ def mock_file_not_found(httpserver_auth: HTTPServer):
3544
return httpserver_auth
3645

3746

47+
@pytest.fixture
48+
def mock_file_not_found_by_xfc_id(httpserver_auth: HTTPServer):
49+
httpserver_auth.expect_request(
50+
f"/v1/files/get-file-by-xfc-content-id/{TEST_XFC_ID}"
51+
).respond_with_data(response_data="", status=404)
52+
return httpserver_auth
53+
54+
3855
def test_download_file_by_sha256_calls_with_correct_parameter(
3956
mock_file_download, tmp_path
4057
):
@@ -70,6 +87,35 @@ def test_stream_file_by_sha256_streams_file(mock_file_download):
7087
assert content == TEST_DATA
7188

7289

90+
def test_download_file_by_xfc_id_calls_with_correct_parameter(
91+
mock_file_download_by_xfc, tmp_path
92+
):
93+
c = Client()
94+
p = tmp_path / "testfile.test"
95+
f = c.files.v1.download_file_by_xfc_content_id(TEST_XFC_ID, p)
96+
with open(f, "rb") as file:
97+
content = file.read()
98+
assert content == TEST_DATA
99+
mock_file_download_by_xfc.check()
100+
101+
102+
def test_download_file_by_xfc_id_raises_error_when_file_not_found(
103+
mock_file_not_found_by_xfc_id,
104+
):
105+
c = Client()
106+
with pytest.raises(HTTPError) as error:
107+
c.files.v1.download_file_by_xfc_content_id(TEST_XFC_ID, "testpath.text")
108+
assert error.value.response.status_code == 404
109+
mock_file_not_found_by_xfc_id.check()
110+
111+
112+
def test_stream_file_by_xfc_id_streams_file(mock_file_download_by_xfc):
113+
c = Client()
114+
response = c.files.v1.stream_file_by_xfc_content_id(TEST_XFC_ID)
115+
content = b"".join([c for c in response.iter_content(chunk_size=128) if c])
116+
assert content == TEST_DATA
117+
118+
73119
# ************************************************ CLI ************************************************
74120

75121

0 commit comments

Comments
 (0)