Repository: intezer/analyze-python-sdk

Issue:
Unimplemented Endpoint: GET analyses/:analysis-id/detect
The Intezer API documentation has documentation for the endpoint `GET analyses/:analysis-id/detect` and hitting the API shows it is active, but the SDK does not have a specific function for this endpoint. Can a function for this endpoint please be added? Thanks!

Discussion:

Patch:
diff --git a/CHANGES b/CHANGES
index b137073..d8c1de6 100644
--- a/CHANGES
+++ b/CHANGES
@@ -1,3 +1,7 @@
+Not released yet
+-------
+- Add detection report
+
 1.12
 -------
 - Add file analysis by download url
diff --git a/intezer_sdk/analysis.py b/intezer_sdk/analysis.py
index 8c94a24..f4acb72 100644
--- a/intezer_sdk/analysis.py
+++ b/intezer_sdk/analysis.py
@@ -1,18 +1,22 @@
+import datetime
 import logging
 import os
 from http import HTTPStatus
 from typing import BinaryIO
 from typing import IO
 from typing import Optional
+from typing import Union
 
 import requests
 from requests import Response
 
 from intezer_sdk import consts
+from intezer_sdk import errors
 from intezer_sdk._util import deprecated
 from intezer_sdk.api import IntezerApi
 from intezer_sdk.api import get_global_api
 from intezer_sdk.base_analysis import BaseAnalysis
+from intezer_sdk import operation
 from intezer_sdk.sub_analysis import SubAnalysis
 
 logger = logging.getLogger(__name__)
@@ -55,6 +59,7 @@ class FileAnalysis(BaseAnalysis):
         self._root_analysis = None
         self._iocs_report = None
         self._dynamic_ttps_report = None
+        self._operations = {}
 
         if self._file_path and not self._file_name:
             self._file_name = os.path.basename(file_path)
@@ -166,6 +171,18 @@ class FileAnalysis(BaseAnalysis):
 
         return self._iocs_report
 
+    def get_detections(self,
+                       wait: Union[bool, int] = False,
+                       wait_timeout: Optional[datetime.timedelta] = None) -> Optional[operation.Operation]:
+        if self._api.on_premise_version:
+            raise errors.UnsupportedOnPremiseVersionError("Detection isn't supported yet on on-premise")
+        self._assert_analysis_finished()
+        result_url = self._api.get_detection_result_url(self.analysis_id)
+        if not result_url:
+            return None
+
+        return operation.handle_operation(self._operations, self._api, 'Detection', result_url, wait, wait_timeout)
+
     @property
     def dynamic_ttps(self) -> dict:
         self._assert_analysis_finished()
@@ -230,7 +247,7 @@ class UrlAnalysis(BaseAnalysis):
 
     @property
     def downloaded_file_analysis(self) -> Optional[FileAnalysis]:
-        if self.status != consts.AnalysisStatusCode.FINISH:
+        if self.status != consts.AnalysisStatusCode.FINISHED:
             raise
         if self._file_analysis:
             return self._file_analysis
diff --git a/intezer_sdk/api.py b/intezer_sdk/api.py
index b806f42..6d881f3 100644
--- a/intezer_sdk/api.py
+++ b/intezer_sdk/api.py
@@ -253,6 +253,15 @@ class IntezerApi:
 
         return response.json()['result']
 
+    def get_detection_result_url(self, analyses_id: str) -> Optional[str]:
+        response = self.request_with_refresh_expired_access_token(path=f'/analyses/{analyses_id}/detect',
+                                                                  method='GET')
+        if response.status_code == HTTPStatus.CONFLICT:
+            return None
+        raise_for_status(response)
+
+        return response.json()['result_url']
+
     def get_dynamic_ttps(self, analyses_id: str) -> Optional[dict]:
         self.assert_on_premise_above_v21_11()
         response = self.request_with_refresh_expired_access_token(path='/analyses/{}/dynamic-ttps'.format(analyses_id),
diff --git a/intezer_sdk/base_analysis.py b/intezer_sdk/base_analysis.py
index d5c5d92..21deada 100644
--- a/intezer_sdk/base_analysis.py
+++ b/intezer_sdk/base_analysis.py
@@ -49,7 +49,7 @@ class Analysis(metaclass=abc.ABCMeta):
                 time.sleep(interval)
             status_code = self.check_status()
 
-            while status_code != consts.AnalysisStatusCode.FINISH:
+            while status_code != consts.AnalysisStatusCode.FINISHED:
                 timeout_passed = timeout and datetime.datetime.utcnow() - start_time > timeout
                 if timeout_passed:
                     raise TimeoutError
@@ -72,7 +72,7 @@ class Analysis(metaclass=abc.ABCMeta):
                 self.status = consts.AnalysisStatusCode.FAILED
                 raise errors.IntezerError('Analysis failed')
             self._report = result['result']
-            self.status = consts.AnalysisStatusCode.FINISH
+            self.status = consts.AnalysisStatusCode.FINISHED
         elif response.status_code == HTTPStatus.ACCEPTED:
             self.status = consts.AnalysisStatusCode.IN_PROGRESS
         else:
@@ -94,12 +94,12 @@ class Analysis(metaclass=abc.ABCMeta):
 
         self.analysis_id = report['analysis_id']
         self._report = report
-        self.status = consts.AnalysisStatusCode.FINISH
+        self.status = consts.AnalysisStatusCode.FINISHED
 
     def _assert_analysis_finished(self):
         if self._is_analysis_running():
             raise errors.AnalysisIsStillRunningError()
-        if self.status != consts.AnalysisStatusCode.FINISH:
+        if self.status != consts.AnalysisStatusCode.FINISHED:
             raise errors.IntezerError('Analysis not finished successfully')
 
     @classmethod
diff --git a/intezer_sdk/consts.py b/intezer_sdk/consts.py
index ff62168..857167d 100644
--- a/intezer_sdk/consts.py
+++ b/intezer_sdk/consts.py
@@ -10,6 +10,7 @@ class AnalysisStatusCode(Enum):
     QUEUED = 'queued'
     FAILED = 'failed'
     FINISH = 'finished'
+    FINISHED = 'finished'
 
 
 class IndexStatusCode(Enum):
diff --git a/intezer_sdk/operation.py b/intezer_sdk/operation.py
index 7c8db92..3c36678 100644
--- a/intezer_sdk/operation.py
+++ b/intezer_sdk/operation.py
@@ -1,6 +1,8 @@
 import datetime
 import time
+from typing import Dict
 from typing import Optional
+from typing import Union
 
 from intezer_sdk.api import IntezerApi
 from intezer_sdk.api import get_global_api
@@ -20,13 +22,13 @@ class Operation:
         self._api = api or get_global_api()
 
     def get_result(self):
-        if self.status != AnalysisStatusCode.FINISH:
+        if self.status != AnalysisStatusCode.FINISHED:
 
             operation_result = self._api.get_url_result(self.url)
 
             if handle_response_status(operation_result.status_code):
                 self.result = operation_result.json()['result']
-                self.status = AnalysisStatusCode.FINISH
+                self.status = AnalysisStatusCode.FINISHED
             else:
                 raise errors.SubAnalysisOperationStillRunningError('operation')
         return self.result
@@ -50,7 +52,7 @@ class Operation:
             time.sleep(interval)
             operation_result = self._api.get_url_result(self.url)
 
-        self.status = AnalysisStatusCode.FINISH
+        self.status = AnalysisStatusCode.FINISHED
         self.result = operation_result.json()['result']
 
 
@@ -59,3 +61,24 @@ def handle_response_status(status):
         raise errors.IntezerError('Error in response status code:{}'.format(status))
 
     return status == HTTPStatus.OK
+
+
+def handle_operation(operations: Dict[str, Operation],
+                     api: IntezerApi,
+                     operation: str,
+                     result_url: str,
+                     wait: Union[bool, int],
+                     wait_timeout: Optional[datetime.timedelta]) -> Operation:
+    if operation not in operations:
+        operations[operation] = Operation(AnalysisStatusCode.IN_PROGRESS, result_url, api=api)
+
+        if wait:
+            if isinstance(wait, bool):
+                operations[operation].wait_for_completion(sleep_before_first_check=True,
+                                                          wait_timeout=wait_timeout)
+            else:
+                operations[operation].wait_for_completion(wait,
+                                                          sleep_before_first_check=True,
+                                                          wait_timeout=wait_timeout)
+
+    return operations[operation]
diff --git a/intezer_sdk/sub_analysis.py b/intezer_sdk/sub_analysis.py
index b283d67..2c14032 100644
--- a/intezer_sdk/sub_analysis.py
+++ b/intezer_sdk/sub_analysis.py
@@ -7,8 +7,7 @@ from typing import Union
 from intezer_sdk import errors
 from intezer_sdk.api import IntezerApi
 from intezer_sdk.api import get_global_api
-from intezer_sdk.consts import AnalysisStatusCode
-from intezer_sdk.operation import Operation
+from intezer_sdk import operation
 
 
 class SubAnalysis:
@@ -108,68 +107,68 @@ class SubAnalysis:
     def find_related_files(self,
                            family_id: str,
                            wait: Union[bool, int] = False,
-                           wait_timeout: Optional[datetime.timedelta] = None) -> Operation:
+                           wait_timeout: Optional[datetime.timedelta] = None) -> operation.Operation:
         result_url = self._api.get_sub_analysis_related_files_by_family_id(self.composed_analysis_id,
                                                                            self.analysis_id,
                                                                            family_id)
-        return self._handle_operation(family_id, result_url, wait, wait_timeout)
+        return operation.handle_operation(self._operations,
+                                          self._api,
+                                          f'Related files: {family_id}',
+                                          result_url,
+                                          wait,
+                                          wait_timeout)
 
     def get_account_related_samples(self,
                                     wait: Union[bool, int] = False,
-                                    wait_timeout: Optional[datetime.timedelta] = None) -> Optional[Operation]:
+                                    wait_timeout: Optional[datetime.timedelta] = None) -> Optional[operation.Operation]:
         try:
             result_url = self._api.get_sub_analysis_account_related_samples_by_id(self.composed_analysis_id,
                                                                                   self.analysis_id)
         except Exception:
             return None
 
-        return self._handle_operation('Account related samples', result_url, wait, wait_timeout)
+        return operation.handle_operation(self._operations,
+                                          self._api,
+                                          'Account files related samples',
+                                          result_url,
+                                          wait,
+                                          wait_timeout)
 
     def generate_vaccine(self,
                          wait: Union[bool, int] = False,
-                         wait_timeout: Optional[datetime.timedelta] = None) -> Operation:
+                         wait_timeout: Optional[datetime.timedelta] = None) -> operation.Operation:
         result_url = self._api.generate_sub_analysis_vaccine_by_id(self.composed_analysis_id, self.analysis_id)
-        return self._handle_operation('Vaccine', result_url, wait, wait_timeout)
+        return operation.handle_operation(self._operations, self._api, 'Vaccine', result_url, wait, wait_timeout)
 
     def get_capabilities(self,
                          wait: Union[bool, int] = False,
-                         wait_timeout: Optional[datetime.timedelta] = None) -> Operation:
+                         wait_timeout: Optional[datetime.timedelta] = None) -> operation.Operation:
         result_url = self._api.get_sub_analysis_capabilities_by_id(self.composed_analysis_id, self.analysis_id)
-        return self._handle_operation('Capabilities', result_url, wait, wait_timeout)
+        return operation.handle_operation(self._operations, self._api, 'Capabilities', result_url, wait, wait_timeout)
 
     def get_strings(self,
                     wait: Union[bool, int] = False,
-                    wait_timeout: Optional[datetime.timedelta] = None) -> Operation:
+                    wait_timeout: Optional[datetime.timedelta] = None) -> operation.Operation:
         result = self._api.get_strings_by_id(self.composed_analysis_id, self.analysis_id)
-        return self._handle_operation('Strings', result['result_url'], wait, wait_timeout)
+        return operation.handle_operation(self._operations,
+                                          self._api,
+                                          'Strings',
+                                          result['result_url'],
+                                          wait, wait_timeout)
 
     def get_string_related_samples(self,
                                    string_value: str,
                                    wait: Union[bool, int] = False,
-                                   wait_timeout: Optional[datetime.timedelta] = None) -> Operation:
+                                   wait_timeout: Optional[datetime.timedelta] = None) -> operation.Operation:
         result_url = self._api.get_string_related_samples_by_id(self.composed_analysis_id,
                                                                 self.analysis_id,
                                                                 string_value)
-        return self._handle_operation(string_value, result_url, wait, wait_timeout)
-
-    def _handle_operation(self,
-                          operation: str,
-                          url: str,
-                          wait: Union[bool, int],
-                          wait_timeout: Optional[datetime.timedelta]) -> Operation:
-        if operation not in self._operations:
-            self._operations[operation] = Operation(AnalysisStatusCode.IN_PROGRESS, url, api=self._api)
-
-            if wait:
-                if isinstance(wait, bool):
-                    self._operations[operation].wait_for_completion(sleep_before_first_check=True,
-                                                                    wait_timeout=wait_timeout)
-                else:
-                    self._operations[operation].wait_for_completion(wait,
-                                                                    sleep_before_first_check=True,
-                                                                    wait_timeout=wait_timeout)
-
-        return self._operations[operation]
+        return operation.handle_operation(self._operations,
+                                          self._api,
+                                          f'Strings related samples: {string_value}',
+                                          result_url,
+                                          wait,
+                                          wait_timeout)
 
     def download_file(self, path: str = None, output_stream: IO = None):
         """


Test patch:
diff --git a/tests/unit/test_analysis.py b/tests/unit/test_analysis.py
index 4e354ef..a4cedb0 100644
--- a/tests/unit/test_analysis.py
+++ b/tests/unit/test_analysis.py
@@ -14,9 +14,7 @@ import responses
 from intezer_sdk import consts
 from intezer_sdk import errors
 from intezer_sdk.analysis import FileAnalysis
-from intezer_sdk.analysis import UrlAnalysis
 from intezer_sdk.api import get_global_api
-from intezer_sdk.consts import AnalysisStatusCode
 from intezer_sdk.consts import OnPremiseVersion
 from intezer_sdk.endpoint_analysis import EndpointAnalysis
 from intezer_sdk.sub_analysis import SubAnalysis
@@ -110,7 +108,7 @@ class FileAnalysisSpec(BaseTest):
                 analysis.send(wait=True)
 
         # Assert
-        self.assertEqual(analysis.status, consts.AnalysisStatusCode.FINISH)
+        self.assertEqual(analysis.status, consts.AnalysisStatusCode.FINISHED)
 
     def test_send_analysis_by_file_sends_analysis_and_waits_specific_time_until_compilation(self):
         # Arrange
@@ -133,7 +131,7 @@ class FileAnalysisSpec(BaseTest):
                 duration = (datetime.datetime.utcnow() - start).total_seconds()
 
         # Assert
-        self.assertEqual(analysis.status, consts.AnalysisStatusCode.FINISH)
+        self.assertEqual(analysis.status, consts.AnalysisStatusCode.FINISHED)
         self.assertGreater(duration, wait)
 
     def test_send_analysis_by_file_sent_analysis_without_wait_and_get_status_finish(self):
@@ -155,7 +153,7 @@ class FileAnalysisSpec(BaseTest):
                 analysis.wait_for_completion()
 
         # Assert
-        self.assertEqual(analysis.status, consts.AnalysisStatusCode.FINISH)
+        self.assertEqual(analysis.status, consts.AnalysisStatusCode.FINISHED)
 
     def test_send_analysis_by_file_sent_analysis_with_pulling_and_get_status_finish(self):
         # Arrange
@@ -184,7 +182,7 @@ class FileAnalysisSpec(BaseTest):
                 analysis.check_status()
 
         # Assert
-        self.assertEqual(analysis.status, consts.AnalysisStatusCode.FINISH)
+        self.assertEqual(analysis.status, consts.AnalysisStatusCode.FINISHED)
 
     def test_send_analysis_by_file_and_get_report(self):
         # Arrange
@@ -203,7 +201,7 @@ class FileAnalysisSpec(BaseTest):
                 analysis.send(wait=True)
 
         # Assert
-        self.assertEqual(analysis.status, consts.AnalysisStatusCode.FINISH)
+        self.assertEqual(analysis.status, consts.AnalysisStatusCode.FINISHED)
         self.assertEqual(analysis.result(), 'report')
 
     def test_send_analysis_by_download_url_and_get_report(self):
@@ -248,7 +246,7 @@ class FileAnalysisSpec(BaseTest):
                 iocs = analysis.iocs
 
         # Assert
-        self.assertEqual(analysis.status, consts.AnalysisStatusCode.FINISH)
+        self.assertEqual(analysis.status, consts.AnalysisStatusCode.FINISHED)
         self.assertEqual(iocs, 'ioc_report')
 
     def test_send_analysis_by_file_and_get_dynamic_ttps(self):
@@ -273,13 +271,13 @@ class FileAnalysisSpec(BaseTest):
                 ttps = analysis.dynamic_ttps
 
         # Assert
-        self.assertEqual(analysis.status, consts.AnalysisStatusCode.FINISH)
+        self.assertEqual(analysis.status, consts.AnalysisStatusCode.FINISHED)
         self.assertEqual(ttps, 'ttps_report')
 
     def test_get_dynamic_ttps_raises_when_on_premise_on_21_11(self):
         # Arrange
         analysis = FileAnalysis(file_path='a')
-        analysis.status = consts.AnalysisStatusCode.FINISH
+        analysis.status = consts.AnalysisStatusCode.FINISHED
         get_global_api().on_premise_version = OnPremiseVersion.V21_11
 
         # Act and Assert
@@ -389,7 +387,7 @@ class FileAnalysisSpec(BaseTest):
                 analysis.send(wait=True)
 
             # Assert
-            self.assertEqual(analysis.status, consts.AnalysisStatusCode.FINISH)
+            self.assertEqual(analysis.status, consts.AnalysisStatusCode.FINISHED)
             self.assertEqual(analysis.result(), 'report')
             request_body = mock.calls[0].request.body.decode()
             self.assertTrue('Content-Disposition: form-data; name="disable_static_extraction"\r\n\r\nTrue'
@@ -417,7 +415,7 @@ class FileAnalysisSpec(BaseTest):
                 analysis.send(wait=True)
 
             # Assert
-            self.assertEqual(analysis.status, consts.AnalysisStatusCode.FINISH)
+            self.assertEqual(analysis.status, consts.AnalysisStatusCode.FINISHED)
             self.assertEqual(analysis.result(), 'report')
             request_body = mock.calls[0].request.body.decode()
             self.assertTrue('Content-Disposition: form-data; name="zip_password"\r\n\r\nasd'
@@ -444,7 +442,7 @@ class FileAnalysisSpec(BaseTest):
                 analysis.send(wait=True)
 
             # Assert
-            self.assertEqual(analysis.status, consts.AnalysisStatusCode.FINISH)
+            self.assertEqual(analysis.status, consts.AnalysisStatusCode.FINISHED)
             self.assertEqual(analysis.result(), 'report')
             request_body = mock.calls[0].request.body.decode()
             self.assertTrue('Content-Disposition: form-data; name="zip_password"\r\n\r\nasd'
@@ -471,7 +469,7 @@ class FileAnalysisSpec(BaseTest):
                 analysis.send(wait=True)
 
             # Assert
-            self.assertEqual(analysis.status, consts.AnalysisStatusCode.FINISH)
+            self.assertEqual(analysis.status, consts.AnalysisStatusCode.FINISHED)
             self.assertEqual(analysis.result(), 'report')
             request_body = mock.calls[0].request.body.decode()
             self.assertTrue('Content-Disposition: form-data; name="zip_password"\r\n\r\nasd'
@@ -885,7 +883,7 @@ class FileAnalysisSpec(BaseTest):
         file_hash = 'hash'
 
         with responses.RequestsMock() as mock:
-            mock.add('GET', url='{}/files/{}'.format(self.full_url, file_hash), status=404)
+            mock.add('GET', url=f'{self.full_url}/files/{file_hash}', status=404)
 
             # Act
             analysis = FileAnalysis.from_latest_hash_analysis(file_hash)
@@ -900,7 +898,7 @@ class FileAnalysisSpec(BaseTest):
 
         with responses.RequestsMock() as mock:
             mock.add('GET',
-                     url='{}/files/{}'.format(self.full_url, file_hash),
+                     url=f'{self.full_url}/files/{file_hash}',
                      status=200,
                      json={'result': analysis_report})
 
@@ -909,7 +907,7 @@ class FileAnalysisSpec(BaseTest):
 
         self.assertIsNotNone(analysis)
         self.assertEqual(analysis_id, analysis.analysis_id)
-        self.assertEqual(consts.AnalysisStatusCode.FINISH, analysis.status)
+        self.assertEqual(consts.AnalysisStatusCode.FINISHED, analysis.status)
         self.assertDictEqual(analysis_report, analysis.result())
 
     def test_get_latest_analysis_analysis_object_when_latest_analysis_found_with_on_premise(self):
@@ -921,7 +919,7 @@ class FileAnalysisSpec(BaseTest):
 
         with responses.RequestsMock() as mock:
             mock.add('GET',
-                     url='{}/files/{}'.format(self.full_url, file_hash),
+                     url=f'{self.full_url}/files/{file_hash}',
                      status=200,
                      json={'result': analysis_report})
 
@@ -931,7 +929,7 @@ class FileAnalysisSpec(BaseTest):
 
         self.assertIsNotNone(analysis)
         self.assertEqual(analysis_id, analysis.analysis_id)
-        self.assertEqual(consts.AnalysisStatusCode.FINISH, analysis.status)
+        self.assertEqual(consts.AnalysisStatusCode.FINISHED, analysis.status)
         self.assertDictEqual(analysis_report, analysis.result())
 
     def test_get_analysis_by_id_analysis_object_when_latest_analysis_found(self):
@@ -941,7 +939,7 @@ class FileAnalysisSpec(BaseTest):
 
         with responses.RequestsMock() as mock:
             mock.add('GET',
-                     url='{}/analyses/{}'.format(self.full_url, analysis_id),
+                     url=f'{self.full_url}/analyses/{analysis_id}',
                      status=200,
                      json={'result': analysis_report, 'status': 'succeeded'})
 
@@ -950,7 +948,7 @@ class FileAnalysisSpec(BaseTest):
 
         self.assertIsNotNone(analysis)
         self.assertEqual(analysis_id, analysis.analysis_id)
-        self.assertEqual(consts.AnalysisStatusCode.FINISH, analysis.status)
+        self.assertEqual(consts.AnalysisStatusCode.FINISHED, analysis.status)
         self.assertDictEqual(analysis_report, analysis.result())
 
     def test_get_analysis_by_id_in_progress(self):
@@ -959,13 +957,13 @@ class FileAnalysisSpec(BaseTest):
 
         with responses.RequestsMock() as mock:
             mock.add('GET',
-                     url='{}/analyses/{}'.format(self.full_url, analysis_id),
+                     url=f'{self.full_url}/analyses/{analysis_id}',
                      status=202,
-                     json={'status': AnalysisStatusCode.IN_PROGRESS.value})
+                     json={'status': consts.AnalysisStatusCode.IN_PROGRESS.value})
 
             # Act
             analysis = FileAnalysis.from_analysis_id(analysis_id)
-            self.assertEqual(AnalysisStatusCode.IN_PROGRESS, analysis.status)
+            self.assertEqual(consts.AnalysisStatusCode.IN_PROGRESS, analysis.status)
             self.assertEqual(analysis_id, analysis.analysis_id)
 
     def test_download_file_path_uses_content_disposition(self):
@@ -1077,198 +1075,69 @@ class FileAnalysisSpec(BaseTest):
             with self.assertRaises(ValueError):
                 analysis.download_file()
 
-
-class UrlAnalysisSpec(BaseTest):
-    def test_get_analysis_by_id_analysis_object_when_latest_analysis_found(self):
+    def test_get_detection(self):
         # Arrange
+        analysis = FileAnalysis(file_hash='a' * 64)
+        analysis.status = consts.AnalysisStatusCode.FINISHED
         analysis_id = 'analysis_id'
-        submitted_url = 'https://url.com'
-        analysis_report = {'analysis_id': analysis_id, 'submitted_url': submitted_url}
+        analysis.analysis_id = analysis_id
+        result_url = f'{self.full_url}/analyses/{analysis_id}/detect'
+        result = {
+            'families': [{'family_id': 'string', 'family_name': 'string'}],
+            'software_type': 'administration_tool',
+            'source': 'string',
+            'severity': 0,
+            'type': 'string',
+            'value': None
+        }
 
         with responses.RequestsMock() as mock:
             mock.add('GET',
-                     url='{}/url/{}'.format(self.full_url, analysis_id),
-                     status=200,
-                     json={'result': analysis_report, 'status': 'succeeded'})
+                     url=result_url,
+                     status=HTTPStatus.CREATED,
+                     json={'status': consts.AnalysisStatusCode.IN_PROGRESS.value,
+                           'result_url': f'/analyses/{analysis_id}/detect'})
 
-            # Act
-            analysis = UrlAnalysis.from_analysis_id(analysis_id)
-
-        self.assertIsNotNone(analysis)
-        self.assertEqual(analysis_id, analysis.analysis_id)
-        self.assertEqual(submitted_url, analysis.url)
-        self.assertEqual(consts.AnalysisStatusCode.FINISH, analysis.status)
-        self.assertDictEqual(analysis_report, analysis.result())
-
-    def test_get_analysis_by_id_in_progress(self):
-        # Arrange
-        analysis_id = 'analysis_id'
-
-        with responses.RequestsMock() as mock:
             mock.add('GET',
-                     url='{}/url/{}'.format(self.full_url, analysis_id),
-                     status=202,
-                     json={'status': AnalysisStatusCode.IN_PROGRESS.value})
+                     url=f'{self.full_url}/analyses/{analysis_id}/detect',
+                     status=HTTPStatus.OK,
+                     json={'status': consts.AnalysisStatusCode.FINISHED.value, 'result': [result]}
+                     )
 
             # Act
-            analysis = UrlAnalysis.from_analysis_id(analysis_id)
+            operation = analysis.get_detections(wait=True)
 
-            # Assert
-            self.assertEqual(AnalysisStatusCode.IN_PROGRESS, analysis.status)
-            self.assertEqual(analysis_id, analysis.analysis_id)
+        # Assert
+        self.assertEqual(operation.status, consts.AnalysisStatusCode.FINISHED)
+        self.assertDictEqual(operation.result[0], result)
 
-    def test_get_analysis_by_id_raises_when_analysis_failed(self):
+    def test_get_detection_return_none_when_no_report(self):
         # Arrange
+        analysis = FileAnalysis(file_hash='a' * 64)
+        analysis.status = consts.AnalysisStatusCode.FINISHED
         analysis_id = 'analysis_id'
+        analysis.analysis_id = analysis_id
 
         with responses.RequestsMock() as mock:
-            mock.add('GET',
-                     url='{}/url/{}'.format(self.full_url, analysis_id),
-                     status=200,
-                     json={'status': AnalysisStatusCode.FAILED.value})
-
-            # Act
-            with self.assertRaises(errors.AnalysisFailedError):
-                UrlAnalysis.from_analysis_id(analysis_id)
-
-    def test_send_perform_request_and_sets_analysis_status(self):
-        # Arrange
-        analysis_id = str(uuid.uuid4())
-        with responses.RequestsMock() as mock:
-            mock.add('POST',
-                     url=self.full_url + '/url/',
-                     status=201,
-                     json={'result_url': '/url/{}'.format(analysis_id)})
-            analysis = UrlAnalysis(url='https://intezer.com')
+            mock.add('GET', url=f'{self.full_url}/analyses/{analysis_id}/detect', status=HTTPStatus.CONFLICT, json={})
 
             # Act
-            analysis.send()
+            operation = analysis.get_detections(wait=True)
 
         # Assert
-        self.assertEqual(analysis.status, consts.AnalysisStatusCode.CREATED)
-        self.assertEqual(analysis_id, analysis.analysis_id)
-
-    def test_send_fail_when_invalid_url(self):
-        # Arrange
-        with responses.RequestsMock() as mock:
-            mock.add('POST',
-                     url=self.full_url + '/url/',
-                     status=400,
-                     json={'error': 'Some error description'})
-            analysis = UrlAnalysis(url='httpdddds://intezer.com')
-
-            # Act
-            with self.assertRaises(errors.ServerError):
-                analysis.send()
+        self.assertIsNone(operation)
 
-    def test_send_fail_when_on_premise(self):
+    def test_get_detection_raises_on_on_premise(self):
         # Arrange
         get_global_api().on_premise_version = OnPremiseVersion.V21_11
+        analysis = FileAnalysis(file_hash='a' * 64)
+        analysis.status = consts.AnalysisStatusCode.FINISHED
+        analysis_id = 'analysis_id'
+        analysis.analysis_id = analysis_id
 
-        # Act
+        # Act and Assert
         with self.assertRaises(errors.UnsupportedOnPremiseVersionError):
-            _ = UrlAnalysis(url='httpdddds://intezer.com')
-
-    def test_send_waits_to_compilation_when_requested(self):
-        # Arrange
-        analysis_id = str(uuid.uuid4())
-
-        with responses.RequestsMock() as mock:
-            mock.add('POST',
-                     url=self.full_url + '/url/',
-                     status=201,
-                     json={'result_url': '/url/{}'.format(analysis_id)})
-            result = {'analysis_id': analysis_id}
-            mock.add('GET',
-                     url='{}/url/{}'.format(self.full_url, analysis_id),
-                     status=200,
-                     json={'result': result, 'status': 'succeeded'})
-            analysis = UrlAnalysis('https://intezer.com')
-
-            # Act
-            analysis.send(wait=True)
-
-        # Assert
-        self.assertEqual(analysis.status, consts.AnalysisStatusCode.FINISH)
-        self.assertDictEqual(analysis.result(), result)
-
-    def test_send_waits_to_compilation_when_requested_and_handles_failure(self):
-        # Arrange
-        analysis_id = str(uuid.uuid4())
-
-        with responses.RequestsMock() as mock:
-            mock.add('POST',
-                     url=self.full_url + '/url/',
-                     status=201,
-                     json={'result_url': '/url/{}'.format(analysis_id)})
-            result = {'analysis_id': analysis_id}
-            mock.add('GET',
-                     url='{}/url/{}'.format(self.full_url, analysis_id),
-                     status=200,
-                     json={'result': result, 'status': 'failed'})
-            analysis = UrlAnalysis('https://intezer.com')
-
-            with self.assertRaises(errors.IntezerError):
-                # Act
-                analysis.send(wait=True)
-
-        # Assert
-        self.assertEqual(consts.AnalysisStatusCode.FAILED, analysis.status)
-
-    def test_url_analysis_references_file_analysis(self):
-        # Arrange
-        url_analysis_id = str(uuid.uuid4())
-        file_analysis_id = str(uuid.uuid4())
-        url_result = {'analysis_id': url_analysis_id,
-                      'downloaded_file': {
-                          'analysis_id': file_analysis_id
-                      }}
-        file_analysis_report = {'analysis_id': file_analysis_id, 'sha256': 'hash'}
-
-        with responses.RequestsMock() as mock:
-            mock.add('POST',
-                     url=self.full_url + '/url/',
-                     status=201,
-                     json={'result_url': '/url/{}'.format(url_analysis_id)})
-            mock.add('GET',
-                     url='{}/url/{}'.format(self.full_url, url_analysis_id),
-                     status=200,
-                     json={'result': url_result, 'status': 'succeeded'})
-            mock.add('GET',
-                     url='{}/analyses/{}'.format(self.full_url, file_analysis_id),
-                     status=200,
-                     json={'result': file_analysis_report, 'status': 'succeeded'})
-            analysis = UrlAnalysis('https://intezer.com')
-
-            # Act
-            analysis.send(wait=True)
-            file_analysis = analysis.downloaded_file_analysis
-
-            # Assert
-            self.assertEqual(file_analysis.analysis_id, file_analysis_id)
-            self.assertEqual(file_analysis.result(), file_analysis_report)
-
-    def test_url_analysis_doesnt_reference_file_analysis_when_not_exists(self):
-        # Arrange
-        analysis_id = str(uuid.uuid4())
-        result = {'analysis_id': analysis_id}
-
-        with responses.RequestsMock() as mock:
-            mock.add('POST',
-                     url=self.full_url + '/url/',
-                     status=201,
-                     json={'result_url': '/url/{}'.format(analysis_id)})
-            mock.add('GET',
-                     url='{}/url/{}'.format(self.full_url, analysis_id),
-                     status=200,
-                     json={'result': result, 'status': 'succeeded'})
-            analysis = UrlAnalysis('https://intezer.com')
-
-            # Act
-            analysis.send(wait=True)
-
-            # Assert
-            self.assertIsNone(analysis.downloaded_file_analysis)
+            operation = analysis.get_detections(wait=True)
 
 
 class EndpointAnalysisSpec(BaseTest):
@@ -1316,7 +1185,7 @@ class EndpointAnalysisSpec(BaseTest):
 
         # Assert
         self.assertIsNotNone(analysis)
-        self.assertEqual(consts.AnalysisStatusCode.FINISH, analysis.status)
+        self.assertEqual(consts.AnalysisStatusCode.FINISHED, analysis.status)
 
     def test_analysis_done(self):
         # Arrange
@@ -1337,7 +1206,7 @@ class EndpointAnalysisSpec(BaseTest):
         # Assert
         self.assertIsNotNone(analysis)
         self.assertEqual(analysis_id, analysis.analysis_id)
-        self.assertEqual(consts.AnalysisStatusCode.FINISH, analysis.status)
+        self.assertEqual(consts.AnalysisStatusCode.FINISHED, analysis.status)
         self.assertDictEqual(result['result'], analysis.result())
 
     def test_analysis_failed(self):
@@ -1373,7 +1242,7 @@ class EndpointAnalysisSpec(BaseTest):
         analysis_id = str(uuid.uuid4())
         sub_analysis_id = str(uuid.uuid4())
         analysis = EndpointAnalysis()
-        analysis.status = consts.AnalysisStatusCode.FINISH
+        analysis.status = consts.AnalysisStatusCode.FINISHED
         analysis.analysis_id = analysis_id
         sha256 = 'a' * 64
         verdict = 'malicious'
diff --git a/tests/unit/test_url_analysis.py b/tests/unit/test_url_analysis.py
new file mode 100644
index 0000000..3fdc63d
--- /dev/null
+++ b/tests/unit/test_url_analysis.py
@@ -0,0 +1,203 @@
+import uuid
+
+import responses
+
+from intezer_sdk import consts
+from intezer_sdk import errors
+from intezer_sdk.analysis import UrlAnalysis
+from intezer_sdk.api import get_global_api
+from intezer_sdk.consts import OnPremiseVersion
+from tests.unit.base_test import BaseTest
+
+
+class UrlAnalysisSpec(BaseTest):
+    def test_get_analysis_by_id_analysis_object_when_latest_analysis_found(self):
+        # Arrange
+        analysis_id = 'analysis_id'
+        submitted_url = 'https://url.com'
+        analysis_report = {'analysis_id': analysis_id, 'submitted_url': submitted_url}
+
+        with responses.RequestsMock() as mock:
+            mock.add('GET',
+                     url='{}/url/{}'.format(self.full_url, analysis_id),
+                     status=200,
+                     json={'result': analysis_report, 'status': 'succeeded'})
+
+            # Act
+            analysis = UrlAnalysis.from_analysis_id(analysis_id)
+
+        self.assertIsNotNone(analysis)
+        self.assertEqual(analysis_id, analysis.analysis_id)
+        self.assertEqual(submitted_url, analysis.url)
+        self.assertEqual(consts.AnalysisStatusCode.FINISHED, analysis.status)
+        self.assertDictEqual(analysis_report, analysis.result())
+
+    def test_get_analysis_by_id_in_progress(self):
+        # Arrange
+        analysis_id = 'analysis_id'
+
+        with responses.RequestsMock() as mock:
+            mock.add('GET',
+                     url='{}/url/{}'.format(self.full_url, analysis_id),
+                     status=202,
+                     json={'status': consts.AnalysisStatusCode.IN_PROGRESS.value})
+
+            # Act
+            analysis = UrlAnalysis.from_analysis_id(analysis_id)
+
+            # Assert
+            self.assertEqual(consts.AnalysisStatusCode.IN_PROGRESS, analysis.status)
+            self.assertEqual(analysis_id, analysis.analysis_id)
+
+    def test_get_analysis_by_id_raises_when_analysis_failed(self):
+        # Arrange
+        analysis_id = 'analysis_id'
+
+        with responses.RequestsMock() as mock:
+            mock.add('GET',
+                     url='{}/url/{}'.format(self.full_url, analysis_id),
+                     status=200,
+                     json={'status': consts.AnalysisStatusCode.FAILED.value})
+
+            # Act
+            with self.assertRaises(errors.AnalysisFailedError):
+                UrlAnalysis.from_analysis_id(analysis_id)
+
+    def test_send_perform_request_and_sets_analysis_status(self):
+        # Arrange
+        analysis_id = str(uuid.uuid4())
+        with responses.RequestsMock() as mock:
+            mock.add('POST',
+                     url=self.full_url + '/url/',
+                     status=201,
+                     json={'result_url': '/url/{}'.format(analysis_id)})
+            analysis = UrlAnalysis(url='https://intezer.com')
+
+            # Act
+            analysis.send()
+
+        # Assert
+        self.assertEqual(analysis.status, consts.AnalysisStatusCode.CREATED)
+        self.assertEqual(analysis_id, analysis.analysis_id)
+
+    def test_send_fail_when_invalid_url(self):
+        # Arrange
+        with responses.RequestsMock() as mock:
+            mock.add('POST',
+                     url=self.full_url + '/url/',
+                     status=400,
+                     json={'error': 'Some error description'})
+            analysis = UrlAnalysis(url='httpdddds://intezer.com')
+
+            # Act
+            with self.assertRaises(errors.ServerError):
+                analysis.send()
+
+    def test_send_fail_when_on_premise(self):
+        # Arrange
+        get_global_api().on_premise_version = OnPremiseVersion.V21_11
+
+        # Act
+        with self.assertRaises(errors.UnsupportedOnPremiseVersionError):
+            _ = UrlAnalysis(url='httpdddds://intezer.com')
+
+    def test_send_waits_to_compilation_when_requested(self):
+        # Arrange
+        analysis_id = str(uuid.uuid4())
+
+        with responses.RequestsMock() as mock:
+            mock.add('POST',
+                     url=self.full_url + '/url/',
+                     status=201,
+                     json={'result_url': '/url/{}'.format(analysis_id)})
+            result = {'analysis_id': analysis_id}
+            mock.add('GET',
+                     url='{}/url/{}'.format(self.full_url, analysis_id),
+                     status=200,
+                     json={'result': result, 'status': 'succeeded'})
+            analysis = UrlAnalysis('https://intezer.com')
+
+            # Act
+            analysis.send(wait=True)
+
+        # Assert
+        self.assertEqual(analysis.status, consts.AnalysisStatusCode.FINISHED)
+        self.assertDictEqual(analysis.result(), result)
+
+    def test_send_waits_to_compilation_when_requested_and_handles_failure(self):
+        # Arrange
+        analysis_id = str(uuid.uuid4())
+
+        with responses.RequestsMock() as mock:
+            mock.add('POST',
+                     url=self.full_url + '/url/',
+                     status=201,
+                     json={'result_url': '/url/{}'.format(analysis_id)})
+            result = {'analysis_id': analysis_id}
+            mock.add('GET',
+                     url='{}/url/{}'.format(self.full_url, analysis_id),
+                     status=200,
+                     json={'result': result, 'status': 'failed'})
+            analysis = UrlAnalysis('https://intezer.com')
+
+            with self.assertRaises(errors.IntezerError):
+                # Act
+                analysis.send(wait=True)
+
+        # Assert
+        self.assertEqual(consts.AnalysisStatusCode.FAILED, analysis.status)
+
+    def test_url_analysis_references_file_analysis(self):
+        # Arrange
+        url_analysis_id = str(uuid.uuid4())
+        file_analysis_id = str(uuid.uuid4())
+        url_result = {'analysis_id': url_analysis_id,
+                      'downloaded_file': {
+                          'analysis_id': file_analysis_id
+                      }}
+        file_analysis_report = {'analysis_id': file_analysis_id, 'sha256': 'hash'}
+
+        with responses.RequestsMock() as mock:
+            mock.add('POST',
+                     url=self.full_url + '/url/',
+                     status=201,
+                     json={'result_url': '/url/{}'.format(url_analysis_id)})
+            mock.add('GET',
+                     url='{}/url/{}'.format(self.full_url, url_analysis_id),
+                     status=200,
+                     json={'result': url_result, 'status': 'succeeded'})
+            mock.add('GET',
+                     url='{}/analyses/{}'.format(self.full_url, file_analysis_id),
+                     status=200,
+                     json={'result': file_analysis_report, 'status': 'succeeded'})
+            analysis = UrlAnalysis('https://intezer.com')
+
+            # Act
+            analysis.send(wait=True)
+            file_analysis = analysis.downloaded_file_analysis
+
+            # Assert
+            self.assertEqual(file_analysis.analysis_id, file_analysis_id)
+            self.assertEqual(file_analysis.result(), file_analysis_report)
+
+    def test_url_analysis_doesnt_reference_file_analysis_when_not_exists(self):
+        # Arrange
+        analysis_id = str(uuid.uuid4())
+        result = {'analysis_id': analysis_id}
+
+        with responses.RequestsMock() as mock:
+            mock.add('POST',
+                     url=self.full_url + '/url/',
+                     status=201,
+                     json={'result_url': '/url/{}'.format(analysis_id)})
+            mock.add('GET',
+                     url='{}/url/{}'.format(self.full_url, analysis_id),
+                     status=200,
+                     json={'result': result, 'status': 'succeeded'})
+            analysis = UrlAnalysis('https://intezer.com')
+
+            # Act
+            analysis.send(wait=True)
+
+            # Assert
+            self.assertIsNone(analysis.downloaded_file_analysis)