polyswarm-api 3.6.0__tar.gz → 3.8.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {polyswarm-api-3.6.0/src/polyswarm_api.egg-info → polyswarm-api-3.8.0}/PKG-INFO +7 -3
- {polyswarm-api-3.6.0 → polyswarm-api-3.8.0}/README.md +1 -1
- polyswarm-api-3.8.0/requirements.txt +7 -0
- {polyswarm-api-3.6.0 → polyswarm-api-3.8.0}/setup.py +8 -6
- {polyswarm-api-3.6.0 → polyswarm-api-3.8.0}/src/polyswarm_api/__init__.py +1 -1
- {polyswarm-api-3.6.0 → polyswarm-api-3.8.0}/src/polyswarm_api/api.py +184 -33
- {polyswarm-api-3.6.0 → polyswarm-api-3.8.0}/src/polyswarm_api/core.py +24 -29
- {polyswarm-api-3.6.0 → polyswarm-api-3.8.0}/src/polyswarm_api/exceptions.py +1 -1
- {polyswarm-api-3.6.0 → polyswarm-api-3.8.0}/src/polyswarm_api/resources.py +149 -65
- {polyswarm-api-3.6.0 → polyswarm-api-3.8.0}/src/polyswarm_api/settings.py +1 -0
- {polyswarm-api-3.6.0 → polyswarm-api-3.8.0/src/polyswarm_api.egg-info}/PKG-INFO +7 -3
- polyswarm-api-3.8.0/src/polyswarm_api.egg-info/requires.txt +2 -0
- polyswarm-api-3.6.0/requirements.txt +0 -7
- polyswarm-api-3.6.0/src/polyswarm_api.egg-info/requires.txt +0 -6
- {polyswarm-api-3.6.0 → polyswarm-api-3.8.0}/LICENSE +0 -0
- {polyswarm-api-3.6.0 → polyswarm-api-3.8.0}/MANIFEST.in +0 -0
- {polyswarm-api-3.6.0 → polyswarm-api-3.8.0}/setup.cfg +0 -0
- {polyswarm-api-3.6.0 → polyswarm-api-3.8.0}/src/polyswarm_api.egg-info/SOURCES.txt +0 -0
- {polyswarm-api-3.6.0 → polyswarm-api-3.8.0}/src/polyswarm_api.egg-info/dependency_links.txt +0 -0
- {polyswarm-api-3.6.0 → polyswarm-api-3.8.0}/src/polyswarm_api.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: polyswarm-api
|
|
3
|
-
Version: 3.
|
|
3
|
+
Version: 3.8.0
|
|
4
4
|
Summary: Client library to simplify interacting with the PolySwarm consumer API
|
|
5
5
|
Home-page: https://github.com/polyswarm/polyswarm-api
|
|
6
6
|
Author: PolySwarm Developers
|
|
@@ -11,8 +11,12 @@ Classifier: License :: OSI Approved :: MIT License
|
|
|
11
11
|
Classifier: Operating System :: OS Independent
|
|
12
12
|
Classifier: Programming Language :: Python :: 3.7
|
|
13
13
|
Classifier: Programming Language :: Python :: 3.8
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
14
18
|
Classifier: Programming Language :: Python :: Implementation :: PyPy
|
|
15
|
-
Requires-Python:
|
|
19
|
+
Requires-Python: >=3.7,<4
|
|
16
20
|
Description-Content-Type: text/markdown
|
|
17
21
|
License-File: LICENSE
|
|
18
22
|
|
|
@@ -21,7 +25,7 @@ License-File: LICENSE
|
|
|
21
25
|
An interface to the PolySwarm Customer APIs.
|
|
22
26
|
For an easy-to-use CLI tool, or as an example of how to use these APIs, please see [polyswarm-cli](https://github.com/polyswarm/polyswarm-cli)
|
|
23
27
|
|
|
24
|
-
Supports Python
|
|
28
|
+
Supports Python 3.7 and greater.
|
|
25
29
|
|
|
26
30
|
## Installation
|
|
27
31
|
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
An interface to the PolySwarm Customer APIs.
|
|
4
4
|
For an easy-to-use CLI tool, or as an example of how to use these APIs, please see [polyswarm-cli](https://github.com/polyswarm/polyswarm-cli)
|
|
5
5
|
|
|
6
|
-
Supports Python
|
|
6
|
+
Supports Python 3.7 and greater.
|
|
7
7
|
|
|
8
8
|
## Installation
|
|
9
9
|
|
|
@@ -9,7 +9,7 @@ with open('README.md', 'r') as readme:
|
|
|
9
9
|
|
|
10
10
|
setup(
|
|
11
11
|
name='polyswarm-api',
|
|
12
|
-
version='3.
|
|
12
|
+
version='3.8.0',
|
|
13
13
|
description='Client library to simplify interacting with the PolySwarm consumer API',
|
|
14
14
|
long_description=long_description,
|
|
15
15
|
long_description_content_type='text/markdown',
|
|
@@ -17,13 +17,11 @@ setup(
|
|
|
17
17
|
author_email='info@polyswarm.io',
|
|
18
18
|
url='https://github.com/polyswarm/polyswarm-api',
|
|
19
19
|
license='MIT',
|
|
20
|
-
python_requires='
|
|
20
|
+
python_requires='>=3.7,<4',
|
|
21
21
|
install_requires=[
|
|
22
|
-
'requests~=2.
|
|
23
|
-
'
|
|
24
|
-
'python-dateutil~=2.8',
|
|
22
|
+
'requests~=2.31',
|
|
23
|
+
'python-dateutil~=2.9',
|
|
25
24
|
],
|
|
26
|
-
extras_require={':python_version < "3.0"': ['enum34==1.1.6']},
|
|
27
25
|
include_package_data=True,
|
|
28
26
|
packages=find_packages('src'),
|
|
29
27
|
package_dir={'': 'src'},
|
|
@@ -33,6 +31,10 @@ setup(
|
|
|
33
31
|
'Operating System :: OS Independent',
|
|
34
32
|
'Programming Language :: Python :: 3.7',
|
|
35
33
|
'Programming Language :: Python :: 3.8',
|
|
34
|
+
'Programming Language :: Python :: 3.9',
|
|
35
|
+
'Programming Language :: Python :: 3.10',
|
|
36
|
+
'Programming Language :: Python :: 3.11',
|
|
37
|
+
'Programming Language :: Python :: 3.12',
|
|
36
38
|
'Programming Language :: Python :: Implementation :: PyPy',
|
|
37
39
|
]
|
|
38
40
|
)
|
|
@@ -3,19 +3,14 @@ import time
|
|
|
3
3
|
|
|
4
4
|
import polyswarm_api.core
|
|
5
5
|
|
|
6
|
-
|
|
7
|
-
from urllib.parse import urlparse
|
|
8
|
-
except ImportError:
|
|
9
|
-
from urlparse import urlparse
|
|
10
|
-
|
|
11
|
-
from future.utils import string_types
|
|
6
|
+
from urllib.parse import urlparse
|
|
12
7
|
|
|
13
8
|
from polyswarm_api import exceptions, resources, settings
|
|
14
9
|
|
|
15
10
|
logger = logging.getLogger(__name__)
|
|
16
11
|
|
|
17
12
|
|
|
18
|
-
class PolyswarmAPI
|
|
13
|
+
class PolyswarmAPI:
|
|
19
14
|
"""A synchronous interface to the public and private PolySwarm APIs."""
|
|
20
15
|
|
|
21
16
|
def __init__(self, key, uri=None, community=None, timeout=None, verify=True, **kwargs):
|
|
@@ -35,6 +30,11 @@ class PolyswarmAPI(object):
|
|
|
35
30
|
self.session = polyswarm_api.core.PolyswarmSession(key, retries=settings.DEFAULT_RETRIES, verify=verify, **kwargs)
|
|
36
31
|
self._engines = None
|
|
37
32
|
|
|
33
|
+
def __repr__(self):
|
|
34
|
+
clsname = f'{self.__class__.__module__}.{self.__class__.__name__}'
|
|
35
|
+
attrs = f'uri={self.uri!r}, community={self.community!r}, timeout={self.timeout!r}'
|
|
36
|
+
return f'<{clsname}({attrs}) at 0x{id(self):x}>'
|
|
37
|
+
|
|
38
38
|
@property
|
|
39
39
|
def engines(self):
|
|
40
40
|
if not self._engines:
|
|
@@ -44,11 +44,11 @@ class PolyswarmAPI(object):
|
|
|
44
44
|
|
|
45
45
|
def refresh_engine_cache(self):
|
|
46
46
|
"""
|
|
47
|
-
|
|
47
|
+
Refresh the cached engine listing
|
|
48
48
|
"""
|
|
49
49
|
engines = list(resources.Engine.list(self).result())
|
|
50
50
|
if not engines:
|
|
51
|
-
raise exceptions.InvalidValueException("
|
|
51
|
+
raise exceptions.InvalidValueException("Received empty engines listing")
|
|
52
52
|
self._engines = engines
|
|
53
53
|
|
|
54
54
|
def wait_for(self, scan, timeout=settings.DEFAULT_SCAN_TIMEOUT):
|
|
@@ -66,8 +66,8 @@ class PolyswarmAPI(object):
|
|
|
66
66
|
if scan_result.failed or scan_result.window_closed:
|
|
67
67
|
return scan_result
|
|
68
68
|
elif -1 < timeout < time.time() - start:
|
|
69
|
-
raise exceptions.TimeoutException(
|
|
70
|
-
|
|
69
|
+
raise exceptions.TimeoutException(
|
|
70
|
+
f'Timed out waiting for scan {scan} to finish. Please try again.')
|
|
71
71
|
else:
|
|
72
72
|
time.sleep(settings.POLL_FREQUENCY)
|
|
73
73
|
|
|
@@ -211,14 +211,26 @@ class PolyswarmAPI(object):
|
|
|
211
211
|
logger.info('Deleting known good ioc %s', id)
|
|
212
212
|
return resources.IOC.delete_known_good(self, id).result()
|
|
213
213
|
|
|
214
|
-
def submit(
|
|
214
|
+
def submit(
|
|
215
|
+
self,
|
|
216
|
+
artifact,
|
|
217
|
+
artifact_type=resources.ArtifactType.FILE,
|
|
218
|
+
artifact_name=None,
|
|
219
|
+
scan_config=None,
|
|
220
|
+
is_zip=False,
|
|
221
|
+
zip_password=None,
|
|
222
|
+
):
|
|
215
223
|
"""
|
|
216
|
-
Submit artifacts to polyswarm
|
|
224
|
+
Submit artifacts to polyswarm
|
|
217
225
|
|
|
218
226
|
:param artifact: A file-like, path to file, url or LocalArtifact instance
|
|
219
227
|
:param artifact_type: The ArtifactType or strings containing "file" or "url"
|
|
220
228
|
:param artifact_name: An appropriate filename for the Artifact
|
|
221
229
|
:param scan_config: The scan configuration to be used, e.g.: "default", "more-time", "most-time"
|
|
230
|
+
:param is_zip: If this flag is set, will handle the file as a zip
|
|
231
|
+
in the server and decompress before processing.
|
|
232
|
+
:param zip_password: Will use this password to decompress the zip file.
|
|
233
|
+
If provided, will handle the file as a zip.
|
|
222
234
|
:return: An ArtifactInstance resource
|
|
223
235
|
"""
|
|
224
236
|
logger.info('Submitting artifact of type %s', artifact_type)
|
|
@@ -229,7 +241,7 @@ class PolyswarmAPI(object):
|
|
|
229
241
|
if hasattr(artifact, 'read') and hasattr(artifact.read, '__call__'):
|
|
230
242
|
artifact = resources.LocalArtifact.from_handle(self, artifact, artifact_name=artifact_name or '',
|
|
231
243
|
artifact_type=artifact_type)
|
|
232
|
-
elif isinstance(artifact,
|
|
244
|
+
elif isinstance(artifact, str):
|
|
233
245
|
if artifact_type == resources.ArtifactType.FILE:
|
|
234
246
|
artifact = resources.LocalArtifact.from_path(self, artifact, artifact_type=artifact_type,
|
|
235
247
|
artifact_name=artifact_name)
|
|
@@ -239,11 +251,15 @@ class PolyswarmAPI(object):
|
|
|
239
251
|
if artifact_type == resources.ArtifactType.URL:
|
|
240
252
|
scan_config = scan_config or 'more-time'
|
|
241
253
|
if isinstance(artifact, resources.LocalArtifact):
|
|
242
|
-
instance = resources.ArtifactInstance.create(
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
254
|
+
instance = resources.ArtifactInstance.create(
|
|
255
|
+
self,
|
|
256
|
+
artifact_name=artifact.artifact_name,
|
|
257
|
+
artifact_type=artifact.artifact_type.name,
|
|
258
|
+
scan_config=scan_config,
|
|
259
|
+
community=self.community,
|
|
260
|
+
is_zip=is_zip,
|
|
261
|
+
zip_password=zip_password,
|
|
262
|
+
).result()
|
|
247
263
|
instance.upload_file(artifact)
|
|
248
264
|
return resources.ArtifactInstance.update(self, id=instance.id, community=self.community).result()
|
|
249
265
|
else:
|
|
@@ -284,7 +300,7 @@ class PolyswarmAPI(object):
|
|
|
284
300
|
return resources.ArtifactInstance.rescan_id(self, scan, scan_config=scan_config).result()
|
|
285
301
|
|
|
286
302
|
def _parse_rule(self, rule):
|
|
287
|
-
if isinstance(rule,
|
|
303
|
+
if isinstance(rule, str):
|
|
288
304
|
rule, rule_id = resources.YaraRuleset(dict(yara=rule), api=self), None
|
|
289
305
|
elif isinstance(rule, (resources.YaraRuleset, int)):
|
|
290
306
|
rule, rule_id = None, rule
|
|
@@ -690,7 +706,17 @@ class PolyswarmAPI(object):
|
|
|
690
706
|
return resources.SandboxTask.create(self, artifact_id=instance_id, provider_slug=provider_slug, vm_slug=vm_slug,
|
|
691
707
|
network_enabled=network_enabled).result()
|
|
692
708
|
|
|
693
|
-
def sandbox_file(
|
|
709
|
+
def sandbox_file(
|
|
710
|
+
self,
|
|
711
|
+
artifact,
|
|
712
|
+
provider_slug,
|
|
713
|
+
vm_slug,
|
|
714
|
+
artifact_type=resources.ArtifactType.FILE,
|
|
715
|
+
artifact_name=None,
|
|
716
|
+
network_enabled=True,
|
|
717
|
+
is_zip=False,
|
|
718
|
+
zip_password=None,
|
|
719
|
+
):
|
|
694
720
|
logger.info('Sandboxing file in provider %s vm %s', provider_slug, vm_slug)
|
|
695
721
|
artifact_type = resources.ArtifactType.parse(artifact_type)
|
|
696
722
|
# TODO This is a python 2.7 check if artifact is a file-like instance, consider changing
|
|
@@ -699,7 +725,7 @@ class PolyswarmAPI(object):
|
|
|
699
725
|
if hasattr(artifact, 'read') and hasattr(artifact.read, '__call__'):
|
|
700
726
|
artifact = resources.LocalArtifact.from_handle(self, artifact, artifact_name=artifact_name or '',
|
|
701
727
|
artifact_type=artifact_type)
|
|
702
|
-
elif isinstance(artifact,
|
|
728
|
+
elif isinstance(artifact, str):
|
|
703
729
|
if artifact_type == resources.ArtifactType.FILE:
|
|
704
730
|
artifact = resources.LocalArtifact.from_path(self, artifact, artifact_type=artifact_type,
|
|
705
731
|
artifact_name=artifact_name)
|
|
@@ -708,13 +734,17 @@ class PolyswarmAPI(object):
|
|
|
708
734
|
artifact_name=artifact_name or artifact,
|
|
709
735
|
artifact_type=artifact_type)
|
|
710
736
|
if isinstance(artifact, resources.LocalArtifact):
|
|
711
|
-
task = resources.SandboxTask.create_file(
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
737
|
+
task = resources.SandboxTask.create_file(
|
|
738
|
+
self,
|
|
739
|
+
artifact_name=artifact.artifact_name,
|
|
740
|
+
artifact_type=artifact.artifact_type.name,
|
|
741
|
+
community=self.community,
|
|
742
|
+
provider_slug=provider_slug,
|
|
743
|
+
vm_slug=vm_slug,
|
|
744
|
+
network_enabled=network_enabled,
|
|
745
|
+
is_zip=is_zip,
|
|
746
|
+
zip_password=zip_password,
|
|
747
|
+
).result()
|
|
718
748
|
task.upload_file(artifact)
|
|
719
749
|
return resources.SandboxTask.update_file(self, id=task.id, community=self.community).result()
|
|
720
750
|
else:
|
|
@@ -823,7 +853,128 @@ class PolyswarmAPI(object):
|
|
|
823
853
|
logger.info('List events')
|
|
824
854
|
return resources.Events.list(self, **kwargs).result()
|
|
825
855
|
|
|
826
|
-
def
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
856
|
+
def report_create(self,
|
|
857
|
+
type,
|
|
858
|
+
format,
|
|
859
|
+
instance_id=None,
|
|
860
|
+
sandbox_task_id=None,
|
|
861
|
+
template_id=None,
|
|
862
|
+
template_metadata=None,
|
|
863
|
+
**kwargs):
|
|
864
|
+
"""
|
|
865
|
+
Create a report, either 'pdf' or 'html' (format argument).
|
|
866
|
+
Regarding the type argument, either instance_id (type='scan')
|
|
867
|
+
or sandbox_task_id (type='sandbox') has to be provided.
|
|
868
|
+
"""
|
|
869
|
+
report = resources.ReportTask.create(self,
|
|
870
|
+
type=type,
|
|
871
|
+
format=format,
|
|
872
|
+
instance_id=instance_id,
|
|
873
|
+
sandbox_task_id=sandbox_task_id,
|
|
874
|
+
template_id=template_id,
|
|
875
|
+
template_metadata=template_metadata,
|
|
876
|
+
**kwargs).result()
|
|
877
|
+
return report
|
|
878
|
+
|
|
879
|
+
def report_get(self, id, **kwargs):
|
|
880
|
+
return resources.ReportTask.get(self, id=id, **kwargs).result()
|
|
881
|
+
|
|
882
|
+
def report_download(self, report_id, folder):
|
|
883
|
+
report = resources.ReportTask.get(self, id=report_id).result()
|
|
884
|
+
if report.state == 'PENDING':
|
|
885
|
+
raise exceptions.InvalidValueException('Report is in PENDING state, wait for completion first')
|
|
886
|
+
if report.state == 'FAILED':
|
|
887
|
+
raise exceptions.InvalidValueException("Report is in FAILED state, won't be generated")
|
|
888
|
+
result = report.download_report(folder=folder).result()
|
|
889
|
+
result.handle.close()
|
|
890
|
+
return result
|
|
891
|
+
|
|
892
|
+
def report_wait_for(self, report_id, timeout=settings.DEFAULT_REPORT_TIMEOUT):
|
|
893
|
+
"""
|
|
894
|
+
Wait for a Report to finish successfully.
|
|
895
|
+
|
|
896
|
+
:param report_id: Report id to wait for
|
|
897
|
+
:param timeout: Maximum time in seconds to wait before raising a TimeoutException
|
|
898
|
+
:return: The ReportTask resource waited on, either in 'SUCCEEDED' or 'FAILED' state
|
|
899
|
+
"""
|
|
900
|
+
logger.info('Waiting for report %s', report_id)
|
|
901
|
+
start = time.time()
|
|
902
|
+
while True:
|
|
903
|
+
report_result = self.report_get(id=report_id)
|
|
904
|
+
if report_result.state != 'PENDING':
|
|
905
|
+
return report_result
|
|
906
|
+
elif -1 < timeout < time.time() - start:
|
|
907
|
+
raise exceptions.TimeoutException(
|
|
908
|
+
f'Timed out waiting for report {report_id} to finish. Please try again.')
|
|
909
|
+
else:
|
|
910
|
+
time.sleep(settings.POLL_FREQUENCY)
|
|
911
|
+
|
|
912
|
+
def report_template_logo_download(self, template_id, folder):
|
|
913
|
+
report = resources.ReportTemplate.get(self, id=template_id).result()
|
|
914
|
+
result = report.download_logo(folder).result()
|
|
915
|
+
result.handle.close()
|
|
916
|
+
return result
|
|
917
|
+
|
|
918
|
+
def report_template_logo_delete(self, template_id):
|
|
919
|
+
report = resources.ReportTemplate.get(self, id=template_id).result()
|
|
920
|
+
result = report.delete_logo().result()
|
|
921
|
+
return result
|
|
922
|
+
|
|
923
|
+
def report_template_logo_upload(self, template_id, logo_file, content_tpe):
|
|
924
|
+
report = resources.ReportTemplate.get(self, id=template_id).result()
|
|
925
|
+
result = report.upload_logo(logo_file, content_tpe).result()
|
|
926
|
+
return result
|
|
927
|
+
|
|
928
|
+
def report_template_create(self,
|
|
929
|
+
template_name,
|
|
930
|
+
is_default=False,
|
|
931
|
+
primary_color=None,
|
|
932
|
+
footer_text=None,
|
|
933
|
+
last_page_text=None,
|
|
934
|
+
includes=None,
|
|
935
|
+
**kwargs):
|
|
936
|
+
"""
|
|
937
|
+
Create a template for reports.
|
|
938
|
+
A team account can have multiple templates, and setting is_default=True
|
|
939
|
+
makes it the default when a report is created without specifying template_id
|
|
940
|
+
(see report_create method).
|
|
941
|
+
The includes argument can be a list with sections to be included
|
|
942
|
+
in the reports. The list can include any of the following section names:
|
|
943
|
+
summary, detections, fileMetadata, network, droppedFiles, extractedConfig, analysis.
|
|
944
|
+
"""
|
|
945
|
+
return resources.ReportTemplate.create(self,
|
|
946
|
+
template_name=template_name,
|
|
947
|
+
is_default=is_default,
|
|
948
|
+
primary_color=primary_color,
|
|
949
|
+
footer_text=footer_text,
|
|
950
|
+
last_page_text=last_page_text,
|
|
951
|
+
includes=includes,
|
|
952
|
+
**kwargs).result()
|
|
953
|
+
|
|
954
|
+
def report_template_update(self,
|
|
955
|
+
template_id,
|
|
956
|
+
template_name=None,
|
|
957
|
+
is_default=None,
|
|
958
|
+
primary_color=None,
|
|
959
|
+
footer_text=None,
|
|
960
|
+
last_page_text=None,
|
|
961
|
+
includes=None,
|
|
962
|
+
**kwargs):
|
|
963
|
+
return resources.ReportTemplate.update(self,
|
|
964
|
+
id=template_id,
|
|
965
|
+
template_name=template_name,
|
|
966
|
+
is_default=is_default,
|
|
967
|
+
primary_color=primary_color,
|
|
968
|
+
footer_text=footer_text,
|
|
969
|
+
last_page_text=last_page_text,
|
|
970
|
+
includes=includes,
|
|
971
|
+
**kwargs).result()
|
|
972
|
+
|
|
973
|
+
def report_template_get(self, template_id):
|
|
974
|
+
return resources.ReportTemplate.get(self, id=template_id).result()
|
|
975
|
+
|
|
976
|
+
def report_template_delete(self, template_id):
|
|
977
|
+
return resources.ReportTemplate.delete(self, id=template_id).result()
|
|
978
|
+
|
|
979
|
+
def report_template_list(self, is_default=None, **kwargs):
|
|
980
|
+
return resources.ReportTemplate.list(self, is_default=is_default, **kwargs).result()
|
|
@@ -3,15 +3,11 @@ import logging
|
|
|
3
3
|
from copy import deepcopy
|
|
4
4
|
from urllib3 import Retry
|
|
5
5
|
from binascii import unhexlify
|
|
6
|
-
|
|
7
|
-
from json.decoder import JSONDecodeError
|
|
8
|
-
except ImportError:
|
|
9
|
-
JSONDecodeError = ValueError
|
|
6
|
+
from json.decoder import JSONDecodeError
|
|
10
7
|
|
|
11
8
|
import requests
|
|
12
9
|
import datetime as dt
|
|
13
10
|
from dateutil import parser
|
|
14
|
-
from future.utils import raise_from
|
|
15
11
|
from requests.adapters import HTTPAdapter
|
|
16
12
|
from urllib3.exceptions import InsecureRequestWarning
|
|
17
13
|
|
|
@@ -22,7 +18,7 @@ logger = logging.getLogger(__name__)
|
|
|
22
18
|
|
|
23
19
|
class PolyswarmSession(requests.Session):
|
|
24
20
|
def __init__(self, key, retries, user_agent=settings.DEFAULT_USER_AGENT, verify=True, **kwargs):
|
|
25
|
-
super(
|
|
21
|
+
super().__init__(**kwargs)
|
|
26
22
|
logger.debug('Creating PolyswarmHTTP instance')
|
|
27
23
|
self.requests_retry_session(retries=retries)
|
|
28
24
|
|
|
@@ -118,13 +114,12 @@ class PolyswarmRequest(object):
|
|
|
118
114
|
|
|
119
115
|
def _bad_status_message(self):
|
|
120
116
|
request_parameters = json.dumps(self.request_parameters, indent=4, sort_keys=True, cls=RequestParamsEncoder)
|
|
121
|
-
message = "Error when running the request:\n{}\n" \
|
|
122
|
-
"Return code: {}\n" \
|
|
123
|
-
"Message: {}"
|
|
124
|
-
self.status_code,
|
|
125
|
-
self._result)
|
|
117
|
+
message = f"Error when running the request:\n{request_parameters}\n" \
|
|
118
|
+
f"Return code: {self.status_code}\n" \
|
|
119
|
+
f"Message: {self._result}"
|
|
126
120
|
if self.errors:
|
|
127
|
-
|
|
121
|
+
errors = '\n'.join(str(error) for error in self.errors)
|
|
122
|
+
message = f'{message}\nErrors:\n{errors}'
|
|
128
123
|
return message
|
|
129
124
|
|
|
130
125
|
def _extract_json_body(self, result):
|
|
@@ -146,10 +141,10 @@ class PolyswarmRequest(object):
|
|
|
146
141
|
if self.status_code // 100 != 2:
|
|
147
142
|
self._extract_json_body(result)
|
|
148
143
|
if self.status_code == 429:
|
|
149
|
-
message = '{} This may mean you need to purchase a ' \
|
|
144
|
+
message = f'{self._result} This may mean you need to purchase a ' \
|
|
150
145
|
'larger package, or that you have exceeded ' \
|
|
151
146
|
'rate limits. If you continue to have issues, ' \
|
|
152
|
-
'please contact us at info@polyswarm.io.'
|
|
147
|
+
'please contact us at info@polyswarm.io.'
|
|
153
148
|
raise exceptions.UsageLimitsExceededException(self, message)
|
|
154
149
|
elif self.status_code == 404:
|
|
155
150
|
raise exceptions.NotFoundException(self, self._result)
|
|
@@ -185,10 +180,10 @@ class PolyswarmRequest(object):
|
|
|
185
180
|
self._result = self.result_parser.parse_result(self.api_instance, result, **self.parser_kwargs)
|
|
186
181
|
except JSONDecodeError as e:
|
|
187
182
|
if self.status_code == 404:
|
|
188
|
-
raise
|
|
183
|
+
raise exceptions.NotFoundException(self, 'The requested endpoint does not exist.') from e
|
|
189
184
|
else:
|
|
190
|
-
err_msg = 'Server returned non-JSON response [{}]: {}'
|
|
191
|
-
raise
|
|
185
|
+
err_msg = f'Server returned non-JSON response [{self.status_code}]: {result}'
|
|
186
|
+
raise exceptions.RequestException(self, err_msg) from e
|
|
192
187
|
|
|
193
188
|
def __iter__(self):
|
|
194
189
|
return self.consume_results()
|
|
@@ -232,12 +227,12 @@ class PolyswarmRequest(object):
|
|
|
232
227
|
).execute()
|
|
233
228
|
|
|
234
229
|
|
|
235
|
-
class BaseResource
|
|
230
|
+
class BaseResource:
|
|
236
231
|
def __init__(self, content, *args, **kwargs):
|
|
237
232
|
# hack to behave as in python 3, signature should be
|
|
238
233
|
# __init__(self, content, *args, api=None, **kwargs)
|
|
239
234
|
api = kwargs.pop('api', None)
|
|
240
|
-
super(
|
|
235
|
+
super().__init__(*args, **kwargs)
|
|
241
236
|
self.api = api
|
|
242
237
|
self._content = content
|
|
243
238
|
|
|
@@ -252,13 +247,13 @@ class BaseJsonResource(BaseResource):
|
|
|
252
247
|
RESOURCE_ID_KEYS = ['id']
|
|
253
248
|
|
|
254
249
|
def __init__(self, content, *args, **kwargs):
|
|
255
|
-
super(
|
|
250
|
+
super().__init__(content, *args, **kwargs)
|
|
256
251
|
self.json = content
|
|
257
252
|
|
|
258
253
|
def __int__(self):
|
|
259
254
|
id_ = getattr(self, 'id', None)
|
|
260
255
|
if id_ is None:
|
|
261
|
-
raise TypeError('Resource {} does not have an id and can not be cast to int'
|
|
256
|
+
raise TypeError(f'Resource {type(self).__name__} does not have an id and can not be cast to int')
|
|
262
257
|
return int(id_)
|
|
263
258
|
|
|
264
259
|
def _get(self, path, default=None, content=None):
|
|
@@ -271,16 +266,16 @@ class BaseJsonResource(BaseResource):
|
|
|
271
266
|
try:
|
|
272
267
|
for attribute in path.split('.'):
|
|
273
268
|
if obj is None:
|
|
274
|
-
raise KeyError('{} is None, can not resolve full path'
|
|
269
|
+
raise KeyError(f'{previous_attribute} is None, can not resolve full path')
|
|
275
270
|
if attribute.endswith(']'):
|
|
276
271
|
# handling the list case, e.g.: "root.list_attr[2]"
|
|
277
272
|
attribute, _, index = attribute.rpartition('[')
|
|
278
273
|
index = int(index.rstrip(']'))
|
|
279
274
|
obj = obj[attribute]
|
|
280
275
|
if obj is None:
|
|
281
|
-
raise KeyError('{} is None, but is it supposed to be a list'
|
|
276
|
+
raise KeyError(f'{attribute} is None, but is it supposed to be a list')
|
|
282
277
|
elif not isinstance(obj, list):
|
|
283
|
-
raise ValueError('Can not access index for {}, it is not a list.'
|
|
278
|
+
raise ValueError(f'Can not access index for {attribute}, it is not a list.')
|
|
284
279
|
else:
|
|
285
280
|
obj = obj[index]
|
|
286
281
|
else:
|
|
@@ -469,7 +464,7 @@ def is_valid_sha256(value):
|
|
|
469
464
|
return is_hex(value)
|
|
470
465
|
|
|
471
466
|
|
|
472
|
-
class Hashable
|
|
467
|
+
class Hashable:
|
|
473
468
|
SUPPORTED_HASH_TYPES = {
|
|
474
469
|
'sha1': is_valid_sha1,
|
|
475
470
|
'sha256': is_valid_sha256,
|
|
@@ -482,7 +477,7 @@ class Hashable(object):
|
|
|
482
477
|
hash_value = kwargs.pop('hash_value', None)
|
|
483
478
|
hash_type = kwargs.pop('hash_type', None)
|
|
484
479
|
validate_hash = kwargs.pop('validate_hash', False)
|
|
485
|
-
super(
|
|
480
|
+
super().__init__(*args, **kwargs)
|
|
486
481
|
|
|
487
482
|
self._hash = hash_value.strip() if hash_value is not None else None
|
|
488
483
|
|
|
@@ -494,7 +489,7 @@ class Hashable(object):
|
|
|
494
489
|
self._hash_type = self.resolve_hash_type()
|
|
495
490
|
|
|
496
491
|
if self._hash_type is None:
|
|
497
|
-
raise exceptions.InvalidValueException('Invalid hash provided: {
|
|
492
|
+
raise exceptions.InvalidValueException(f'Invalid hash provided: {self._hash}')
|
|
498
493
|
|
|
499
494
|
if validate_hash:
|
|
500
495
|
self.validate()
|
|
@@ -514,8 +509,8 @@ class Hashable(object):
|
|
|
514
509
|
def validate(self):
|
|
515
510
|
hash_type = self.resolve_hash_type()
|
|
516
511
|
if self.hash_type != hash_type:
|
|
517
|
-
raise exceptions.InvalidValueException(
|
|
518
|
-
|
|
512
|
+
raise exceptions.InvalidValueException(
|
|
513
|
+
f'Detected hash type {hash_type}, got type {self.hash_type} for hash {self.hash}')
|
|
519
514
|
|
|
520
515
|
def resolve_hash_type(self):
|
|
521
516
|
for hash_type, validator in self.SUPPORTED_HASH_TYPES.items():
|
|
@@ -5,15 +5,9 @@ import functools
|
|
|
5
5
|
import requests
|
|
6
6
|
from enum import Enum
|
|
7
7
|
from hashlib import sha256 as _sha256, sha1 as _sha1, md5 as _md5
|
|
8
|
+
from urllib.parse import urlparse
|
|
8
9
|
|
|
9
|
-
|
|
10
|
-
from urllib.parse import urlparse
|
|
11
|
-
except ImportError:
|
|
12
|
-
from urlparse import urlparse
|
|
13
|
-
|
|
14
|
-
from future.utils import raise_from, string_types
|
|
15
|
-
|
|
16
|
-
# Windows might rase an OSError instead of an ImportError like this
|
|
10
|
+
# Windows might raise an OSError instead of an ImportError like this
|
|
17
11
|
# OSError: [WinError 193] %1 is not a valid Win32 application
|
|
18
12
|
try:
|
|
19
13
|
import yara
|
|
@@ -33,7 +27,7 @@ class Engine(core.BaseJsonResource):
|
|
|
33
27
|
RESOURCE_ENDPOINT = '/microengines'
|
|
34
28
|
|
|
35
29
|
def __init__(self, content, api=None):
|
|
36
|
-
super(
|
|
30
|
+
super().__init__(content=content, api=api)
|
|
37
31
|
self.id = str(content['id'])
|
|
38
32
|
self.name = content['name']
|
|
39
33
|
|
|
@@ -95,7 +89,7 @@ class Metadata(core.BaseJsonResource):
|
|
|
95
89
|
KNOWN_KEYS = {'artifact', 'exiftool', 'hash', 'lief', 'pefile', 'scan', 'strings'}
|
|
96
90
|
|
|
97
91
|
def __init__(self, content, api=None):
|
|
98
|
-
super(
|
|
92
|
+
super().__init__(content=content, api=api)
|
|
99
93
|
self.created = core.parse_isoformat(self.artifact.get('created'))
|
|
100
94
|
|
|
101
95
|
self.id = self._get('artifact.id')
|
|
@@ -144,7 +138,7 @@ class Metadata(core.BaseJsonResource):
|
|
|
144
138
|
params.extend(('ips', v) for v in ips)
|
|
145
139
|
params.extend(('urls', v) for v in urls)
|
|
146
140
|
params.extend(('domains', v) for v in domains)
|
|
147
|
-
super_params, json_params = super(
|
|
141
|
+
super_params, json_params = super()._get_params(**kwargs)
|
|
148
142
|
params.extend(super_params.items())
|
|
149
143
|
return params, json_params
|
|
150
144
|
|
|
@@ -159,7 +153,7 @@ class IOC(core.BaseJsonResource):
|
|
|
159
153
|
api,
|
|
160
154
|
{
|
|
161
155
|
'method': 'GET',
|
|
162
|
-
'url': '{}/{}/{}/{}'
|
|
156
|
+
'url': f'{api.uri}/{path}/{hash_type}/{hash_value}',
|
|
163
157
|
'params': {
|
|
164
158
|
'hide_known_good': hide_known_good,
|
|
165
159
|
'community': api.community,
|
|
@@ -183,7 +177,7 @@ class IOC(core.BaseJsonResource):
|
|
|
183
177
|
api,
|
|
184
178
|
{
|
|
185
179
|
'method': 'GET',
|
|
186
|
-
'url': '{}/ioc/search'
|
|
180
|
+
'url': f'{api.uri}/ioc/search',
|
|
187
181
|
'params': params
|
|
188
182
|
},
|
|
189
183
|
result_parser=cls,
|
|
@@ -195,7 +189,7 @@ class IOC(core.BaseJsonResource):
|
|
|
195
189
|
api,
|
|
196
190
|
{
|
|
197
191
|
'method': 'GET',
|
|
198
|
-
'url': '{}/ioc/known'
|
|
192
|
+
'url': f'{api.uri}/ioc/known',
|
|
199
193
|
'params': {
|
|
200
194
|
'ip': ips,
|
|
201
195
|
'domain': domains
|
|
@@ -210,7 +204,7 @@ class IOC(core.BaseJsonResource):
|
|
|
210
204
|
api,
|
|
211
205
|
{
|
|
212
206
|
'method': 'POST',
|
|
213
|
-
'url': '{}/ioc/known'
|
|
207
|
+
'url': f'{api.uri}/ioc/known',
|
|
214
208
|
'json': {
|
|
215
209
|
'type': type,
|
|
216
210
|
'host': host,
|
|
@@ -227,7 +221,7 @@ class IOC(core.BaseJsonResource):
|
|
|
227
221
|
api,
|
|
228
222
|
{
|
|
229
223
|
'method': 'POST',
|
|
230
|
-
'url': '{}/ioc/known'
|
|
224
|
+
'url': f'{api.uri}/ioc/known',
|
|
231
225
|
'json': {
|
|
232
226
|
'type': type,
|
|
233
227
|
'host': host,
|
|
@@ -244,7 +238,7 @@ class IOC(core.BaseJsonResource):
|
|
|
244
238
|
api,
|
|
245
239
|
{
|
|
246
240
|
'method': 'PUT',
|
|
247
|
-
'url': '{}/ioc/known'
|
|
241
|
+
'url': f'{api.uri}/ioc/known',
|
|
248
242
|
'json': {
|
|
249
243
|
'id': id,
|
|
250
244
|
'type': type,
|
|
@@ -262,7 +256,7 @@ class IOC(core.BaseJsonResource):
|
|
|
262
256
|
api,
|
|
263
257
|
{
|
|
264
258
|
'method': 'DELETE',
|
|
265
|
-
'url': '{}/ioc/known'
|
|
259
|
+
'url': f'{api.uri}/ioc/known',
|
|
266
260
|
'params': {
|
|
267
261
|
'id': id
|
|
268
262
|
}
|
|
@@ -275,8 +269,8 @@ class ArtifactInstance(core.BaseJsonResource, core.Hashable):
|
|
|
275
269
|
RESOURCE_ENDPOINT = '/instance'
|
|
276
270
|
|
|
277
271
|
def __init__(self, content, api=None):
|
|
278
|
-
super(
|
|
279
|
-
|
|
272
|
+
super().__init__(content=content, api=api,
|
|
273
|
+
hash_value=content['sha256'], hash_type='sha256')
|
|
280
274
|
# Artifact fields
|
|
281
275
|
self.sha256 = content['sha256']
|
|
282
276
|
self.artifact_id = content.get('artifact_id')
|
|
@@ -342,7 +336,7 @@ class ArtifactInstance(core.BaseJsonResource, core.Hashable):
|
|
|
342
336
|
api,
|
|
343
337
|
{
|
|
344
338
|
'method': 'HEAD',
|
|
345
|
-
'url': '{}/search/hash/{}'
|
|
339
|
+
'url': f'{api.uri}/search/hash/{hash_type}',
|
|
346
340
|
'params': {
|
|
347
341
|
'hash': hash_value,
|
|
348
342
|
'community': api.community,
|
|
@@ -356,7 +350,7 @@ class ArtifactInstance(core.BaseJsonResource, core.Hashable):
|
|
|
356
350
|
api,
|
|
357
351
|
{
|
|
358
352
|
'method': 'GET',
|
|
359
|
-
'url': '{}/search/hash/{}'
|
|
353
|
+
'url': f'{api.uri}/search/hash/{hash_type}',
|
|
360
354
|
'params': {
|
|
361
355
|
'hash': hash_value,
|
|
362
356
|
'community': api.community,
|
|
@@ -371,7 +365,7 @@ class ArtifactInstance(core.BaseJsonResource, core.Hashable):
|
|
|
371
365
|
api,
|
|
372
366
|
{
|
|
373
367
|
'method': 'GET',
|
|
374
|
-
'url': '{}/search/url'
|
|
368
|
+
'url': f'{api.uri}/search/url',
|
|
375
369
|
'params': {
|
|
376
370
|
'url': url,
|
|
377
371
|
'community': api.community,
|
|
@@ -386,7 +380,7 @@ class ArtifactInstance(core.BaseJsonResource, core.Hashable):
|
|
|
386
380
|
api,
|
|
387
381
|
{
|
|
388
382
|
'method': 'GET',
|
|
389
|
-
'url': '{}/search/instances'
|
|
383
|
+
'url': f'{api.uri}/search/instances',
|
|
390
384
|
'params': {
|
|
391
385
|
'hash': hash_value,
|
|
392
386
|
'community': api.community,
|
|
@@ -399,7 +393,7 @@ class ArtifactInstance(core.BaseJsonResource, core.Hashable):
|
|
|
399
393
|
def submit(cls, api, artifact, artifact_name, artifact_type, scan_config=None):
|
|
400
394
|
parameters = {
|
|
401
395
|
'method': 'POST',
|
|
402
|
-
'url': '{}/consumer/submission/{
|
|
396
|
+
'url': f'{api.uri}/consumer/submission/{api.community}',
|
|
403
397
|
'files': {
|
|
404
398
|
'file': (artifact_name, artifact),
|
|
405
399
|
},
|
|
@@ -420,8 +414,8 @@ class ArtifactInstance(core.BaseJsonResource, core.Hashable):
|
|
|
420
414
|
def rescan(cls, api, hash_value, hash_type, scan_config=None):
|
|
421
415
|
parameters = {
|
|
422
416
|
'method': 'POST',
|
|
423
|
-
'url': '{}/consumer/submission/{}/rescan/{}/{}'
|
|
424
|
-
'data': {
|
|
417
|
+
'url': f'{api.uri}/consumer/submission/{api.community}/rescan/{hash_type}/{hash_value}',
|
|
418
|
+
'data': {'community': api.community}
|
|
425
419
|
}
|
|
426
420
|
if scan_config:
|
|
427
421
|
parameters['data']['scan-config'] = scan_config
|
|
@@ -435,7 +429,7 @@ class ArtifactInstance(core.BaseJsonResource, core.Hashable):
|
|
|
435
429
|
def rescan_id(cls, api, submission_id, scan_config=None):
|
|
436
430
|
parameters = {
|
|
437
431
|
'method': 'POST',
|
|
438
|
-
'url': '{}/consumer/submission/{}/rescan/{
|
|
432
|
+
'url': f'{api.uri}/consumer/submission/{api.community}/rescan/{int(submission_id)}',
|
|
439
433
|
}
|
|
440
434
|
if scan_config:
|
|
441
435
|
parameters.setdefault('data', {})['scan-config'] = scan_config
|
|
@@ -451,7 +445,7 @@ class ArtifactInstance(core.BaseJsonResource, core.Hashable):
|
|
|
451
445
|
api,
|
|
452
446
|
{
|
|
453
447
|
'method': 'GET',
|
|
454
|
-
'url': '{}/consumer/submission/{}/{
|
|
448
|
+
'url': f'{api.uri}/consumer/submission/{api.community}/{int(submission_id)}',
|
|
455
449
|
},
|
|
456
450
|
result_parser=cls,
|
|
457
451
|
).execute()
|
|
@@ -460,7 +454,7 @@ class ArtifactInstance(core.BaseJsonResource, core.Hashable):
|
|
|
460
454
|
def metadata_rerun(cls, api, hashes, analyses=None, skip_es=None):
|
|
461
455
|
parameters = {
|
|
462
456
|
'method': 'POST',
|
|
463
|
-
'url': '{}/consumer/metadata'
|
|
457
|
+
'url': f'{api.uri}/consumer/metadata',
|
|
464
458
|
'json': {'hashes': hashes},
|
|
465
459
|
}
|
|
466
460
|
if analyses:
|
|
@@ -499,7 +493,7 @@ class ArtifactArchive(core.BaseJsonResource):
|
|
|
499
493
|
RESOURCE_ENDPOINT = '/consumer/download/stream'
|
|
500
494
|
|
|
501
495
|
def __init__(self, content, api=None):
|
|
502
|
-
super(
|
|
496
|
+
super().__init__(content=content, api=api)
|
|
503
497
|
self.id = content['id']
|
|
504
498
|
self.community = content['community']
|
|
505
499
|
self.created = core.parse_isoformat(content['created'])
|
|
@@ -510,7 +504,7 @@ class AssertionsJob(core.BaseJsonResource):
|
|
|
510
504
|
RESOURCE_ENDPOINT = '/consumer/assertions-job'
|
|
511
505
|
|
|
512
506
|
def __init__(self, content, api=None):
|
|
513
|
-
super(
|
|
507
|
+
super().__init__(content=content, api=api)
|
|
514
508
|
self.id = content['id']
|
|
515
509
|
self.engine_id = content['engine_id']
|
|
516
510
|
self.created = core.parse_isoformat(content['created'])
|
|
@@ -530,7 +524,7 @@ class VotesJob(core.BaseJsonResource):
|
|
|
530
524
|
RESOURCE_ENDPOINT = '/consumer/votes-job'
|
|
531
525
|
|
|
532
526
|
def __init__(self, content, api=None):
|
|
533
|
-
super(
|
|
527
|
+
super().__init__(content=content, api=api)
|
|
534
528
|
self.id = content['id']
|
|
535
529
|
self.engine_id = content['engine_id']
|
|
536
530
|
self.created = core.parse_isoformat(content['created'])
|
|
@@ -581,7 +575,7 @@ class LocalArtifact(core.BaseResource, core.Hashable):
|
|
|
581
575
|
raise exceptions.InvalidValueException('At least one of path or handle must be defined.')
|
|
582
576
|
|
|
583
577
|
# initialize super classes and default values
|
|
584
|
-
super(
|
|
578
|
+
super().__init__(response, api=api, hash_type='sha256')
|
|
585
579
|
self.sha256 = None
|
|
586
580
|
self.sha1 = None
|
|
587
581
|
self.md5 = None
|
|
@@ -614,13 +608,8 @@ class LocalArtifact(core.BaseResource, core.Hashable):
|
|
|
614
608
|
remove_on_error = False
|
|
615
609
|
try:
|
|
616
610
|
if folder:
|
|
617
|
-
# TODO: this should be replaced with os.makedirs(path, exist_ok=True)
|
|
618
|
-
# once we drop support to python 2.7
|
|
619
611
|
if not os.path.exists(folder):
|
|
620
|
-
|
|
621
|
-
os.makedirs(folder)
|
|
622
|
-
except FileExistsError:
|
|
623
|
-
pass
|
|
612
|
+
os.makedirs(folder, exist_ok=True)
|
|
624
613
|
remove_on_error = True
|
|
625
614
|
self.handle = open(os.path.join(folder, self.artifact_name), mode='wb+', **kwargs)
|
|
626
615
|
else:
|
|
@@ -652,7 +641,7 @@ class LocalArtifact(core.BaseResource, core.Hashable):
|
|
|
652
641
|
api,
|
|
653
642
|
{
|
|
654
643
|
'method': 'GET',
|
|
655
|
-
'url': '{}/consumer/download/{}/{}'
|
|
644
|
+
'url': f'{api.uri}/consumer/download/{hash_type}/{hash_value}',
|
|
656
645
|
'stream': True,
|
|
657
646
|
'params': { 'community': api.community },
|
|
658
647
|
},
|
|
@@ -668,7 +657,7 @@ class LocalArtifact(core.BaseResource, core.Hashable):
|
|
|
668
657
|
api,
|
|
669
658
|
{
|
|
670
659
|
'method': 'GET',
|
|
671
|
-
'url': '{}/instance/download'
|
|
660
|
+
'url': f'{api.uri}/instance/download',
|
|
672
661
|
'stream': True,
|
|
673
662
|
'params': {'instance_id': instance_id},
|
|
674
663
|
},
|
|
@@ -729,7 +718,7 @@ class LocalArtifact(core.BaseResource, core.Hashable):
|
|
|
729
718
|
|
|
730
719
|
@classmethod
|
|
731
720
|
def from_path(cls, api, path, artifact_type=None, analyze=False, artifact_name=None, **kwargs):
|
|
732
|
-
if not isinstance(path,
|
|
721
|
+
if not isinstance(path, str):
|
|
733
722
|
raise exceptions.InvalidValueException('Path should be a string')
|
|
734
723
|
artifact_name = artifact_name or os.path.basename(path)
|
|
735
724
|
handle = open(path, mode='rb', **kwargs)
|
|
@@ -739,7 +728,7 @@ class LocalArtifact(core.BaseResource, core.Hashable):
|
|
|
739
728
|
|
|
740
729
|
@classmethod
|
|
741
730
|
def from_content(cls, api, content, artifact_name=None, artifact_type=None, analyze=False):
|
|
742
|
-
if isinstance(content,
|
|
731
|
+
if isinstance(content, str):
|
|
743
732
|
content = content.encode("utf8")
|
|
744
733
|
handle = io.BytesIO(content)
|
|
745
734
|
# create the LocalHandle with the given handle and don't write anything to it
|
|
@@ -749,7 +738,7 @@ class LocalArtifact(core.BaseResource, core.Hashable):
|
|
|
749
738
|
@property
|
|
750
739
|
def hash(self):
|
|
751
740
|
self.analyze_artifact()
|
|
752
|
-
return super(
|
|
741
|
+
return super().hash
|
|
753
742
|
|
|
754
743
|
def analyze_artifact(self, force=False):
|
|
755
744
|
if not self.analyzed or force:
|
|
@@ -776,7 +765,7 @@ class YaraRuleset(core.BaseJsonResource):
|
|
|
776
765
|
RESOURCE_ENDPOINT = '/hunt/rule'
|
|
777
766
|
|
|
778
767
|
def __init__(self, content, api=None):
|
|
779
|
-
super(
|
|
768
|
+
super().__init__(content, api=api)
|
|
780
769
|
self.id = content.get('id')
|
|
781
770
|
self.livescan_id = content.get('livescan_id')
|
|
782
771
|
self.livescan_created = content.get('livescan_created')
|
|
@@ -796,7 +785,7 @@ class LiveHuntResult(core.BaseJsonResource):
|
|
|
796
785
|
RESOURCE_ENDPOINT = '/hunt/live'
|
|
797
786
|
|
|
798
787
|
def __init__(self, content, api=None):
|
|
799
|
-
super(
|
|
788
|
+
super().__init__(content=content, api=api)
|
|
800
789
|
self.id = content['id']
|
|
801
790
|
self.livescan_id = content['livescan_id']
|
|
802
791
|
self.instance_id = content['instance_id']
|
|
@@ -820,7 +809,7 @@ class HistoricalHunt(core.BaseJsonResource):
|
|
|
820
809
|
RESOURCE_ENDPOINT = '/hunt/historical'
|
|
821
810
|
|
|
822
811
|
def __init__(self, content, api=None):
|
|
823
|
-
super(
|
|
812
|
+
super().__init__(content=content, api=api)
|
|
824
813
|
# active only present for live hunts
|
|
825
814
|
self.id = content['id']
|
|
826
815
|
self.created = core.parse_isoformat(content['created'])
|
|
@@ -842,7 +831,7 @@ class HistoricalHuntResult(core.BaseJsonResource):
|
|
|
842
831
|
RESOURCE_ENDPOINT = '/hunt/historical/results'
|
|
843
832
|
|
|
844
833
|
def __init__(self, content, api=None):
|
|
845
|
-
super(
|
|
834
|
+
super().__init__(content=content, api=api)
|
|
846
835
|
self.id = content['id']
|
|
847
836
|
self.historicalscan_id = content['historicalscan_id']
|
|
848
837
|
self.instance_id = content['instance_id']
|
|
@@ -856,6 +845,7 @@ class HistoricalHuntResult(core.BaseJsonResource):
|
|
|
856
845
|
self.download_url = content.get('download_url')
|
|
857
846
|
self.community = content.get('community')
|
|
858
847
|
|
|
848
|
+
|
|
859
849
|
class HistoricalHuntResultList(HistoricalHuntResult):
|
|
860
850
|
RESOURCE_ENDPOINT = '/hunt/historical/results/list'
|
|
861
851
|
|
|
@@ -865,7 +855,7 @@ class TagLink(core.BaseJsonResource):
|
|
|
865
855
|
RESOURCE_ID_KEYS = ['hash']
|
|
866
856
|
|
|
867
857
|
def __init__(self, content, api=None):
|
|
868
|
-
super(
|
|
858
|
+
super().__init__(content, api=api)
|
|
869
859
|
self.id = content.get('id')
|
|
870
860
|
self.sha256 = content.get('sha256')
|
|
871
861
|
self.created = core.parse_isoformat(content.get('created'))
|
|
@@ -892,7 +882,7 @@ class MalwareFamily(core.BaseJsonResource):
|
|
|
892
882
|
RESOURCE_ID_KEYS = ['name']
|
|
893
883
|
|
|
894
884
|
def __init__(self, content, api=None):
|
|
895
|
-
super(
|
|
885
|
+
super().__init__(content, api=api)
|
|
896
886
|
self.id = content.get('id')
|
|
897
887
|
self.created = core.parse_isoformat(content.get('created'))
|
|
898
888
|
self.updated = core.parse_isoformat(content.get('updated'))
|
|
@@ -905,7 +895,7 @@ class Tag(core.BaseJsonResource):
|
|
|
905
895
|
RESOURCE_ID_KEYS = ['name']
|
|
906
896
|
|
|
907
897
|
def __init__(self, content, api=None):
|
|
908
|
-
super(
|
|
898
|
+
super().__init__(content, api=api)
|
|
909
899
|
self.id = content.get('id')
|
|
910
900
|
self.created = core.parse_isoformat(content.get('created'))
|
|
911
901
|
self.updated = core.parse_isoformat(content.get('updated'))
|
|
@@ -919,7 +909,7 @@ class Tag(core.BaseJsonResource):
|
|
|
919
909
|
|
|
920
910
|
class Assertion(core.BaseJsonResource):
|
|
921
911
|
def __init__(self, content, api=None, scanfile=None):
|
|
922
|
-
super(
|
|
912
|
+
super().__init__(content, api=api)
|
|
923
913
|
self.scanfile = scanfile
|
|
924
914
|
self.author = content['author']
|
|
925
915
|
self.author_name = content['author_name']
|
|
@@ -936,7 +926,7 @@ class Assertion(core.BaseJsonResource):
|
|
|
936
926
|
|
|
937
927
|
class Vote(core.BaseJsonResource):
|
|
938
928
|
def __init__(self, content, api=None, scanfile=None):
|
|
939
|
-
super(
|
|
929
|
+
super().__init__(content, api=api)
|
|
940
930
|
self.scanfile = scanfile
|
|
941
931
|
self.arbiter = content['arbiter']
|
|
942
932
|
self.vote = content['vote']
|
|
@@ -961,9 +951,8 @@ class ArtifactType(Enum):
|
|
|
961
951
|
try:
|
|
962
952
|
return ArtifactType[value.upper()]
|
|
963
953
|
except Exception as e:
|
|
964
|
-
raise
|
|
965
|
-
|
|
966
|
-
'Unable to get the artifact type from the provided value {}'.format(value)), e)
|
|
954
|
+
raise exceptions.InvalidValueException(
|
|
955
|
+
f'Unable to get the artifact type from the provided value {value}') from e
|
|
967
956
|
|
|
968
957
|
@staticmethod
|
|
969
958
|
def to_string(artifact_type):
|
|
@@ -984,7 +973,7 @@ class ArtifactType(Enum):
|
|
|
984
973
|
|
|
985
974
|
class Hash(core.Hashable):
|
|
986
975
|
def __init__(self, hash_, hash_type=None, validate_hash=True):
|
|
987
|
-
super(
|
|
976
|
+
super().__init__(hash_value=hash_, hash_type=hash_type, validate_hash=validate_hash)
|
|
988
977
|
|
|
989
978
|
@classmethod
|
|
990
979
|
def from_hashable(cls, hash_, hash_type=None):
|
|
@@ -998,8 +987,8 @@ class Hash(core.Hashable):
|
|
|
998
987
|
"""
|
|
999
988
|
if issubclass(type(hash_), core.Hashable):
|
|
1000
989
|
if hash_type and hash_.hash_type != hash_type:
|
|
1001
|
-
raise exceptions.InvalidValueException(
|
|
1002
|
-
|
|
990
|
+
raise exceptions.InvalidValueException(
|
|
991
|
+
f'Detected hash type {hash_.hash_type}, got {hash_type} for hashable {hash_.hash}')
|
|
1003
992
|
return Hash(hash_.hash, hash_type=hash_type)
|
|
1004
993
|
return Hash(hash_, hash_type=hash_type)
|
|
1005
994
|
|
|
@@ -1010,14 +999,14 @@ class Hash(core.Hashable):
|
|
|
1010
999
|
return self.hash
|
|
1011
1000
|
|
|
1012
1001
|
def __repr__(self):
|
|
1013
|
-
return "{}={
|
|
1002
|
+
return f"{self.hash_type}={self.hash}"
|
|
1014
1003
|
|
|
1015
1004
|
|
|
1016
1005
|
class SandboxTask(core.BaseJsonResource):
|
|
1017
1006
|
RESOURCE_ENDPOINT = '/sandbox/sandboxtask'
|
|
1018
1007
|
|
|
1019
1008
|
def __init__(self, content, api=None):
|
|
1020
|
-
super(
|
|
1009
|
+
super().__init__(content, api=api)
|
|
1021
1010
|
self.id = content['id']
|
|
1022
1011
|
self.community = content['community']
|
|
1023
1012
|
self.sandbox = content['sandbox']
|
|
@@ -1083,7 +1072,7 @@ class SandboxTask(core.BaseJsonResource):
|
|
|
1083
1072
|
|
|
1084
1073
|
class SandboxArtifact(core.BaseJsonResource):
|
|
1085
1074
|
def __init__(self, content, api=None):
|
|
1086
|
-
super(
|
|
1075
|
+
super().__init__(content, api=api)
|
|
1087
1076
|
self.created = content['created']
|
|
1088
1077
|
self.id = content['id']
|
|
1089
1078
|
self.instance_id = content['instance_id']
|
|
@@ -1097,7 +1086,7 @@ class SandboxProvider(core.BaseJsonResource):
|
|
|
1097
1086
|
RESOURCE_ENDPOINT = "/sandbox/provider"
|
|
1098
1087
|
|
|
1099
1088
|
def __init__(self, content, api=None):
|
|
1100
|
-
super(
|
|
1089
|
+
super().__init__(content, api=api)
|
|
1101
1090
|
self.slug = content['slug']
|
|
1102
1091
|
self.name = content['name']
|
|
1103
1092
|
self.tool = content['tool']
|
|
@@ -1113,9 +1102,104 @@ class Events(core.BaseJsonResource):
|
|
|
1113
1102
|
RESOURCE_ENDPOINT = "/activity"
|
|
1114
1103
|
|
|
1115
1104
|
def __init__(self, content, api=None):
|
|
1116
|
-
super(
|
|
1105
|
+
super().__init__(content, api=api)
|
|
1117
1106
|
self.event_timestamp = content['event_timestamp']
|
|
1118
1107
|
self.event_type = content['event_type']
|
|
1119
1108
|
self.source = content['source']
|
|
1120
1109
|
self.team_account_id = content['team_account_id']
|
|
1121
1110
|
self.user_account_id = content['user_account_id']
|
|
1111
|
+
|
|
1112
|
+
|
|
1113
|
+
class ReportTask(core.BaseJsonResource):
|
|
1114
|
+
RESOURCE_ENDPOINT = "/reports"
|
|
1115
|
+
|
|
1116
|
+
def __init__(self, content, api=None):
|
|
1117
|
+
super().__init__(content, api=api)
|
|
1118
|
+
self.id = content['id']
|
|
1119
|
+
self.type = content['type']
|
|
1120
|
+
self.format = content['format']
|
|
1121
|
+
self.state = content['state']
|
|
1122
|
+
self.community = content['community']
|
|
1123
|
+
self.created = content['created']
|
|
1124
|
+
self.template_id = content.get('template_id')
|
|
1125
|
+
self.template_metadata = content.get('template_metadata', {})
|
|
1126
|
+
self.sandbox_task_id = content.get('sandbox_task_id')
|
|
1127
|
+
self.instance_id = content.get('instance_id')
|
|
1128
|
+
self.url = content['url']
|
|
1129
|
+
|
|
1130
|
+
def download_report(self, folder=None):
|
|
1131
|
+
""" This method is special, in that it is simply for downloading from S3 """
|
|
1132
|
+
return core.PolyswarmRequest(
|
|
1133
|
+
self.api,
|
|
1134
|
+
{
|
|
1135
|
+
'method': 'GET',
|
|
1136
|
+
'url': self.url,
|
|
1137
|
+
'stream': True,
|
|
1138
|
+
'headers': {'Authorization': None}
|
|
1139
|
+
},
|
|
1140
|
+
result_parser=LocalArtifact,
|
|
1141
|
+
folder=folder,
|
|
1142
|
+
).execute()
|
|
1143
|
+
|
|
1144
|
+
|
|
1145
|
+
class ReportTemplate(core.BaseJsonResource):
|
|
1146
|
+
RESOURCE_ENDPOINT = "/reports/templates"
|
|
1147
|
+
|
|
1148
|
+
def __init__(self, content, api=None):
|
|
1149
|
+
super().__init__(content, api=api)
|
|
1150
|
+
if content:
|
|
1151
|
+
self.id = content['id']
|
|
1152
|
+
self.created = content['created']
|
|
1153
|
+
self.template_name = content['template_name']
|
|
1154
|
+
self.includes = content.get('includes')
|
|
1155
|
+
self.primary_color = content.get('primary_color')
|
|
1156
|
+
self.footer_text = content.get('footer_text')
|
|
1157
|
+
self.last_page_text = content.get('last_page_text')
|
|
1158
|
+
self.is_default = content.get('is_default', False)
|
|
1159
|
+
self.logo_content_length = content.get('logo_content_length')
|
|
1160
|
+
self.logo_url = f"{self.api.uri}/reports/templates/logo?id={self.id}"
|
|
1161
|
+
self.logo_content_type = content.get('logo_content_type')
|
|
1162
|
+
self.logo_height = content.get('logo_height')
|
|
1163
|
+
self.logo_width = content.get('logo_width')
|
|
1164
|
+
|
|
1165
|
+
def download_logo(self, folder):
|
|
1166
|
+
return core.PolyswarmRequest(
|
|
1167
|
+
self.api,
|
|
1168
|
+
{
|
|
1169
|
+
'method': 'GET',
|
|
1170
|
+
'url': self.logo_url,
|
|
1171
|
+
},
|
|
1172
|
+
result_parser=LocalArtifact,
|
|
1173
|
+
folder=folder,
|
|
1174
|
+
).execute()
|
|
1175
|
+
|
|
1176
|
+
def delete_logo(self):
|
|
1177
|
+
return core.PolyswarmRequest(
|
|
1178
|
+
self.api,
|
|
1179
|
+
{
|
|
1180
|
+
'method': 'DELETE',
|
|
1181
|
+
'url': self.logo_url,
|
|
1182
|
+
},
|
|
1183
|
+
).execute()
|
|
1184
|
+
|
|
1185
|
+
def upload_logo(self, logo_file, content_tpe):
|
|
1186
|
+
if not logo_file:
|
|
1187
|
+
raise exceptions.InvalidValueException('A local file must be provided in order to upload')
|
|
1188
|
+
logo_file.seek(0, io.SEEK_END)
|
|
1189
|
+
length = logo_file.tell()
|
|
1190
|
+
logo_file.seek(0)
|
|
1191
|
+
if not length:
|
|
1192
|
+
raise exceptions.InvalidValueException('Empty file')
|
|
1193
|
+
# r = requests.put(self.upload_url, data=logo_file, **kwargs)
|
|
1194
|
+
# r.raise_for_status()
|
|
1195
|
+
# return r
|
|
1196
|
+
return core.PolyswarmRequest(
|
|
1197
|
+
self.api,
|
|
1198
|
+
{
|
|
1199
|
+
'method': 'PUT',
|
|
1200
|
+
'url': f'{self.api.uri}/reports/templates/logo?id={self.id}',
|
|
1201
|
+
'data': logo_file,
|
|
1202
|
+
'headers': {'Content-Type': content_tpe}
|
|
1203
|
+
},
|
|
1204
|
+
result_parser=self.__class__
|
|
1205
|
+
).execute()
|
|
@@ -7,6 +7,7 @@ DEFAULT_GLOBAL_API = 'https://api.polyswarm.network/v3'
|
|
|
7
7
|
DEFAULT_PERMALINK_BASE = os.getenv('POLYSWARM_PORTAL_URI', 'https://polyswarm.network/scan/results/file')
|
|
8
8
|
DEFAULT_COMMUNITY = 'default'
|
|
9
9
|
DEFAULT_SCAN_TIMEOUT = 60*15
|
|
10
|
+
DEFAULT_REPORT_TIMEOUT = 30
|
|
10
11
|
RESULT_CHUNK_SIZE = 100
|
|
11
12
|
POLL_FREQUENCY = 1
|
|
12
13
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: polyswarm-api
|
|
3
|
-
Version: 3.
|
|
3
|
+
Version: 3.8.0
|
|
4
4
|
Summary: Client library to simplify interacting with the PolySwarm consumer API
|
|
5
5
|
Home-page: https://github.com/polyswarm/polyswarm-api
|
|
6
6
|
Author: PolySwarm Developers
|
|
@@ -11,8 +11,12 @@ Classifier: License :: OSI Approved :: MIT License
|
|
|
11
11
|
Classifier: Operating System :: OS Independent
|
|
12
12
|
Classifier: Programming Language :: Python :: 3.7
|
|
13
13
|
Classifier: Programming Language :: Python :: 3.8
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
14
18
|
Classifier: Programming Language :: Python :: Implementation :: PyPy
|
|
15
|
-
Requires-Python:
|
|
19
|
+
Requires-Python: >=3.7,<4
|
|
16
20
|
Description-Content-Type: text/markdown
|
|
17
21
|
License-File: LICENSE
|
|
18
22
|
|
|
@@ -21,7 +25,7 @@ License-File: LICENSE
|
|
|
21
25
|
An interface to the PolySwarm Customer APIs.
|
|
22
26
|
For an easy-to-use CLI tool, or as an example of how to use these APIs, please see [polyswarm-cli](https://github.com/polyswarm/polyswarm-cli)
|
|
23
27
|
|
|
24
|
-
Supports Python
|
|
28
|
+
Supports Python 3.7 and greater.
|
|
25
29
|
|
|
26
30
|
## Installation
|
|
27
31
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|