Skip to content

Commit 7def585

Browse files
committed
Submit test malware to check attachment scanning
Problem: A test is required that malware is scanned and marked as bad. Solution: Create an asset if not existing with 2 attachments - one good and one bad. Check that if they have been scanned then each returns OK or BAD as required. First attachment is clean, second attachment is test malware. Signed-off-by: Paul Hewlett <phewlett76@gmail.com>
1 parent 498d8c4 commit 7def585

12 files changed

Lines changed: 434 additions & 43 deletions

archivist/attachments.py

Lines changed: 83 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
# pylint:disable=too-few-public-methods
2626

2727
from copy import deepcopy
28+
from io import BytesIO
2829
from logging import getLogger
2930
from typing import BinaryIO, Dict, Optional
3031

@@ -34,8 +35,15 @@
3435
# pylint:disable=cyclic-import # but pylint doesn't understand this feature
3536
from . import archivist as type_helper
3637

37-
from .constants import ATTACHMENTS_SUBPATH, ATTACHMENTS_LABEL
38+
from .constants import (
39+
ASSETS_SUBPATH,
40+
ATTACHMENTS_SUBPATH,
41+
ATTACHMENTS_LABEL,
42+
ATTACHMENTS_ASSETS_EVENTS_LABEL,
43+
SEP,
44+
)
3845
from .dictmerge import _deepmerge
46+
from .utils import get_url
3947
from .type_aliases import NoneOnError
4048

4149
LOGGER = getLogger(__name__)
@@ -81,7 +89,15 @@ def create(self, data: Dict) -> Dict: # pragma: no cover
8189
filename: functests/test_resources/doors/assets/gdn_front.jpg
8290
content_type: image/jpg
8391
84-
Both 'filename' and 'content_type' are required.
92+
OR
93+
94+
.. code-block:: yaml
95+
96+
url: https://secure.eicar.org/eicar.com"
97+
content_type: image/jpg
98+
99+
Either 'filename' or 'url' is required.
100+
'content_type' is required.
85101
86102
Returns:
87103
@@ -97,13 +113,22 @@ def create(self, data: Dict) -> Dict: # pragma: no cover
97113
98114
"""
99115
result = None
100-
with open(data["filename"], "rb") as fd:
116+
filename = data.get("filename", None)
117+
if filename is not None:
118+
with open(filename, "rb") as fd:
119+
attachment = self.upload(fd, mtype=data.get("content_type"))
120+
121+
else:
122+
url = data["url"]
123+
fd = BytesIO()
124+
get_url(url, fd)
101125
attachment = self.upload(fd, mtype=data.get("content_type"))
102-
result = {
103-
"arc_attachment_identity": attachment["identity"],
104-
"arc_hash_alg": attachment["hash"]["alg"],
105-
"arc_hash_value": attachment["hash"]["value"],
106-
}
126+
127+
result = {
128+
"arc_attachment_identity": attachment["identity"],
129+
"arc_hash_alg": attachment["hash"]["alg"],
130+
"arc_hash_value": attachment["hash"]["value"],
131+
}
107132

108133
return result
109134

@@ -141,6 +166,7 @@ def download(
141166
fd: BinaryIO,
142167
*,
143168
params: Optional[Dict] = None,
169+
asset_or_event_id: Optional[str] = None,
144170
) -> Response:
145171
"""Read attachment
146172
@@ -150,16 +176,64 @@ def download(
150176
151177
Args:
152178
identity (str): attachment identity e.g. attachments/xxxxxxxxxxxxxxxxxxxxxxx
153-
fd (file): opened file escriptor or other file-type sink..
179+
fd (file): opened file descriptor or other file-type sink..
154180
params (dict): e.g. {"allow_insecure": "true"} OR {"strict": "true" }
181+
asset_or_event_id (str): optional asset identity
182+
e.g. assets/xxxxxxxxxxxxxxxxxxxxxxxxxx
183+
assets/xxxxx/events/xxxx
155184
156185
Returns:
157186
REST response
158187
159188
"""
189+
if asset_or_event_id is not None:
190+
uuid = identity.split(SEP)[1]
191+
identity = SEP.join((asset_or_event_id, uuid))
192+
return self._archivist.get_file(
193+
SEP.join((ASSETS_SUBPATH, ATTACHMENTS_ASSETS_EVENTS_LABEL)),
194+
identity,
195+
fd,
196+
params=self.__params(params),
197+
)
198+
160199
return self._archivist.get_file(
161200
ATTACHMENTS_SUBPATH,
162201
identity,
163202
fd,
164203
params=self.__params(params),
165204
)
205+
206+
def info(
207+
self,
208+
identity: str,
209+
*,
210+
asset_or_event_id: Optional[str] = None,
211+
) -> Response:
212+
"""Read attachment info
213+
214+
Reads attachment info
215+
216+
Args:
217+
identity (str): attachment identity e.g. attachments/xxxxxxxxxxxxxxxxxxxxxxx
218+
asset_or_event_id (str): optional asset identity
219+
e.g. assets/xxxxxxxxxxxxxxxxxxxxxxxxxx
220+
assets/xxxxx/events/xxxx
221+
222+
Returns:
223+
REST response
224+
225+
"""
226+
if asset_or_event_id is not None:
227+
uuid = identity.split(SEP)[1]
228+
identity = SEP.join((asset_or_event_id, uuid))
229+
return self._archivist.get(
230+
SEP.join((ASSETS_SUBPATH, ATTACHMENTS_ASSETS_EVENTS_LABEL)),
231+
identity,
232+
tail="info",
233+
)
234+
235+
return self._archivist.get(
236+
ATTACHMENTS_SUBPATH,
237+
identity,
238+
tail="info",
239+
)

archivist/constants.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232

3333
ATTACHMENTS_SUBPATH = "v1"
3434
ATTACHMENTS_LABEL = "blobs"
35+
ATTACHMENTS_ASSETS_EVENTS_LABEL = "attachments"
3536

3637
ACCESS_POLICIES_SUBPATH = "iam/v1"
3738
ACCESS_POLICIES_LABEL = "access_policies"

archivist/runner.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,10 @@ class _ActionMap(dict):
3232

3333
def __init__(self, archivist: "type_helper.Archivist"):
3434
super().__init__()
35+
self["ASSETS_ATTACHMENT_INFO"] = {
36+
"action": archivist.attachments.info,
37+
"keywords": ("asset_or_event_id",),
38+
}
3539
self["ASSETS_CREATE_IF_NOT_EXISTS"] = {
3640
"action": archivist.assets.create_if_not_exists,
3741
"keywords": ("confirm",),
@@ -357,7 +361,7 @@ def set_deletions(self, response: Dict, delete_method):
357361
def delete(self):
358362
"""Deletes all entities"""
359363
for identity, delete_method in self.deletions.items():
360-
LOGGER.info("Delete %s -> %s", identity, delete_method)
364+
LOGGER.info("Delete %s", identity)
361365
delete_method(identity)
362366

363367
def asset_id(self, name: str) -> Optional[str]:

archivist/utils.py

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
1-
"""Some covenience for confirmers
1+
"""Some convenience for confirmers
22
"""
33

4+
from io import BytesIO
45
from logging import getLogger
6+
from requests import get as requests_get
57

68
LOGGER = getLogger(__name__)
79

@@ -29,3 +31,18 @@ def backoff_handler(details):
2931
client,
3032
identity,
3133
)
34+
35+
36+
def get_url(url: str, fd: BytesIO): # pragma no cover
37+
"""GET method (REST) - chunked
38+
39+
Downloads a binary object from upstream storage.
40+
"""
41+
response = requests_get(
42+
url,
43+
stream=True,
44+
)
45+
46+
for chunk in response.iter_content(chunk_size=4096):
47+
if chunk:
48+
fd.write(chunk)

functests/execassets.py

Lines changed: 109 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,13 @@
33
"""
44

55
from copy import copy, deepcopy
6-
import json
6+
from json import dumps as json_dumps
77
from os import environ
88
from unittest import skip, TestCase
99
from uuid import uuid4
1010

1111
from archivist.archivist import Archivist
12+
from archivist.assets import BEHAVIOURS
1213
from archivist.logger import set_logger
1314
from archivist.proof_mechanism import ProofMechanism
1415

@@ -27,6 +28,35 @@
2728
"some_custom_attribute": "value",
2829
}
2930

31+
ASSET_NAME = "Telephone with 2 attachments - one bad or not scanned 2022-03-01"
32+
REQUEST_EXISTS_ATTACHMENTS = {
33+
"signature": {
34+
"attributes": {
35+
"arc_display_name": ASSET_NAME,
36+
"arc_namespace": "namespace",
37+
},
38+
},
39+
"behaviours": BEHAVIOURS,
40+
"proof_mechanism": ProofMechanism.SIMPLE_HASH.name,
41+
"attributes": {
42+
"arc_firmware_version": "1.0",
43+
"arc_serial_number": "vtl-x4-07",
44+
"arc_description": "Traffic flow control light at A603 North East",
45+
"arc_display_type": "Traffic light with violation camera",
46+
"some_custom_attribute": "value",
47+
},
48+
"attachments": [
49+
{
50+
"filename": "functests/test_resources/telephone.jpg",
51+
"content_type": "image/jpg",
52+
},
53+
{
54+
"url": "https://secure.eicar.org/eicarcom2.zip",
55+
"content_type": "application/zip",
56+
},
57+
],
58+
}
59+
3060

3161
class TestAssetCreate(TestCase):
3262
"""
@@ -140,7 +170,7 @@ def test_asset_create_event(self):
140170
# get identity of first asset
141171
identity = None
142172
for asset in self.arch.assets.list():
143-
print("asset", json.dumps(asset, sort_keys=True, indent=4))
173+
print("asset", json_dumps(asset, sort_keys=True, indent=4))
144174
identity = asset["identity"]
145175
break
146176

@@ -176,4 +206,80 @@ def test_asset_create_event(self):
176206
event = self.arch.events.create(
177207
identity, props=props, attrs=attrs, confirm=True
178208
)
179-
print("event", json.dumps(event, sort_keys=True, indent=4))
209+
print("event", json_dumps(event, sort_keys=True, indent=4))
210+
211+
212+
class TestAssetCreateIfNotExists(TestCase):
213+
"""
214+
Test Archivist Asset CreateIfNotExists method
215+
"""
216+
217+
maxDiff = None
218+
219+
def setUp(self):
220+
with open(environ["TEST_AUTHTOKEN_FILENAME"], encoding="utf-8") as fd:
221+
auth = fd.read().strip()
222+
self.arch = Archivist(
223+
environ["TEST_ARCHIVIST"], auth, verify=False, max_time=300
224+
)
225+
226+
def tearDown(self):
227+
self.arch = None
228+
229+
def test_asset_create_if_not_exists_with_bad_attachment(self):
230+
"""
231+
Test asset creation if not exists - check attachment for scanned status.
232+
233+
Because we use create_if_not_exists the asset and attachments will persist.
234+
235+
The test checks the scanned timestamp and checks scanned status.
236+
The first attachment should return OK after 24 hours and the second attachment
237+
should return bad after 24 hours.
238+
239+
"""
240+
asset, existed = self.arch.assets.create_if_not_exists(
241+
REQUEST_EXISTS_ATTACHMENTS,
242+
confirm=True,
243+
)
244+
print("asset", json_dumps(asset, indent=4))
245+
print("existed", existed)
246+
247+
# first attachment is ok....
248+
attachment_id = asset["attributes"]["arc_attachments"][0][
249+
"arc_attachment_identity"
250+
]
251+
info = self.arch.attachments.info(
252+
attachment_id,
253+
asset_or_event_id=asset["identity"],
254+
)
255+
print("info attachment1", json_dumps(info, indent=4))
256+
timestamp = info["scanned_timestamp"]
257+
if timestamp:
258+
print(attachment_id, "scanned last at", timestamp)
259+
print(attachment_id, "scanned status", info["scanned_status"])
260+
print(attachment_id, "scanned reason", info["scanned_reason"])
261+
self.assertEqual(
262+
info["scanned_status"],
263+
"SCANNED_OK",
264+
msg="First attachment is not clean",
265+
)
266+
267+
# second attachment is bad when scanned....
268+
attachment_id = asset["attributes"]["arc_attachments"][1][
269+
"arc_attachment_identity"
270+
]
271+
info = self.arch.attachments.info(
272+
attachment_id,
273+
asset_or_event_id=asset["identity"],
274+
)
275+
print("info attachment1", json_dumps(info, indent=4))
276+
timestamp = info["scanned_timestamp"]
277+
if timestamp:
278+
print(attachment_id, "scanned last at", timestamp)
279+
print(attachment_id, "scanned status", info["scanned_status"])
280+
print(attachment_id, "scanned reason", info["scanned_reason"])
281+
self.assertEqual(
282+
info["scanned_status"],
283+
"SCANNED_BAD",
284+
msg="First attachment should not be clean",
285+
)

0 commit comments

Comments
 (0)