Skip to content

Commit ee8f7d0

Browse files
author
Jan Miller
authored
Merge pull request #35 from PayloadSecurity/v2
Refactor application and add broad support for API V2
2 parents ffeb425 + 4a256ec commit ee8f7d0

File tree

250 files changed

+4287
-1436
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

250 files changed

+4287
-1436
lines changed

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,5 @@ api_classes/__pycache__
55
cli_classes/__pycache__/*.pyc
66
/output
77
*.pyc
8+
/cli.log
9+
/cache

README.md

Lines changed: 32 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,28 @@ The Falcon Sandbox Python API Connector (e.g. for https://www.hybrid-analysis.co
1313
> Using Debian/Ubuntu OS, this can be done by calling `sudo apt-get install python3-pip`. It will then be available via `pip3`
1414
> Using Windows, this can be done automatically when installing `python` (proper checkbox on the installer has to be checked). It should be available via `pip`
1515
16+
Versions
17+
---
18+
19+
### V2
20+
21+
This version has broad support for all capabilities of VxWebService APIv2 and much more. New features include:
22+
23+
- support for APIv2
24+
- improved application performance
25+
- unified and simplified CLI schema
26+
- bulk quick scan and sandbox submissions
27+
- improved file handling
28+
- test coverage
29+
30+
Example: `python3 vxapi.py scan_file C:\file-repo all`
31+
32+
![Alt text](/img/scan_example.png?raw=true "Falcon Sandbox API CLI Example Bulk Quick Scan")
33+
34+
### V1
35+
36+
The legacy app utilizing the APIv1 is not supported anymore. For backward compatibility, it is still available in the `v1` branch.
37+
1638
Usage
1739
---
1840

@@ -22,12 +44,11 @@ Copy the `config_tpl.py` and name it `config.py`.
2244

2345
The configuration file specifies a triplet of api key/secret and server:
2446

25-
- api_key
26-
- api_secret
27-
- server - full url of the WebService e.g. `https://www.hybrid-analysis.com`
47+
- api_key (should be compatible with API v2 - should contains at least 60 chars)
48+
- server - full url of the WebService instance e.g. `https://www.hybrid-analysis.com`
2849

2950
Please fill them with the appropriate data. You can generate a public (restricted) API key by following these instructions:
30-
https://www.hybrid-analysis.com/apikeys/info
51+
https://www.hybrid-analysis.com/knowledge-base/issuing-self-signed-api-key
3152

3253
If you have the full version of Falcon Sandbox, create any kind of API key in the admin area:
3354
https://www.hybrid-analysis.com/apikeys
@@ -80,6 +101,13 @@ After choosing the `action_name`
80101
> To ensure that the program will work correctly, please use `python3`.
81102
> In Windows after having installed `python`, please add the parent folder to `PATH` environment variable. Now use `python` to callout the script.
82103
104+
### FAQ
105+
106+
##### My API Key authorization level was updated, but VxApi is still showing the old value.
107+
108+
VxApi is caching key data response. To get the fresh one, please remove `cache` directory content and try to use application once again.
109+
110+
83111
### License
84112

85113
Licensed GNU GENERAL PUBLIC LICENSE, Version 3, 29 June 2007

_version.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
__version__ = '1.2.2'
1+
__version__ = '2.0.0'

api_classes/api_caller.py renamed to api/callers/api_caller.py

Lines changed: 38 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
from exceptions import ResponseObjectNotExistError
33
from exceptions import UrlBuildError
44
from exceptions import JsonParseError
5-
from requests.auth import HTTPBasicAuth
5+
from exceptions import ConfigError
66
import json
77

88

@@ -20,7 +20,6 @@ class ApiCaller:
2020
CONST_API_AUTH_LEVEL_SUPER = 1000
2121

2222
api_key = ''
23-
api_secret = ''
2423

2524
server = ''
2625
endpoint_url = ''
@@ -34,16 +33,14 @@ class ApiCaller:
3433
api_unexpected_error_msg = 'Unexpected error has occurred (HTTP code: {}). Please try again later or contact with the support'
3534
api_unexpected_error_404_msg = 'Unexpected error has occurred (HTTP code: {}). This error is mostly occurring when called webservice is outdated and so does not support current action. If you believe it is an error, please contact with the support'
3635
api_success_msg = 'Your request was successfully processed by Falcon Sandbox'
37-
api_expected_error_msg = 'API error has occurred. HTTP code: {}, API error code: {}, message: \'{}\''
38-
response_msg_success_nature = False
36+
api_expected_error_msg = 'API error has occurred. HTTP code: {}, message: \'{}\''
3937

40-
api_response = None
38+
api_response = None # TODO - rebuild the way how we set those values to use abstract methods instead
4139
api_expected_data_type = CONST_EXPECTED_DATA_TYPE_JSON
4240
api_response_json = {}
4341

44-
def __init__(self, api_key: str, api_secret: str, server: str):
42+
def __init__(self, api_key: str, server: str):
4543
self.api_key = api_key
46-
self.api_secret = api_secret
4744
self.server = server
4845
self.check_class_options()
4946

@@ -62,11 +59,16 @@ def check_class_options(self):
6259
raise OptionNotDeclaredError('Value for \'{}\' should be declared in class \'{}\'.'.format(requested_field, self.__class__.__name__))
6360

6461
def call(self, request_handler, headers={'User-agent': 'VxApi Connector'}):
65-
if ':' in self.endpoint_url:
62+
if '$' in self.endpoint_url:
6663
raise UrlBuildError('Can\'t call API endpoint with url \'{}\', when some placeholders are still not filled.'.format(self.endpoint_url))
6764

6865
caller_function = getattr(request_handler, self.request_method_name)
69-
self.api_response = caller_function(self.server + self.endpoint_url, data=self.data, params=self.params, files=self.files, headers=headers, verify=False, auth=HTTPBasicAuth(self.api_key, self.api_secret))
66+
headers['api-key'] = self.api_key
67+
68+
self.api_response = caller_function(self.get_full_endpoint_url(), data=self.data, params=self.params, files=self.files, headers=headers, verify=False, allow_redirects=False)
69+
if self.if_request_redirect():
70+
raise ConfigError('Got redirect while trying to reach server URL. Please try to update it and pass the same URL base which is visible in the browser URL bar. '
71+
'For example: it should be \'https://www.hybrid-analysis.com\', instead of \'http://www.hybrid-analysis.com\' or \'https://hybrid-analysis.com\'')
7072
self.api_result_msg = self.prepare_response_msg()
7173

7274
def attach_data(self, options):
@@ -80,49 +82,30 @@ def attach_params(self, params):
8082
def attach_files(self, files):
8183
self.files = files
8284

85+
def if_request_success(self):
86+
return str(self.api_response.status_code).startswith('2') # 20x status code
87+
88+
def if_request_redirect(self):
89+
return str(self.api_response.status_code).startswith('3') # 30x status code
90+
8391
def prepare_response_msg(self) -> str:
8492
if self.api_response is None:
8593
raise ResponseObjectNotExistError('It\'s not possible to get response message since API was not called.')
8694

87-
'''
88-
Unfortunately some instances can return valid json with text/html content type.
89-
So we're assuming that that case it's not 'not expected error' and in other steps,
90-
we will try to get proper error message from that json.
91-
'''
92-
if self.api_response.headers['Content-Type'].startswith('text/html') and self.api_response.status_code != 200:
93-
if self.api_response.status_code == 404:
94-
self.api_result_msg = self.api_unexpected_error_404_msg.format(self.api_response.status_code)
95-
else:
96-
self.api_result_msg = self.api_unexpected_error_msg.format(self.api_response.status_code)
95+
if self.if_request_success() is True:
96+
if self.api_expected_data_type == self.CONST_EXPECTED_DATA_TYPE_JSON:
97+
self.api_response_json = self.get_response_json()
98+
99+
self.api_result_msg = self.api_success_msg
97100
else:
98-
if self.api_response.status_code == 200:
99-
if self.api_expected_data_type == self.CONST_EXPECTED_DATA_TYPE_JSON:
100-
self.api_response_json = self.get_response_json()
101-
if 'response_code' in self.api_response_json: # Unfortunately not all of endpoints return the unified json.
102-
if self.api_response_json['response_code'] == 0:
103-
self.api_result_msg = self.api_success_msg
104-
self.response_msg_success_nature = True
105-
else:
106-
self.api_result_msg = self.api_expected_error_msg.format(self.api_response.status_code, self.api_response_json['response_code'], self.api_response_json['response']['error'])
107-
else:
108-
self.api_result_msg = self.api_success_msg
109-
self.response_msg_success_nature = True
110-
else: # Few endpoints can return files
111-
# Endpoint which is returning file, can also return json file. Then we need to find if we get error msg or that json file.
112-
if 'response_code' in self.get_response_json():
113-
self.api_result_msg = self.api_expected_error_msg.format(self.api_response.status_code, self.api_response_json['response_code'], self.api_response_json['response']['error'])
114-
else:
115-
self.api_result_msg = self.api_success_msg
116-
self.response_msg_success_nature = True
101+
if self.api_response.headers['Content-Type'] == 'application/json':
102+
self.api_response_json = self.api_response.json()
103+
self.api_result_msg = self.api_expected_error_msg.format(self.api_response.status_code, self.api_response_json['message'])
117104
else:
118-
if self.api_expected_data_type == self.CONST_EXPECTED_DATA_TYPE_JSON and bool(self.api_response.json()) is True: # Sometimes response can has status code different than 200 and store json with error msg
119-
self.api_response_json = self.api_response.json()
120-
self.api_result_msg = self.api_expected_error_msg.format(self.api_response.status_code, self.api_response_json['response_code'], self.api_response_json['response']['error'])
105+
if self.api_response.status_code == 404:
106+
self.api_result_msg = self.api_unexpected_error_404_msg.format(self.api_response.status_code)
121107
else:
122-
if self.api_response.status_code == 404:
123-
self.api_result_msg = self.api_unexpected_error_404_msg.format(self.api_response.status_code)
124-
else:
125-
self.api_result_msg = self.api_unexpected_error_msg.format(self.api_response.status_code)
108+
self.api_result_msg = self.api_unexpected_error_msg.format(self.api_response.status_code)
126109

127110
return self.api_result_msg
128111

@@ -132,12 +115,6 @@ def get_api_response(self):
132115

133116
return self.api_response
134117

135-
def get_response_msg_success_nature(self):
136-
if self.api_response is None:
137-
raise ResponseObjectNotExistError('It\'s not possible to define api msg nature since API was not called.')
138-
139-
return self.response_msg_success_nature
140-
141118
def get_response_status_code(self):
142119
if self.api_response is None:
143120
raise ResponseObjectNotExistError('It\'s not possible to get response code since API was not called.')
@@ -179,14 +156,20 @@ def get_response_json(self):
179156

180157
return self.api_response_json
181158

159+
def get_headers(self):
160+
if self.api_response is None:
161+
raise ResponseObjectNotExistError('It\'s not possible to get response headers since API was not called.')
162+
163+
return self.api_response.headers
164+
182165
def build_url(self, params):
183-
if ':' in self.endpoint_url:
166+
if '$' in self.endpoint_url:
184167
url_data = params
185168
url_data_copy = url_data.copy()
186169
for key, value in url_data.items():
187-
searched_key = ':' + key
170+
searched_key = '$' + key
188171
if searched_key in self.endpoint_url:
189-
self.endpoint_url = self.endpoint_url.replace(':' + key, value)
172+
self.endpoint_url = self.endpoint_url.replace('$' + key, str(value))
190173
del url_data_copy[key] # Working on copy, since it's not possible to manipulate dict size, during iteration
191174

192175
if self.request_method_name == self.CONST_REQUEST_METHOD_GET:
@@ -195,4 +178,4 @@ def build_url(self, params):
195178
self.data = url_data_copy
196179

197180
def get_full_endpoint_url(self):
198-
return self.server + self.endpoint_url
181+
return '{}/api/v2{}'.format(self.server, self.endpoint_url)

api/callers/feed/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
from api.callers.feed.api_feed import ApiFeed
2+
from api.callers.feed.api_feed_latest import ApiFeedLatest

api/callers/feed/api_feed.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
from api.callers.api_caller import ApiCaller
2+
3+
4+
class ApiFeed(ApiCaller):
5+
endpoint_url = '/feed/$days/days'
6+
endpoint_auth_level = ApiCaller.CONST_API_AUTH_LEVEL_ELEVATED
7+
request_method_name = ApiCaller.CONST_REQUEST_METHOD_GET
Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
from api_classes.api_caller import ApiCaller
1+
from api.callers.api_caller import ApiCaller
22

33

4-
class ApiApiQueryLimits(ApiCaller):
5-
endpoint_url = '/api/api-limits'
6-
request_method_name = ApiCaller.CONST_REQUEST_METHOD_GET
4+
class ApiFeedLatest(ApiCaller):
5+
endpoint_url = '/feed/latest'
76
endpoint_auth_level = ApiCaller.CONST_API_AUTH_LEVEL_RESTRICTED
7+
request_method_name = ApiCaller.CONST_REQUEST_METHOD_GET

api/callers/key/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
from api.callers.key.api_key_create import ApiKeyCreate
2+
from api.callers.key.api_key_current import ApiKeyCurrent
Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
from api_classes.api_caller import ApiCaller
1+
from api.callers.api_caller import ApiCaller
22

33

4-
class ApiNssfList(ApiCaller):
5-
endpoint_url = '/api/nssf/list'
6-
request_method_name = ApiCaller.CONST_REQUEST_METHOD_POST
4+
class ApiKeyCreate(ApiCaller):
5+
endpoint_url = '/key/create'
76
endpoint_auth_level = ApiCaller.CONST_API_AUTH_LEVEL_ELEVATED
7+
request_method_name = ApiCaller.CONST_REQUEST_METHOD_POST
Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
from api_classes.api_caller import ApiCaller
1+
from api.callers.api_caller import ApiCaller
22

33

4-
class ApiEnvironments(ApiCaller):
5-
endpoint_url = '/api/environments'
6-
request_method_name = ApiCaller.CONST_REQUEST_METHOD_GET
4+
class ApiKeyCurrent(ApiCaller):
5+
endpoint_url = '/key/current'
76
endpoint_auth_level = ApiCaller.CONST_API_AUTH_LEVEL_RESTRICTED
7+
request_method_name = ApiCaller.CONST_REQUEST_METHOD_GET

0 commit comments

Comments
 (0)