polyswarm-api 3.7.0__tar.gz → 3.9.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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: polyswarm-api
3
- Version: 3.7.0
3
+ Version: 3.9.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
@@ -9,7 +9,7 @@ with open('README.md', 'r') as readme:
9
9
 
10
10
  setup(
11
11
  name='polyswarm-api',
12
- version='3.7.0',
12
+ version='3.9.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',
@@ -1,5 +1,5 @@
1
1
  # https://www.python.org/dev/peps/pep-0008/#module-level-dunder-names
2
- __version__ = '3.7.0'
2
+ __version__ = '3.9.0'
3
3
  __release_url__ = 'https://api.github.com/repos/polyswarm/polyswarm-api/releases/latest'
4
4
 
5
5
  from . import api
@@ -1,3 +1,4 @@
1
+ import io
1
2
  import logging
2
3
  import time
3
4
 
@@ -211,22 +212,33 @@ class PolyswarmAPI:
211
212
  logger.info('Deleting known good ioc %s', id)
212
213
  return resources.IOC.delete_known_good(self, id).result()
213
214
 
214
- def submit(self, artifact, artifact_type=resources.ArtifactType.FILE, artifact_name=None, scan_config=None):
215
+ def submit(
216
+ self,
217
+ artifact,
218
+ artifact_type=resources.ArtifactType.FILE,
219
+ artifact_name=None,
220
+ scan_config=None,
221
+ preprocessing=None,
222
+ ):
215
223
  """
216
- Submit artifacts to polyswarm and return UUIDs
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 preprocessing: Preprocessing settings to be applied to the artifact, None means no preprocessing,
231
+ otherwise a dict with the following attributes can be passed:
232
+ - type (string): either "zip" or "qrcode", the first mean the file is a zip that
233
+ the server has to decompress to then scan the content (only one file inside allowed).
234
+ "qrcode" means the file is a QR Code image with a URL as payload, and you want
235
+ to scan the URL, not the actual file (artifact_type has to be "URL").
236
+ - password (string, optional): will use this password to decompress the zip file.
222
237
  :return: An ArtifactInstance resource
223
238
  """
224
239
  logger.info('Submitting artifact of type %s', artifact_type)
225
240
  artifact_type = resources.ArtifactType.parse(artifact_type)
226
- # TODO This is a python 2.7 check if artifact is a file-like instance, consider changing
227
- # to isinstance(artifact, io.IOBase) when deprecating 2.7 and implementing making LocalHandle
228
- # inherit io.IOBase, although this will change the method delegation logic in the resource
229
- if hasattr(artifact, 'read') and hasattr(artifact.read, '__call__'):
241
+ if isinstance(artifact, io.IOBase):
230
242
  artifact = resources.LocalArtifact.from_handle(self, artifact, artifact_name=artifact_name or '',
231
243
  artifact_type=artifact_type)
232
244
  elif isinstance(artifact, str):
@@ -234,16 +246,27 @@ class PolyswarmAPI:
234
246
  artifact = resources.LocalArtifact.from_path(self, artifact, artifact_type=artifact_type,
235
247
  artifact_name=artifact_name)
236
248
  elif artifact_type == resources.ArtifactType.URL:
237
- artifact = resources.LocalArtifact.from_content(self, artifact, artifact_name=artifact_name or artifact,
238
- artifact_type=artifact_type)
249
+ if preprocessing and preprocessing['type'] == 'qrcode':
250
+ artifact = resources.LocalArtifact.from_path(self,
251
+ artifact,
252
+ artifact_type=artifact_type,
253
+ artifact_name=artifact_name)
254
+ else:
255
+ artifact = resources.LocalArtifact.from_content(self,
256
+ artifact,
257
+ artifact_name=artifact_name or artifact,
258
+ artifact_type=artifact_type)
239
259
  if artifact_type == resources.ArtifactType.URL:
240
260
  scan_config = scan_config or 'more-time'
241
261
  if isinstance(artifact, resources.LocalArtifact):
242
- instance = resources.ArtifactInstance.create(self,
243
- artifact_name=artifact.artifact_name,
244
- artifact_type=artifact.artifact_type.name,
245
- scan_config=scan_config,
246
- community=self.community).result()
262
+ instance = resources.ArtifactInstance.create(
263
+ self,
264
+ artifact_name=artifact.artifact_name,
265
+ artifact_type=artifact.artifact_type.name,
266
+ scan_config=scan_config,
267
+ community=self.community,
268
+ preprocessing=preprocessing,
269
+ ).result()
247
270
  instance.upload_file(artifact)
248
271
  return resources.ArtifactInstance.update(self, id=instance.id, community=self.community).result()
249
272
  else:
@@ -690,13 +713,43 @@ class PolyswarmAPI:
690
713
  return resources.SandboxTask.create(self, artifact_id=instance_id, provider_slug=provider_slug, vm_slug=vm_slug,
691
714
  network_enabled=network_enabled).result()
692
715
 
693
- def sandbox_file(self, artifact, provider_slug, vm_slug, artifact_type=resources.ArtifactType.FILE, artifact_name=None, network_enabled=True):
694
- logger.info('Sandboxing file in provider %s vm %s', provider_slug, vm_slug)
716
+ def sandbox_file(
717
+ self,
718
+ artifact,
719
+ provider_slug,
720
+ vm_slug,
721
+ artifact_type=resources.ArtifactType.FILE,
722
+ artifact_name=None,
723
+ network_enabled=True,
724
+ preprocessing=None,
725
+ ):
726
+ """
727
+ Submit artifacts to Polyswarm Sandboxing system.
728
+
729
+ :param artifact: A file-like, path to file, url or LocalArtifact instance
730
+ :param artifact_name: An appropriate filename for the Artifact
731
+ :artifact_type: the type of artifact to be submitted, default FILE. Use URL type
732
+ with the preprocessing field to scan URLs inside QR Code images
733
+ :param network_enabled: Whether the VM has to have Internet connection enabled or not.
734
+ :param provider_slug: the slug of the provider that is going to execute the sandboxing, e.g.
735
+ 'cape' or 'triage'.
736
+ Use `sandbox_providers()` to get the list of providers.
737
+ :param vm_slug: The slug of the virtual machine used for the sandboxing, depending on the
738
+ VM chosen is the OS and software that is going to host the sandboxing. Each
739
+ provider (provider_slug) support different VMs, so check with `sandbox_providers()`
740
+ the list of VMs available.
741
+ :param preprocessing: Preprocessing settings to be applied to the artifact, None means no preprocessing,
742
+ otherwise a dict with the following attributes can be passed:
743
+ - type (string): either "zip" or "qrcode", the first mean the file is a zip that
744
+ the server has to decompress to then scan the content (only one file inside allowed).
745
+ "qrcode" means the file is a QR Code image with a URL as payload, and you want
746
+ to scan the URL, not the actual file (artifact_type has to be "URL").
747
+ - password (string, optional): will use this password to decompress the zip file.
748
+ :return: An ArtifactInstance resource
749
+ """
750
+ logger.info('Sandboxing %s in provider %s vm %s', artifact_type.name.lower(), provider_slug, vm_slug)
695
751
  artifact_type = resources.ArtifactType.parse(artifact_type)
696
- # TODO This is a python 2.7 check if artifact is a file-like instance, consider changing
697
- # to isinstance(artifact, io.IOBase) when deprecating 2.7 and implementing making LocalHandle
698
- # inherit io.IOBase, although this will change the method delegation logic in the resource
699
- if hasattr(artifact, 'read') and hasattr(artifact.read, '__call__'):
752
+ if isinstance(artifact, io.IOBase):
700
753
  artifact = resources.LocalArtifact.from_handle(self, artifact, artifact_name=artifact_name or '',
701
754
  artifact_type=artifact_type)
702
755
  elif isinstance(artifact, str):
@@ -708,29 +761,84 @@ class PolyswarmAPI:
708
761
  artifact_name=artifact_name or artifact,
709
762
  artifact_type=artifact_type)
710
763
  if isinstance(artifact, resources.LocalArtifact):
711
- task = resources.SandboxTask.create_file(self,
712
- artifact_name=artifact.artifact_name,
713
- artifact_type=artifact.artifact_type.name,
714
- community=self.community,
715
- provider_slug=provider_slug,
716
- vm_slug=vm_slug,
717
- network_enabled=network_enabled).result()
764
+ task = resources.SandboxTask.create_file(
765
+ self,
766
+ artifact_name=artifact.artifact_name,
767
+ artifact_type=artifact.artifact_type.name,
768
+ community=self.community,
769
+ provider_slug=provider_slug,
770
+ vm_slug=vm_slug,
771
+ network_enabled=network_enabled,
772
+ preprocessing=preprocessing,
773
+ ).result()
718
774
  task.upload_file(artifact)
719
775
  return resources.SandboxTask.update_file(self, id=task.id, community=self.community).result()
720
776
  else:
721
777
  raise exceptions.InvalidValueException(
722
778
  'Artifacts should be a path to a file or a LocalArtifact instance')
723
779
 
724
- def sandbox_url(self, url, provider_slug, vm_slug, browser=None):
780
+ def sandbox_url(self,
781
+ url,
782
+ provider_slug,
783
+ vm_slug,
784
+ browser=None,
785
+ artifact=None,
786
+ artifact_name=None,
787
+ preprocessing=None):
788
+ """
789
+ Submit URL to Polyswarm Sandboxing system.
790
+ :param url: A string with the URL to be submitted. Set to `None` if the URL is provided
791
+ through a QR Code image in the artifact param (see artifact and preprocessing params).
792
+ :param provider_slug: the slug of the provider that is going to execute the sandboxing, e.g.
793
+ 'cape' or 'triage'.
794
+ Use `sandbox_providers()` to get the list of providers.
795
+ :param vm_slug: The slug of the virtual machine used for the sandboxing, depending on the
796
+ VM chosen is the OS and software that is going to host the sandboxing. Each
797
+ provider (provider_slug) support different VMs, so check with `sandbox_providers()`
798
+ the list of VMs available.
799
+ :param browser: browser name, e.g. 'edge' or 'firefox'.
800
+ :param artifact: A file-like, path to file, or LocalArtifact instance (default None). The url
801
+ param has to be None to use this param. For now QR Code image files
802
+ are supported
803
+ :param artifact_name: name of the artifact, if None (default) the URL string is used as a name,
804
+ or the file name if artifact is provided (QR Code image file).
805
+ :param preprocessing: Preprocessing settings to be applied to the artifact, None means no preprocessing,
806
+ otherwise a dict with the following attributes can be passed:
807
+ - type (string): only "qrcode" is supported for URL sandboxing:
808
+ the file is a QR Code image with a URL as payload, and you want
809
+ to scan the URL, not the actual file. This argument has to be used with the
810
+ `artifact` param, and `url` set to `None`.
811
+ """
725
812
  logger.info('Sandboxing url in provider %s vm %s', provider_slug, vm_slug)
726
- artifact = resources.LocalArtifact.from_content(self, url, artifact_name=url, artifact_type='URL')
813
+ if artifact and url:
814
+ raise exceptions.InvalidValueException("Cannot use artifact with url param")
815
+ if artifact:
816
+ if not preprocessing or preprocessing.get('type') != 'qrcode':
817
+ raise exceptions.InvalidValueException("Only artifact of type qrcode is supported")
818
+ if isinstance(artifact, str):
819
+ artifact = resources.LocalArtifact.from_path(self,
820
+ artifact,
821
+ artifact_type='URL',
822
+ artifact_name=artifact_name)
823
+ else:
824
+ artifact = resources.LocalArtifact.from_handle(self,
825
+ artifact,
826
+ artifact_name=artifact_name or '',
827
+ artifact_type='URL')
828
+ else:
829
+ artifact_name = url
830
+ artifact = resources.LocalArtifact.from_content(self,
831
+ url,
832
+ artifact_name=artifact_name,
833
+ artifact_type='URL')
727
834
  task = resources.SandboxTask.create_file(self,
728
- artifact_name=url,
835
+ artifact_name=artifact_name,
729
836
  artifact_type="URL",
730
837
  community=self.community,
731
838
  provider_slug=provider_slug,
732
839
  vm_slug=vm_slug,
733
840
  browser=browser,
841
+ preprocessing=preprocessing,
734
842
  network_enabled=True).result()
735
843
  task.upload_file(artifact)
736
844
  return resources.SandboxTask.update_file(self, id=task.id, community=self.community).result()
@@ -823,18 +931,62 @@ class PolyswarmAPI:
823
931
  logger.info('List events')
824
932
  return resources.Events.list(self, **kwargs).result()
825
933
 
826
- def report_create(self, **kwargs):
827
- return resources.ReportTask.create(self, **kwargs).result()
828
-
829
- def report_get(self, **kwargs):
830
- return resources.ReportTask.get(self, **kwargs).result()
934
+ def report_create(self,
935
+ type,
936
+ format,
937
+ instance_id=None,
938
+ sandbox_task_id=None,
939
+ template_id=None,
940
+ template_metadata=None,
941
+ **kwargs):
942
+ """
943
+ Create a report, either 'pdf' or 'html' (format argument).
944
+ Regarding the type argument, either instance_id (type='scan')
945
+ or sandbox_task_id (type='sandbox') has to be provided.
946
+ """
947
+ report = resources.ReportTask.create(self,
948
+ type=type,
949
+ format=format,
950
+ instance_id=instance_id,
951
+ sandbox_task_id=sandbox_task_id,
952
+ template_id=template_id,
953
+ template_metadata=template_metadata,
954
+ **kwargs).result()
955
+ return report
956
+
957
+ def report_get(self, id, **kwargs):
958
+ return resources.ReportTask.get(self, id=id, **kwargs).result()
831
959
 
832
960
  def report_download(self, report_id, folder):
833
961
  report = resources.ReportTask.get(self, id=report_id).result()
962
+ if report.state == 'PENDING':
963
+ raise exceptions.InvalidValueException('Report is in PENDING state, wait for completion first')
964
+ if report.state == 'FAILED':
965
+ raise exceptions.InvalidValueException("Report is in FAILED state, won't be generated")
834
966
  result = report.download_report(folder=folder).result()
835
967
  result.handle.close()
836
968
  return result
837
969
 
970
+ def report_wait_for(self, report_id, timeout=settings.DEFAULT_REPORT_TIMEOUT):
971
+ """
972
+ Wait for a Report to finish successfully.
973
+
974
+ :param report_id: Report id to wait for
975
+ :param timeout: Maximum time in seconds to wait before raising a TimeoutException
976
+ :return: The ReportTask resource waited on, either in 'SUCCEEDED' or 'FAILED' state
977
+ """
978
+ logger.info('Waiting for report %s', report_id)
979
+ start = time.time()
980
+ while True:
981
+ report_result = self.report_get(id=report_id)
982
+ if report_result.state != 'PENDING':
983
+ return report_result
984
+ elif -1 < timeout < time.time() - start:
985
+ raise exceptions.TimeoutException(
986
+ f'Timed out waiting for report {report_id} to finish. Please try again.')
987
+ else:
988
+ time.sleep(settings.POLL_FREQUENCY)
989
+
838
990
  def report_template_logo_download(self, template_id, folder):
839
991
  report = resources.ReportTemplate.get(self, id=template_id).result()
840
992
  result = report.download_logo(folder).result()
@@ -851,11 +1003,50 @@ class PolyswarmAPI:
851
1003
  result = report.upload_logo(logo_file, content_tpe).result()
852
1004
  return result
853
1005
 
854
- def report_template_create(self, **kwargs):
855
- return resources.ReportTemplate.create(self, **kwargs).result()
856
-
857
- def report_template_update(self, template_id, **kwargs):
858
- return resources.ReportTemplate.update(self, id=template_id, **kwargs).result()
1006
+ def report_template_create(self,
1007
+ template_name,
1008
+ is_default=False,
1009
+ primary_color=None,
1010
+ footer_text=None,
1011
+ last_page_text=None,
1012
+ includes=None,
1013
+ **kwargs):
1014
+ """
1015
+ Create a template for reports.
1016
+ A team account can have multiple templates, and setting is_default=True
1017
+ makes it the default when a report is created without specifying template_id
1018
+ (see report_create method).
1019
+ The includes argument can be a list with sections to be included
1020
+ in the reports. The list can include any of the following section names:
1021
+ summary, detections, fileMetadata, network, droppedFiles, extractedConfig, analysis.
1022
+ """
1023
+ return resources.ReportTemplate.create(self,
1024
+ template_name=template_name,
1025
+ is_default=is_default,
1026
+ primary_color=primary_color,
1027
+ footer_text=footer_text,
1028
+ last_page_text=last_page_text,
1029
+ includes=includes,
1030
+ **kwargs).result()
1031
+
1032
+ def report_template_update(self,
1033
+ template_id,
1034
+ template_name=None,
1035
+ is_default=None,
1036
+ primary_color=None,
1037
+ footer_text=None,
1038
+ last_page_text=None,
1039
+ includes=None,
1040
+ **kwargs):
1041
+ return resources.ReportTemplate.update(self,
1042
+ id=template_id,
1043
+ template_name=template_name,
1044
+ is_default=is_default,
1045
+ primary_color=primary_color,
1046
+ footer_text=footer_text,
1047
+ last_page_text=last_page_text,
1048
+ includes=includes,
1049
+ **kwargs).result()
859
1050
 
860
1051
  def report_template_get(self, template_id):
861
1052
  return resources.ReportTemplate.get(self, id=template_id).result()
@@ -863,8 +1054,11 @@ class PolyswarmAPI:
863
1054
  def report_template_delete(self, template_id):
864
1055
  return resources.ReportTemplate.delete(self, id=template_id).result()
865
1056
 
866
- def report_template_list(self, is_default=None):
867
- params = {}
868
- if is_default is not None:
869
- params['is_default'] = is_default
870
- return resources.ReportTemplate.list(self, **params).result()
1057
+ def report_template_list(self, is_default=None, **kwargs):
1058
+ return resources.ReportTemplate.list(self, is_default=is_default, **kwargs).result()
1059
+
1060
+ def account_whois(self, **kwargs):
1061
+ return resources.WhoIs.get(self, **kwargs).result()
1062
+
1063
+ def account_features(self, **kwargs):
1064
+ return resources.AccountFeatures.get(self, **kwargs).result()
@@ -1019,6 +1019,8 @@ class SandboxTask(core.BaseJsonResource):
1019
1019
  self.sha256 = content['sha256']
1020
1020
  self.report = content['report']
1021
1021
  self.upload_url = content['upload_url']
1022
+ self.config = content['config']
1023
+ self.artifact = content['artifact']
1022
1024
  self.sandbox_artifacts = [SandboxArtifact(a, api=api) for a in content.get('sandbox_artifacts', [])]
1023
1025
 
1024
1026
  def upload_file(self, artifact, attempts=3, **kwargs):
@@ -1203,3 +1205,48 @@ class ReportTemplate(core.BaseJsonResource):
1203
1205
  },
1204
1206
  result_parser=self.__class__
1205
1207
  ).execute()
1208
+
1209
+
1210
+ class AccountFeatures(core.BaseJsonResource):
1211
+ RESOURCE_ENDPOINT = "/public/accounts"
1212
+
1213
+ def __init__(self, content, api=None):
1214
+ super().__init__(content, api=api)
1215
+ self.account_number = content['account_number']
1216
+ self.user_account_number = content.get('user_account_number')
1217
+ self.account_plan_name = content['account_plan_name']
1218
+ self.plan_period_start = content['plan_period_start']
1219
+ self.plan_period_end = content['plan_period_end']
1220
+ self.window_start = content['window_start']
1221
+ self.window_end = content['window_end']
1222
+ self.tenant = content.get('tenant')
1223
+ self.daily_api_limit = content['daily_api_limit']
1224
+ self.daily_api_remaining = content['daily_api_remaining']
1225
+ self.has_stream_access = content['has_stream_access']
1226
+ self.is_trial = content['is_trial']
1227
+ self.is_trial_expired = content['is_trial_expired']
1228
+ self.trial_started_at = content['trial_started_at']
1229
+ self.trial_ended_at = content['trial_ended_at']
1230
+ self.features = []
1231
+ for feature in content['features']:
1232
+ self.features.append({
1233
+ 'base_uses': feature['base_uses'],
1234
+ 'name': feature['name'],
1235
+ 'overage': feature['overage'],
1236
+ 'remaining_uses': feature['remaining_uses'],
1237
+ 'tag': feature['tag'],
1238
+ 'value': feature['value'],
1239
+ })
1240
+
1241
+
1242
+ class WhoIs(core.BaseJsonResource):
1243
+ RESOURCE_ENDPOINT = "/public/accounts/whois"
1244
+
1245
+ def __init__(self, content, api=None):
1246
+ super().__init__(content, api=api)
1247
+ self.account_number = content['account_number']
1248
+ self.user_account_number = content.get('user_account_number')
1249
+ self.account_name = content['account_name']
1250
+ self.account_type = content['account_type']
1251
+ self.communities = content['communities']
1252
+ self.tenant = content.get('tenant')
@@ -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.7.0
3
+ Version: 3.9.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
File without changes
File without changes
File without changes
File without changes