├── tests ├── resources │ ├── fireeye_auth_login.json │ ├── fireeye_config.json │ ├── fireeye_submissions.json │ ├── falcon_system_queue-size.json │ ├── cuckoo_tasks_create_file.json │ ├── vmray_job_sample_done.json │ ├── fireeye_submissions_status.json │ ├── falcon_report_state.json │ ├── joe_server_online.json │ ├── joe_analysis_submit.json │ ├── joe_submission_new.json │ ├── triage_check.json │ ├── falcon_submit_file.json │ ├── falcon_system_heartbeat.json │ ├── fireeye_unauthorized.json │ ├── triage_analyze.json │ ├── vmray_system_info.json │ ├── cuckoo_tasks_view.json │ ├── fireeye_submissions_results.json │ ├── joe_analysis_info.json │ ├── cuckoo_tasks_report.json │ ├── cuckoo_status.json │ ├── vmray_analysis_archive_logs_summary.json │ ├── vmray_sample.json │ ├── vmray_submission.json │ ├── triage_report.json │ ├── joe_analysis_download.json │ ├── vmray_submission_sample.json │ ├── vmray_job_sample.json │ ├── cuckoo_tasks_list.json │ ├── falcon_report_summary.json │ ├── opswat_submissions_result_not_finished.json │ ├── vmray_sample_submit_errors.json │ ├── vmray_analysis_sample.json │ ├── vmray_analysis_submission.json │ └── vmray_sample_submit.json ├── __init__.py ├── test_sandboxapi.py ├── test_joe.py ├── test_falcon.py ├── test_triage.py ├── test_opswat.py ├── test_vmray.py ├── test_cuckoo.py └── test_fireeye.py ├── requirements.txt ├── MANIFEST.in ├── .coveragerc ├── docs ├── _static │ ├── favicon.ico │ ├── sandboxapi.png │ ├── fonts │ │ ├── 2C60D5_25_0.eot │ │ ├── 2C60D5_25_0.ttf │ │ ├── 2C60D5_28_0.eot │ │ ├── 2C60D5_28_0.ttf │ │ ├── 2C60D5_25_0.woff │ │ ├── 2C60D5_25_0.woff2 │ │ ├── 2C60D5_28_0.woff │ │ └── 2C60D5_28_0.woff2 │ ├── inquest-magnifying-glass@2x.png │ └── custom.css ├── Makefile ├── make.bat ├── index.rst ├── _templates │ └── links.html └── conf.py ├── CONTRIBUTING.md ├── .gitignore ├── Pipfile ├── .github └── workflows │ └── tests.yml ├── .travis.yml ├── setup.py ├── sandboxapi ├── joe.py ├── __init__.py ├── wildfire.py ├── vmray.py ├── triage.py ├── opswat.py ├── cuckoo.py ├── fireeye.py └── falcon.py ├── README.rst └── LICENSE /tests/resources/fireeye_auth_login.json: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/resources/fireeye_config.json: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | requests 2 | jbxapi 3 | xmltodict 4 | -------------------------------------------------------------------------------- /tests/resources/fireeye_submissions.json: -------------------------------------------------------------------------------- 1 | { 2 | "ID": 1 3 | } 4 | -------------------------------------------------------------------------------- /tests/resources/falcon_system_queue-size.json: -------------------------------------------------------------------------------- 1 | { 2 | "value": 10 3 | } 4 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include LICENSE 2 | include README.rst 3 | include requirements.txt 4 | -------------------------------------------------------------------------------- /tests/resources/cuckoo_tasks_create_file.json: -------------------------------------------------------------------------------- 1 | { 2 | "task_id" : 1 3 | } 4 | -------------------------------------------------------------------------------- /.coveragerc: -------------------------------------------------------------------------------- 1 | [report] 2 | 3 | exclude_lines = 4 | if __name__ == .__main__.: 5 | -------------------------------------------------------------------------------- /tests/resources/vmray_job_sample_done.json: -------------------------------------------------------------------------------- 1 | { 2 | "data": [], 3 | "result": "ok" 4 | } -------------------------------------------------------------------------------- /tests/resources/fireeye_submissions_status.json: -------------------------------------------------------------------------------- 1 | { 2 | "submissionStatus": "Done" 3 | } 4 | -------------------------------------------------------------------------------- /docs/_static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/InQuest/sandboxapi/HEAD/docs/_static/favicon.ico -------------------------------------------------------------------------------- /tests/resources/falcon_report_state.json: -------------------------------------------------------------------------------- 1 | { 2 | "state": "SUCCESS", 3 | "error": null 4 | } 5 | -------------------------------------------------------------------------------- /docs/_static/sandboxapi.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/InQuest/sandboxapi/HEAD/docs/_static/sandboxapi.png -------------------------------------------------------------------------------- /tests/resources/joe_server_online.json: -------------------------------------------------------------------------------- 1 | { 2 | "data": { 3 | "online": true 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /tests/resources/joe_analysis_submit.json: -------------------------------------------------------------------------------- 1 | { 2 | "data": { 3 | "webids": ["100001"] 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /tests/resources/joe_submission_new.json: -------------------------------------------------------------------------------- 1 | { 2 | "data": { 3 | "submission_id": "100001" 4 | } 5 | } -------------------------------------------------------------------------------- /tests/resources/triage_check.json: -------------------------------------------------------------------------------- 1 | { 2 | "sample":"200615-8jbndpgg9n", 3 | "status":"reported" 4 | } 5 | -------------------------------------------------------------------------------- /docs/_static/fonts/2C60D5_25_0.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/InQuest/sandboxapi/HEAD/docs/_static/fonts/2C60D5_25_0.eot -------------------------------------------------------------------------------- /docs/_static/fonts/2C60D5_25_0.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/InQuest/sandboxapi/HEAD/docs/_static/fonts/2C60D5_25_0.ttf -------------------------------------------------------------------------------- /docs/_static/fonts/2C60D5_28_0.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/InQuest/sandboxapi/HEAD/docs/_static/fonts/2C60D5_28_0.eot -------------------------------------------------------------------------------- /docs/_static/fonts/2C60D5_28_0.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/InQuest/sandboxapi/HEAD/docs/_static/fonts/2C60D5_28_0.ttf -------------------------------------------------------------------------------- /docs/_static/fonts/2C60D5_25_0.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/InQuest/sandboxapi/HEAD/docs/_static/fonts/2C60D5_25_0.woff -------------------------------------------------------------------------------- /docs/_static/fonts/2C60D5_25_0.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/InQuest/sandboxapi/HEAD/docs/_static/fonts/2C60D5_25_0.woff2 -------------------------------------------------------------------------------- /docs/_static/fonts/2C60D5_28_0.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/InQuest/sandboxapi/HEAD/docs/_static/fonts/2C60D5_28_0.woff -------------------------------------------------------------------------------- /docs/_static/fonts/2C60D5_28_0.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/InQuest/sandboxapi/HEAD/docs/_static/fonts/2C60D5_28_0.woff2 -------------------------------------------------------------------------------- /docs/_static/inquest-magnifying-glass@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/InQuest/sandboxapi/HEAD/docs/_static/inquest-magnifying-glass@2x.png -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | How to Contribute 2 | ================= 3 | 4 | Read the [LICENSE](LICENSE). By submitting a pull request, you agree to 5 | release your changes under this license. 6 | -------------------------------------------------------------------------------- /tests/resources/falcon_submit_file.json: -------------------------------------------------------------------------------- 1 | { 2 | "job_id": "1", 3 | "environment_id": "100", 4 | "sha256": "01ba4719c80b6fe911b091a7c05124b64eeece964e09c058ef8f9805daca546b" 5 | } 6 | -------------------------------------------------------------------------------- /tests/resources/falcon_system_heartbeat.json: -------------------------------------------------------------------------------- 1 | { 2 | "last_update": "1970-01-01 01:00:00", 3 | "number_of_seconds_since_last_update": 1555555555, 4 | "system_time":"2018-10-10 10:10:10" 5 | } 6 | -------------------------------------------------------------------------------- /tests/resources/fireeye_unauthorized.json: -------------------------------------------------------------------------------- 1 | {"fireeyeapis":{"@version":"v1.2.0","description":"Please check if the cookie has expired and retry. code:AUTH004","httpStatus":401,"message":"Unauthorized"}} 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.py[cdo] 2 | .coverage 3 | *.xml 4 | *.egg-info/ 5 | build/ 6 | dist/ 7 | _build/ 8 | /build_dist.sh 9 | .vscode/ 10 | /.virtualenv/ 11 | /.pytest_cache/ 12 | Pipfile.lock 13 | .DS_Store 14 | -------------------------------------------------------------------------------- /tests/resources/triage_analyze.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "200707-pht1cwk3ls", 3 | "status": "pending", 4 | "kind": "file", 5 | "filename": "KfhizGC7.ps1", 6 | "private": false, 7 | "submitted": "2020-07-07T13:19:44Z" 8 | } -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | import os 2 | import json 3 | 4 | def read_resource(resource): 5 | with open(os.path.join('tests', 'resources', '{r}.json'.format(r=resource)), 'r') as f: 6 | return json.loads(f.read()) 7 | -------------------------------------------------------------------------------- /Pipfile: -------------------------------------------------------------------------------- 1 | [[source]] 2 | url = "https://pypi.org/simple" 3 | verify_ssl = true 4 | name = "pypi" 5 | 6 | [packages] 7 | requests = "*" 8 | jbxapi = "*" 9 | xmltodict = "*" 10 | 11 | [dev-packages] 12 | pytest = "*" 13 | coverage = "*" 14 | responses = "*" 15 | "collective.checkdocs" = "*" 16 | pygments = "*" 17 | 18 | [requires] 19 | python_version = "3.9" 20 | -------------------------------------------------------------------------------- /tests/resources/vmray_system_info.json: -------------------------------------------------------------------------------- 1 | { 2 | "data": { 3 | "api_items_per_request": 100, 4 | "file_param_http_scheme_enabled": true, 5 | "max_api_items": 1000, 6 | "version": "2.1.0", 7 | "version_major": 2, 8 | "version_minor": 1, 9 | "version_revision": 0, 10 | "webif_alias": "Cloud", 11 | "webif_base_url": "https://cloud.vmray.com", 12 | "webif_max_sample_size": 209715200, 13 | "webif_max_upload_size": 209715200 14 | }, 15 | "result": "ok" 16 | } -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | SPHINXPROJ = sandboxapi 8 | SOURCEDIR = . 9 | BUILDDIR = _build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) -------------------------------------------------------------------------------- /tests/resources/cuckoo_tasks_view.json: -------------------------------------------------------------------------------- 1 | { 2 | "task": { 3 | "category": "url", 4 | "machine": null, 5 | "errors": [], 6 | "target": "http://www.malicious.site", 7 | "package": null, 8 | "sample_id": null, 9 | "guest": {}, 10 | "custom": null, 11 | "owner": "", 12 | "priority": 1, 13 | "platform": null, 14 | "options": null, 15 | "status": "completed", 16 | "enforce_timeout": false, 17 | "timeout": 0, 18 | "memory": false, 19 | "tags": [ 20 | "32bit", 21 | "acrobat_6" 22 | ], 23 | "id": 1, 24 | "added_on": "2012-12-19 14:18:25", 25 | "completed_on": null 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: sandboxapi 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | strategy: 9 | matrix: 10 | python-version: ["3.7", "3.8", "3.9"] 11 | 12 | steps: 13 | - uses: actions/checkout@v3 14 | - name: Set up Python ${{ matrix.python-version }} 15 | uses: actions/setup-python@v4 16 | with: 17 | python-version: ${{ matrix.python-version }} 18 | - name: Install dependencies 19 | run: | 20 | pip install -r requirements.txt 21 | pip install pytest pytest-mock coverage requests-mock responses collective.checkdocs Pygments nose 22 | - name: Test scripts 23 | run: | 24 | coverage run -m unittest discover 25 | nosetests tests/* 26 | -------------------------------------------------------------------------------- /tests/resources/fireeye_submissions_results.json: -------------------------------------------------------------------------------- 1 | { 2 | "msg": "concise", 3 | "alertsCount": 1, 4 | "version": "MAS (MAS) 7.7.7.777777", 5 | "appliance": "MAS", 6 | "alert": [ 7 | { 8 | "src": {}, 9 | "product": "MAS", 10 | "name": "MALWARE_OBJECT", 11 | "explanation": { 12 | "osChanges": [], 13 | "malwareDetected": { 14 | "malware": [{"md5Sum": "68b329da9893e34099c7d8ad5cb9c940"}] 15 | } 16 | }, 17 | "occurred": 1000000000000, 18 | "alertUrl": "https://10.10.10.10/malware_analysis/analyses?maid=1", 19 | "id": 1, 20 | "action": "notified", 21 | "dst": {}, 22 | "severity": "MAJR" 23 | } 24 | ] 25 | } 26 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | jobs: 3 | include: 4 | - name: "Python 3.9" 5 | python: 3.9 6 | install: 7 | - "pip install -r requirements.txt" 8 | - "pip install pytest pytest-mock coverage requests-mock responses collective.checkdocs Pygments" 9 | script: 10 | - coverage run -m pytest 11 | - python setup.py checkdocs 12 | after_success: 13 | - coveralls 14 | - coverage xml 15 | - if [ "$TRAVIS_BRANCH" = "master" ]; then bash <(curl -Ls https://coverage.codacy.com/get.sh) report -r coverage.xml; fi 16 | - name: "Python 2.7" 17 | python: 2.7 18 | install: 19 | - "pip install -r requirements.txt" 20 | - "pip install nose mock requests-mock responses collective.checkdocs Pygments" 21 | script: 22 | - nosetests 23 | -------------------------------------------------------------------------------- /tests/resources/joe_analysis_info.json: -------------------------------------------------------------------------------- 1 | { 2 | "data": { 3 | "webid": "1", 4 | "status": "finished", 5 | "comments": "a sample comment", 6 | "filename": "sample.exe", 7 | "scriptname": "default.jbs", 8 | "time": "2017-08-11T16:06:32+02:00", 9 | "md5": "0cbc6611f5540bd0809a388dc95a615b", 10 | "sha1": "640ab2bae07bedc4c163f679a746f7ab7fb5d1fa", 11 | "sha256": "532eaabd9574880 [...] 299550d7a6e0f345e25", 12 | "runs": [{ 13 | "detection": "unknown", 14 | "error": "Unable to run", 15 | "system": "w7", 16 | "yara": false 17 | }, { 18 | "detection": "malicious", 19 | "error": null, 20 | "system": "w7x64", 21 | "yara": false 22 | }] 23 | } 24 | } 25 | 26 | -------------------------------------------------------------------------------- /tests/resources/cuckoo_tasks_report.json: -------------------------------------------------------------------------------- 1 | { 2 | "signatures": [], 3 | "virustotal": { 4 | }, 5 | "static": { 6 | }, 7 | "dropped": [], 8 | "network": { 9 | }, 10 | "info": { 11 | "category": "file", 12 | "package": "", 13 | "score": 5, 14 | "started": "2016-08-26 15:24:51", 15 | "custom": "", 16 | "machine": {}, 17 | "ended": "2016-08-26 15:28:08", 18 | "version": "1.1", 19 | "duration": 197, 20 | "id": 8 21 | }, 22 | "target": { 23 | "category": "file", 24 | "file": { 25 | } 26 | }, 27 | "behavior": {}, 28 | "memory": {}, 29 | "debug": { 30 | }, 31 | "strings": [ 32 | "!This program cannot be run in DOS mode.", 33 | "`.rdata", 34 | "@.data" 35 | ] 36 | } 37 | -------------------------------------------------------------------------------- /tests/resources/cuckoo_status.json: -------------------------------------------------------------------------------- 1 | { 2 | "tasks": { 3 | "reported": 165, 4 | "running": 2, 5 | "total": 167, 6 | "completed": 0, 7 | "pending": 0 8 | }, 9 | "diskspace": { 10 | "analyses": { 11 | "total": 491271233536, 12 | "free": 71403470848, 13 | "used": 419867762688 14 | }, 15 | "binaries": { 16 | "total": 491271233536, 17 | "free": 71403470848, 18 | "used": 419867762688 19 | }, 20 | "temporary": { 21 | "total": 491271233536, 22 | "free": 71403470848, 23 | "used": 419867762688 24 | } 25 | }, 26 | "version": "1.0", 27 | "protocol_version": 1, 28 | "hostname": "Patient0", 29 | "machines": { 30 | "available": 4, 31 | "total": 5 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /tests/resources/vmray_analysis_archive_logs_summary.json: -------------------------------------------------------------------------------- 1 | { 2 | "analysis_details": { 3 | "creation_time": "2017-10-23 22:01 (UTC+2)", 4 | "execution_successful": true, 5 | "number_of_processes": 15, 6 | "reputation_enabled": true, 7 | "termination_reason": "timeout", 8 | "type": "analysis_details", 9 | "version": 2, 10 | "vm_analysis_duration_time": "00:02:27" 11 | }, 12 | "artifacts": {}, 13 | "extracted_files": [], 14 | "process_dumps": [], 15 | "processes": [], 16 | "remarks": {}, 17 | "sample_details": {}, 18 | "screenshots": [], 19 | "type": "", 20 | "version": 1, 21 | "vm_and_analyzer_details": {}, 22 | "vti": { 23 | "type": "vti", 24 | "version": 1, 25 | "vti_built_in_rules_version": "2.6", 26 | "vti_rule_type": "Default (PE, ...)", 27 | "vti_rule_matches": [], 28 | "vti_score": 20 29 | }, 30 | "yara": {} 31 | } 32 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=sphinx-build 9 | ) 10 | set SOURCEDIR=. 11 | set BUILDDIR=_build 12 | set SPHINXPROJ=sandboxapi 13 | 14 | if "%1" == "" goto help 15 | 16 | %SPHINXBUILD% >NUL 2>NUL 17 | if errorlevel 9009 ( 18 | echo. 19 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 20 | echo.installed, then set the SPHINXBUILD environment variable to point 21 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 22 | echo.may add the Sphinx directory to PATH. 23 | echo. 24 | echo.If you don't have Sphinx installed, grab it from 25 | echo.http://sphinx-doc.org/ 26 | exit /b 1 27 | ) 28 | 29 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% 30 | goto end 31 | 32 | :help 33 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% 34 | 35 | :end 36 | popd 37 | -------------------------------------------------------------------------------- /tests/resources/vmray_sample.json: -------------------------------------------------------------------------------- 1 | { 2 | "data": { 3 | "sample_created": "2017-10-23T19:50:17", 4 | "sample_filename": "privatetunnel-win-2.7.exe", 5 | "sample_filesize": 30736384, 6 | "sample_highest_vti_score": 67, 7 | "sample_highest_vti_severity": "suspicious", 8 | "sample_id": 1169850, 9 | "sample_is_multipart": false, 10 | "sample_last_md_score": null, 11 | "sample_last_reputation_severity": "unknown", 12 | "sample_last_vt_score": null, 13 | "sample_md5hash": "7371a86afb7446f6db2582ab43e4f974", 14 | "sample_priority": 1, 15 | "sample_score": 67, 16 | "sample_severity": "suspicious", 17 | "sample_sha1hash": "e2b1ddf0ba3536caf1788d79da9d2fac0a1fc1e5", 18 | "sample_sha256hash": "c3cd7a4ee74d0444435bbb3eecddc58254dd0ca6d9e93a5d3e48b95d3a78c5e7", 19 | "sample_type": "Windows Exe (x86-32)", 20 | "sample_url": null, 21 | "sample_vti_score": 67, 22 | "sample_webif_url": "https://cloud.vmray.com/user/sample/view?id=1169850" 23 | }, 24 | "result": "ok" 25 | } 26 | -------------------------------------------------------------------------------- /tests/test_sandboxapi.py: -------------------------------------------------------------------------------- 1 | from unittest import TestCase 2 | 3 | try: 4 | from unittest.mock import patch, ANY as MOCK_ANY 5 | except ImportError: 6 | from mock import patch, ANY as MOCK_ANY 7 | 8 | import sandboxapi 9 | 10 | class TestSandboxAPI(TestCase): 11 | 12 | @patch('requests.post') 13 | @patch('requests.get') 14 | def test_proxies_is_passed_to_requests(self, m_get, m_post): 15 | m_get.return_value.status_code = 200 16 | m_post.return_value.status_code = 200 17 | 18 | proxies = { 19 | 'http': 'http://10.10.1.10:3128', 20 | 'https': 'http://10.10.1.10:1080', 21 | } 22 | 23 | api = sandboxapi.SandboxAPI(proxies=proxies) 24 | api.api_url = 'http://sandbox.mock' 25 | api._request('/test') 26 | 27 | m_get.assert_called_once_with('http://sandbox.mock/test', auth=None, 28 | headers=None, params=None, proxies=proxies, 29 | verify=True) 30 | 31 | api._request('/test', method='POST') 32 | 33 | m_post.assert_called_once_with('http://sandbox.mock/test', auth=None, 34 | headers=None, data=None, files=None, 35 | proxies=proxies, verify=True) 36 | -------------------------------------------------------------------------------- /tests/resources/vmray_submission.json: -------------------------------------------------------------------------------- 1 | { 2 | "data": { 3 | "submission_comment": null, 4 | "submission_created": "2017-10-23T19:50:17", 5 | "submission_document_password": null, 6 | "submission_filename": "privatetunnel-win-2.7.exe", 7 | "submission_finish_time": "2017-10-23T20:04:51", 8 | "submission_finished": true, 9 | "submission_has_errors": false, 10 | "submission_id": 1374306, 11 | "submission_ip_id": 0, 12 | "submission_ip_ip": "10.10.10.10", 13 | "submission_original_filename": "privatetunnel-win-2.7.exe", 14 | "submission_prescript_id": null, 15 | "submission_priority": 1, 16 | "submission_reputation_mode": "auxiliary", 17 | "submission_sample_id": 1169850, 18 | "submission_sample_md5": "7371a86afb7446f6db2582ab43e4f974", 19 | "submission_sample_sha1": "e2b1ddf0ba3536caf1788d79da9d2fac0a1fc1e5", 20 | "submission_sample_sha256": "c3cd7a4ee74d0444435bbb3eecddc58254dd0ca6d9e93a5d3e48b95d3a78c5e7", 21 | "submission_shareable": false, 22 | "submission_tags": [], 23 | "submission_triage_error_handling": null, 24 | "submission_type": "api", 25 | "submission_user_email": "a@example.com", 26 | "submission_user_id": 0, 27 | "submission_webif_url": "https://cloud.vmray.com/user/sample/view?id=1169850" 28 | }, 29 | "result": "ok" 30 | } 31 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import os 2 | from setuptools import setup 3 | 4 | README = open(os.path.join(os.path.dirname(__file__), 'README.rst')).read() 5 | 6 | # allow setup.py to be run from any path 7 | os.chdir(os.path.normpath(os.path.join(os.path.abspath(__file__), os.pardir))) 8 | 9 | requirements = open(os.path.join(os.path.dirname(__file__), 10 | 'requirements.txt')).read() 11 | requires = requirements.strip().split('\n') 12 | 13 | setup( 14 | name='sandboxapi', 15 | version='1.8.0', 16 | include_package_data=True, 17 | packages=[ 18 | 'sandboxapi', 19 | ], 20 | install_requires=requires, 21 | license='GPL', 22 | description='Minimal, consistent API for building integrations with malware sandboxes.', 23 | long_description=README, 24 | url='https://github.com/InQuest/sandboxapi', 25 | author='InQuest Labs', 26 | author_email='labs@inquest.net', 27 | classifiers=[ 28 | 'Intended Audience :: Developers', 29 | 'Intended Audience :: Information Technology', 30 | 'Intended Audience :: System Administrators', 31 | 'License :: OSI Approved :: GNU General Public License v2 (GPLv2)', 32 | 'Operating System :: OS Independent', 33 | 'Programming Language :: Python', 34 | 'Topic :: Software Development :: Libraries', 35 | 'Topic :: Internet', 36 | ], 37 | ) 38 | -------------------------------------------------------------------------------- /tests/resources/triage_report.json: -------------------------------------------------------------------------------- 1 | { 2 | "sample": "200615-8jbndpgg9n", 3 | "status": "reported", 4 | "custom": "frontend:b77b1c44-2965-4580-b444-a63f02d3a9d3", 5 | "owner": "shark2.ams5.hatching.io", 6 | "target": "2ebe4c68225206161c70cf3e0da39294e9353ee295db2dc5d4f86ce7901210c5", 7 | "created": "2020-06-15T10:04:02Z", 8 | "completed": "2020-06-15T10:06:44Z", 9 | "score": 10, 10 | "sha256": "2ebe4c68225206161c70cf3e0da39294e9353ee295db2dc5d4f86ce7901210c5", 11 | "tasks": { 12 | "200615-8jbndpgg9n-behavioral1": { 13 | "kind": "behavioral", 14 | "status": "reported", 15 | "tags": ["ransomware", "persistence", "family:nemty"], 16 | "score": 10, 17 | "target": "2ebe4c68225206161c70cf3e0da39294e9353ee295db2dc5d4f86ce7901210c5.exe", 18 | "backend": "horse2", 19 | "resource": "win7v200430", 20 | "platform": "windows7_x64", 21 | "queue_id": 1355315 22 | }, 23 | "200615-8jbndpgg9n-behavioral2": { 24 | "kind": "behavioral", 25 | "status": "reported", 26 | "tags": ["ransomware", "family:nemty", "persistence"], 27 | "score": 10, 28 | "target": "2ebe4c68225206161c70cf3e0da39294e9353ee295db2dc5d4f86ce7901210c5.exe", 29 | "backend": "horse2", 30 | "resource": "win10v200430", 31 | "platform": "windows10_x64", 32 | "queue_id": 1355316 33 | }, 34 | "200615-8jbndpgg9n-static1": { 35 | "kind": "static", 36 | "status": "reported" 37 | } 38 | } 39 | } -------------------------------------------------------------------------------- /tests/resources/joe_analysis_download.json: -------------------------------------------------------------------------------- 1 | { 2 | "analysis": { 3 | "signatureconfidence": {}, 4 | "signatureinfo": {}, 5 | "simulations": null, 6 | "droppedinfo": null, 7 | "fileinfo": {}, 8 | "signatureclassifications": {}, 9 | "patches": null, 10 | "eventlog": {}, 11 | "comments": {}, 12 | "sigscore": {}, 13 | "avhit": {}, 14 | "patterninfo": null, 15 | "behavior": {}, 16 | "domaininfo": null, 17 | "successnotices": null, 18 | "generalinfo": {}, 19 | "warninginfo": {}, 20 | "signaturedetections": { 21 | "strategy": [ 22 | { 23 | "@count": "0", 24 | "@name": "atleastonemalicioussig", 25 | "$": "false" 26 | }, 27 | { 28 | "malicious": false, 29 | "@name": "empiric", 30 | "unknown": false, 31 | "minScore": 0, 32 | "suspicious": false, 33 | "detection": "CLEAN", 34 | "score": 1, 35 | "clean": true, 36 | "maxScore": 100 37 | } 38 | ] 39 | }, 40 | "yara": {}, 41 | "ipinfo": null, 42 | "runtimemessages": "", 43 | "analysistime": {}, 44 | "context": {}, 45 | "errorinfo": null 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /tests/resources/vmray_submission_sample.json: -------------------------------------------------------------------------------- 1 | { 2 | "data": [ 3 | { 4 | "submission_comment": null, 5 | "submission_created": "2017-10-23T19:50:17", 6 | "submission_document_password": null, 7 | "submission_filename": "privatetunnel-win-2.7.exe", 8 | "submission_finish_time": "2017-10-23T20:04:51", 9 | "submission_finished": true, 10 | "submission_has_errors": false, 11 | "submission_id": 1374306, 12 | "submission_ip_id": 0, 13 | "submission_ip_ip": "10.10.10.10", 14 | "submission_original_filename": "privatetunnel-win-2.7.exe", 15 | "submission_prescript_id": null, 16 | "submission_priority": 1, 17 | "submission_reputation_mode": "auxiliary", 18 | "submission_sample_id": 1169850, 19 | "submission_sample_md5": "7371a86afb7446f6db2582ab43e4f974", 20 | "submission_sample_sha1": "e2b1ddf0ba3536caf1788d79da9d2fac0a1fc1e5", 21 | "submission_sample_sha256": "c3cd7a4ee74d0444435bbb3eecddc58254dd0ca6d9e93a5d3e48b95d3a78c5e7", 22 | "submission_shareable": false, 23 | "submission_tags": [], 24 | "submission_triage_error_handling": null, 25 | "submission_type": "api", 26 | "submission_user_email": "a@example.com", 27 | "submission_user_id": 0, 28 | "submission_webif_url": "https://cloud.vmray.com/user/sample/view?id=1169850" 29 | } 30 | ], 31 | "result": "ok" 32 | } 33 | -------------------------------------------------------------------------------- /tests/resources/vmray_job_sample.json: -------------------------------------------------------------------------------- 1 | { 2 | "data": [ 3 | { 4 | "job_analyzer_id": 1, 5 | "job_analyzer_name": "vmray", 6 | "job_configuration_id": 48, 7 | "job_configuration_name": "exe", 8 | "job_created": "2017-10-23T19:50:17", 9 | "job_document_password": null, 10 | "job_id": 1097212, 11 | "job_jobrule_id": 13, 12 | "job_jobrule_sampletype": "Windows PE (x86-32)", 13 | "job_parent_analysis_id": null, 14 | "job_prescript_id": null, 15 | "job_priority": 1, 16 | "job_reputation_job_id": 5880, 17 | "job_sample_id": 1169850, 18 | "job_sample_md5": "7371a86afb7446f6db2582ab43e4f974", 19 | "job_sample_sha1": "e2b1ddf0ba3536caf1788d79da9d2fac0a1fc1e5", 20 | "job_sample_sha256": "c3cd7a4ee74d0444435bbb3eecddc58254dd0ca6d9e93a5d3e48b95d3a78c5e7", 21 | "job_snapshot_id": 1, 22 | "job_snapshot_name": "def", 23 | "job_status": "inwork", 24 | "job_statuschanged": "2017-10-23T20:00:57", 25 | "job_submission_id": 1374306, 26 | "job_tracking_state": "//running/vm/executing", 27 | "job_type": "full_analysis", 28 | "job_user_email": "a@example.com", 29 | "job_user_id": 0, 30 | "job_vm_id": 18, 31 | "job_vm_name": "win8.1_64", 32 | "job_vmhost_id": 4, 33 | "job_vmhost_name": "cloud-worker-02", 34 | "job_vminstance_num": 6, 35 | "job_vnc_token": "" 36 | } 37 | ], 38 | "result": "ok" 39 | } 40 | -------------------------------------------------------------------------------- /tests/resources/cuckoo_tasks_list.json: -------------------------------------------------------------------------------- 1 | { 2 | "tasks": [ 3 | { 4 | "category": "url", 5 | "machine": null, 6 | "errors": [], 7 | "target": "http://www.malicious.site", 8 | "package": null, 9 | "sample_id": null, 10 | "guest": {}, 11 | "custom": null, 12 | "owner": "", 13 | "priority": 1, 14 | "platform": null, 15 | "options": null, 16 | "status": "pending", 17 | "enforce_timeout": false, 18 | "timeout": 0, 19 | "memory": false, 20 | "tags": [], 21 | "id": 1, 22 | "added_on": "2012-12-19 14:18:25", 23 | "completed_on": null 24 | }, 25 | { 26 | "category": "file", 27 | "machine": null, 28 | "errors": [], 29 | "target": "/tmp/malware.exe", 30 | "package": null, 31 | "sample_id": 1, 32 | "guest": {}, 33 | "custom": null, 34 | "owner": "", 35 | "priority": 1, 36 | "platform": null, 37 | "options": null, 38 | "status": "pending", 39 | "enforce_timeout": false, 40 | "timeout": 0, 41 | "memory": false, 42 | "tags": [ 43 | "32bit", 44 | "acrobat_6" 45 | ], 46 | "id": 2, 47 | "added_on": "2012-12-19 14:18:25", 48 | "completed_on": null 49 | } 50 | ] 51 | } 52 | -------------------------------------------------------------------------------- /tests/resources/falcon_report_summary.json: -------------------------------------------------------------------------------- 1 | { 2 | "environment_id": "100", 3 | "threat_score": 5, 4 | "target_url": null, 5 | "total_processes": 0, 6 | "threat_level": 1, 7 | "size": 100000, 8 | "job_id": "1", 9 | "vx_family": null, 10 | "interesting": false, 11 | "compromised_hosts": null, 12 | "state": "SUCCESS", 13 | "certificates": null, 14 | "hosts": [], 15 | "sha256": "01ba4719c80b6fe911b091a7c05124b64eeece964e09c058ef8f9805daca546b", 16 | "sha512": "be688838ca8686e5c90689bf2ab585cef1137c999b48c70b92f67a5c34dc15697b5d11c982ed6d71be1e1e7f7b4e0733884aa97c3f7a339a8ed03577cf74be09", 17 | "extracted_files": null, 18 | "analysis_start_time": "2018-10-10 10:10:10", 19 | "imphash": "Unknown", 20 | "total_network_connections": 0, 21 | "av_detect": 0, 22 | "classification_tags": [ 23 | "pdfjsc", 24 | "pidief" 25 | ], 26 | "submit_name": "example.pdf", 27 | "ssdeep": "Unknown", 28 | "md5": "68b329da9893e34099c7d8ad5cb9c940", 29 | "total_signatures": 3, 30 | "processes": null, 31 | "sha1": "adc83b19e793491b1c6ea0fd8b46cd9f32e592fc", 32 | "url_analysis": false, 33 | "type": "PDF document, version 1.6", 34 | "file_metadata": { 35 | "file_compositions": [], 36 | "file_analysis": [ 37 | "%PDF-1.6\\n", 38 | "%\\xe2\\xe3\\xcf\\xd3\\r\\n", 39 | "%%EOF\\r\\n" 40 | ], 41 | "total_file_compositions_imports": null, 42 | "imported_objects": [] 43 | }, 44 | "environment_description": "Windows 7 32 bit", 45 | "verdict": "no verdict", 46 | "domains": [], 47 | "type_short": [ 48 | "pdf" 49 | ] 50 | } 51 | 52 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | .. sandboxapi documentation master file, created by 2 | sphinx-quickstart on Thu Apr 19 10:17:12 2018. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | .. include:: ../README.rst 7 | 8 | .. toctree:: 9 | :maxdepth: 2 10 | :caption: Contents: 11 | 12 | 13 | Module Documentation 14 | -------------------- 15 | 16 | API documentation for sandbox classes. 17 | 18 | .. automodule:: sandboxapi 19 | :members: 20 | :undoc-members: 21 | :show-inheritance: 22 | :member-order: bysource 23 | 24 | Cuckoo Sandbox 25 | ~~~~~~~~~~~~~~ 26 | 27 | .. automodule:: sandboxapi.cuckoo 28 | :members: 29 | :undoc-members: 30 | :show-inheritance: 31 | :member-order: bysource 32 | 33 | Falcon Sandbox 34 | ~~~~~~~~~~~~~~ 35 | 36 | .. automodule:: sandboxapi.falcon 37 | :members: 38 | :undoc-members: 39 | :show-inheritance: 40 | :member-order: bysource 41 | 42 | FireEye AX 43 | ~~~~~~~~~~ 44 | 45 | .. automodule:: sandboxapi.fireeye 46 | :members: 47 | :undoc-members: 48 | :show-inheritance: 49 | :member-order: bysource 50 | 51 | Joe Sandbox 52 | ~~~~~~~~~~~ 53 | 54 | .. automodule:: sandboxapi.joe 55 | :members: 56 | :undoc-members: 57 | :show-inheritance: 58 | :member-order: bysource 59 | 60 | VMRay Analyzer 61 | ~~~~~~~~~~~~~~ 62 | 63 | .. automodule:: sandboxapi.vmray 64 | :members: 65 | :undoc-members: 66 | :show-inheritance: 67 | :member-order: bysource 68 | 69 | WildFire Sandbox 70 | ~~~~~~~~~~~~~~~~ 71 | 72 | .. automodule:: sandboxapi.wildfire 73 | :members: 74 | :undoc-members: 75 | :show-inheritance: 76 | :member-order: bysource 77 | 78 | 79 | Indices and tables 80 | ================== 81 | 82 | * :ref:`genindex` 83 | * :ref:`modindex` 84 | * :ref:`search` 85 | -------------------------------------------------------------------------------- /tests/resources/opswat_submissions_result_not_finished.json: -------------------------------------------------------------------------------- 1 | { 2 | "flowId": "65316f10ba877ae559118c99", 3 | "allFinished": false, 4 | "allFilesDownloadFinished": false, 5 | "allAdditionalStepsDone": false, 6 | "reportsAmount": 1, 7 | "priority": "max", 8 | "pollPause": 5, 9 | "fileSize": 13370880, 10 | "fileReadProgressBytes": 13370880, 11 | "reports": { 12 | "761590d3-9fec-4ab9-846f-12db39b156b2": { 13 | "finalVerdict": { 14 | "verdict": "UNKNOWN", 15 | "threatLevel": 0, 16 | "confidence": 1 17 | }, 18 | "allTags": [], 19 | "overallState": "in_progress", 20 | "taskReference": { 21 | "name": "transform-file", 22 | "additionalInfo": { 23 | "submitName": "bad_file.exe", 24 | "submitTime": 1697738514610, 25 | "digests": { 26 | "SHA-256": "834d1dbfab8330ea5f1844f6e905ed0ac19d1033ee9a9f1122ad2051c56783dc" 27 | } 28 | }, 29 | "ID": "84e354e5-4d3c-4790-b6be-6b75c9fa9160", 30 | "state": "IN_PROGRESS", 31 | "opcount": 0, 32 | "processTime": 0 33 | }, 34 | "subtaskReferences": [], 35 | "allSignalGroups": [], 36 | "iocs": {}, 37 | "filter_errors": [ 38 | "Resource not found: ['osint', 'file']" 39 | ], 40 | "file": { 41 | "name": "bad_file.exe", 42 | "hash": "834d1dbfab8330ea5f1844f6e905ed0ac19d1033ee9a9f1122ad2051c56783dc", 43 | "type": null 44 | }, 45 | "filesDownloadFinished": false, 46 | "additionalStepsRunning": [ 47 | "similarity_search" 48 | ], 49 | "additionalStepsDone": false, 50 | "created_date": "10/19/2023, 18:01:53", 51 | "defaultOptionsUsed": false, 52 | "scanOptions": { 53 | "rapid_mode": null, 54 | "osint": true, 55 | "extended_osint": true, 56 | "extracted_files_osint": true, 57 | "visualization": true, 58 | "files_download": true, 59 | "resolve_domains": true, 60 | "input_file_yara": true, 61 | "extracted_files_yara": true, 62 | "whois": true, 63 | "ips_meta": true, 64 | "images_ocr": true 65 | }, 66 | "estimatedTime": "8", 67 | "estimated_progress": 0.40424999594688416 68 | } 69 | } 70 | } -------------------------------------------------------------------------------- /tests/test_joe.py: -------------------------------------------------------------------------------- 1 | import io 2 | from unittest import TestCase 3 | 4 | try: 5 | from unittest.mock import patch 6 | except ImportError: 7 | from mock import patch 8 | 9 | import responses 10 | import sandboxapi.joe 11 | import jbxapi 12 | from . import read_resource 13 | 14 | class TestJoe(TestCase): 15 | 16 | def setUp(self): 17 | self.sandbox = sandboxapi.joe.JoeAPI('key', 'http://joe.mock/api', True) 18 | 19 | @responses.activate 20 | def test_analyze(self): 21 | if not jbxapi.__version__.startswith("2"): 22 | responses.add(responses.POST, 'http://joe.mock/api/v2/submission/new', 23 | json=read_resource('joe_submission_new')) 24 | else: 25 | responses.add(responses.POST, 'http://joe.mock/api/v2/analysis/submit', 26 | json=read_resource('joe_analysis_submit')) 27 | self.assertEqual(self.sandbox.analyze(io.BytesIO('test'.encode('ascii')), 'filename'), '100001') 28 | 29 | @responses.activate 30 | def test_check(self): 31 | if not jbxapi.__version__.startswith("2"): 32 | responses.add(responses.POST, 'http://joe.mock/api/v2/analysis/info', 33 | json=read_resource('joe_analysis_info')) 34 | else: 35 | responses.add(responses.POST, 'http://joe.mock/api/v2/analysis/info', 36 | json=read_resource('joe_analysis_info')) 37 | self.assertEqual(self.sandbox.check('1'), True) 38 | 39 | @responses.activate 40 | def test_is_available(self): 41 | responses.add(responses.POST, 'http://joe.mock/api/v2/server/online', 42 | json=read_resource('joe_server_online')) 43 | self.assertTrue(self.sandbox.is_available()) 44 | 45 | @responses.activate 46 | def test_not_is_available(self): 47 | self.assertFalse(self.sandbox.is_available()) 48 | responses.add(responses.POST, 'http://joe.mock/api/v2/server/online', 49 | status=500) 50 | self.assertFalse(self.sandbox.is_available()) 51 | 52 | @responses.activate 53 | def test_report(self): 54 | responses.add(responses.POST, 'http://joe.mock/api/v2/analysis/download', 55 | json=read_resource('joe_analysis_download')) 56 | self.assertEqual(self.sandbox.report(8)['analysis']['signaturedetections']['strategy'][1]['score'], 1) 57 | 58 | @responses.activate 59 | def test_score(self): 60 | responses.add(responses.POST, 'http://joe.mock/api/v2/analysis/download', 61 | json=read_resource('joe_analysis_download')) 62 | self.assertEqual(self.sandbox.score(self.sandbox.report(1)), 1) 63 | 64 | @patch('requests.post') 65 | @patch('requests.get') 66 | def test_proxies_is_passed_to_requests(self, m_get, m_post): 67 | 68 | m_get.return_value.status_code = 200 69 | m_post.return_value.status_code = 200 70 | 71 | proxies = { 72 | 'http': 'http://10.10.1.10:3128', 73 | 'https': 'http://10.10.1.10:1080', 74 | } 75 | 76 | api = sandboxapi.joe.JoeAPI('key', self.sandbox.jbx.apiurl, True, 77 | proxies=proxies) 78 | self.assertEqual(api.jbx.session.proxies, proxies) 79 | -------------------------------------------------------------------------------- /tests/test_falcon.py: -------------------------------------------------------------------------------- 1 | import io 2 | from unittest import TestCase 3 | 4 | try: 5 | from unittest.mock import patch, ANY as MOCK_ANY 6 | except ImportError: 7 | from mock import patch, ANY as MOCK_ANY 8 | 9 | import responses 10 | import sandboxapi.falcon 11 | 12 | from . import read_resource 13 | 14 | class TestFalcon(TestCase): 15 | 16 | def setUp(self): 17 | self.sandbox = sandboxapi.falcon.FalconAPI('key', 'http://falcon.mock/api/v2') 18 | 19 | @responses.activate 20 | def test_analyze(self): 21 | responses.add(responses.POST, 'http://falcon.mock/api/v2/submit/file', 22 | json=read_resource('falcon_submit_file'), status=201) 23 | self.assertEqual(self.sandbox.analyze(io.BytesIO('test'.encode('ascii')), 'filename'), '1') 24 | 25 | @responses.activate 26 | def test_check(self): 27 | responses.add(responses.GET, 'http://falcon.mock/api/v2/report/1/state', 28 | json=read_resource('falcon_report_state')) 29 | self.assertEqual(self.sandbox.check('1'), True) 30 | 31 | @responses.activate 32 | def test_is_available(self): 33 | responses.add(responses.GET, 'http://falcon.mock/api/v2/system/heartbeat', 34 | json=read_resource('falcon_system_heartbeat')) 35 | self.assertTrue(self.sandbox.is_available()) 36 | 37 | @responses.activate 38 | def test_not_is_available(self): 39 | self.assertFalse(self.sandbox.is_available()) 40 | responses.add(responses.GET, 'http://falcon.mock/api/v2/system/heartbeat', 41 | status=500) 42 | self.assertFalse(self.sandbox.is_available()) 43 | 44 | @responses.activate 45 | def test_report(self): 46 | responses.add(responses.GET, 'http://falcon.mock/api/v2/report/1/summary', 47 | json=read_resource('falcon_report_summary')) 48 | self.assertEqual(self.sandbox.report(1)['job_id'], '1') 49 | 50 | @responses.activate 51 | def test_score(self): 52 | responses.add(responses.GET, 'http://falcon.mock/api/v2/report/1/summary', 53 | json=read_resource('falcon_report_summary')) 54 | self.assertEqual(self.sandbox.score(self.sandbox.report(1)), 5) 55 | 56 | @patch('requests.post') 57 | @patch('requests.get') 58 | def test_proxies_is_passed_to_requests(self, m_get, m_post): 59 | 60 | m_get.return_value.status_code = 200 61 | m_post.return_value.status_code = 200 62 | 63 | proxies = { 64 | 'http': 'http://10.10.1.10:3128', 65 | 'https': 'http://10.10.1.10:1080', 66 | } 67 | 68 | api = sandboxapi.falcon.FalconAPI('key', self.sandbox.api_url, 69 | proxies=proxies) 70 | api._request('/test') 71 | 72 | m_get.assert_called_once_with(api.api_url + '/test', auth=MOCK_ANY, 73 | headers=MOCK_ANY, params=MOCK_ANY, 74 | proxies=proxies, verify=MOCK_ANY) 75 | 76 | api._request('/test', method='POST') 77 | 78 | m_post.assert_called_once_with(api.api_url + '/test', auth=MOCK_ANY, 79 | headers=MOCK_ANY, data=MOCK_ANY, 80 | files=None, proxies=proxies, 81 | verify=MOCK_ANY) 82 | -------------------------------------------------------------------------------- /tests/test_triage.py: -------------------------------------------------------------------------------- 1 | import io 2 | import unittest 3 | 4 | try: 5 | from unittest.mock import patch, ANY as MOCK_ANY 6 | except ImportError: 7 | from mock import patch, ANY as MOCK_ANY 8 | 9 | import responses 10 | import sandboxapi.triage 11 | from . import read_resource 12 | 13 | class TestTriage(unittest.TestCase): 14 | def setUp(self): 15 | self.sandbox = sandboxapi.triage.TriageAPI("key", "https://tria.mock") 16 | 17 | @unittest.skip("Need to update tests JSON response data") 18 | @responses.activate 19 | def test_analyze(self): 20 | responses.add(responses.POST, "https://tria.mock/api/v0/samples", 21 | json=read_resource('triage_analyze'), status=200) 22 | triage_id = self.sandbox.analyze(io.BytesIO('test'.encode('ascii')), "testfile") 23 | self.assertEqual(triage_id, "200707-pht1cwk3ls") 24 | 25 | @unittest.skip("Need to update tests JSON response data") 26 | @responses.activate 27 | def test_check(self): 28 | responses.add(responses.GET, 29 | 'https://tria.mock/api/v0/samples/test/status', 30 | json=read_resource('triage_check'), status=200) 31 | self.assertTrue(self.sandbox.check("test")) 32 | 33 | @unittest.skip("Need to update tests JSON response data") 34 | @responses.activate 35 | def test_is_available(self): 36 | responses.add(responses.GET, 'https://tria.mock/api/v0/samples', 37 | json=read_resource('triage_available'), status=200) 38 | self.assertTrue(self.sandbox.is_available()) 39 | 40 | @unittest.skip("Need to update tests JSON response data") 41 | @responses.activate 42 | def test_report(self): 43 | responses.add(responses.GET, 44 | 'https://tria.mock/api/v0/samples/test/summary', 45 | json=read_resource('triage_report'), status=200) 46 | data = self.sandbox.report("test") 47 | self.assertEqual( 48 | 10, data["tasks"]["200615-8jbndpgg9n-behavioral1"]["score"]) 49 | 50 | @unittest.skip("Need to update tests JSON response data") 51 | @responses.activate 52 | def test_score(self): 53 | responses.add(responses.GET, 54 | 'https://tria.mock/api/v0/samples/test/summary', 55 | json=read_resource('triage_report'), status=200) 56 | score = self.sandbox.score("test") 57 | self.assertEqual(10, score) 58 | 59 | @unittest.skip("Need to update tests JSON response data") 60 | @responses.activate 61 | def test_full_report(self): 62 | responses.add(responses.GET, 63 | 'https://tria.mock/v0/api/samples/200615-8jbndpgg9n/summary', 64 | json=read_resource('triage_report'), status=200) 65 | responses.add(responses.GET, 66 | 'https://tria.mock/api/v0/samples/200615-8jbndpgg9n/behavioral1/report_triage.json', 67 | json=read_resource('triage_behavioral1'), status=200) 68 | responses.add(responses.GET, 69 | 'https://tria.mock/api/v0/samples/200615-8jbndpgg9n/behavioral2/report_triage.json', 70 | json=read_resource('triage_behavioral2'), status=200) 71 | 72 | full_report = self.sandbox.full_report("200615-8jbndpgg9n") 73 | self.assertTrue(full_report["tasks"]["behavioral1"]["sample"]["score"], 74 | 10) 75 | self.assertTrue(full_report["tasks"]["behavioral2"]["sample"]["score"], 76 | 10) 77 | -------------------------------------------------------------------------------- /tests/test_opswat.py: -------------------------------------------------------------------------------- 1 | import io 2 | from unittest import TestCase 3 | 4 | try: 5 | from unittest.mock import patch 6 | except ImportError: 7 | from mock import patch 8 | 9 | import responses 10 | import sandboxapi.opswat 11 | from . import read_resource 12 | 13 | 14 | URL = "http://filescanio.mock" 15 | 16 | 17 | class TestMetaDefenderSandbox(TestCase): 18 | def setUp(self): 19 | self.sandbox = sandboxapi.opswat.MetaDefenderSandboxAPI("key", URL, True) 20 | 21 | # analyze 22 | @responses.activate 23 | def test_analyze(self): 24 | sent_file_response = {"flow_id": "1234"} 25 | 26 | responses.add(responses.POST, f"{URL}/api/scan/file", json=sent_file_response) 27 | self.assertEqual( 28 | self.sandbox.analyze(io.BytesIO("test".encode("ascii")), "filename"), "1234" 29 | ) 30 | 31 | # check 32 | @responses.activate 33 | def test_check(self): 34 | flow_id = 1 35 | finished = [ 36 | ("opswat_submissions_result_malicious", True), 37 | ("opswat_submissions_result_not_finished", False), 38 | ] 39 | for report in finished: 40 | responses.add( 41 | responses.GET, 42 | f"{URL}/api/scan/{flow_id}/report", 43 | json=read_resource(report[0]), 44 | ) 45 | self.assertEqual(self.sandbox.check("1"), report[1]) 46 | 47 | # is available 48 | @responses.activate 49 | def test_is_available(self): 50 | response = { 51 | "accountId": "1234", 52 | } 53 | responses.add(responses.GET, f"{URL}/api/users/me", json=response) 54 | self.assertTrue(self.sandbox.is_available()) 55 | 56 | @responses.activate 57 | def test_not_available(self): 58 | response = { 59 | "accountId": "1234", 60 | } 61 | responses.add(responses.GET, f"{URL}/api/users/me", json=response, status=404) 62 | self.assertFalse(self.sandbox.is_available()) 63 | 64 | # report 65 | @responses.activate 66 | def test_report(self): 67 | id = 1 68 | url = f"{URL}/api/scan/{id}/report?filter=general&filter=finalVerdict&filter=allTags&filter=overallState&filter=taskReference&filter=subtaskReferences&filter=allSignalGroups&filter=iocs" 69 | 70 | responses.add( 71 | responses.GET, 72 | url, 73 | json=read_resource("opswat_submissions_result_malicious"), 74 | ) 75 | 76 | response = self.sandbox.report(id) 77 | self.assertEqual( 78 | response, 79 | read_resource("opswat_submissions_result_malicious"), 80 | ) 81 | 82 | self.assertEqual( 83 | response["reports"]["f7977db1-6a99-46c3-8567-de1c88c93aa4"]["finalVerdict"][ 84 | "verdict" 85 | ], 86 | "MALICIOUS", 87 | ) 88 | 89 | # score 90 | @responses.activate 91 | def test_score(self): 92 | id = 1 93 | files_and_score = [ 94 | ("opswat_submissions_result_malicious", 100), 95 | ("opswat_submissions_result_suspicious", 50), 96 | ("opswat_submissions_result_benign", 0), 97 | ("opswat_submissions_result_likely_malicious", 75), 98 | ] 99 | 100 | for file_and_score in files_and_score: 101 | responses.add( 102 | responses.GET, 103 | f"{URL}/api/scan/{id}/report?filter=general&filter=finalVerdict&filter=allTags&filter=overallState&filter=taskReference&filter=subtaskReferences&filter=allSignalGroups&filter=iocs", 104 | json=read_resource(file_and_score[0]), 105 | ) 106 | self.assertEqual( 107 | self.sandbox.score(self.sandbox.report(id)), file_and_score[1] 108 | ) 109 | -------------------------------------------------------------------------------- /tests/test_vmray.py: -------------------------------------------------------------------------------- 1 | import io 2 | from unittest import TestCase 3 | 4 | try: 5 | from unittest.mock import patch, ANY as MOCK_ANY 6 | except ImportError: 7 | from mock import patch, ANY as MOCK_ANY 8 | 9 | import responses 10 | import sandboxapi.vmray 11 | from . import read_resource 12 | 13 | class TestVMRay(TestCase): 14 | 15 | def setUp(self): 16 | self.sandbox = sandboxapi.vmray.VMRayAPI('key', 'http://vmray.mock') 17 | 18 | @responses.activate 19 | def test_analyze(self): 20 | responses.add(responses.POST, 'http://vmray.mock/rest/sample/submit', 21 | json=read_resource('vmray_sample_submit')) 22 | self.assertEqual(self.sandbox.analyze(io.BytesIO('test'.encode('ascii')), 'filename'), 1169850) 23 | 24 | @responses.activate 25 | def test_analyze_with_errors(self): 26 | responses.add(responses.POST, 'http://vmray.mock/rest/sample/submit', 27 | json=read_resource('vmray_sample_submit_errors')) 28 | with self.assertRaises(sandboxapi.SandboxError): 29 | self.assertEqual(self.sandbox.analyze(io.BytesIO('test'.encode('ascii')), 'filename')) 30 | 31 | @responses.activate 32 | def test_check(self): 33 | responses.add(responses.GET, 'http://vmray.mock/rest/submission/sample/1', 34 | json=read_resource('vmray_submission_sample')) 35 | self.assertEqual(self.sandbox.check('1'), True) 36 | 37 | @responses.activate 38 | def test_is_available(self): 39 | responses.add(responses.GET, 'http://vmray.mock/rest/system_info', 40 | json=read_resource('vmray_system_info')) 41 | self.assertTrue(self.sandbox.is_available()) 42 | 43 | @responses.activate 44 | def test_not_is_available(self): 45 | self.assertFalse(self.sandbox.is_available()) 46 | responses.add(responses.GET, 'http://vmray.mock/rest/system_info', 47 | status=500) 48 | self.assertFalse(self.sandbox.is_available()) 49 | 50 | @responses.activate 51 | def test_report(self): 52 | responses.add(responses.GET, 'http://vmray.mock/rest/analysis/sample/1', 53 | json=read_resource('vmray_analysis_sample')) 54 | responses.add(responses.GET, 'http://vmray.mock/rest/analysis/1097123/archive/logs/summary.json', 55 | json=read_resource('vmray_analysis_archive_logs_summary')) 56 | self.assertEqual(self.sandbox.report(1)['version'], 1) 57 | 58 | @responses.activate 59 | def test_score(self): 60 | responses.add(responses.GET, 'http://vmray.mock/rest/analysis/sample/1', 61 | json=read_resource('vmray_analysis_sample')) 62 | responses.add(responses.GET, 'http://vmray.mock/rest/analysis/1097123/archive/logs/summary.json', 63 | json=read_resource('vmray_analysis_archive_logs_summary')) 64 | self.assertEqual(self.sandbox.score(self.sandbox.report(1)), 20) 65 | 66 | @patch('requests.post') 67 | @patch('requests.get') 68 | def test_proxies_is_passed_to_requests(self, m_get, m_post): 69 | 70 | m_get.return_value.status_code = 200 71 | m_post.return_value.status_code = 200 72 | 73 | proxies = { 74 | 'http': 'http://10.10.1.10:3128', 75 | 'https': 'http://10.10.1.10:1080', 76 | } 77 | 78 | api = sandboxapi.vmray.VMRayAPI('key', self.sandbox.api_url, 79 | proxies=proxies) 80 | api._request('/test') 81 | 82 | m_get.assert_called_once_with(api.api_url + '/test', auth=MOCK_ANY, 83 | headers=MOCK_ANY, params=MOCK_ANY, 84 | proxies=proxies, verify=MOCK_ANY) 85 | 86 | api._request('/test', method='POST') 87 | 88 | m_post.assert_called_once_with(api.api_url + '/test', auth=MOCK_ANY, 89 | headers=MOCK_ANY, data=MOCK_ANY, 90 | files=None, proxies=proxies, 91 | verify=MOCK_ANY) 92 | -------------------------------------------------------------------------------- /tests/test_cuckoo.py: -------------------------------------------------------------------------------- 1 | import io 2 | from unittest import TestCase 3 | 4 | try: 5 | from unittest.mock import patch, ANY as MOCK_ANY 6 | except ImportError: 7 | from mock import patch, ANY as MOCK_ANY 8 | 9 | import responses 10 | import sandboxapi.cuckoo 11 | from . import read_resource 12 | 13 | class TestCuckoo(TestCase): 14 | 15 | def setUp(self): 16 | self.sandbox = sandboxapi.cuckoo.CuckooAPI('http://cuckoo.mock:8090/') 17 | 18 | @responses.activate 19 | def test_analyses(self): 20 | responses.add(responses.GET, 'http://cuckoo.mock:8090/tasks/list', 21 | json=read_resource('cuckoo_tasks_list')) 22 | self.assertEqual(len(self.sandbox.analyses()), 2) 23 | 24 | @responses.activate 25 | def test_analyze(self): 26 | responses.add(responses.POST, 'http://cuckoo.mock:8090/tasks/create/file', 27 | json=read_resource('cuckoo_tasks_create_file')) 28 | self.assertEqual(self.sandbox.analyze(io.BytesIO('test'.encode('ascii')), 'filename'), '1') 29 | 30 | @responses.activate 31 | def test_check(self): 32 | responses.add(responses.GET, 'http://cuckoo.mock:8090/tasks/view/1', 33 | json=read_resource('cuckoo_tasks_view')) 34 | self.assertEqual(self.sandbox.check('1'), True) 35 | 36 | @responses.activate 37 | def test_is_available(self): 38 | responses.add(responses.GET, 'http://cuckoo.mock:8090/cuckoo/status', 39 | json=read_resource('cuckoo_status')) 40 | self.assertTrue(self.sandbox.is_available()) 41 | 42 | @responses.activate 43 | def test_not_is_available(self): 44 | self.assertFalse(self.sandbox.is_available()) 45 | responses.add(responses.GET, 'http://cuckoo.mock:8090/cuckoo/status', 46 | status=500) 47 | self.assertFalse(self.sandbox.is_available()) 48 | 49 | @responses.activate 50 | def test_report(self): 51 | responses.add(responses.GET, 'http://cuckoo.mock:8090/tasks/report/8/json', 52 | json=read_resource('cuckoo_tasks_report')) 53 | self.assertEqual(self.sandbox.report(8)['info']['id'], 8) 54 | 55 | @responses.activate 56 | def test_score(self): 57 | responses.add(responses.GET, 'http://cuckoo.mock:8090/tasks/report/8/json', 58 | json=read_resource('cuckoo_tasks_report')) 59 | self.assertEqual(self.sandbox.score(self.sandbox.report(8)), 5) 60 | 61 | @patch('requests.post') 62 | @patch('requests.get') 63 | def test_proxies_is_passed_to_requests(self, m_get, m_post): 64 | 65 | m_get.return_value.status_code = 200 66 | m_post.return_value.status_code = 200 67 | 68 | proxies = { 69 | 'http': 'http://10.10.1.10:3128', 70 | 'https': 'http://10.10.1.10:1080', 71 | } 72 | 73 | api = sandboxapi.cuckoo.CuckooAPI('cuckoo.mock', 74 | proxies=proxies) 75 | api._request('/test') 76 | 77 | m_get.assert_called_once_with(api.api_url + '/test', auth=MOCK_ANY, 78 | headers=MOCK_ANY, params=MOCK_ANY, 79 | proxies=proxies, verify=MOCK_ANY) 80 | 81 | api._request('/test', method='POST') 82 | 83 | m_post.assert_called_once_with(api.api_url + '/test', auth=MOCK_ANY, 84 | headers=MOCK_ANY, data=MOCK_ANY, 85 | files=None, proxies=proxies, 86 | verify=MOCK_ANY) 87 | 88 | @responses.activate 89 | def test_cuckoo_old_style_host_port_path(self): 90 | sandbox = sandboxapi.cuckoo.CuckooAPI('cuckoo.mock') 91 | responses.add(responses.GET, 'http://cuckoo.mock:8090/tasks/list', 92 | json=read_resource('cuckoo_tasks_list')) 93 | self.assertEqual(len(self.sandbox.analyses()), 2) 94 | 95 | sandbox = sandboxapi.cuckoo.CuckooAPI('cuckoo.mock', 9090, '/test') 96 | responses.add(responses.GET, 'http://cuckoo.mock:9090/test/tasks/list', 97 | json=read_resource('cuckoo_tasks_list')) 98 | self.assertEqual(len(self.sandbox.analyses()), 2) 99 | -------------------------------------------------------------------------------- /sandboxapi/joe.py: -------------------------------------------------------------------------------- 1 | import json 2 | import jbxapi 3 | import sandboxapi 4 | 5 | class JoeAPI(sandboxapi.SandboxAPI): 6 | """Joe Sandbox API wrapper. 7 | 8 | This class is actually just a convenience wrapper around jbxapi.JoeSandbox. 9 | """ 10 | 11 | def __init__(self, apikey, apiurl, accept_tac, timeout=None, verify_ssl=True, retries=3, chunked=False, **kwargs): 12 | """Initialize the interface to Joe Sandbox API.""" 13 | sandboxapi.SandboxAPI.__init__(self) 14 | if not jbxapi.__version__.startswith("2"): 15 | self._chunked = chunked 16 | self.jbx = jbxapi.JoeSandbox(apikey, apiurl or jbxapi.API_URL, accept_tac, timeout, bool(int(verify_ssl)), retries, **kwargs) 17 | 18 | def analyze(self, handle, filename): 19 | """Submit a file for analysis. 20 | 21 | :type handle: File handle 22 | :param handle: Handle to file to upload for analysis. 23 | :type filename: str 24 | :param filename: File name. 25 | 26 | :rtype: str 27 | :return: Task ID as a string 28 | """ 29 | # ensure the handle is at offset 0. 30 | handle.seek(0) 31 | 32 | file_data = (filename, handle) 33 | 34 | try: 35 | if not jbxapi.__version__.startswith("2"): 36 | return self.jbx.submit_sample(file_data, _chunked_upload=self._chunked)['submission_id'] 37 | else: 38 | return self.jbx.submit_sample(file_data)['webids'][0] 39 | except (jbxapi.JoeException, KeyError, IndexError) as e: 40 | raise sandboxapi.SandboxError("error in analyze: {e}".format(e=e)) 41 | 42 | def check(self, item_id): 43 | """Check if an analysis is complete. 44 | 45 | :type item_id: str 46 | :param item_id: File ID to check. 47 | 48 | :rtype: bool 49 | :return: Boolean indicating if a report is done or not. 50 | """ 51 | try: 52 | if not jbxapi.__version__.startswith("2"): 53 | return self.jbx.analysis_info(item_id).get('status').lower() == 'finished' 54 | else: 55 | return self.jbx.info(item_id).get('status').lower() == 'finished' 56 | except jbxapi.JoeException: 57 | return False 58 | 59 | def is_available(self): 60 | """Determine if the Joe Sandbox API server is alive. 61 | 62 | :rtype: bool 63 | :return: True if service is available, False otherwise. 64 | """ 65 | # if the availability flag is raised, return True immediately. 66 | # NOTE: subsequent API failures will lower this flag. we do this here 67 | # to ensure we don't keep hitting Joe with requests while availability 68 | # is there. 69 | if self.server_available: 70 | return True 71 | 72 | # otherwise, we have to check with the cloud. 73 | else: 74 | 75 | try: 76 | self.server_available = self.jbx.server_online() 77 | return self.server_available 78 | except jbxapi.JoeException: 79 | pass 80 | 81 | self.server_available = False 82 | return False 83 | 84 | def report(self, item_id, report_format="json"): 85 | """Retrieves the specified report for the analyzed item, referenced by item_id. 86 | 87 | For available report formats, see online Joe Sandbox documentation. 88 | 89 | :type item_id: str 90 | :param item_id: File ID number 91 | :type report_format: str 92 | :param report_format: Return format 93 | 94 | :rtype: dict 95 | :return: Dictionary representing the JSON parsed data or raw, for other 96 | formats / JSON parsing failure. 97 | """ 98 | if report_format == "json": 99 | report_format = "jsonfixed" 100 | 101 | try: 102 | if not jbxapi.__version__.startswith("2"): 103 | return json.loads(self.jbx.analysis_download(item_id, report_format)[1].decode('utf-8')) 104 | else: 105 | return json.loads(self.jbx.download(item_id, report_format)[1].decode('utf-8')) 106 | except (jbxapi.JoeException, ValueError, IndexError) as e: 107 | raise sandboxapi.SandboxError("error in report fetch: {e}".format(e=e)) 108 | 109 | def score(self, report): 110 | """Pass in the report from self.report(), get back an int.""" 111 | try: 112 | return report['analysis']['signaturedetections']['strategy'][1]['score'] 113 | except (KeyError, IndexError): 114 | return 0 115 | 116 | 117 | if __name__ == "__main__": 118 | print("use jbxapi.py instead") 119 | -------------------------------------------------------------------------------- /docs/_templates/links.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | rss 6 | 7 | 8 | 9 | 10 | twitter 11 | 12 | 13 | 14 | 15 | github 16 | 17 | 18 | 19 | 20 | linkedin 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 |

Other Projects

29 | 30 |

More InQuest projects:

31 | 37 | 38 |

Useful Links

39 | 45 | 46 |

Stay Informed

47 | 48 | -------------------------------------------------------------------------------- /docs/_static/custom.css: -------------------------------------------------------------------------------- 1 | /* -------------------------------------------------------------------------- */ 2 | /* HTML DEFS ---------------------------------------------------------------- */ 3 | /* -------------------------------------------------------------------------- */ 4 | 5 | /* Proxima Nova Light = normal, 400 */ 6 | @font-face { 7 | font-family: 'Proxima Nova'; 8 | src: url('fonts/2C60D5_28_0.eot'); 9 | src: url('fonts/2C60D5_28_0.eot?#iefix') format('embedded-opentype'), 10 | url('fonts/2C60D5_28_0.woff2') format('woff2'), 11 | url('fonts/2C60D5_28_0.woff') format('woff'), 12 | url('fonts/2C60D5_28_0.ttf') format('truetype'); 13 | 14 | font-weight: normal; 15 | font-style: normal; 16 | } 17 | 18 | /* Proxima Nova Regular = bold, 700 */ 19 | @font-face { 20 | font-family: 'Proxima Nova'; 21 | src: url('fonts/2C60D5_25_0.eot'); 22 | src: url('fonts/2C60D5_25_0.eot?#iefix') format('embedded-opentype'), 23 | url('fonts/2C60D5_25_0.woff2') format('woff2'), 24 | url('fonts/2C60D5_25_0.woff') format('woff'), 25 | url('fonts/2C60D5_25_0.ttf') format('truetype'); 26 | 27 | font-weight: bold; 28 | font-style: normal; 29 | } 30 | 31 | h1, h2, h3, h4, h5, h6 { 32 | font-weight: normal; 33 | } 34 | 35 | /* -------------------------------------------------------------------------- */ 36 | /* TOP LEVEL ---------------------------------------------------------------- */ 37 | /* -------------------------------------------------------------------------- */ 38 | 39 | div.document { 40 | position: absolute; 41 | top: 0; 42 | left: 0; 43 | right: 0; 44 | bottom: 0; 45 | bottom: 67px; 46 | display: flex; 47 | flex-direction: row; 48 | margin: 0; 49 | } 50 | 51 | div.footer { 52 | position: absolute; 53 | bottom: 0; 54 | left: 0; 55 | right: 0; 56 | width: auto; 57 | padding: 20px 20px 30px 20px; 58 | margin: 0; 59 | background: #23272d; 60 | } 61 | 62 | @media screen and (max-width: 875px) { 63 | div.document { 64 | display: block; 65 | position: static; 66 | border-top: 0; 67 | } 68 | 69 | div.footer { 70 | display: block; 71 | position: static; 72 | border-top: solid 1px #ccc; 73 | padding: 40px 0 0 0; 74 | margin: 40px 0 0 0; 75 | background: #fff; 76 | } 77 | } 78 | 79 | /* -------------------------------------------------------------------------- */ 80 | /* DOCUMENT LEVEL ----------------------------------------------------------- */ 81 | /* -------------------------------------------------------------------------- */ 82 | 83 | div.sphinxsidebar, 84 | div.documentwrapper { 85 | position: static; 86 | overflow-x: hidden; 87 | overflow-y: auto; 88 | padding: 20px; 89 | } 90 | 91 | div.sphinxsidebar { 92 | flex-grow: 0; 93 | flex-shrink: 0; 94 | color: #1f292f; 95 | background: #f0f2f5; 96 | } 97 | 98 | div.documentwrapper { 99 | background: #fff; 100 | } 101 | 102 | @media screen and (max-width: 875px) { 103 | div.sphinxsidebar { 104 | padding: 10px 20px; 105 | } 106 | 107 | div.documentwrapper { 108 | padding: 0px; 109 | } 110 | } 111 | 112 | /* -------------------------------------------------------------------------- */ 113 | /* SIDE BAR LEVEL ----------------------------------------------------------- */ 114 | /* -------------------------------------------------------------------------- */ 115 | 116 | /*div.sphinxsidebarwrapper h1.logo { 117 | background: transparent url(inquest-magnifying-glass@2x.png) left 5px no-repeat; 118 | padding-left: 40px; 119 | }*/ 120 | 121 | div.sphinxsidebarwrapper h1.logo-name { 122 | text-align: center; 123 | } 124 | 125 | div.sphinxsidebar hr { 126 | background: #ccc; 127 | } 128 | 129 | @media screen and (max-width: 875px) { 130 | /* 'narrow_sidebar_fg': '#1f292f' doesn't work */ 131 | div.sphinxsidebar h3, 132 | div.sphinxsidebar h4, 133 | div.sphinxsidebar p, 134 | div.sphinxsidebar h3 a { 135 | color: #1f292f; 136 | } 137 | } 138 | 139 | /* -------------------------------------------------------------------------- */ 140 | /* INNER LEVEL -------------------------------------------------------------- */ 141 | /* -------------------------------------------------------------------------- */ 142 | 143 | div.bodywrapper { 144 | margin-left: 0; 145 | } 146 | 147 | div.body { 148 | min-width: auto; 149 | max-width: none; 150 | } 151 | 152 | /* -------------------------------------------------------------------------- */ 153 | /* BLOCKS ------------------------------------------------------------------- */ 154 | /* -------------------------------------------------------------------------- */ 155 | .note, 156 | .warning { 157 | border-radius: 5px; 158 | } 159 | 160 | .responsive-table table.docutils, 161 | .responsive-table table.docutils td, 162 | .responsive-table table.docutils th { 163 | border-color: #dddddd; 164 | } 165 | 166 | .responsive-table table.docutils th { 167 | white-space: nowrap; 168 | vertical-align: top; 169 | background-color: #f3f4f5; 170 | } 171 | 172 | .responsive-table table.docutils td { 173 | color: #686e71; 174 | } 175 | 176 | .responsive-table .row-even { 177 | background: #f7f9fc; 178 | } 179 | 180 | .responsive-table .row-odd { 181 | background: #ffffff; 182 | } 183 | 184 | /* -------------------------------------------------------------------------- */ 185 | /* CUSTOM BLOCKS ------------------------------------------------------------ */ 186 | /* -------------------------------------------------------------------------- */ 187 | .responsive-table { 188 | overflow-x: auto; 189 | } 190 | 191 | /* -------------------------------------------------------------------------- */ 192 | /* SOCIAL ICONS ------------------------------------------------------------- */ 193 | /* -------------------------------------------------------------------------- */ 194 | svg.icon { 195 | height: 15px; 196 | width: 15px; 197 | vertical-align: middle; 198 | } 199 | -------------------------------------------------------------------------------- /sandboxapi/__init__.py: -------------------------------------------------------------------------------- 1 | import time 2 | import random 3 | 4 | import requests 5 | 6 | __all__ = [ 7 | 'cuckoo', 8 | 'fireeye', 9 | 'joe', 10 | 'triage', 11 | 'opswat', 12 | 'vmray', 13 | 'falcon', 14 | 'wildfire', 15 | 'SandboxAPI', 16 | 'SandboxError', 17 | ] 18 | 19 | 20 | class SandboxError(Exception): 21 | """ 22 | Custom exception class to be raised by known errors in SandboxAPI and its 23 | subclasses, and caught where this library is used. 24 | """ 25 | pass 26 | 27 | 28 | class SandboxAPI(object): 29 | """Sandbox API wrapper base class.""" 30 | 31 | def __init__(self, *args, **kwargs): 32 | """Initialize the interface to Sandbox API. 33 | 34 | :type proxies: dict 35 | :param proxies: Optional proxies dict passed to requests calls. 36 | """ 37 | 38 | self.api_url = None 39 | 40 | # assume is *not* available. 41 | self.server_available = False 42 | 43 | # turn SSL verify on by default 44 | self.verify_ssl = True 45 | 46 | # allow passing in requests options directly. 47 | # be careful using this! 48 | self.proxies = kwargs.get('proxies') 49 | 50 | def _request(self, uri, method='GET', params=None, files=None, headers=None, auth=None): 51 | """Robustness wrapper. Tries up to 3 times to dance with the Sandbox API. 52 | 53 | :type uri: str 54 | :param uri: URI to append to base_url. 55 | :type params: dict 56 | :param params: Optional parameters for API. 57 | :type files: dict 58 | :param files: Optional dictionary of files for multipart post. 59 | :type headers: dict 60 | :param headers: Optional headers to send to the API. 61 | :type auth: dict 62 | :param auth: Optional authentication object to send to the API. 63 | 64 | :rtype: requests.response. 65 | :return: Response object. 66 | 67 | :raises SandboxError: If all attempts failed. 68 | """ 69 | 70 | # make up to three attempts to dance with the API, use a jittered 71 | # exponential back-off delay 72 | for i in range(3): 73 | try: 74 | full_url = '{b}{u}'.format(b=self.api_url, u=uri) 75 | 76 | response = None 77 | if method == 'POST': 78 | response = requests.post(full_url, data=params, files=files, headers=headers, 79 | verify=self.verify_ssl, auth=auth, proxies=self.proxies) 80 | else: 81 | response = requests.get(full_url, params=params, headers=headers, 82 | verify=self.verify_ssl, auth=auth, proxies=self.proxies) 83 | 84 | # if the status code is 503, is no longer available. 85 | if response.status_code >= 500: 86 | # server error 87 | self.server_available = False 88 | raise SandboxError("server returned {c} status code on {u}, assuming unavailable...".format( 89 | c=response.status_code, u=response.url)) 90 | else: 91 | return response 92 | 93 | # 0.4, 1.6, 6.4, 25.6, ... 94 | except requests.exceptions.RequestException: 95 | time.sleep(random.uniform(0, 4 ** i * 100 / 1000.0)) 96 | 97 | # if we couldn't reach the API, we assume that the box is down and lower availability flag. 98 | self.server_available = False 99 | 100 | # raise an exception. 101 | msg = "exceeded 3 attempts with sandbox API: {u}, p:{p}, f:{f}".format(u=full_url, 102 | p=params, f=files) 103 | try: 104 | msg += "\n" + response.content.decode('utf-8') 105 | except AttributeError: 106 | pass 107 | 108 | raise SandboxError(msg) 109 | 110 | def analyses(self): 111 | """Retrieve a list of analyzed samples. 112 | 113 | :rtype: list 114 | :return: List of objects referencing each analyzed file. 115 | """ 116 | raise NotImplementedError 117 | 118 | def analyze(self, handle, filename): 119 | """Submit a file for analysis. 120 | 121 | :type handle: File handle 122 | :param handle: Handle to file to upload for analysis. 123 | :type filename: str 124 | :param filename: File name. 125 | 126 | :rtype: str 127 | :return: Item ID as a string 128 | """ 129 | raise NotImplementedError 130 | 131 | def check(self, item_id): 132 | """Check if an analysis is complete 133 | 134 | :type item_id: int | str 135 | :param item_id: item_id to check. 136 | 137 | :rtype: bool 138 | :return: Boolean indicating if a report is done or not. 139 | """ 140 | raise NotImplementedError 141 | 142 | def delete(self, item_id): 143 | """Delete the reports associated with the given item_id. 144 | 145 | :type item_id: int | str 146 | :param item_id: Report ID to delete. 147 | 148 | :rtype: bool 149 | :return: True on success, False otherwise. 150 | """ 151 | raise NotImplementedError 152 | 153 | def is_available(self): 154 | """Determine if the Sandbox API servers are alive or in maintenance mode. 155 | 156 | :rtype: bool 157 | :return: True if service is available, False otherwise. 158 | """ 159 | raise NotImplementedError 160 | 161 | def queue_size(self): 162 | """Determine sandbox queue length 163 | 164 | :rtype: int 165 | :return: Number of submissions in sandbox queue. 166 | """ 167 | raise NotImplementedError 168 | 169 | def report(self, item_id, report_format="json"): 170 | """Retrieves the specified report for the analyzed item, referenced by item_id. 171 | 172 | :type item_id: int | str 173 | :param item_id: Item ID 174 | 175 | :rtype: dict 176 | :return: Dictionary representing the JSON parsed data or raw, for other 177 | formats / JSON parsing failure. 178 | """ 179 | raise NotImplementedError 180 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Configuration file for the Sphinx documentation builder. 4 | # 5 | # This file does only contain a selection of the most common options. For a 6 | # full list see the documentation: 7 | # http://www.sphinx-doc.org/en/stable/config 8 | 9 | # -- Path setup -------------------------------------------------------------- 10 | 11 | # If extensions (or modules to document with autodoc) are in another directory, 12 | # add these directories to sys.path here. If the directory is relative to the 13 | # documentation root, use os.path.abspath to make it absolute, like shown here. 14 | # 15 | # import os 16 | # import sys 17 | # sys.path.insert(0, os.path.abspath('.')) 18 | 19 | 20 | # -- Project information ----------------------------------------------------- 21 | 22 | project = u'sandboxapi' 23 | copyright = u'2019 InQuest, LLC' 24 | author = u'InQuest Labs' 25 | 26 | # The short X.Y version 27 | version = u'' 28 | # The full version, including alpha/beta/rc tags 29 | release = u'' 30 | 31 | 32 | # -- General configuration --------------------------------------------------- 33 | 34 | # If your documentation needs a minimal Sphinx version, state it here. 35 | # 36 | # needs_sphinx = '1.0' 37 | 38 | # Add any Sphinx extension module names here, as strings. They can be 39 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 40 | # ones. 41 | extensions = [ 42 | 'sphinx.ext.autodoc', 43 | 'sphinx.ext.intersphinx', 44 | ] 45 | 46 | # Add any paths that contain templates here, relative to this directory. 47 | templates_path = ['_templates'] 48 | 49 | # The suffix(es) of source filenames. 50 | # You can specify multiple suffix as a list of string: 51 | # 52 | # source_suffix = ['.rst', '.md'] 53 | source_suffix = '.rst' 54 | 55 | # The master toctree document. 56 | master_doc = 'index' 57 | 58 | # The language for content autogenerated by Sphinx. Refer to documentation 59 | # for a list of supported languages. 60 | # 61 | # This is also used if you do content translation via gettext catalogs. 62 | # Usually you set "language" from the command line for these cases. 63 | language = None 64 | 65 | # List of patterns, relative to source directory, that match files and 66 | # directories to ignore when looking for source files. 67 | # This pattern also affects html_static_path and html_extra_path . 68 | exclude_patterns = [u'_build', 'Thumbs.db', '.DS_Store'] 69 | 70 | # The name of the Pygments (syntax highlighting) style to use. 71 | pygments_style = 'sphinx' 72 | 73 | 74 | # -- Options for HTML output ------------------------------------------------- 75 | 76 | # The theme to use for HTML and HTML Help pages. See the documentation for 77 | # a list of builtin themes. 78 | # 79 | html_theme = 'alabaster' 80 | 81 | # Theme options are theme-specific and customize the look and feel of a theme 82 | # further. For a list of options available for each theme, see the 83 | # documentation. 84 | # 85 | html_theme_options = { 86 | 'fixed_sidebar': 'true', 87 | 'logo': 'sandboxapi.png', 88 | 'font_family': '"Proxima Nova", "Helvetica Neue", Helvetica, Arial, sans-serif', 89 | 'head_font_family': '"Proxima Nova", "Helvetica Neue", Helvetica, Arial, sans-serif', 90 | 'logo_name': 'true', 91 | 'description': 'Minimal, consistent Python API for building integrations with malware sandboxes.', 92 | 'github_user': 'InQuest', 93 | 'github_repo': 'sandboxapi', 94 | 'github_type': 'star', 95 | 'show_powered_by': 'false', 96 | 'page_width': 'auto', 97 | 'sidebar_width': '320px', 98 | 'gray_1': '#23272d', 99 | 'gray_2': '#f7f9fc', 100 | 'gray_3': '#fefeff', 101 | 'pink_1': '#ff796e', 102 | 'pink_2': '#e12a26', 103 | 'body_text': '#1f292f', 104 | 'footer_text': '#919396', 105 | 'link': '#e03f26', 106 | 'link_hover': '#e03f26', 107 | 'sidebar_search_button': '#ccc', 108 | 'narrow_sidebar_bg': '#f0f2f5', 109 | 'narrow_sidebar_link': '#1f292f' 110 | } 111 | 112 | # Add any paths that contain custom static files (such as style sheets) here, 113 | # relative to this directory. They are copied after the builtin static files, 114 | # so a file named "default.css" will overwrite the builtin "default.css". 115 | html_static_path = ['_static'] 116 | 117 | # Custom sidebar templates, must be a dictionary that maps document names 118 | # to template names. 119 | # 120 | # The default sidebars (for documents that don't match any pattern) are 121 | # defined by theme itself. Builtin themes are using these templates by 122 | # default: ``['localtoc.html', 'relations.html', 'sourcelink.html', 123 | # 'searchbox.html']``. 124 | # 125 | html_sidebars = { 126 | '**': [ 127 | 'about.html', 128 | 'localtoc.html', 129 | 'relations.html', 130 | 'links.html', 131 | 'searchbox.html', 132 | ] 133 | } 134 | 135 | # -- Options for HTMLHelp output --------------------------------------------- 136 | 137 | # Output file base name for HTML help builder. 138 | htmlhelp_basename = 'sandboxapidoc' 139 | 140 | 141 | # -- Options for LaTeX output ------------------------------------------------ 142 | 143 | latex_elements = { 144 | # The paper size ('letterpaper' or 'a4paper'). 145 | # 146 | # 'papersize': 'letterpaper', 147 | 148 | # The font size ('10pt', '11pt' or '12pt'). 149 | # 150 | # 'pointsize': '10pt', 151 | 152 | # Additional stuff for the LaTeX preamble. 153 | # 154 | # 'preamble': '', 155 | 156 | # Latex figure (float) alignment 157 | # 158 | # 'figure_align': 'htbp', 159 | } 160 | 161 | # Grouping the document tree into LaTeX files. List of tuples 162 | # (source start file, target name, title, 163 | # author, documentclass [howto, manual, or own class]). 164 | latex_documents = [ 165 | (master_doc, 'sandboxapi.tex', u'sandboxapi Documentation', 166 | u'InQuest Labs', 'manual'), 167 | ] 168 | 169 | 170 | # -- Options for manual page output ------------------------------------------ 171 | 172 | # One entry per manual page. List of tuples 173 | # (source start file, name, description, authors, manual section). 174 | man_pages = [ 175 | (master_doc, 'sandboxapi', u'sandboxapi Documentation', 176 | [author], 1) 177 | ] 178 | 179 | 180 | # -- Options for Texinfo output ---------------------------------------------- 181 | 182 | # Grouping the document tree into Texinfo files. List of tuples 183 | # (source start file, target name, title, author, 184 | # dir menu entry, description, category) 185 | texinfo_documents = [ 186 | (master_doc, 'sandboxapi', u'sandboxapi Documentation', 187 | author, 'sandboxapi', 'Minimal, consistent Python API for building integrations with malware sandboxes.', 188 | 'Miscellaneous'), 189 | ] 190 | 191 | 192 | # -- Extension configuration ------------------------------------------------- 193 | 194 | # -- Options for intersphinx extension --------------------------------------- 195 | 196 | # Example configuration for intersphinx: refer to the Python standard library. 197 | intersphinx_mapping = {'https://docs.python.org/': None} 198 | -------------------------------------------------------------------------------- /sandboxapi/wildfire.py: -------------------------------------------------------------------------------- 1 | from __future__ import print_function 2 | 3 | import json 4 | import sys 5 | 6 | import xmltodict 7 | 8 | import sandboxapi 9 | 10 | 11 | BENIGN = 0 12 | MALWARE = 1 13 | GRAYWARE = 2 14 | PHISHING = 4 15 | 16 | 17 | class WildFireAPI(sandboxapi.SandboxAPI): 18 | """WildFire Sandbox API wrapper.""" 19 | 20 | def __init__(self, api_key='', url='', verify_ssl=True, **kwargs): 21 | """Initialize the interface to the WildFire Sandbox API. 22 | 23 | :param str api_key: The customer API key. 24 | :param str url: The WildFire API URL. 25 | """ 26 | super(WildFireAPI, self).__init__(**kwargs) 27 | self.base_url = url or 'https://wildfire.paloaltonetworks.com' 28 | self.api_url = self.base_url + '/publicapi' 29 | self._api_key = api_key 30 | self._score = BENIGN 31 | self.verify_ssl = verify_ssl 32 | 33 | def analyze(self, handle, filename): 34 | """Submit a file for analysis. 35 | 36 | :param BytesIO handle: File handle 37 | :param str filename: File name 38 | :rtype: str 39 | :return: File ID as a string 40 | """ 41 | # multipart post files. 42 | files = {"file": (filename, handle)} 43 | 44 | # ensure the handle is at offset 0. 45 | handle.seek(0) 46 | 47 | data = {'apikey': self._api_key} 48 | 49 | response = self._request('/submit/file', method='POST', files=files, params=data) 50 | 51 | try: 52 | if response.status_code == 200: 53 | output = self.decode(response) 54 | return output['wildfire']['upload-file-info']['sha256'] 55 | else: 56 | raise sandboxapi.SandboxError("api error in analyze ({}): {}".format(response.url, response.content)) 57 | except (ValueError, KeyError, IndexError) as e: 58 | raise sandboxapi.SandboxError("error in analyze {}".format(e)) 59 | 60 | def decode(self, response): 61 | """Convert a xml response to a python dictionary. 62 | 63 | :param requests.Response response: A Response object with xml content. 64 | :rtype: dict 65 | :return: The xml content converted to a dictionary. 66 | """ 67 | # This weird conversion to and from JSON is because the XML is being parsed as an Ordereddict. 68 | # TODO: See if there's a better way to do this without having to convert to JSON. 69 | output = json.loads(json.dumps(xmltodict.parse(response.content.decode('utf-8')))) 70 | if 'error' in output: 71 | raise sandboxapi.SandboxError(output['error']['error-message']) 72 | return output 73 | 74 | def check(self, item_id): 75 | """Check if an analysis is complete. 76 | 77 | :param str item_id: The hash of the file to check. 78 | :rtype: bool 79 | :return: True if the report is ready, otherwise False. 80 | """ 81 | data = { 82 | 'apikey': self._api_key, 83 | 'hash': item_id, 84 | } 85 | response = self._request('/get/verdict', method='POST', params=data) 86 | 87 | if not response.ok: 88 | raise sandboxapi.SandboxError("{}: {}".format(response.status_code, response.content)) 89 | 90 | output = self.decode(response) 91 | try: 92 | status = int(output['wildfire']['get-verdict-info']['verdict']) 93 | if status >= 0: 94 | self._score = status 95 | return True 96 | elif status == -100: 97 | return False 98 | elif status == -101: 99 | raise sandboxapi.SandboxError('An error occurred while processing the sample.') 100 | elif status == -102: 101 | raise sandboxapi.SandboxError('Unknown sample in the Wildfire database.') 102 | elif status == -103: 103 | raise sandboxapi.SandboxError('Invalid hash value.') 104 | else: 105 | raise sandboxapi.SandboxError('Unknown status.') 106 | except (ValueError, IndexError) as e: 107 | raise sandboxapi.SandboxError(e) 108 | 109 | def is_available(self): 110 | """Checks to see if the WildFire sandbox is up and running. 111 | 112 | :rtype: bool 113 | :return: True if the WildFire sandbox is responding, otherwise False. 114 | 115 | WildFire doesn't have an explicit endpoint for checking the sandbox status, so this is kind of a hack. 116 | """ 117 | try: 118 | # Making a GET request to the API should always give a code 405 if the service is running. 119 | # Relying on this fact to get a reliable 405 if the service is up. 120 | response = self._request('/get/sample', params={'apikey': self._api_key}) 121 | if response.status_code == 405: 122 | return True 123 | else: 124 | return False 125 | except sandboxapi.SandboxError: 126 | return False 127 | 128 | def report(self, item_id, report_format='json'): 129 | """Retrieves the specified report for the analyzed item, referenced by item_id. 130 | 131 | :param str item_id: The hash of the file. 132 | :param str report_format: Return format. 133 | :rtype: dic 134 | :return: Dictionary representing the JSON parsed data. 135 | """ 136 | data = { 137 | 'apikey': self._api_key, 138 | 'hash': item_id, 139 | 'format': 'xml', 140 | } 141 | response = self._request('/get/report', method='POST', params=data) 142 | if not response.ok: 143 | raise sandboxapi.SandboxError("{}: {}".format(response.status_code, response.content)) 144 | return self.decode(response) 145 | 146 | def score(self): 147 | """Get the threat score for the submitted sample. 148 | 149 | :rtype: int 150 | :return: The assigned threat score. 151 | """ 152 | if self._score == MALWARE: 153 | return 8 154 | elif self._score == GRAYWARE: 155 | return 2 156 | elif self._score == PHISHING: 157 | return 5 158 | else: 159 | return self._score 160 | 161 | 162 | if __name__ == "__main__": 163 | 164 | def usage(): 165 | msg = "{}: available | submit | report | check ".format(sys.argv[0]) 166 | print(msg) 167 | sys.exit(1) 168 | 169 | api_key_ = '' 170 | url_ = '' 171 | arg = '' 172 | cmd = '' 173 | if len(sys.argv) == 5: 174 | arg = sys.argv.pop() 175 | cmd = sys.argv.pop().lower() 176 | api_key_ = sys.argv.pop() 177 | url_ = sys.argv.pop() 178 | elif len(sys.argv) == 4: 179 | arg = sys.argv.pop() 180 | cmd = sys.argv.pop().lower() 181 | api_key_ = sys.argv.pop() 182 | elif len(sys.argv) == 3: 183 | cmd = sys.argv.pop().lower() 184 | api_key_ = sys.argv.pop() 185 | else: 186 | usage() 187 | 188 | wildfire = WildFireAPI(api_key=api_key_, url=url_) if url_ else WildFireAPI(api_key_) 189 | 190 | if cmd == 'available': 191 | print(wildfire.is_available()) 192 | elif cmd == 'submit': 193 | with open(arg, "rb") as handle: 194 | print(wildfire.analyze(handle, arg)) 195 | elif cmd == "report": 196 | print(wildfire.report(arg)) 197 | elif cmd == "check": 198 | print(wildfire.check(arg)) 199 | else: 200 | usage() 201 | -------------------------------------------------------------------------------- /tests/resources/vmray_sample_submit_errors.json: -------------------------------------------------------------------------------- 1 | { 2 | "data": { 3 | "errors": [ 4 | { 5 | "error_msg": "Denied: The vmray quota for account 100 (AAAA (Account)) is exceeded. (00/000 used, 10 requested at 1000-00-00 10:00 (UTC+0) by aaaa@aaaa.com)", 6 | "submission_filename": "aaaa" 7 | } 8 | ], 9 | "jobs": [], 10 | "md_jobs": [], 11 | "reputation_jobs": [ 12 | { 13 | "reputation_job_created": "0000-00-00T00:00:00", 14 | "reputation_job_id": 10000, 15 | "reputation_job_priority": 1, 16 | "reputation_job_sample_id": 1000000, 17 | "reputation_job_sample_md5": "aaaa", 18 | "reputation_job_sample_sha1": "aaaa", 19 | "reputation_job_sample_sha256": "aaaa", 20 | "reputation_job_sample_ssdeep": "aaaa", 21 | "reputation_job_status": "queued", 22 | "reputation_job_statuschanged": "0000-00-00T00:00:00", 23 | "reputation_job_submission_id": 1000000, 24 | "reputation_job_user_email": "aaaa@aaaa.com", 25 | "reputation_job_user_id": 100 26 | }, 27 | { 28 | "reputation_job_created": "0000-00-00T00:00:00", 29 | "reputation_job_id": 10000, 30 | "reputation_job_priority": 1, 31 | "reputation_job_sample_id": 1000000, 32 | "reputation_job_sample_md5": "aaaa", 33 | "reputation_job_sample_sha1": "aaaa", 34 | "reputation_job_sample_sha256": "aaaa", 35 | "reputation_job_sample_ssdeep": "aaaa", 36 | "reputation_job_status": "queued", 37 | "reputation_job_statuschanged": "0000-00-00T00:00:00", 38 | "reputation_job_submission_id": 1000000, 39 | "reputation_job_user_email": "aaaa@aaaa.com", 40 | "reputation_job_user_id": 100 41 | } 42 | ], 43 | "samples": [ 44 | { 45 | "sample_created": "0000-00-00T00:00:00", 46 | "sample_filename": "aaaa", 47 | "sample_filesize": 10000, 48 | "sample_id": 1000000, 49 | "sample_imphash": null, 50 | "sample_is_multipart": false, 51 | "sample_md5hash": "aaaa", 52 | "sample_priority": 1, 53 | "sample_sha1hash": "aaaa", 54 | "sample_sha256hash": "aaaa", 55 | "sample_ssdeephash": "aaaa", 56 | "sample_type": "Python script", 57 | "sample_url": null, 58 | "sample_webif_url": "https://cloud.vmray.com/user/sample/view?id=0000000", 59 | "submission_filename": "aaaa" 60 | }, 61 | { 62 | "sample_created": "0000-00-00T00:00:00", 63 | "sample_filename": "aaaa", 64 | "sample_filesize": 1000, 65 | "sample_id": 1000000, 66 | "sample_imphash": null, 67 | "sample_is_multipart": false, 68 | "sample_md5hash": "aaaa", 69 | "sample_priority": 1, 70 | "sample_sha1hash": "aaaa", 71 | "sample_sha256hash": "aaaa", 72 | "sample_ssdeephash": "aaaa", 73 | "sample_type": "aaaa", 74 | "sample_url": null, 75 | "sample_webif_url": "https://cloud.vmray.com/user/sample/view?id=0000000", 76 | "submission_filename": "aaaa" 77 | } 78 | ], 79 | "static_jobs": [], 80 | "submissions": [ 81 | { 82 | "submission_analyzer_mode_analyzer_mode": "reputation_static_dynamic", 83 | "submission_analyzer_mode_enable_reputation": true, 84 | "submission_analyzer_mode_enable_triage": false, 85 | "submission_analyzer_mode_enable_whois": true, 86 | "submission_analyzer_mode_id": 100, 87 | "submission_analyzer_mode_triage_error_handling": null, 88 | "submission_comment": null, 89 | "submission_created": "0000-00-00T00:00:00", 90 | "submission_dll_call_mode": null, 91 | "submission_dll_calls": null, 92 | "submission_document_password": null, 93 | "submission_filename": "aaaa", 94 | "submission_finish_time": null, 95 | "submission_finished": false, 96 | "submission_has_errors": null, 97 | "submission_id": 1000000, 98 | "submission_ip_id": 1000, 99 | "submission_ip_ip": "00.00.000.0", 100 | "submission_known_configuration": false, 101 | "submission_original_filename": "aaaa", 102 | "submission_prescript_force_admin": false, 103 | "submission_prescript_id": null, 104 | "submission_priority": 1, 105 | "submission_reputation_mode": "disabled", 106 | "submission_sample_id": 1000000, 107 | "submission_sample_md5": "aaaa", 108 | "submission_sample_sha1": "aaaa", 109 | "submission_sample_sha256": "aaaa", 110 | "submission_sample_ssdeep": "aaaa", 111 | "submission_shareable": false, 112 | "submission_system_time": null, 113 | "submission_tags": [], 114 | "submission_triage_error_handling": null, 115 | "submission_type": "api", 116 | "submission_user_account_id": 10, 117 | "submission_user_account_name": "AAAA", 118 | "submission_user_account_subscription_mode": "aaaa", 119 | "submission_user_email": "aaaa@aaaa.com", 120 | "submission_user_id": 100, 121 | "submission_webif_url": "https://cloud.vmray.com/user/sample/view?id=0000000", 122 | "submission_whois_mode": "disabled" 123 | }, 124 | { 125 | "submission_analyzer_mode_analyzer_mode": "reputation_static_dynamic", 126 | "submission_analyzer_mode_enable_reputation": true, 127 | "submission_analyzer_mode_enable_triage": false, 128 | "submission_analyzer_mode_enable_whois": true, 129 | "submission_analyzer_mode_id": 100, 130 | "submission_analyzer_mode_triage_error_handling": null, 131 | "submission_comment": null, 132 | "submission_created": "0000-00-00T00:00:00", 133 | "submission_dll_call_mode": null, 134 | "submission_dll_calls": null, 135 | "submission_document_password": null, 136 | "submission_filename": "aaaa", 137 | "submission_finish_time": null, 138 | "submission_finished": false, 139 | "submission_has_errors": null, 140 | "submission_id": 1000000, 141 | "submission_ip_id": 1000, 142 | "submission_ip_ip": "00.00.000.0", 143 | "submission_known_configuration": false, 144 | "submission_original_filename": "aaaa", 145 | "submission_prescript_force_admin": false, 146 | "submission_prescript_id": null, 147 | "submission_priority": 1, 148 | "submission_reputation_mode": "disabled", 149 | "submission_sample_id": 1000000, 150 | "submission_sample_md5": "aaaa", 151 | "submission_sample_sha1": "aaaa", 152 | "submission_sample_sha256": "aaaa", 153 | "submission_sample_ssdeep": "aaaa", 154 | "submission_shareable": false, 155 | "submission_system_time": null, 156 | "submission_tags": [], 157 | "submission_triage_error_handling": null, 158 | "submission_type": "api", 159 | "submission_user_account_id": 10, 160 | "submission_user_account_name": "AAAA", 161 | "submission_user_account_subscription_mode": "aaaa", 162 | "submission_user_email": "aaaa@aaaa.com", 163 | "submission_user_id": 100, 164 | "submission_webif_url": "https://cloud.vmray.com/user/sample/view?id=0000000", 165 | "submission_whois_mode": "disabled" 166 | } 167 | ], 168 | "vt_jobs": [], 169 | "whois_jobs": [] 170 | }, 171 | "result": "ok" 172 | } 173 | -------------------------------------------------------------------------------- /sandboxapi/vmray.py: -------------------------------------------------------------------------------- 1 | from __future__ import print_function 2 | 3 | import sys 4 | import time 5 | 6 | import sandboxapi 7 | 8 | class VMRayAPI(sandboxapi.SandboxAPI): 9 | """VMRay Sandbox API wrapper.""" 10 | 11 | def __init__(self, api_key, url=None, verify_ssl=True, **kwargs): 12 | """Initialize the interface to VMRay Sandbox API.""" 13 | sandboxapi.SandboxAPI.__init__(self, **kwargs) 14 | 15 | self.base_url = url or 'https://cloud.vmray.com' 16 | self.api_url = self.base_url + '/rest' 17 | self.api_key = api_key 18 | self.verify_ssl = verify_ssl 19 | 20 | # define once and use later 21 | self.headers = {'Authorization': 'api_key {a}'.format(a=api_key)} 22 | 23 | def analyze(self, handle, filename): 24 | """Submit a file for analysis. 25 | 26 | :type handle: File handle 27 | :param handle: Handle to file to upload for analysis. 28 | :type filename: str 29 | :param filename: File name. 30 | 31 | :rtype: str 32 | :return: File ID as a string 33 | """ 34 | # multipart post files. 35 | files = {"sample_file": (filename, handle)} 36 | 37 | # ensure the handle is at offset 0. 38 | handle.seek(0) 39 | 40 | response = self._request("/sample/submit", method='POST', files=files, headers=self.headers) 41 | 42 | try: 43 | if response.status_code == 200 and not response.json()['data']['errors']: 44 | # only support single-file submissions; just grab the first one. 45 | return response.json()['data']['samples'][0]['sample_id'] 46 | else: 47 | raise sandboxapi.SandboxError("api error in analyze ({u}): {r}".format(u=response.url, r=response.content)) 48 | except (ValueError, KeyError, IndexError) as e: 49 | raise sandboxapi.SandboxError("error in analyze: {e}".format(e=e)) 50 | 51 | def check(self, item_id): 52 | """Check if an analysis is complete. 53 | 54 | :type item_id: str 55 | :param item_id: File ID to check. 56 | 57 | :rtype: bool 58 | :return: Boolean indicating if a report is done or not. 59 | """ 60 | response = self._request("/submission/sample/{sample_id}".format(sample_id=item_id), headers=self.headers) 61 | 62 | if response.status_code == 404: 63 | # unknown id 64 | return False 65 | 66 | try: 67 | finished = False 68 | for submission in response.json()['data']: 69 | finished = finished or submission['submission_finished'] 70 | if finished: 71 | return True 72 | 73 | except (ValueError, KeyError) as e: 74 | raise sandboxapi.SandboxError(e) 75 | 76 | return False 77 | 78 | def is_available(self): 79 | """Determine if the VMRay API server is alive. 80 | 81 | :rtype: bool 82 | :return: True if service is available, False otherwise. 83 | """ 84 | # if the availability flag is raised, return True immediately. 85 | # NOTE: subsequent API failures will lower this flag. we do this here 86 | # to ensure we don't keep hitting VMRay with requests while 87 | # availability is there. 88 | if self.server_available: 89 | return True 90 | 91 | # otherwise, we have to check with the cloud. 92 | else: 93 | try: 94 | response = self._request("/system_info", headers=self.headers) 95 | 96 | # we've got vmray. 97 | if response.status_code == 200: 98 | self.server_available = True 99 | return True 100 | 101 | except sandboxapi.SandboxError: 102 | pass 103 | 104 | self.server_available = False 105 | return False 106 | 107 | def report(self, item_id, report_format="json"): 108 | """Retrieves the specified report for the analyzed item, referenced by item_id. 109 | 110 | Available formats include: json. 111 | 112 | :type item_id: str 113 | :param item_id: File ID number 114 | :type report_format: str 115 | :param report_format: Return format 116 | 117 | :rtype: dict 118 | :return: Dictionary representing the JSON parsed data or raw, for other 119 | formats / JSON parsing failure. 120 | """ 121 | if report_format == "html": 122 | return "Report Unavailable" 123 | 124 | # grab an analysis id from the submission id. 125 | response = self._request("/analysis/sample/{sample_id}".format(sample_id=item_id), 126 | headers=self.headers) 127 | 128 | try: 129 | # the highest score is probably the most interesting. 130 | # vmray uses this internally with sample_highest_vti_score so this seems like a safe assumption. 131 | analysis_id = 0 132 | top_score = -1 133 | for analysis in response.json()['data']: 134 | if analysis['analysis_vti_score'] > top_score: 135 | top_score = analysis['analysis_vti_score'] 136 | analysis_id = analysis['analysis_id'] 137 | 138 | except (ValueError, KeyError) as e: 139 | raise sandboxapi.SandboxError(e) 140 | 141 | # assume report format json. 142 | response = self._request("/analysis/{analysis_id}/archive/logs/summary.json".format(analysis_id=analysis_id), 143 | headers=self.headers) 144 | 145 | # if response is JSON, return it as an object. 146 | try: 147 | return response.json() 148 | except ValueError: 149 | pass 150 | 151 | # otherwise, return the raw content. 152 | return response.content 153 | 154 | def score(self, report): 155 | """Pass in the report from self.report(), get back an int 0-100""" 156 | try: 157 | return report['vti']['vti_score'] 158 | except KeyError: 159 | return 0 160 | 161 | 162 | def vmray_loop(vmray, filename): 163 | # test run 164 | with open(arg, "rb") as handle: 165 | fileid = vmray.analyze(handle, filename) 166 | print("file {f} submitted for analysis, id {i}".format(f=filename, i=fileid)) 167 | 168 | while not vmray.check(fileid): 169 | print("not done yet, sleeping 10 seconds...") 170 | time.sleep(10) 171 | 172 | print("analysis complete. fetching report...") 173 | print(vmray.report(fileid)) 174 | 175 | 176 | if __name__ == "__main__": 177 | 178 | def usage(): 179 | msg = "%s: | available | report | analyze " 180 | print(msg % sys.argv[0]) 181 | sys.exit(1) 182 | 183 | if len(sys.argv) == 4: 184 | cmd = sys.argv.pop().lower() 185 | api_key = sys.argv.pop() 186 | url = sys.argv.pop() 187 | arg = None 188 | 189 | elif len(sys.argv) == 5: 190 | arg = sys.argv.pop() 191 | cmd = sys.argv.pop().lower() 192 | api_key = sys.argv.pop() 193 | url = sys.argv.pop() 194 | 195 | else: 196 | usage() 197 | 198 | # instantiate VMRay Sandbox API interface. 199 | vmray = VMRayAPI(api_key) 200 | 201 | # process command line arguments. 202 | if "submit" in cmd: 203 | if arg is None: 204 | usage() 205 | else: 206 | with open(arg, "rb") as handle: 207 | print(vmray.analyze(handle, arg)) 208 | 209 | elif "available" in cmd: 210 | print(vmray.is_available()) 211 | 212 | elif "report" in cmd: 213 | if arg is None: 214 | usage() 215 | else: 216 | print(vmray.report(arg)) 217 | 218 | elif "analyze" in cmd: 219 | if arg is None: 220 | usage() 221 | else: 222 | vmray_loop(vmray, arg) 223 | 224 | else: 225 | usage() 226 | -------------------------------------------------------------------------------- /sandboxapi/triage.py: -------------------------------------------------------------------------------- 1 | from __future__ import print_function 2 | 3 | import os 4 | import sys 5 | import json 6 | 7 | import sandboxapi 8 | 9 | class TriageAPI(sandboxapi.SandboxAPI): 10 | 11 | def __init__(self, api_key, url=None, api_path=None, verify_ssl=True, 12 | **kwargs): 13 | """ 14 | :type api_key: str 15 | :param api_key: The API key which can be found on the /account page 16 | on the Triage web interface 17 | 18 | :type url str 19 | :param url The url (including the port) of the Triage instance 20 | defaults to https://api.tria.ge 21 | 22 | :type api_path str 23 | :param api_path The path to the API on the Triage instance 24 | defaults to /v0 25 | """ 26 | sandboxapi.SandboxAPI.__init__(self, **kwargs) 27 | 28 | self.api_key = api_key 29 | self.base_url = url or "https://api.tria.ge" 30 | 31 | self.api_url = self.base_url + (api_path or "/v0") 32 | 33 | self.headers = {'Authorization': 'Bearer {:s}'.format(api_key)} 34 | 35 | self.verify_ssl = verify_ssl 36 | 37 | def request(self, uri, method='GET', params=None, files=None, auth=None): 38 | 39 | response = self._request( 40 | uri, method, params, files, self.headers, auth) 41 | 42 | # Try parsing the response as JSON to see if we got a valid object 43 | try: 44 | data = response.json() 45 | except ValueError as e: 46 | raise sandboxapi.SandboxError( 47 | "Triage returned a non JSON response {:s}", e) 48 | 49 | # If we got a normal object check whether we didn't receive an error 50 | # object 51 | if "error" in data.keys(): 52 | raise sandboxapi.SandboxError( 53 | "Triage raised an error: {:s} - {:s}".format( 54 | data["error"], data["message"])) 55 | 56 | # Everything is good to go 57 | return data 58 | 59 | def analyze(self, handle, filename): 60 | """Submit a file for analysis. 61 | 62 | :type handle: File handle 63 | :param handle: Handle to file to upload for analysis. 64 | :type filename: str 65 | :param filename: File name. 66 | 67 | :rtype: str 68 | :return: File ID as a string 69 | """ 70 | files = {"file": (filename, handle)} 71 | params = {"_json": json.dumps({ 72 | "kind": "file", 73 | "interactive": False 74 | })} 75 | 76 | # Ensure the handle is at offset 0. 77 | handle.seek(0) 78 | 79 | # Make the request to Triage 80 | data = self.request("/samples", method='POST', files=files, 81 | params=params) 82 | 83 | if "id" in data.keys(): 84 | return data["id"] 85 | else: 86 | raise sandboxapi.SandboxError("Triage returned no ID") 87 | 88 | def check(self, item_id): 89 | """Check if an analysis is complete. 90 | 91 | :type item_id: str 92 | :param item_id: Analysis ID to check. 93 | 94 | :rtype: bool 95 | :return: Boolean indicating if a report is done or not. 96 | """ 97 | 98 | data = self.request("/samples/{:s}/status".format(item_id)) 99 | 100 | if "status" in data.keys(): 101 | return data["status"] == "reported" 102 | else: 103 | raise sandboxapi.SandboxError("Triage didn't return a status") 104 | 105 | def is_available(self): 106 | """Determine if the Triage server is alive. 107 | 108 | :rtype: bool 109 | :return: True if service is available, False otherwise. 110 | """ 111 | 112 | try: 113 | self.request("/samples") 114 | except sandboxapi.SandboxError: 115 | return False 116 | 117 | return True 118 | 119 | def report(self, item_id, report_format="json"): 120 | """Retrieves the specified report for the analyzed item, 121 | referenced by item_id. Note that the summary is returned and more 122 | detailed information is available. 123 | 124 | :param str item_id: The id of the submitted file. 125 | :param str report_format: In here for compatibility though Triage 126 | only supports the JSON format 127 | 128 | :rtype: dic 129 | :return: Dictionary representing the JSON parsed data. 130 | """ 131 | 132 | if report_format != "json": 133 | raise sandboxapi.SandboxError( 134 | "Triage api only supports the json report format") 135 | 136 | data = self.request("/samples/{:s}/summary".format(item_id)) 137 | 138 | return data 139 | 140 | def score(self, item_id): 141 | """Gives back the highest score choosing from all the analyses 142 | 143 | :param str item_id: The id of the submitted file. 144 | 145 | :rtype: int 146 | :return: int on a scale from 1 til 10 147 | """ 148 | report = self.report(item_id) 149 | # 1 Is the Triage base score 150 | score = 1 151 | 152 | # Loop over the available reports to pick the highest score 153 | for task_id, task in report["tasks"].items(): 154 | if "score" in task: 155 | if task["score"] > score: 156 | score = task["score"] 157 | 158 | return score 159 | 160 | def full_report(self, item_id): 161 | """Retrieves the summary report and the full report of each task for 162 | the analyzed item, referenced by item_id. 163 | 164 | :param str item_id: The id of the submitted file. 165 | 166 | :rtype: dic 167 | :return: Dictionary representing the JSON parsed data. 168 | """ 169 | report = self.report(item_id) 170 | full_report = { 171 | 'summary': report, 172 | 'tasks': {} 173 | } 174 | 175 | # Loop over all the tasks in the summary 176 | for task_id in report["tasks"]: 177 | # Remove the sample ID to get the task names 178 | task_name = task_id.replace("{:s}-".format(item_id), "") 179 | 180 | try: 181 | # Try to retrieve each full report 182 | triage_report = self.request( 183 | "/samples/{:s}/{:s}/report_triage.json".format( 184 | item_id, task_name)) 185 | full_report["tasks"][task_name] = triage_report 186 | except sandboxapi.SandboxError: 187 | continue 188 | 189 | return full_report 190 | 191 | 192 | if __name__ == "__main__": 193 | 194 | def usage(): 195 | msg = "%s: | | | " 196 | print(msg % sys.argv[0]) 197 | sys.exit(1) 198 | 199 | if len(sys.argv) == 4: 200 | arg = sys.argv.pop() 201 | cmd = sys.argv.pop().lower() 202 | api_key = sys.argv.pop() 203 | 204 | else: 205 | usage() 206 | 207 | triage = TriageAPI(api_key) 208 | 209 | try: 210 | if "submit" in cmd: 211 | with open(arg, "r") as f: 212 | sample_id = triage.analyze(f, os.path.basename(f.name)) 213 | print("Sample ID: {:s}".format(sample_id)) 214 | 215 | elif "check" in cmd: 216 | if triage.check(arg): 217 | print("Report done") 218 | else: 219 | print("Report is not done yet") 220 | 221 | elif "full_report" in cmd: 222 | sample = triage.full_report(arg) 223 | print(sample) 224 | 225 | elif "report" in cmd: 226 | sample = triage.report(arg) 227 | print(sample) 228 | 229 | elif "score" in cmd: 230 | score = triage.score(arg) 231 | print(score) 232 | 233 | except sandboxapi.SandboxError as e: 234 | print("Unable to complete the action: {:s}".format(str(e))) 235 | -------------------------------------------------------------------------------- /tests/resources/vmray_analysis_sample.json: -------------------------------------------------------------------------------- 1 | { 2 | "data": [ 3 | { 4 | "analysis_analyzer_id": 1, 5 | "analysis_analyzer_name": "vmray", 6 | "analysis_analyzer_version": "2.1.0", 7 | "analysis_configuration_id": 48, 8 | "analysis_configuration_name": "exe", 9 | "analysis_created": "2017-10-23T20:04:51", 10 | "analysis_document_password": null, 11 | "analysis_id": 1097131, 12 | "analysis_job_id": 1097212, 13 | "analysis_job_started": "2017-10-23T20:04:50", 14 | "analysis_jobrule_id": 13, 15 | "analysis_jobrule_sampletype": "Windows PE (x86-32)", 16 | "analysis_prescript_id": null, 17 | "analysis_priority": 1, 18 | "analysis_result_code": 1, 19 | "analysis_result_str": "Operation completed successfully", 20 | "analysis_sample_id": 1169850, 21 | "analysis_sample_md5": "7371a86afb7446f6db2582ab43e4f974", 22 | "analysis_sample_sha1": "e2b1ddf0ba3536caf1788d79da9d2fac0a1fc1e5", 23 | "analysis_sample_sha256": "c3cd7a4ee74d0444435bbb3eecddc58254dd0ca6d9e93a5d3e48b95d3a78c5e7", 24 | "analysis_serialized_result": { 25 | "code": 1, 26 | "extra_args": {}, 27 | "fmt_args": [] 28 | }, 29 | "analysis_severity": "not_suspicious", 30 | "analysis_size": 48140912, 31 | "analysis_snapshot_id": 1, 32 | "analysis_snapshot_name": "def", 33 | "analysis_submission_id": 1374306, 34 | "analysis_tags": [], 35 | "analysis_user_email": "a@example.com", 36 | "analysis_user_id": 0, 37 | "analysis_vm_id": 18, 38 | "analysis_vm_name": "win8.1_64", 39 | "analysis_vmhost_id": 4, 40 | "analysis_vmhost_name": "cloud-worker-02", 41 | "analysis_vti_built_in_rules_version": 2.6, 42 | "analysis_vti_custom_rules_hash": "d41d8cd98f00b204e9800998ecf8427e", 43 | "analysis_vti_score": 20, 44 | "analysis_webif_url": "https://cloud.vmray.com/user/analysis/view?id=1097131&sub=%2Freport%2Foverview.html", 45 | "analysis_yara_latest_ruleset_date": "2017-08-01T13:06:11", 46 | "analysis_yara_match_count": 0 47 | }, 48 | { 49 | "analysis_analyzer_id": 1, 50 | "analysis_analyzer_name": "vmray", 51 | "analysis_analyzer_version": "2.1.0", 52 | "analysis_configuration_id": 48, 53 | "analysis_configuration_name": "exe", 54 | "analysis_created": "2017-10-23T20:00:57", 55 | "analysis_document_password": null, 56 | "analysis_id": 1097125, 57 | "analysis_job_id": 1097211, 58 | "analysis_job_started": "2017-10-23T20:00:55", 59 | "analysis_jobrule_id": 13, 60 | "analysis_jobrule_sampletype": "Windows PE (x86-32)", 61 | "analysis_prescript_id": null, 62 | "analysis_priority": 1, 63 | "analysis_result_code": 1, 64 | "analysis_result_str": "Operation completed successfully", 65 | "analysis_sample_id": 1169850, 66 | "analysis_sample_md5": "7371a86afb7446f6db2582ab43e4f974", 67 | "analysis_sample_sha1": "e2b1ddf0ba3536caf1788d79da9d2fac0a1fc1e5", 68 | "analysis_sample_sha256": "c3cd7a4ee74d0444435bbb3eecddc58254dd0ca6d9e93a5d3e48b95d3a78c5e7", 69 | "analysis_serialized_result": { 70 | "code": 1, 71 | "extra_args": {}, 72 | "fmt_args": [] 73 | }, 74 | "analysis_severity": "not_suspicious", 75 | "analysis_size": 62179698, 76 | "analysis_snapshot_id": 1, 77 | "analysis_snapshot_name": "def", 78 | "analysis_submission_id": 1374306, 79 | "analysis_tags": [], 80 | "analysis_user_email": "a@example.com", 81 | "analysis_user_id": 0, 82 | "analysis_vm_id": 17, 83 | "analysis_vm_name": "win7_32_sp1", 84 | "analysis_vmhost_id": 3, 85 | "analysis_vmhost_name": "cloud-worker-01", 86 | "analysis_vti_built_in_rules_version": 2.6, 87 | "analysis_vti_custom_rules_hash": "d41d8cd98f00b204e9800998ecf8427e", 88 | "analysis_vti_score": 20, 89 | "analysis_webif_url": "https://cloud.vmray.com/user/analysis/view?id=1097125&sub=%2Freport%2Foverview.html", 90 | "analysis_yara_latest_ruleset_date": "2017-08-01T13:06:11", 91 | "analysis_yara_match_count": 0 92 | }, 93 | { 94 | "analysis_analyzer_id": 1, 95 | "analysis_analyzer_name": "vmray", 96 | "analysis_analyzer_version": "2.1.0", 97 | "analysis_configuration_id": 48, 98 | "analysis_configuration_name": "exe", 99 | "analysis_created": "2017-10-23T19:57:14", 100 | "analysis_document_password": null, 101 | "analysis_id": 1097124, 102 | "analysis_job_id": 1097210, 103 | "analysis_job_started": "2017-10-23T19:57:13", 104 | "analysis_jobrule_id": 13, 105 | "analysis_jobrule_sampletype": "Windows PE (x86-32)", 106 | "analysis_prescript_id": null, 107 | "analysis_priority": 1, 108 | "analysis_result_code": 1, 109 | "analysis_result_str": "Operation completed successfully", 110 | "analysis_sample_id": 1169850, 111 | "analysis_sample_md5": "7371a86afb7446f6db2582ab43e4f974", 112 | "analysis_sample_sha1": "e2b1ddf0ba3536caf1788d79da9d2fac0a1fc1e5", 113 | "analysis_sample_sha256": "c3cd7a4ee74d0444435bbb3eecddc58254dd0ca6d9e93a5d3e48b95d3a78c5e7", 114 | "analysis_serialized_result": { 115 | "code": 1, 116 | "extra_args": {}, 117 | "fmt_args": [] 118 | }, 119 | "analysis_severity": "not_suspicious", 120 | "analysis_size": 40434490, 121 | "analysis_snapshot_id": 1, 122 | "analysis_snapshot_name": "def", 123 | "analysis_submission_id": 1374306, 124 | "analysis_tags": [], 125 | "analysis_user_email": "a@example.com", 126 | "analysis_user_id": 0, 127 | "analysis_vm_id": 9, 128 | "analysis_vm_name": "win7_64_sp1", 129 | "analysis_vmhost_id": 4, 130 | "analysis_vmhost_name": "cloud-worker-02", 131 | "analysis_vti_built_in_rules_version": 2.6, 132 | "analysis_vti_custom_rules_hash": "d41d8cd98f00b204e9800998ecf8427e", 133 | "analysis_vti_score": 20, 134 | "analysis_webif_url": "https://cloud.vmray.com/user/analysis/view?id=1097124&sub=%2Freport%2Foverview.html", 135 | "analysis_yara_latest_ruleset_date": "2017-08-01T13:06:11", 136 | "analysis_yara_match_count": 0 137 | }, 138 | { 139 | "analysis_analyzer_id": 1, 140 | "analysis_analyzer_name": "vmray", 141 | "analysis_analyzer_version": "2.1.0", 142 | "analysis_configuration_id": 48, 143 | "analysis_configuration_name": "exe", 144 | "analysis_created": "2017-10-23T19:54:38", 145 | "analysis_document_password": null, 146 | "analysis_id": 1097123, 147 | "analysis_job_id": 1097209, 148 | "analysis_job_started": "2017-10-23T19:54:35", 149 | "analysis_jobrule_id": 13, 150 | "analysis_jobrule_sampletype": "Windows PE (x86-32)", 151 | "analysis_prescript_id": null, 152 | "analysis_priority": 1, 153 | "analysis_result_code": 1, 154 | "analysis_result_str": "Operation completed successfully", 155 | "analysis_sample_id": 1169850, 156 | "analysis_sample_md5": "7371a86afb7446f6db2582ab43e4f974", 157 | "analysis_sample_sha1": "e2b1ddf0ba3536caf1788d79da9d2fac0a1fc1e5", 158 | "analysis_sample_sha256": "c3cd7a4ee74d0444435bbb3eecddc58254dd0ca6d9e93a5d3e48b95d3a78c5e7", 159 | "analysis_serialized_result": { 160 | "code": 1, 161 | "extra_args": {}, 162 | "fmt_args": [] 163 | }, 164 | "analysis_severity": "suspicious", 165 | "analysis_size": 112868205, 166 | "analysis_snapshot_id": 1, 167 | "analysis_snapshot_name": "def", 168 | "analysis_submission_id": 1374306, 169 | "analysis_tags": [], 170 | "analysis_user_email": "a@example.com", 171 | "analysis_user_id": 0, 172 | "analysis_vm_id": 20, 173 | "analysis_vm_name": "win10_64", 174 | "analysis_vmhost_id": 4, 175 | "analysis_vmhost_name": "cloud-worker-02", 176 | "analysis_vti_built_in_rules_version": 2.6, 177 | "analysis_vti_custom_rules_hash": "d41d8cd98f00b204e9800998ecf8427e", 178 | "analysis_vti_score": 67, 179 | "analysis_webif_url": "https://cloud.vmray.com/user/analysis/view?id=1097123&sub=%2Freport%2Foverview.html", 180 | "analysis_yara_latest_ruleset_date": "2017-08-01T13:06:11", 181 | "analysis_yara_match_count": 0 182 | } 183 | ], 184 | "result": "ok" 185 | } 186 | -------------------------------------------------------------------------------- /tests/resources/vmray_analysis_submission.json: -------------------------------------------------------------------------------- 1 | { 2 | "data": [ 3 | { 4 | "analysis_analyzer_id": 1, 5 | "analysis_analyzer_name": "vmray", 6 | "analysis_analyzer_version": "2.1.0", 7 | "analysis_configuration_id": 48, 8 | "analysis_configuration_name": "exe", 9 | "analysis_created": "2017-10-23T20:04:51", 10 | "analysis_document_password": null, 11 | "analysis_id": 1097131, 12 | "analysis_job_id": 1097212, 13 | "analysis_job_started": "2017-10-23T20:04:50", 14 | "analysis_jobrule_id": 13, 15 | "analysis_jobrule_sampletype": "Windows PE (x86-32)", 16 | "analysis_prescript_id": null, 17 | "analysis_priority": 1, 18 | "analysis_result_code": 1, 19 | "analysis_result_str": "Operation completed successfully", 20 | "analysis_sample_id": 1169850, 21 | "analysis_sample_md5": "7371a86afb7446f6db2582ab43e4f974", 22 | "analysis_sample_sha1": "e2b1ddf0ba3536caf1788d79da9d2fac0a1fc1e5", 23 | "analysis_sample_sha256": "c3cd7a4ee74d0444435bbb3eecddc58254dd0ca6d9e93a5d3e48b95d3a78c5e7", 24 | "analysis_serialized_result": { 25 | "code": 1, 26 | "extra_args": {}, 27 | "fmt_args": [] 28 | }, 29 | "analysis_severity": "not_suspicious", 30 | "analysis_size": 48140912, 31 | "analysis_snapshot_id": 1, 32 | "analysis_snapshot_name": "def", 33 | "analysis_submission_id": 1374306, 34 | "analysis_tags": [], 35 | "analysis_user_email": "a@example.com", 36 | "analysis_user_id": 0, 37 | "analysis_vm_id": 18, 38 | "analysis_vm_name": "win8.1_64", 39 | "analysis_vmhost_id": 4, 40 | "analysis_vmhost_name": "cloud-worker-02", 41 | "analysis_vti_built_in_rules_version": 2.6, 42 | "analysis_vti_custom_rules_hash": "d41d8cd98f00b204e9800998ecf8427e", 43 | "analysis_vti_score": 20, 44 | "analysis_webif_url": "https://cloud.vmray.com/user/analysis/view?id=1097131&sub=%2Freport%2Foverview.html", 45 | "analysis_yara_latest_ruleset_date": "2017-08-01T13:06:11", 46 | "analysis_yara_match_count": 0 47 | }, 48 | { 49 | "analysis_analyzer_id": 1, 50 | "analysis_analyzer_name": "vmray", 51 | "analysis_analyzer_version": "2.1.0", 52 | "analysis_configuration_id": 48, 53 | "analysis_configuration_name": "exe", 54 | "analysis_created": "2017-10-23T20:00:57", 55 | "analysis_document_password": null, 56 | "analysis_id": 1097125, 57 | "analysis_job_id": 1097211, 58 | "analysis_job_started": "2017-10-23T20:00:55", 59 | "analysis_jobrule_id": 13, 60 | "analysis_jobrule_sampletype": "Windows PE (x86-32)", 61 | "analysis_prescript_id": null, 62 | "analysis_priority": 1, 63 | "analysis_result_code": 1, 64 | "analysis_result_str": "Operation completed successfully", 65 | "analysis_sample_id": 1169850, 66 | "analysis_sample_md5": "7371a86afb7446f6db2582ab43e4f974", 67 | "analysis_sample_sha1": "e2b1ddf0ba3536caf1788d79da9d2fac0a1fc1e5", 68 | "analysis_sample_sha256": "c3cd7a4ee74d0444435bbb3eecddc58254dd0ca6d9e93a5d3e48b95d3a78c5e7", 69 | "analysis_serialized_result": { 70 | "code": 1, 71 | "extra_args": {}, 72 | "fmt_args": [] 73 | }, 74 | "analysis_severity": "not_suspicious", 75 | "analysis_size": 62179698, 76 | "analysis_snapshot_id": 1, 77 | "analysis_snapshot_name": "def", 78 | "analysis_submission_id": 1374306, 79 | "analysis_tags": [], 80 | "analysis_user_email": "a@example.com", 81 | "analysis_user_id": 0, 82 | "analysis_vm_id": 17, 83 | "analysis_vm_name": "win7_32_sp1", 84 | "analysis_vmhost_id": 3, 85 | "analysis_vmhost_name": "cloud-worker-01", 86 | "analysis_vti_built_in_rules_version": 2.6, 87 | "analysis_vti_custom_rules_hash": "d41d8cd98f00b204e9800998ecf8427e", 88 | "analysis_vti_score": 20, 89 | "analysis_webif_url": "https://cloud.vmray.com/user/analysis/view?id=1097125&sub=%2Freport%2Foverview.html", 90 | "analysis_yara_latest_ruleset_date": "2017-08-01T13:06:11", 91 | "analysis_yara_match_count": 0 92 | }, 93 | { 94 | "analysis_analyzer_id": 1, 95 | "analysis_analyzer_name": "vmray", 96 | "analysis_analyzer_version": "2.1.0", 97 | "analysis_configuration_id": 48, 98 | "analysis_configuration_name": "exe", 99 | "analysis_created": "2017-10-23T19:57:14", 100 | "analysis_document_password": null, 101 | "analysis_id": 1097124, 102 | "analysis_job_id": 1097210, 103 | "analysis_job_started": "2017-10-23T19:57:13", 104 | "analysis_jobrule_id": 13, 105 | "analysis_jobrule_sampletype": "Windows PE (x86-32)", 106 | "analysis_prescript_id": null, 107 | "analysis_priority": 1, 108 | "analysis_result_code": 1, 109 | "analysis_result_str": "Operation completed successfully", 110 | "analysis_sample_id": 1169850, 111 | "analysis_sample_md5": "7371a86afb7446f6db2582ab43e4f974", 112 | "analysis_sample_sha1": "e2b1ddf0ba3536caf1788d79da9d2fac0a1fc1e5", 113 | "analysis_sample_sha256": "c3cd7a4ee74d0444435bbb3eecddc58254dd0ca6d9e93a5d3e48b95d3a78c5e7", 114 | "analysis_serialized_result": { 115 | "code": 1, 116 | "extra_args": {}, 117 | "fmt_args": [] 118 | }, 119 | "analysis_severity": "not_suspicious", 120 | "analysis_size": 40434490, 121 | "analysis_snapshot_id": 1, 122 | "analysis_snapshot_name": "def", 123 | "analysis_submission_id": 1374306, 124 | "analysis_tags": [], 125 | "analysis_user_email": "a@example.com", 126 | "analysis_user_id": 0, 127 | "analysis_vm_id": 9, 128 | "analysis_vm_name": "win7_64_sp1", 129 | "analysis_vmhost_id": 4, 130 | "analysis_vmhost_name": "cloud-worker-02", 131 | "analysis_vti_built_in_rules_version": 2.6, 132 | "analysis_vti_custom_rules_hash": "d41d8cd98f00b204e9800998ecf8427e", 133 | "analysis_vti_score": 20, 134 | "analysis_webif_url": "https://cloud.vmray.com/user/analysis/view?id=1097124&sub=%2Freport%2Foverview.html", 135 | "analysis_yara_latest_ruleset_date": "2017-08-01T13:06:11", 136 | "analysis_yara_match_count": 0 137 | }, 138 | { 139 | "analysis_analyzer_id": 1, 140 | "analysis_analyzer_name": "vmray", 141 | "analysis_analyzer_version": "2.1.0", 142 | "analysis_configuration_id": 48, 143 | "analysis_configuration_name": "exe", 144 | "analysis_created": "2017-10-23T19:54:38", 145 | "analysis_document_password": null, 146 | "analysis_id": 1097123, 147 | "analysis_job_id": 1097209, 148 | "analysis_job_started": "2017-10-23T19:54:35", 149 | "analysis_jobrule_id": 13, 150 | "analysis_jobrule_sampletype": "Windows PE (x86-32)", 151 | "analysis_prescript_id": null, 152 | "analysis_priority": 1, 153 | "analysis_result_code": 1, 154 | "analysis_result_str": "Operation completed successfully", 155 | "analysis_sample_id": 1169850, 156 | "analysis_sample_md5": "7371a86afb7446f6db2582ab43e4f974", 157 | "analysis_sample_sha1": "e2b1ddf0ba3536caf1788d79da9d2fac0a1fc1e5", 158 | "analysis_sample_sha256": "c3cd7a4ee74d0444435bbb3eecddc58254dd0ca6d9e93a5d3e48b95d3a78c5e7", 159 | "analysis_serialized_result": { 160 | "code": 1, 161 | "extra_args": {}, 162 | "fmt_args": [] 163 | }, 164 | "analysis_severity": "suspicious", 165 | "analysis_size": 112868205, 166 | "analysis_snapshot_id": 1, 167 | "analysis_snapshot_name": "def", 168 | "analysis_submission_id": 1374306, 169 | "analysis_tags": [], 170 | "analysis_user_email": "a@example.com", 171 | "analysis_user_id": 0, 172 | "analysis_vm_id": 20, 173 | "analysis_vm_name": "win10_64", 174 | "analysis_vmhost_id": 4, 175 | "analysis_vmhost_name": "cloud-worker-02", 176 | "analysis_vti_built_in_rules_version": 2.6, 177 | "analysis_vti_custom_rules_hash": "d41d8cd98f00b204e9800998ecf8427e", 178 | "analysis_vti_score": 67, 179 | "analysis_webif_url": "https://cloud.vmray.com/user/analysis/view?id=1097123&sub=%2Freport%2Foverview.html", 180 | "analysis_yara_latest_ruleset_date": "2017-08-01T13:06:11", 181 | "analysis_yara_match_count": 0 182 | } 183 | ], 184 | "result": "ok" 185 | } 186 | -------------------------------------------------------------------------------- /tests/test_fireeye.py: -------------------------------------------------------------------------------- 1 | import io 2 | from unittest import TestCase 3 | 4 | try: 5 | from unittest.mock import patch, ANY as MOCK_ANY 6 | except ImportError: 7 | from mock import patch, ANY as MOCK_ANY 8 | 9 | import responses 10 | import sandboxapi.fireeye 11 | from . import read_resource 12 | 13 | class TestFireEye(TestCase): 14 | sandbox = sandboxapi.fireeye.FireEyeAPI('username', 'password', 'http://fireeye.mock', 'profile') 15 | 16 | @responses.activate 17 | def test_analyze(self): 18 | responses.add(responses.POST, 'http://fireeye.mock/wsapis/v1.2.0/auth/login', 19 | headers={'X-FeApi-Token': 'MOCK'}) 20 | responses.add(responses.POST, 'http://fireeye.mock/wsapis/v1.2.0/submissions', 21 | json=read_resource('fireeye_submissions')) 22 | self.assertEqual(self.sandbox.analyze(io.BytesIO('test'.encode('ascii')), 'filename'), 1) 23 | 24 | @responses.activate 25 | def test_check(self): 26 | responses.add(responses.POST, 'http://fireeye.mock/wsapis/v1.2.0/auth/login', 27 | headers={'X-FeApi-Token': 'MOCK'}) 28 | responses.add(responses.GET, 'http://fireeye.mock/wsapis/v1.2.0/submissions/status/1', 29 | json=read_resource('fireeye_submissions_status')) 30 | self.assertEqual(self.sandbox.check('1'), True) 31 | 32 | @responses.activate 33 | def test_is_available(self): 34 | responses.add(responses.POST, 'http://fireeye.mock/wsapis/v1.2.0/auth/login', 35 | headers={'X-FeApi-Token': 'MOCK'}) 36 | responses.add(responses.GET, 'http://fireeye.mock/wsapis/v1.2.0/config', 37 | json=read_resource('fireeye_config')) 38 | self.assertTrue(self.sandbox.is_available()) 39 | 40 | @responses.activate 41 | def test_not_is_available(self): 42 | self.assertFalse(self.sandbox.is_available()) 43 | responses.add(responses.POST, 'http://fireeye.mock/wsapis/v1.2.0/auth/login', 44 | headers={'X-FeApi-Token': 'MOCK'}) 45 | responses.add(responses.GET, 'http://fireeye.mock/wsapis/v1.2.0/config', 46 | status=500) 47 | self.assertFalse(self.sandbox.is_available()) 48 | 49 | @responses.activate 50 | def test_report(self): 51 | responses.add(responses.POST, 'http://fireeye.mock/wsapis/v1.2.0/auth/login', 52 | headers={'X-FeApi-Token': 'MOCK'}) 53 | responses.add(responses.GET, 'http://fireeye.mock/wsapis/v1.2.0/submissions/results/1', 54 | json=read_resource('fireeye_submissions_results')) 55 | self.assertEqual(self.sandbox.report(1)['msg'], 'concise') 56 | 57 | @responses.activate 58 | def test_score(self): 59 | responses.add(responses.POST, 'http://fireeye.mock/wsapis/v1.2.0/auth/login', 60 | headers={'X-FeApi-Token': 'MOCK'}) 61 | responses.add(responses.GET, 'http://fireeye.mock/wsapis/v1.2.0/submissions/results/1', 62 | json=read_resource('fireeye_submissions_results')) 63 | self.assertEqual(self.sandbox.score(self.sandbox.report(1)), 8) 64 | 65 | # Core functionality. 66 | @patch('requests.post') 67 | @patch('requests.get') 68 | def test_proxies_is_passed_to_requests(self, m_get, m_post): 69 | 70 | m_get.return_value.status_code = 200 71 | m_get.return_value.content = b'' 72 | m_post.return_value.status_code = 200 73 | m_post.return_value.content = b'' 74 | 75 | proxies = { 76 | 'http': 'http://10.10.1.10:3128', 77 | 'https': 'http://10.10.1.10:1080', 78 | } 79 | 80 | api = sandboxapi.fireeye.FireEyeAPI('username', 'password', 81 | self.sandbox.api_url, 'profile', 82 | proxies=proxies) 83 | api._request('/test') 84 | 85 | m_get.assert_called_once_with(api.api_url + '/test', auth=MOCK_ANY, 86 | headers=MOCK_ANY, params=MOCK_ANY, 87 | proxies=proxies, verify=MOCK_ANY) 88 | 89 | api._request('/test', method='POST') 90 | 91 | m_post.assert_called_with(api.api_url + '/test', auth=MOCK_ANY, 92 | headers=MOCK_ANY, data=MOCK_ANY, 93 | files=None, proxies=proxies, 94 | verify=MOCK_ANY) 95 | 96 | @responses.activate 97 | def test_reauthenticates_if_logged_out_http_401(self): 98 | responses.add(responses.POST, 'http://fireeye.mock/wsapis/v1.2.0/auth/login', 99 | headers={'X-FeApi-Token': 'MOCK'}) 100 | responses.add(responses.GET, 'http://fireeye.mock/wsapis/v1.2.0/submissions/status/1', 101 | status=401) 102 | responses.add(responses.GET, 'http://fireeye.mock/wsapis/v1.2.0/submissions/status/1', 103 | json=read_resource('fireeye_submissions_status')) 104 | self.assertEqual(self.sandbox.check('1'), True) 105 | 106 | @responses.activate 107 | def test_reauthenticates_if_logged_out_json_401(self): 108 | responses.add(responses.POST, 'http://fireeye.mock/wsapis/v1.2.0/auth/login', 109 | headers={'X-FeApi-Token': 'MOCK'}) 110 | responses.add(responses.GET, 'http://fireeye.mock/wsapis/v1.2.0/submissions/status/1', 111 | json=read_resource('fireeye_unauthorized')) 112 | responses.add(responses.GET, 'http://fireeye.mock/wsapis/v1.2.0/submissions/status/1', 113 | json=read_resource('fireeye_submissions_status')) 114 | self.assertEqual(self.sandbox.check('1'), True) 115 | 116 | class TestFireEyeLegacy(TestCase): 117 | legacy_sandbox = sandboxapi.fireeye.FireEyeAPI('username', 'password', 'http://fireeye.mock', 'profile', legacy_api=True) 118 | 119 | # Legacy API support 120 | @responses.activate 121 | def legacy_test_analyze(self): 122 | responses.add(responses.POST, 'http://fireeye.mock/wsapis/v1.1.0/auth/login', 123 | headers={'X-FeApi-Token': 'MOCK'}) 124 | responses.add(responses.POST, 'http://fireeye.mock/wsapis/v1.1.0/submissions', 125 | json=read_resource('fireeye_submissions')) 126 | self.assertEqual(self.legacy_sandbox.analyze(io.BytesIO('test'.encode('ascii')), 'filename'), 1) 127 | 128 | @responses.activate 129 | def legacy_test_check(self): 130 | responses.add(responses.POST, 'http://fireeye.mock/wsapis/v1.1.0/auth/login', 131 | headers={'X-FeApi-Token': 'MOCK'}) 132 | responses.add(responses.GET, 'http://fireeye.mock/wsapis/v1.1.0/submissions/status/1', 133 | json=read_resource('fireeye_submissions_status')) 134 | self.assertEqual(self.legacy_sandbox.check('1'), True) 135 | 136 | @responses.activate 137 | def legacy_test_is_available(self): 138 | responses.add(responses.POST, 'http://fireeye.mock/wsapis/v1.1.0/auth/login', 139 | headers={'X-FeApi-Token': 'MOCK'}) 140 | responses.add(responses.GET, 'http://fireeye.mock/wsapis/v1.1.0/config', 141 | json=read_resource('fireeye_config')) 142 | self.assertTrue(self.legacy_sandbox.is_available()) 143 | 144 | @responses.activate 145 | def legacy_test_not_is_available(self): 146 | self.assertFalse(self.legacy_sandbox.is_available()) 147 | responses.add(responses.POST, 'http://fireeye.mock/wsapis/v1.1.0/auth/login', 148 | headers={'X-FeApi-Token': 'MOCK'}) 149 | responses.add(responses.GET, 'http://fireeye.mock/wsapis/v1.1.0/config', 150 | status=500) 151 | self.assertFalse(self.legacy_sandbox.is_available()) 152 | 153 | @responses.activate 154 | def legacy_test_report(self): 155 | responses.add(responses.POST, 'http://fireeye.mock/wsapis/v1.1.0/auth/login', 156 | headers={'X-FeApi-Token': 'MOCK'}) 157 | responses.add(responses.GET, 'http://fireeye.mock/wsapis/v1.1.0/submissions/results/1', 158 | json=read_resource('fireeye_submissions_results')) 159 | self.assertEqual(self.legacy_sandbox.report(1)['msg'], 'concise') 160 | 161 | @responses.activate 162 | def legacy_test_score(self): 163 | responses.add(responses.POST, 'http://fireeye.mock/wsapis/v1.1.0/auth/login', 164 | headers={'X-FeApi-Token': 'MOCK'}) 165 | responses.add(responses.GET, 'http://fireeye.mock/wsapis/v1.1.0/submissions/results/1', 166 | json=read_resource('fireeye_submissions_results')) 167 | self.assertEqual(self.legacy_sandbox.score(self.legacy_sandbox.report(1)), 8) -------------------------------------------------------------------------------- /tests/resources/vmray_sample_submit.json: -------------------------------------------------------------------------------- 1 | { 2 | "data": { 3 | "errors": [], 4 | "jobs": [ 5 | { 6 | "job_analyzer_id": 1, 7 | "job_analyzer_name": "vmray", 8 | "job_configuration_id": 48, 9 | "job_configuration_name": "exe", 10 | "job_created": "2017-10-23T19:50:17", 11 | "job_document_password": null, 12 | "job_id": 1097209, 13 | "job_jobrule_id": 13, 14 | "job_jobrule_sampletype": "Windows PE (x86-32)", 15 | "job_parent_analysis_id": null, 16 | "job_prescript_id": null, 17 | "job_priority": 1, 18 | "job_reputation_job_id": 5880, 19 | "job_sample_id": 1169850, 20 | "job_sample_md5": "7371a86afb7446f6db2582ab43e4f974", 21 | "job_sample_sha1": "e2b1ddf0ba3536caf1788d79da9d2fac0a1fc1e5", 22 | "job_sample_sha256": "c3cd7a4ee74d0444435bbb3eecddc58254dd0ca6d9e93a5d3e48b95d3a78c5e7", 23 | "job_snapshot_id": 1, 24 | "job_snapshot_name": "def", 25 | "job_status": "queued", 26 | "job_statuschanged": "2017-10-23T19:50:17", 27 | "job_submission_id": 1374306, 28 | "job_tracking_state": "//waiting", 29 | "job_type": "full_analysis", 30 | "job_user_email": "a@example.com", 31 | "job_user_id": 0, 32 | "job_vm_id": 20, 33 | "job_vm_name": "win10_64", 34 | "job_vmhost_id": null, 35 | "job_vminstance_num": null, 36 | "job_vnc_token": "" 37 | }, 38 | { 39 | "job_analyzer_id": 1, 40 | "job_analyzer_name": "vmray", 41 | "job_configuration_id": 48, 42 | "job_configuration_name": "exe", 43 | "job_created": "2017-10-23T19:50:17", 44 | "job_document_password": null, 45 | "job_id": 1097210, 46 | "job_jobrule_id": 13, 47 | "job_jobrule_sampletype": "Windows PE (x86-32)", 48 | "job_parent_analysis_id": null, 49 | "job_prescript_id": null, 50 | "job_priority": 1, 51 | "job_reputation_job_id": 5880, 52 | "job_sample_id": 1169850, 53 | "job_sample_md5": "7371a86afb7446f6db2582ab43e4f974", 54 | "job_sample_sha1": "e2b1ddf0ba3536caf1788d79da9d2fac0a1fc1e5", 55 | "job_sample_sha256": "c3cd7a4ee74d0444435bbb3eecddc58254dd0ca6d9e93a5d3e48b95d3a78c5e7", 56 | "job_snapshot_id": 1, 57 | "job_snapshot_name": "def", 58 | "job_status": "queued", 59 | "job_statuschanged": "2017-10-23T19:50:17", 60 | "job_submission_id": 1374306, 61 | "job_tracking_state": "//waiting", 62 | "job_type": "full_analysis", 63 | "job_user_email": "a@example.com", 64 | "job_user_id": 0, 65 | "job_vm_id": 9, 66 | "job_vm_name": "win7_64_sp1", 67 | "job_vmhost_id": null, 68 | "job_vminstance_num": null, 69 | "job_vnc_token": "" 70 | }, 71 | { 72 | "job_analyzer_id": 1, 73 | "job_analyzer_name": "vmray", 74 | "job_configuration_id": 48, 75 | "job_configuration_name": "exe", 76 | "job_created": "2017-10-23T19:50:17", 77 | "job_document_password": null, 78 | "job_id": 1097211, 79 | "job_jobrule_id": 13, 80 | "job_jobrule_sampletype": "Windows PE (x86-32)", 81 | "job_parent_analysis_id": null, 82 | "job_prescript_id": null, 83 | "job_priority": 1, 84 | "job_reputation_job_id": 5880, 85 | "job_sample_id": 1169850, 86 | "job_sample_md5": "7371a86afb7446f6db2582ab43e4f974", 87 | "job_sample_sha1": "e2b1ddf0ba3536caf1788d79da9d2fac0a1fc1e5", 88 | "job_sample_sha256": "c3cd7a4ee74d0444435bbb3eecddc58254dd0ca6d9e93a5d3e48b95d3a78c5e7", 89 | "job_snapshot_id": 1, 90 | "job_snapshot_name": "def", 91 | "job_status": "queued", 92 | "job_statuschanged": "2017-10-23T19:50:17", 93 | "job_submission_id": 1374306, 94 | "job_tracking_state": "//waiting", 95 | "job_type": "full_analysis", 96 | "job_user_email": "a@example.com", 97 | "job_user_id": 0, 98 | "job_vm_id": 17, 99 | "job_vm_name": "win7_32_sp1", 100 | "job_vmhost_id": null, 101 | "job_vminstance_num": null, 102 | "job_vnc_token": "" 103 | }, 104 | { 105 | "job_analyzer_id": 1, 106 | "job_analyzer_name": "vmray", 107 | "job_configuration_id": 48, 108 | "job_configuration_name": "exe", 109 | "job_created": "2017-10-23T19:50:17", 110 | "job_document_password": null, 111 | "job_id": 1097212, 112 | "job_jobrule_id": 13, 113 | "job_jobrule_sampletype": "Windows PE (x86-32)", 114 | "job_parent_analysis_id": null, 115 | "job_prescript_id": null, 116 | "job_priority": 1, 117 | "job_reputation_job_id": 5880, 118 | "job_sample_id": 1169850, 119 | "job_sample_md5": "7371a86afb7446f6db2582ab43e4f974", 120 | "job_sample_sha1": "e2b1ddf0ba3536caf1788d79da9d2fac0a1fc1e5", 121 | "job_sample_sha256": "c3cd7a4ee74d0444435bbb3eecddc58254dd0ca6d9e93a5d3e48b95d3a78c5e7", 122 | "job_snapshot_id": 1, 123 | "job_snapshot_name": "def", 124 | "job_status": "queued", 125 | "job_statuschanged": "2017-10-23T19:50:17", 126 | "job_submission_id": 1374306, 127 | "job_tracking_state": "//waiting", 128 | "job_type": "full_analysis", 129 | "job_user_email": "a@example.com", 130 | "job_user_id": 0, 131 | "job_vm_id": 18, 132 | "job_vm_name": "win8.1_64", 133 | "job_vmhost_id": null, 134 | "job_vminstance_num": null, 135 | "job_vnc_token": "" 136 | } 137 | ], 138 | "md_jobs": [], 139 | "reputation_jobs": [ 140 | { 141 | "reputation_job_created": "2017-10-23T19:50:17", 142 | "reputation_job_id": 5880, 143 | "reputation_job_priority": 1, 144 | "reputation_job_sample_id": 1169850, 145 | "reputation_job_sample_md5": "7371a86afb7446f6db2582ab43e4f974", 146 | "reputation_job_sample_sha1": "e2b1ddf0ba3536caf1788d79da9d2fac0a1fc1e5", 147 | "reputation_job_sample_sha256": "c3cd7a4ee74d0444435bbb3eecddc58254dd0ca6d9e93a5d3e48b95d3a78c5e7", 148 | "reputation_job_status": "queued", 149 | "reputation_job_statuschanged": "2017-10-23T19:50:17", 150 | "reputation_job_submission_id": 1374306, 151 | "reputation_job_user_email": "a@example.com", 152 | "reputation_job_user_id": 0 153 | } 154 | ], 155 | "samples": [ 156 | { 157 | "sample_created": "2017-10-23T19:50:17", 158 | "sample_filename": "c3cd7a4ee74d0444435bbb3eecddc58254dd0ca6d9e93a5d3e48b95d3a78c5e7.exe", 159 | "sample_filesize": 30736384, 160 | "sample_id": 1169850, 161 | "sample_is_multipart": false, 162 | "sample_md5hash": "7371a86afb7446f6db2582ab43e4f974", 163 | "sample_priority": 1, 164 | "sample_sha1hash": "e2b1ddf0ba3536caf1788d79da9d2fac0a1fc1e5", 165 | "sample_sha256hash": "c3cd7a4ee74d0444435bbb3eecddc58254dd0ca6d9e93a5d3e48b95d3a78c5e7", 166 | "sample_type": "Windows Exe (x86-32)", 167 | "sample_url": null, 168 | "sample_webif_url": "https://cloud.vmray.com/user/sample/view?id=1169850", 169 | "submission_filename": "privatetunnel-win-2.7.exe" 170 | } 171 | ], 172 | "submissions": [ 173 | { 174 | "submission_comment": null, 175 | "submission_created": "2017-10-23T19:50:17", 176 | "submission_document_password": null, 177 | "submission_filename": "privatetunnel-win-2.7.exe", 178 | "submission_finish_time": null, 179 | "submission_finished": false, 180 | "submission_has_errors": null, 181 | "submission_id": 1374306, 182 | "submission_ip_id": 0, 183 | "submission_ip_ip": "10.10.10.10", 184 | "submission_original_filename": "privatetunnel-win-2.7.exe", 185 | "submission_prescript_id": null, 186 | "submission_priority": 1, 187 | "submission_reputation_mode": "auxiliary", 188 | "submission_sample_id": 1169850, 189 | "submission_sample_md5": "7371a86afb7446f6db2582ab43e4f974", 190 | "submission_sample_sha1": "e2b1ddf0ba3536caf1788d79da9d2fac0a1fc1e5", 191 | "submission_sample_sha256": "c3cd7a4ee74d0444435bbb3eecddc58254dd0ca6d9e93a5d3e48b95d3a78c5e7", 192 | "submission_shareable": false, 193 | "submission_tags": [], 194 | "submission_triage_error_handling": null, 195 | "submission_type": "api", 196 | "submission_user_email": "a@example.com", 197 | "submission_user_id": 0, 198 | "submission_webif_url": "https://cloud.vmray.com/user/sample/view?id=1169850" 199 | } 200 | ], 201 | "vt_jobs": [] 202 | }, 203 | "result": "ok" 204 | } 205 | -------------------------------------------------------------------------------- /sandboxapi/opswat.py: -------------------------------------------------------------------------------- 1 | from __future__ import print_function 2 | 3 | import sandboxapi 4 | import sys 5 | import time 6 | 7 | 8 | class MetaDefenderSandboxAPI(sandboxapi.SandboxAPI): 9 | """MetaDefender Sandbox API wrapper.""" 10 | 11 | def __init__( 12 | self, api_key, url="https://www.filescan.io", verify_ssl=True, **kwargs 13 | ): 14 | """Initialize the interface to MetaDefender Sandbox API. 15 | :type api_key: str 16 | :param api_key: MetaDefender Sandbox API key 17 | 18 | :type url str 19 | :param url The url (including the port) of the MetaDefender Sandbox 20 | instance defaults to https://www.filescan.io 21 | """ 22 | sandboxapi.SandboxAPI.__init__(self, **kwargs) 23 | self.api_key = api_key 24 | self.api_url = url 25 | self.headers = {"X-Api-Key": self.api_key} 26 | self.verify_ssl = verify_ssl 27 | 28 | def analyze(self, handle, filename, password=None, is_private=False): 29 | """Submit a file for analysis. 30 | 31 | :type handle: File handle 32 | :param handle: Handle to file to upload for analysis. 33 | :type filename: str 34 | :param filename: File name. 35 | :type password: str 36 | :param password: Custom password, in case uploaded archive is protected. 37 | :type is_private: boolean 38 | :param is_private: If file should not be available for download by other users. 39 | 40 | :rtype: str 41 | :return: flow_id as a string 42 | """ 43 | 44 | if not self.api_key: 45 | raise sandboxapi.SandboxError("Missing API key") 46 | 47 | # multipart post files. 48 | files = {"file": (filename, handle)} 49 | 50 | # ensure the handle is at offset 0. 51 | handle.seek(0) 52 | 53 | try: 54 | params = {"password": password, "is_private": is_private} 55 | 56 | response = self._request( 57 | "/api/scan/file", 58 | method="POST", 59 | params=params, 60 | headers=self.headers, 61 | files=files, 62 | ) 63 | 64 | if response.status_code == 200 and response and response.json(): 65 | # send file, get flow_id 66 | if "flow_id" in response.json(): 67 | return response.json()["flow_id"] 68 | 69 | raise sandboxapi.SandboxError( 70 | "api error in analyze ({u}): {r}".format( 71 | u=response.url, r=response.content 72 | ) 73 | ) 74 | except (ValueError, KeyError) as e: 75 | raise sandboxapi.SandboxError("error in analyze: {e}".format(e=e)) 76 | 77 | def check(self, item_id): 78 | """Check if an analysis is complete. 79 | 80 | :type item_id: str 81 | :param item_id: flow_id to check. 82 | 83 | :rtype: bool 84 | :return: Boolean indicating if a report is done or not. 85 | """ 86 | response = self._request( 87 | "/api/scan/{flow_id}/report".format(flow_id=item_id), headers=self.headers 88 | ) 89 | 90 | if response.status_code == 404: 91 | # unknown id 92 | return False 93 | 94 | try: 95 | if "allFinished" in response.json() and response.json()["allFinished"]: 96 | return True 97 | elif "allFinished" not in response.json(): 98 | raise sandboxapi.SandboxError( 99 | "api error in check ({u}): {r}".format( 100 | u=response.url, r=response.content 101 | ) 102 | ) 103 | 104 | except ValueError as e: 105 | raise sandboxapi.SandboxError(e) 106 | 107 | return False 108 | 109 | def is_available(self): 110 | """Determine if the MetaDefender Sandbox API server is alive. 111 | 112 | :rtype: bool 113 | :return: True if service is available, False otherwise. 114 | """ 115 | # if the availability flag is raised, return True immediately. 116 | # NOTE: subsequent API failures will lower this flag. we do this here 117 | # to ensure we don't keep hitting Opswat with requests while 118 | # availability is there. 119 | if self.server_available: 120 | return True 121 | 122 | # otherwise, we have to check with the cloud. 123 | else: 124 | try: 125 | response = self._request("/api/users/me", headers=self.headers) 126 | 127 | # we've got opswat. 128 | if response.status_code == 200 and "accountId" in response.json(): 129 | self.server_available = True 130 | return True 131 | except sandboxapi.SandboxError: 132 | pass 133 | 134 | self.server_available = False 135 | return False 136 | 137 | def report(self, item_id, report_format="json"): 138 | """Retrieves the specified report for the analyzed item, referenced by item_id. 139 | 140 | Available formats include: json. 141 | 142 | :type item_id: str 143 | :param item_id: flow_id number 144 | :type report_format: str 145 | :param report_format: Return format 146 | 147 | :rtype: dict 148 | :return: Dictionary representing the JSON parsed data or raw, for other 149 | formats / JSON parsing failure. 150 | """ 151 | if report_format == "html": 152 | return "Report Unavailable" 153 | 154 | filters = [ 155 | "filter=general", 156 | "filter=finalVerdict", 157 | "filter=allTags", 158 | "filter=overallState", 159 | "filter=taskReference", 160 | "filter=subtaskReferences", 161 | "filter=allSignalGroups", 162 | "filter=iocs", 163 | ] 164 | 165 | postfix = "&".join(filters) 166 | url_suffix = "/api/scan/{flow_id}/report?{postfix}".format( 167 | flow_id=item_id, postfix=postfix 168 | ) 169 | 170 | response = self._request(url_suffix, headers=self.headers) 171 | 172 | try: 173 | return response.json() 174 | except ValueError: 175 | pass 176 | 177 | # otherwise, return the raw content. 178 | return response.content.decode("utf-8") 179 | 180 | def score(self, report): 181 | """Pass in the report from self.report(), get back an int.""" 182 | report_scores = [0] 183 | reports = report.get("reports", {}) 184 | for report_value in reports.values(): 185 | score = 0 186 | threat_level = report_value.get("finalVerdict", {}).get("threatLevel", 0) 187 | report_scores.append(max(0, threat_level) * 100) 188 | 189 | score = max(report_scores) 190 | return score 191 | 192 | 193 | def md_sandbox_loop(md_sandbox, filename): 194 | # test run 195 | with open(arg, "rb") as handle: 196 | flow_id = md_sandbox.analyze(handle, filename) 197 | print("file {f} submitted for analysis, id {i}".format(f=filename, i=flow_id)) 198 | 199 | while not md_sandbox.check(flow_id): 200 | print("not done yet, sleeping 10 seconds...") 201 | time.sleep(10) 202 | 203 | print("Analysis complete. fetching report...") 204 | print(md_sandbox.report(flow_id)) 205 | 206 | 207 | if __name__ == "__main__": 208 | 209 | def usage(): 210 | msg = "%s: | available | report | score | analyze " 211 | print(msg % sys.argv[0]) 212 | sys.exit(1) 213 | 214 | cmd = None 215 | api_key = None 216 | url = None 217 | 218 | if len(sys.argv) == 4: 219 | cmd = sys.argv.pop().lower() 220 | api_key = sys.argv.pop() 221 | url = sys.argv.pop() 222 | arg = None 223 | 224 | elif len(sys.argv) == 5: 225 | arg = sys.argv.pop() 226 | cmd = sys.argv.pop().lower() 227 | api_key = sys.argv.pop() 228 | url = sys.argv.pop() 229 | 230 | else: 231 | usage() 232 | 233 | md_sandbox = MetaDefenderSandboxAPI(api_key, url) 234 | 235 | if arg is None and "available" not in cmd: 236 | usage() 237 | 238 | # process command line arguments. 239 | if "submit" in cmd: 240 | with open(arg, "rb") as handle: 241 | print(md_sandbox.analyze(handle, arg)) 242 | 243 | elif "available" in cmd: 244 | print(md_sandbox.is_available()) 245 | 246 | elif "report" in cmd: 247 | print(md_sandbox.report(arg)) 248 | 249 | elif "analyze" in cmd: 250 | md_sandbox_loop(md_sandbox, arg) 251 | 252 | elif "score" in cmd: 253 | score = md_sandbox.score(arg) 254 | print(score) 255 | 256 | else: 257 | usage() 258 | -------------------------------------------------------------------------------- /sandboxapi/cuckoo.py: -------------------------------------------------------------------------------- 1 | from __future__ import print_function 2 | 3 | import sys 4 | import json 5 | 6 | import sandboxapi 7 | 8 | class CuckooAPI(sandboxapi.SandboxAPI): 9 | """Cuckoo Sandbox API wrapper.""" 10 | 11 | def __init__(self, url, port=8090, api_path='/', verify_ssl=False, **kwargs): 12 | """Initialize the interface to Cuckoo Sandbox API with host and port. 13 | 14 | :type url: str 15 | :param url: Cuckoo API URL. (Currently treated as host if not a fully formed URL - 16 | this will be removed in a future version.) 17 | :type port: int 18 | :param port: DEPRECATED! Use fully formed url instead. Will be removed in future version. 19 | :type api_path: str 20 | :param api_path: DEPRECATED! Use fully formed url instead. Will be removed in future version. 21 | """ 22 | sandboxapi.SandboxAPI.__init__(self, **kwargs) 23 | 24 | if not url: 25 | url = '' 26 | 27 | # NOTE: host/port/api_path support is DEPRECATED! 28 | if url.startswith('http://') or url.startswith('https://'): 29 | # Assume new-style url param. Ignore port and api_path. 30 | self.api_url = url 31 | else: 32 | # This is for backwards compatability and will be removed in a future version. 33 | self.api_url = 'http://' + url + ':' + str(port) + api_path 34 | 35 | self.verify_ssl = verify_ssl 36 | 37 | # assume Cuckoo is *not* available. 38 | self.server_available = False 39 | 40 | def analyses(self): 41 | """Retrieve a list of analyzed samples. 42 | 43 | :rtype: list 44 | :return: List of objects referencing each analyzed file. 45 | """ 46 | response = self._request("tasks/list") 47 | 48 | return json.loads(response.content.decode('utf-8'))['tasks'] 49 | 50 | def analyze(self, handle, filename): 51 | """Submit a file for analysis. 52 | 53 | :type handle: File handle 54 | :param handle: Handle to file to upload for analysis. 55 | :type filename: str 56 | :param filename: File name. 57 | 58 | :rtype: str 59 | :return: Task ID as a string 60 | """ 61 | # multipart post files. 62 | files = {"file": (filename, handle)} 63 | 64 | # ensure the handle is at offset 0. 65 | handle.seek(0) 66 | 67 | response = self._request("tasks/create/file", method='POST', files=files) 68 | 69 | # return task id; try v1.3 and v2.0 API response formats 70 | try: 71 | return str(json.loads(response.content.decode('utf-8'))["task_id"]) 72 | except KeyError: 73 | return str(json.loads(response.content.decode('utf-8'))["task_ids"][0]) 74 | 75 | def check(self, item_id): 76 | """Check if an analysis is complete 77 | 78 | :type item_id: int 79 | :param item_id: task_id to check. 80 | 81 | :rtype: bool 82 | :return: Boolean indicating if a report is done or not. 83 | """ 84 | response = self._request("tasks/view/{id}".format(id=item_id)) 85 | 86 | if response.status_code == 404: 87 | # probably an unknown task id 88 | return False 89 | 90 | try: 91 | content = json.loads(response.content.decode('utf-8')) 92 | status = content['task']["status"] 93 | if status == 'completed' or status == "reported": 94 | return True 95 | 96 | except ValueError as e: 97 | raise sandboxapi.SandboxError(e) 98 | 99 | return False 100 | 101 | def delete(self, item_id): 102 | """Delete the reports associated with the given item_id. 103 | 104 | :type item_id: int 105 | :param item_id: Report ID to delete. 106 | 107 | :rtype: bool 108 | :return: True on success, False otherwise. 109 | """ 110 | try: 111 | response = self._request("tasks/delete/{id}".format(id=item_id)) 112 | 113 | if response.status_code == 200: 114 | return True 115 | 116 | except sandboxapi.SandboxError: 117 | pass 118 | 119 | return False 120 | 121 | def is_available(self): 122 | """Determine if the Cuckoo Sandbox API servers are alive or in maintenance mode. 123 | 124 | :rtype: bool 125 | :return: True if service is available, False otherwise. 126 | """ 127 | # if the availability flag is raised, return True immediately. 128 | # NOTE: subsequent API failures will lower this flag. we do this here 129 | # to ensure we don't keep hitting Cuckoo with requests while 130 | # availability is there. 131 | if self.server_available: 132 | return True 133 | 134 | # otherwise, we have to check with the cloud. 135 | else: 136 | try: 137 | response = self._request("cuckoo/status") 138 | 139 | # we've got cuckoo. 140 | if response.status_code == 200: 141 | self.server_available = True 142 | return True 143 | 144 | except sandboxapi.SandboxError: 145 | pass 146 | 147 | self.server_available = False 148 | return False 149 | 150 | def queue_size(self): 151 | """Determine Cuckoo sandbox queue length 152 | 153 | There isn't a built in way to do this like with Joe 154 | 155 | :rtype: int 156 | :return: Number of submissions in sandbox queue. 157 | """ 158 | response = self._request("tasks/list") 159 | tasks = json.loads(response.content.decode('utf-8'))["tasks"] 160 | 161 | return len([t for t in tasks if t['status'] == 'pending']) 162 | 163 | def report(self, item_id, report_format="json"): 164 | """Retrieves the specified report for the analyzed item, referenced by item_id. 165 | 166 | Available formats include: json, html, all, dropped, package_files. 167 | 168 | :type item_id: int 169 | :param item_id: Task ID number 170 | :type report_format: str 171 | :param report_format: Return format 172 | 173 | :rtype: dict 174 | :return: Dictionary representing the JSON parsed data or raw, for other 175 | formats / JSON parsing failure. 176 | """ 177 | report_format = report_format.lower() 178 | 179 | response = self._request("tasks/report/{id}/{format}".format(id=item_id, format=report_format)) 180 | 181 | # if response is JSON, return it as an object 182 | if report_format == "json": 183 | try: 184 | return json.loads(response.content.decode('utf-8')) 185 | except ValueError: 186 | pass 187 | 188 | # otherwise, return the raw content. 189 | return response.content 190 | 191 | def score(self, report): 192 | """Pass in the report from self.report(), get back an int.""" 193 | score = 0 194 | 195 | try: 196 | # cuckoo-modified format 197 | score = report['malscore'] 198 | except KeyError: 199 | # cuckoo-2.0 format 200 | score = report.get('info', {}).get('score', 0) 201 | except TypeError as e: 202 | raise sandboxapi.SandboxError(e) 203 | 204 | return score 205 | 206 | 207 | if __name__ == "__main__": 208 | 209 | def usage(): 210 | msg = "%s: | available | delete | queue | report " 211 | print(msg % sys.argv[0]) 212 | sys.exit(1) 213 | 214 | if len(sys.argv) == 3: 215 | cmd = sys.argv.pop().lower() 216 | host = sys.argv.pop().lower() 217 | arg = None 218 | 219 | elif len(sys.argv) == 4: 220 | arg = sys.argv.pop() 221 | cmd = sys.argv.pop().lower() 222 | host = sys.argv.pop().lower() 223 | 224 | else: 225 | usage() 226 | 227 | # instantiate Cuckoo Sandbox API interface. 228 | cuckoo = CuckooAPI(host) 229 | 230 | # process command line arguments. 231 | if "analyses" in cmd: 232 | for a in cuckoo.analyses(): 233 | print(a["id"], a["status"], a["tags"], a["target"]) 234 | 235 | elif "analyze" in cmd: 236 | if arg is None: 237 | usage() 238 | else: 239 | with open(arg, "rb") as handle: 240 | print(cuckoo.analyze(handle, arg)) 241 | 242 | elif "available" in cmd: 243 | print(cuckoo.is_available()) 244 | 245 | elif "delete" in cmd: 246 | if arg is None: 247 | usage() 248 | else: 249 | print(cuckoo.delete(arg)) 250 | 251 | elif "queue" in cmd: 252 | print(cuckoo.queue_size()) 253 | 254 | elif "report" in cmd: 255 | if arg is None: 256 | usage() 257 | else: 258 | print(cuckoo.report(arg)) 259 | 260 | else: 261 | usage() 262 | -------------------------------------------------------------------------------- /sandboxapi/fireeye.py: -------------------------------------------------------------------------------- 1 | from __future__ import print_function 2 | 3 | import sys 4 | import time 5 | import json 6 | 7 | from requests.auth import HTTPBasicAuth 8 | 9 | import sandboxapi 10 | 11 | class FireEyeAPI(sandboxapi.SandboxAPI): 12 | """FireEye Sandbox API wrapper.""" 13 | 14 | def __init__(self, username, password, url, profile, legacy_api=False, verify_ssl=True, **kwargs): 15 | """Initialize the interface to FireEye Sandbox API.""" 16 | sandboxapi.SandboxAPI.__init__(self, **kwargs) 17 | 18 | self.base_url = url 19 | self.username = username 20 | self.password = password 21 | self.profile = profile or 'winxp-sp3' 22 | self.api_token = None 23 | self.verify_ssl = verify_ssl 24 | 25 | if legacy_api: 26 | # Use v1.1.0 endpoints for v7.x appliances. 27 | self.api_url = url + '/wsapis/v1.1.0' 28 | else: 29 | self.api_url = url + '/wsapis/v1.2.0' 30 | 31 | def _request(self, uri, method='GET', params=None, files=None, headers=None, auth=None): 32 | """Override the parent _request method. 33 | 34 | We have to do this here because FireEye requires some extra 35 | authentication steps. On each request we pass the auth headers, and 36 | if the session has expired, we automatically reauthenticate. 37 | """ 38 | if headers: 39 | headers['Accept'] = 'application/json' 40 | else: 41 | headers = { 42 | 'Accept': 'application/json', 43 | } 44 | 45 | if not self.api_token: 46 | # need to log in 47 | response = sandboxapi.SandboxAPI._request(self, '/auth/login', 'POST', headers=headers, 48 | auth=HTTPBasicAuth(self.username, self.password)) 49 | if response.status_code != 200: 50 | raise sandboxapi.SandboxError("Can't log in, HTTP Error {e}".format(e=response.status_code)) 51 | # we are now logged in, save the token 52 | self.api_token = response.headers.get('X-FeApi-Token') 53 | 54 | headers['X-FeApi-Token'] = self.api_token 55 | 56 | response = sandboxapi.SandboxAPI._request(self, uri, method, params, files, headers) 57 | 58 | # handle session timeout 59 | unauthorized = False 60 | try: 61 | if json.loads(response.content.decode('utf-8'))['fireeyeapis']['httpStatus'] == 401: 62 | unauthorized = True 63 | except (ValueError, KeyError, TypeError): 64 | # non-JSON response, or no such keys. 65 | pass 66 | 67 | if response.status_code == 401 or unauthorized: 68 | self.api_token = None 69 | try: 70 | headers.pop('X-FeApi-Token') 71 | except KeyError: 72 | pass 73 | 74 | # recurse 75 | return self._request(uri, method, params, files, headers) 76 | 77 | return response 78 | 79 | def analyze(self, handle, filename): 80 | """Submit a file for analysis. 81 | 82 | :type handle: File handle 83 | :param handle: Handle to file to upload for analysis. 84 | :type filename: str 85 | :param filename: File name. 86 | 87 | :rtype: str 88 | :return: File ID as a string 89 | """ 90 | # multipart post files. 91 | files = {"file": (filename, handle)} 92 | 93 | # ensure the handle is at offset 0. 94 | handle.seek(0) 95 | 96 | # add submission options 97 | data = { 98 | #FIXME: These may need to change, see docs page 36 99 | 'options': '{"application":"0","timeout":"500","priority":"0","profiles":["%s"],"analysistype":"0","force":"true","prefetch":"1"}' % self.profile, 100 | } 101 | 102 | response = self._request("/submissions", method='POST', params=data, files=files) 103 | 104 | try: 105 | if response.status_code == 200: 106 | # good response 107 | try: 108 | return response.json()['ID'] 109 | except TypeError: 110 | return response.json()[0]['ID'] 111 | else: 112 | raise sandboxapi.SandboxError("api error in analyze ({u}): {r}".format(u=response.url, r=response.content)) 113 | except (ValueError, KeyError) as e: 114 | raise sandboxapi.SandboxError("error in analyze: {e}".format(e=e)) 115 | 116 | def check(self, item_id): 117 | """Check if an analysis is complete. 118 | 119 | :type item_id: str 120 | :param item_id: File ID to check. 121 | 122 | :rtype: bool 123 | :return: Boolean indicating if a report is done or not. 124 | """ 125 | response = self._request("/submissions/status/{file_id}".format(file_id=item_id)) 126 | 127 | if response.status_code == 404: 128 | # unknown id 129 | return False 130 | 131 | try: 132 | status = response.json()['submissionStatus'] 133 | if status == 'Done': 134 | return True 135 | 136 | except ValueError as e: 137 | raise sandboxapi.SandboxError(e) 138 | 139 | return False 140 | 141 | def is_available(self): 142 | """Determine if the FireEye API server is alive. 143 | 144 | :rtype: bool 145 | :return: True if service is available, False otherwise. 146 | """ 147 | 148 | try: 149 | response = self._request("/config") 150 | 151 | # Successfully connected to FireEye 152 | if response.status_code == 200: 153 | self.server_available = True 154 | return True 155 | 156 | # Unable to connect to FireEye 157 | if response.status_code >= 500: 158 | self.server_available = False 159 | return False 160 | except sandboxapi.SandboxError: 161 | pass 162 | 163 | self.server_available = False 164 | return False 165 | 166 | def report(self, item_id, report_format="json"): 167 | """Retrieves the specified report for the analyzed item, referenced by item_id. 168 | 169 | Available formats include: json. 170 | 171 | :type item_id: str 172 | :param item_id: File ID number 173 | :type report_format: str 174 | :param report_format: Return format 175 | 176 | :rtype: dict 177 | :return: Dictionary representing the JSON parsed data or raw, for other 178 | formats / JSON parsing failure. 179 | """ 180 | if report_format == "html": 181 | return "Report Unavailable" 182 | 183 | # else we try JSON 184 | response = self._request("/submissions/results/{file_id}?info_level=extended".format(file_id=item_id)) 185 | 186 | # if response is JSON, return it as an object 187 | try: 188 | return response.json() 189 | except ValueError: 190 | pass 191 | 192 | # otherwise, return the raw content. 193 | return response.content 194 | 195 | def score(self, report): 196 | """Pass in the report from self.report(), get back an int.""" 197 | score = 0 198 | if report['alert'][0]['severity'] == 'MAJR': 199 | score = 8 200 | 201 | return score 202 | 203 | def logout(self): 204 | """The FireEye AX has a limit of 100 concurrent sessions, so be sure to logout""" 205 | if self.api_token: 206 | self._request("/auth/logout") 207 | 208 | 209 | def fireeye_loop(fireeye, filename): 210 | # test run 211 | with open(arg, "rb") as handle: 212 | fileid = fireeye.analyze(handle, filename) 213 | print("file {f} submitted for analysis, id {i}".format(f=filename, i=fileid)) 214 | 215 | while not fireeye.check(fileid): 216 | print("not done yet, sleeping 10 seconds...") 217 | time.sleep(10) 218 | 219 | print("analysis complete. fetching report...") 220 | print(fireeye.report(fileid)) 221 | 222 | 223 | if __name__ == "__main__": 224 | 225 | def usage(): 226 | msg = "%s: | available | report | analyze " 227 | print(msg % sys.argv[0]) 228 | sys.exit(1) 229 | 230 | if len(sys.argv) == 5: 231 | cmd = sys.argv.pop().lower() 232 | password = sys.argv.pop() 233 | username = sys.argv.pop() 234 | url = sys.argv.pop() 235 | arg = None 236 | 237 | elif len(sys.argv) == 6: 238 | arg = sys.argv.pop() 239 | cmd = sys.argv.pop().lower() 240 | password = sys.argv.pop() 241 | username = sys.argv.pop() 242 | url = sys.argv.pop() 243 | 244 | else: 245 | usage() 246 | 247 | # instantiate FireEye Sandbox API interface. 248 | fireeye = FireEyeAPI(username, password, url, 'winxp-sp3') 249 | 250 | # process command line arguments. 251 | if "submit" in cmd: 252 | if arg is None: 253 | usage() 254 | else: 255 | with open(arg, "rb") as handle: 256 | print(fireeye.analyze(handle, arg)) 257 | 258 | elif "available" in cmd: 259 | print(fireeye.is_available()) 260 | 261 | elif "report" in cmd: 262 | if arg is None: 263 | usage() 264 | else: 265 | print(fireeye.report(arg)) 266 | 267 | elif "analyze" in cmd: 268 | if arg is None: 269 | usage() 270 | else: 271 | fireeye_loop(fireeye, arg) 272 | 273 | else: 274 | usage() 275 | 276 | fireeye.logout() 277 | -------------------------------------------------------------------------------- /sandboxapi/falcon.py: -------------------------------------------------------------------------------- 1 | from __future__ import print_function 2 | 3 | import sys 4 | import json 5 | 6 | import sandboxapi 7 | 8 | class FalconAPI(sandboxapi.SandboxAPI): 9 | """Falcon Sandbox API wrapper.""" 10 | 11 | def __init__(self, key, url=None, env=100, **kwargs): 12 | """Initialize the interface to Falcon Sandbox API with key and secret.""" 13 | sandboxapi.SandboxAPI.__init__(self, **kwargs) 14 | 15 | self.api_url = url or 'https://www.reverse.it/api/v2' 16 | self.key = key 17 | self.env_id = str(env) 18 | 19 | def _request(self, uri, method='GET', params=None, files=None, headers=None, auth=None): 20 | """Override the parent _request method. 21 | 22 | We have to do this here because FireEye requires some extra 23 | authentication steps. 24 | """ 25 | if params: 26 | params['environment_id'] = self.env_id 27 | else: 28 | params = { 29 | 'environment_id': self.env_id, 30 | } 31 | 32 | if headers: 33 | headers['api-key'] = self.key 34 | headers['User-Agent'] = 'Falcon Sandbox' 35 | headers['Accept'] = 'application/json' 36 | else: 37 | headers = { 38 | 'api-key': self.key, 39 | 'User-Agent': 'Falcon Sandbox', 40 | 'Accept': 'application/json', 41 | } 42 | 43 | return sandboxapi.SandboxAPI._request(self, uri, method, params, files, headers) 44 | 45 | def analyze(self, handle, filename): 46 | """Submit a file for analysis. 47 | 48 | :type handle: File handle 49 | :param handle: Handle to file to upload for analysis. 50 | :type filename: str 51 | :param filename: File name. 52 | 53 | :rtype: str 54 | :return: File hash as a string 55 | """ 56 | # multipart post files. 57 | files = {"file" : (filename, handle)} 58 | 59 | # ensure the handle is at offset 0. 60 | handle.seek(0) 61 | 62 | response = self._request("/submit/file", method='POST', files=files) 63 | 64 | try: 65 | if response.status_code == 201: 66 | # good response 67 | return response.json()['job_id'] 68 | else: 69 | raise sandboxapi.SandboxError("api error in analyze: {r}".format(r=response.content.decode('utf-8'))) 70 | except (ValueError, KeyError) as e: 71 | raise sandboxapi.SandboxError("error in analyze: {e}".format(e=e)) 72 | 73 | def check(self, item_id): 74 | """Check if an analysis is complete. 75 | 76 | :type item_id: str 77 | :param item_id: Job ID to check. 78 | 79 | :rtype: bool 80 | :return: Boolean indicating if a report is done or not. 81 | """ 82 | 83 | response = self._request("/report/{job_id}/state".format(job_id=item_id)) 84 | 85 | if response.status_code in (404, 429): 86 | # unknown job id, api request limit exceeded 87 | return False 88 | 89 | try: 90 | content = json.loads(response.content.decode('utf-8')) 91 | status = content['state'] 92 | if status == 'SUCCESS' or status == 'ERROR': 93 | return True 94 | 95 | except (ValueError, KeyError) as e: 96 | raise sandboxapi.SandboxError(e) 97 | 98 | return False 99 | 100 | def is_available(self): 101 | """Determine if the Falcon API server is alive. 102 | 103 | :rtype: bool 104 | :return: True if service is available, False otherwise. 105 | """ 106 | # if the availability flag is raised, return True immediately. 107 | # NOTE: subsequent API failures will lower this flag. we do this here 108 | # to ensure we don't keep hitting Falcon with requests while 109 | # availability is there. 110 | if self.server_available: 111 | return True 112 | 113 | # otherwise, we have to check with the cloud. 114 | else: 115 | 116 | try: 117 | # Try the on-prem endpoint. 118 | response = self._request("/system/heartbeat") 119 | 120 | # we've got falcon. 121 | if response.status_code == 200: 122 | self.server_available = True 123 | return True 124 | elif response.status_code == 403: 125 | # Try the public sandbox endpoint. 126 | response = self._request("/system/version") 127 | if response.status_code == 200: 128 | self.server_available = True 129 | return True 130 | 131 | except sandboxapi.SandboxError: 132 | pass 133 | 134 | self.server_available = False 135 | return False 136 | 137 | def queue_size(self): 138 | """Determine Falcon sandbox queue length 139 | 140 | :rtype: str 141 | :return: Details on the queue size. 142 | """ 143 | response = self._request("/system/queue-size") 144 | 145 | return response.content.decode('utf-8') 146 | 147 | def report(self, item_id, report_format="json"): 148 | """Retrieves the specified report for the analyzed item, referenced by item_id. 149 | 150 | Available formats include: json, html. 151 | 152 | :type item_id: str 153 | :param item_id: File ID number 154 | :type report_format: str 155 | :param report_format: Return format 156 | 157 | :rtype: dict 158 | :return: Dictionary representing the JSON parsed data or raw, for other 159 | formats / JSON parsing failure. 160 | """ 161 | report_format = report_format.lower() 162 | 163 | response = self._request("/report/{job_id}/summary".format(job_id=item_id)) 164 | 165 | if response.status_code == 429: 166 | raise sandboxapi.SandboxError('API rate limit exceeded while fetching report') 167 | 168 | # if response is JSON, return it as an object 169 | if report_format == "json": 170 | try: 171 | return json.loads(response.content.decode('utf-8')) 172 | except ValueError: 173 | pass 174 | 175 | # otherwise, return the raw content. 176 | return response.content.decode('utf-8') 177 | 178 | def full_report(self, item_id, report_format="json"): 179 | """Retrieves a more detailed report""" 180 | report_format = report_format.lower() 181 | 182 | response = self._request("/report/{job_id}/file/{report_format}".format(job_id=item_id, report_format=report_format)) 183 | 184 | if response.status_code == 429: 185 | raise sandboxapi.SandboxError('API rate limit exceeded while fetching report') 186 | 187 | # if response is JSON, return it as an object 188 | if report_format == "json": 189 | try: 190 | return json.loads(response.content.decode('utf-8')) 191 | except ValueError: 192 | pass 193 | 194 | # otherwise, return the raw content. 195 | return response.content.decode('utf-8') 196 | 197 | def score(self, report): 198 | """Pass in the report from self.report(), get back an int 0-10.""" 199 | 200 | try: 201 | threatlevel = int(report['threat_level']) 202 | threatscore = int(report['threat_score']) 203 | except (KeyError, IndexError, ValueError, TypeError) as e: 204 | raise sandboxapi.SandboxError(e) 205 | 206 | # from falcon docs: 207 | # threatlevel is the verdict field with values: 0 = no threat, 1 = suspicious, 2 = malicious 208 | # threascore is the "heuristic" confidence value of Falcon Sandbox in the verdict and is a value between 0 209 | # and 100. A value above 75/100 is "pretty sure", a value above 90/100 is "very sure". 210 | 211 | # the scoring below converts these values to a scalar. modify as needed. 212 | score = 0 213 | if threatlevel == 2 and threatscore >= 90: 214 | score = 10 215 | elif threatlevel == 2 and threatscore >= 75: 216 | score = 9 217 | elif threatlevel == 2: 218 | score = 8 219 | elif threatlevel == 1 and threatscore >= 90: 220 | score = 7 221 | elif threatlevel == 1 and threatscore >= 75: 222 | score = 6 223 | elif threatlevel == 1: 224 | score = 5 225 | elif threatlevel == 0 and threatscore < 75: 226 | score = 1 227 | 228 | return score 229 | 230 | 231 | if __name__ == "__main__": 232 | 233 | def usage(): 234 | msg = "%s: | available | queue | report >" 235 | print(msg % sys.argv[0]) 236 | sys.exit(1) 237 | 238 | if len(sys.argv) == 4: 239 | cmd = sys.argv.pop().lower() 240 | secret = sys.argv.pop().lower() 241 | key = sys.argv.pop().lower() 242 | arg = None 243 | 244 | elif len(sys.argv) == 5: 245 | arg = sys.argv.pop() 246 | cmd = sys.argv.pop().lower() 247 | secret = sys.argv.pop().lower() 248 | key = sys.argv.pop().lower() 249 | 250 | else: 251 | usage() 252 | 253 | # instantiate Falcon Sandbox API interface. 254 | falcon = FalconAPI(key, secret) 255 | 256 | # process command line arguments. 257 | if "analyze" in cmd: 258 | if arg is None: 259 | usage() 260 | else: 261 | with open(arg, "rb") as handle: 262 | print(falcon.analyze(handle, arg)) 263 | 264 | elif "available" in cmd: 265 | print(falcon.is_available()) 266 | 267 | elif "queue" in cmd: 268 | print(falcon.queue_size()) 269 | 270 | elif "report" in cmd: 271 | if arg is None: 272 | usage() 273 | else: 274 | print(falcon.report(arg)) 275 | 276 | else: 277 | usage() 278 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | sandboxapi 2 | ========== 3 | 4 | .. image:: https://inquest.net/images/inquest-badge.svg 5 | :target: https://inquest.net/ 6 | :alt: Developed by InQuest 7 | .. image:: https://github.com/InQuest/sandboxapi/workflows/sandboxapi/badge.svg?branch=master 8 | :target: https://github.com/InQuest/sandboxapi/actions 9 | :alt: Build Status (GitHub Workflow) 10 | .. image:: https://readthedocs.org/projects/sandboxapi/badge/?version=latest 11 | :target: https://inquest.readthedocs.io/projects/sandboxapi/en/latest/?badge=latest 12 | :alt: Documentation Status 13 | .. image:: http://img.shields.io/pypi/v/sandboxapi.svg 14 | :target: https://pypi.python.org/pypi/sandboxapi 15 | :alt: PyPi Version 16 | 17 | A minimal, consistent API for building integrations with malware sandboxes. 18 | 19 | This library currently supports the following sandbox systems: 20 | 21 | * `Cuckoo Sandbox`_ 22 | * `Falcon Sandbox`_ (Formerly VxStream) 23 | * `FireEye AX Series`_ 24 | * `Hatching Triage`_ 25 | * `Joe Sandbox`_ 26 | * `MetaDefender Sandbox`_ 27 | * `VMRay Analyzer`_ 28 | * `WildFire Sandbox`_ 29 | 30 | It provides at least the following methods for each sandbox: 31 | 32 | * ``is_available()``: Check if the sandbox is operable and reachable; returns a boolean 33 | * ``analyze(handle, filename)``: Submit a file for analysis; returns an ``item_id`` 34 | * ``check(item_id)``: Check if analysis has completed for a file; returns a boolean 35 | * ``report(item_id, report_format='json')``: Retrieve the report for a submitted file 36 | * ``score(report)``: Parse out and return an integer score from the report object 37 | 38 | Some sandbox classes may have additional methods implemented. See inline 39 | documentation for more details. 40 | 41 | Note that the value returned from the ``score`` method may be on the range 42 | 0-10, or 0-100, depending on the sandbox in question, so you should refer to 43 | the specific sandbox's documentation when interpreting this value. 44 | 45 | Installation 46 | ------------ 47 | 48 | Install through pip:: 49 | 50 | pip install sandboxapi 51 | 52 | Supports Python 2.7+. 53 | 54 | Usage 55 | ----- 56 | 57 | Basic usage is as follows: 58 | 59 | .. code-block:: python 60 | 61 | import sys 62 | import time 63 | import pprint 64 | 65 | from sandboxapi import cuckoo 66 | 67 | # connect to the sandbox 68 | sandbox = cuckoo.CuckooAPI('http://192.168.0.20:8090/') 69 | 70 | # verify connectivity 71 | if not sandbox.is_available(): 72 | print("sandbox is down, exiting") 73 | sys.exit(1) 74 | 75 | # submit a file 76 | with open('myfile.exe', "rb") as handle: 77 | file_id = sandbox.analyze(handle, 'myfile.exe') 78 | print("file {f} submitted for analysis, id {i}".format(f=filename, i=file_id)) 79 | 80 | # wait for the analysis to complete 81 | while not sandbox.check(file_id): 82 | print("not done yet, sleeping 10 seconds...") 83 | time.sleep(10) 84 | 85 | # print the report 86 | print("analysis complete. fetching report...") 87 | report = sandbox.report(file_id) 88 | pprint.pprint(report) 89 | print("Score: {score}".format(score=sandbox.score(report))) 90 | 91 | Since the library provides a consistent API, you can treat all sandoxes 92 | the same way: 93 | 94 | .. code-block:: python 95 | 96 | import sys 97 | import time 98 | import pprint 99 | 100 | from sandboxapi import cuckoo, fireeye, joe 101 | 102 | # connect to the sandbox 103 | sandboxes = [ 104 | cuckoo.CuckooAPI('http://192.168.0.20:8090/'), 105 | fireeye.FireEyeAPI('myusername', 'mypassword', 'https://192.168.0.21', 'winxp-sp3'), 106 | joe.JoeAPI('mykey', 'https://jbxcloud.joesecurity.org/api', True) 107 | ] 108 | 109 | for sandbox in sandboxes: 110 | # verify connectivity 111 | if not sandbox.is_available(): 112 | print("sandbox is down, exiting") 113 | sys.exit(1) 114 | 115 | # submit a file 116 | with open('myfile.exe', "rb") as handle: 117 | file_id = sandbox.analyze(handle, 'myfile.exe') 118 | print("file {f} submitted for analysis, id {i}".format(f=filename, i=file_id)) 119 | 120 | # wait for the analysis to complete 121 | while not sandbox.check(file_id): 122 | print("not done yet, sleeping 10 seconds...") 123 | time.sleep(10) 124 | 125 | # print the report 126 | print("analysis complete. fetching report...") 127 | report = sandbox.report(file_id) 128 | pprint.pprint(report) 129 | print("Score: {score}".format(score=sandbox.score(report))) 130 | 131 | Cuckoo Sandbox 132 | ~~~~~~~~~~~~~~ 133 | 134 | Constructor signature:: 135 | 136 | CuckooAPI(url, verify_ssl=False) 137 | 138 | Example:: 139 | 140 | CuckooAPI('http://192.168.0.20:8090/') 141 | 142 | This library attempts to support any Cuckoo-like API, including older 1.x 143 | installations (though those without a score won't be able to use the ``.score`` 144 | method), compatible forks like spender-sandbox and CAPE, and the latest 2.x 145 | Cuckoo releases. If you find a version that doesn't work, let us know. 146 | 147 | There is an `unofficial Cuckoo library`_ written by @keithjjones with much 148 | more functionality. For more information on the Cuckoo API, see the `Cuckoo API 149 | documentation`_. 150 | 151 | FireEye AX 152 | ~~~~~~~~~~ 153 | 154 | Constructor signature:: 155 | 156 | FireEyeAPI(username, password, url, profile, legacy_api=False, verify_ssl=True) 157 | 158 | Example:: 159 | 160 | FireEyeAPI('myusername', 'mypassword', 'https://192.168.0.20', 'winxp-sp3') 161 | 162 | By default, the ``FireEyeAPI`` class uses v1.2.0 of the FireEye API, which is 163 | available on v8.x FireEye AX series appliances. The v1.1.0 API, which is 164 | available on v7.x appliances, is also supported - just set ``legacy_api=True`` 165 | to use the older version. 166 | 167 | There is some limited `FireEye API documentation`_ on their blog. For more 168 | information on FireEye's sandbox systems, see the `AX Series product page`_. 169 | FireEye customers have access to more API documentation. 170 | 171 | Joe Sandbox 172 | ~~~~~~~~~~~ 173 | 174 | Constructor signature:: 175 | 176 | JoeAPI(apikey, apiurl, accept_tac, timeout=None, verify_ssl=True, retries=3) 177 | 178 | Example:: 179 | 180 | JoeAPI('mykey', 'https://jbxcloud.joesecurity.org/api', True) 181 | 182 | There is an `official Joe Sandbox library`_ with much more functionality. 183 | This library is installed as a dependency of sandboxapi, and wrapped by the 184 | ``sandboxapi.joe.JoeSandbox`` class. 185 | 186 | VMRay Analyzer 187 | ~~~~~~~~~~~~~~ 188 | 189 | Constructor signature:: 190 | 191 | VMRayAPI(api_key, url='https://cloud.vmray.com', verify_ssl=True) 192 | 193 | Example:: 194 | 195 | VMRayAPI('mykey') 196 | 197 | VMRay customers have access to a Python library with much more functionality. 198 | Check your VMRay documentation for more details. 199 | 200 | Falcon Sandbox 201 | ~~~~~~~~~~~~~~ 202 | 203 | Constructor signature:: 204 | 205 | FalconAPI(key, url='https://www.reverse.it/api/v2', env=100) 206 | 207 | Example:: 208 | 209 | FalconAPI('mykey') 210 | 211 | This class only supports version 2.0+ of the Falcon API, which is available 212 | in version 8.0.0+ of the Falcon Sandbox. 213 | 214 | There is an `official Falcon library`_ with much more functionality, that 215 | supports the current and older versions of the Falcon API. Note that the 216 | official library only supports Python 3.4+. 217 | 218 | 219 | WildFire Sandbox 220 | ~~~~~~~~~~~~~~~~ 221 | 222 | Constructor signature:: 223 | 224 | WildFireAPI(api_key, url='https://wildfire.paloaltonetworks.com/publicapi') 225 | 226 | Example:: 227 | 228 | WildFireAPI('mykey') 229 | 230 | Currently, only the WildFire cloud sandbox is supported and not the WildFire appliance. 231 | 232 | 233 | MetaDefender Sandbox 234 | ~~~~~~~~~~~~~~~~~~~~ 235 | 236 | Constructor signature:: 237 | 238 | MetaDefenderSandboxAPI(api_key, url=None, verify_ssl=True) 239 | 240 | Example:: 241 | 242 | MetaDefenderSandboxAPI('mykey') 243 | 244 | MetaDefender Sandbox (previously known as OPSWAT Filescan Sandbox). You can use the Activation Key that you received 245 | from your OPSWAT Sales Representative, and follow the instructions on the 246 | `OPSWAT Licence Activation`_ page or you can create an API key on the 247 | `MetaDefender Sandbox Community Site`_ under API Key tab. 248 | 249 | More details in the `MetaDefender Sandbox API documentation`_. 250 | 251 | 252 | Hatching Triage 253 | ~~~~~~~~~~~~~~~~ 254 | 255 | Constructor signature:: 256 | 257 | TriageAPI(api_key, url='https://api.tria.ge', api_path='/v0') 258 | 259 | Example:: 260 | 261 | TriageAPI("ApiKeyHere") 262 | 263 | You're able to use this class with both the `Triage public cloud`_ and the 264 | private Triage instances. Look up the documentation for the right host and 265 | api path for your specific instance. 266 | 267 | For more information on what is returned from the API you can look up the 268 | official `Triage API documentation`_. 269 | 270 | 271 | Notes 272 | ----- 273 | 274 | You may also be interested in `malsub`_, a similar project with support for a 275 | number of online analysis services. 276 | 277 | 278 | .. _Cuckoo Sandbox: https://www.cuckoosandbox.org/ 279 | .. _Fireeye AX Series: https://www.fireeye.com/products/malware-analysis.html 280 | .. _Joe Sandbox: https://www.joesecurity.org/ 281 | .. _MetaDefender Sandbox: https://docs.opswat.com/filescan 282 | .. _VMRay Analyzer: https://www.vmray.com/ 283 | .. _Falcon Sandbox: https://www.falcon-sandbox.com/ 284 | .. _WildFire Sandbox: https://www.paloaltonetworks.com/products/secure-the-network/wildfire 285 | .. _Hatching Triage: https://tria.ge/ 286 | .. _unofficial Cuckoo library: https://github.com/keithjjones/cuckoo-api 287 | .. _Cuckoo API documentation: https://cuckoo.sh/docs/usage/api.html 288 | .. _FireEye API documentation: https://www.fireeye.com/blog/products-and-services/2015/12/restful_apis_thatdo.html 289 | .. _AX Series product page: https://www.fireeye.com/products/malware-analysis.html 290 | .. _official Joe Sandbox library: https://github.com/joesecurity/joesandboxcloudapi 291 | .. _official Falcon library: https://github.com/PayloadSecurity/VxAPI 292 | .. _OPSWAT Licence Activation: https://docs.opswat.com/filescan/installation/license-activation 293 | .. _MetaDefender Sandbox Community Site: https://www.filescan.io/users/profile?active=apikeyinfo 294 | .. _MetaDefender Sandbox API documentation: https://docs.opswat.com/filescan/metadefender-sandbox-api-reference-v1 295 | .. _malsub: https://github.com/diogo-fernan/malsub 296 | .. _Triage public cloud: https://tria.ge/ 297 | .. _Triage API documentation: https://tria.ge/docs/ 298 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 2, June 1991 3 | 4 | Copyright (C) 1989, 1991 Free Software Foundation, Inc., 5 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 6 | Everyone is permitted to copy and distribute verbatim copies 7 | of this license document, but changing it is not allowed. 8 | 9 | Preamble 10 | 11 | The licenses for most software are designed to take away your 12 | freedom to share and change it. By contrast, the GNU General Public 13 | License is intended to guarantee your freedom to share and change free 14 | software--to make sure the software is free for all its users. This 15 | General Public License applies to most of the Free Software 16 | Foundation's software and to any other program whose authors commit to 17 | using it. (Some other Free Software Foundation software is covered by 18 | the GNU Lesser General Public License instead.) You can apply it to 19 | your programs, too. 20 | 21 | When we speak of free software, we are referring to freedom, not 22 | price. Our General Public Licenses are designed to make sure that you 23 | have the freedom to distribute copies of free software (and charge for 24 | this service if you wish), that you receive source code or can get it 25 | if you want it, that you can change the software or use pieces of it 26 | in new free programs; and that you know you can do these things. 27 | 28 | To protect your rights, we need to make restrictions that forbid 29 | anyone to deny you these rights or to ask you to surrender the rights. 30 | These restrictions translate to certain responsibilities for you if you 31 | distribute copies of the software, or if you modify it. 32 | 33 | For example, if you distribute copies of such a program, whether 34 | gratis or for a fee, you must give the recipients all the rights that 35 | you have. You must make sure that they, too, receive or can get the 36 | source code. And you must show them these terms so they know their 37 | rights. 38 | 39 | We protect your rights with two steps: (1) copyright the software, and 40 | (2) offer you this license which gives you legal permission to copy, 41 | distribute and/or modify the software. 42 | 43 | Also, for each author's protection and ours, we want to make certain 44 | that everyone understands that there is no warranty for this free 45 | software. If the software is modified by someone else and passed on, we 46 | want its recipients to know that what they have is not the original, so 47 | that any problems introduced by others will not reflect on the original 48 | authors' reputations. 49 | 50 | Finally, any free program is threatened constantly by software 51 | patents. We wish to avoid the danger that redistributors of a free 52 | program will individually obtain patent licenses, in effect making the 53 | program proprietary. To prevent this, we have made it clear that any 54 | patent must be licensed for everyone's free use or not licensed at all. 55 | 56 | The precise terms and conditions for copying, distribution and 57 | modification follow. 58 | 59 | GNU GENERAL PUBLIC LICENSE 60 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 61 | 62 | 0. This License applies to any program or other work which contains 63 | a notice placed by the copyright holder saying it may be distributed 64 | under the terms of this General Public License. The "Program", below, 65 | refers to any such program or work, and a "work based on the Program" 66 | means either the Program or any derivative work under copyright law: 67 | that is to say, a work containing the Program or a portion of it, 68 | either verbatim or with modifications and/or translated into another 69 | language. (Hereinafter, translation is included without limitation in 70 | the term "modification".) Each licensee is addressed as "you". 71 | 72 | Activities other than copying, distribution and modification are not 73 | covered by this License; they are outside its scope. The act of 74 | running the Program is not restricted, and the output from the Program 75 | is covered only if its contents constitute a work based on the 76 | Program (independent of having been made by running the Program). 77 | Whether that is true depends on what the Program does. 78 | 79 | 1. You may copy and distribute verbatim copies of the Program's 80 | source code as you receive it, in any medium, provided that you 81 | conspicuously and appropriately publish on each copy an appropriate 82 | copyright notice and disclaimer of warranty; keep intact all the 83 | notices that refer to this License and to the absence of any warranty; 84 | and give any other recipients of the Program a copy of this License 85 | along with the Program. 86 | 87 | You may charge a fee for the physical act of transferring a copy, and 88 | you may at your option offer warranty protection in exchange for a fee. 89 | 90 | 2. You may modify your copy or copies of the Program or any portion 91 | of it, thus forming a work based on the Program, and copy and 92 | distribute such modifications or work under the terms of Section 1 93 | above, provided that you also meet all of these conditions: 94 | 95 | a) You must cause the modified files to carry prominent notices 96 | stating that you changed the files and the date of any change. 97 | 98 | b) You must cause any work that you distribute or publish, that in 99 | whole or in part contains or is derived from the Program or any 100 | part thereof, to be licensed as a whole at no charge to all third 101 | parties under the terms of this License. 102 | 103 | c) If the modified program normally reads commands interactively 104 | when run, you must cause it, when started running for such 105 | interactive use in the most ordinary way, to print or display an 106 | announcement including an appropriate copyright notice and a 107 | notice that there is no warranty (or else, saying that you provide 108 | a warranty) and that users may redistribute the program under 109 | these conditions, and telling the user how to view a copy of this 110 | License. (Exception: if the Program itself is interactive but 111 | does not normally print such an announcement, your work based on 112 | the Program is not required to print an announcement.) 113 | 114 | These requirements apply to the modified work as a whole. If 115 | identifiable sections of that work are not derived from the Program, 116 | and can be reasonably considered independent and separate works in 117 | themselves, then this License, and its terms, do not apply to those 118 | sections when you distribute them as separate works. But when you 119 | distribute the same sections as part of a whole which is a work based 120 | on the Program, the distribution of the whole must be on the terms of 121 | this License, whose permissions for other licensees extend to the 122 | entire whole, and thus to each and every part regardless of who wrote it. 123 | 124 | Thus, it is not the intent of this section to claim rights or contest 125 | your rights to work written entirely by you; rather, the intent is to 126 | exercise the right to control the distribution of derivative or 127 | collective works based on the Program. 128 | 129 | In addition, mere aggregation of another work not based on the Program 130 | with the Program (or with a work based on the Program) on a volume of 131 | a storage or distribution medium does not bring the other work under 132 | the scope of this License. 133 | 134 | 3. You may copy and distribute the Program (or a work based on it, 135 | under Section 2) in object code or executable form under the terms of 136 | Sections 1 and 2 above provided that you also do one of the following: 137 | 138 | a) Accompany it with the complete corresponding machine-readable 139 | source code, which must be distributed under the terms of Sections 140 | 1 and 2 above on a medium customarily used for software interchange; or, 141 | 142 | b) Accompany it with a written offer, valid for at least three 143 | years, to give any third party, for a charge no more than your 144 | cost of physically performing source distribution, a complete 145 | machine-readable copy of the corresponding source code, to be 146 | distributed under the terms of Sections 1 and 2 above on a medium 147 | customarily used for software interchange; or, 148 | 149 | c) Accompany it with the information you received as to the offer 150 | to distribute corresponding source code. (This alternative is 151 | allowed only for noncommercial distribution and only if you 152 | received the program in object code or executable form with such 153 | an offer, in accord with Subsection b above.) 154 | 155 | The source code for a work means the preferred form of the work for 156 | making modifications to it. For an executable work, complete source 157 | code means all the source code for all modules it contains, plus any 158 | associated interface definition files, plus the scripts used to 159 | control compilation and installation of the executable. However, as a 160 | special exception, the source code distributed need not include 161 | anything that is normally distributed (in either source or binary 162 | form) with the major components (compiler, kernel, and so on) of the 163 | operating system on which the executable runs, unless that component 164 | itself accompanies the executable. 165 | 166 | If distribution of executable or object code is made by offering 167 | access to copy from a designated place, then offering equivalent 168 | access to copy the source code from the same place counts as 169 | distribution of the source code, even though third parties are not 170 | compelled to copy the source along with the object code. 171 | 172 | 4. You may not copy, modify, sublicense, or distribute the Program 173 | except as expressly provided under this License. Any attempt 174 | otherwise to copy, modify, sublicense or distribute the Program is 175 | void, and will automatically terminate your rights under this License. 176 | However, parties who have received copies, or rights, from you under 177 | this License will not have their licenses terminated so long as such 178 | parties remain in full compliance. 179 | 180 | 5. You are not required to accept this License, since you have not 181 | signed it. However, nothing else grants you permission to modify or 182 | distribute the Program or its derivative works. These actions are 183 | prohibited by law if you do not accept this License. Therefore, by 184 | modifying or distributing the Program (or any work based on the 185 | Program), you indicate your acceptance of this License to do so, and 186 | all its terms and conditions for copying, distributing or modifying 187 | the Program or works based on it. 188 | 189 | 6. Each time you redistribute the Program (or any work based on the 190 | Program), the recipient automatically receives a license from the 191 | original licensor to copy, distribute or modify the Program subject to 192 | these terms and conditions. You may not impose any further 193 | restrictions on the recipients' exercise of the rights granted herein. 194 | You are not responsible for enforcing compliance by third parties to 195 | this License. 196 | 197 | 7. If, as a consequence of a court judgment or allegation of patent 198 | infringement or for any other reason (not limited to patent issues), 199 | conditions are imposed on you (whether by court order, agreement or 200 | otherwise) that contradict the conditions of this License, they do not 201 | excuse you from the conditions of this License. If you cannot 202 | distribute so as to satisfy simultaneously your obligations under this 203 | License and any other pertinent obligations, then as a consequence you 204 | may not distribute the Program at all. For example, if a patent 205 | license would not permit royalty-free redistribution of the Program by 206 | all those who receive copies directly or indirectly through you, then 207 | the only way you could satisfy both it and this License would be to 208 | refrain entirely from distribution of the Program. 209 | 210 | If any portion of this section is held invalid or unenforceable under 211 | any particular circumstance, the balance of the section is intended to 212 | apply and the section as a whole is intended to apply in other 213 | circumstances. 214 | 215 | It is not the purpose of this section to induce you to infringe any 216 | patents or other property right claims or to contest validity of any 217 | such claims; this section has the sole purpose of protecting the 218 | integrity of the free software distribution system, which is 219 | implemented by public license practices. Many people have made 220 | generous contributions to the wide range of software distributed 221 | through that system in reliance on consistent application of that 222 | system; it is up to the author/donor to decide if he or she is willing 223 | to distribute software through any other system and a licensee cannot 224 | impose that choice. 225 | 226 | This section is intended to make thoroughly clear what is believed to 227 | be a consequence of the rest of this License. 228 | 229 | 8. If the distribution and/or use of the Program is restricted in 230 | certain countries either by patents or by copyrighted interfaces, the 231 | original copyright holder who places the Program under this License 232 | may add an explicit geographical distribution limitation excluding 233 | those countries, so that distribution is permitted only in or among 234 | countries not thus excluded. In such case, this License incorporates 235 | the limitation as if written in the body of this License. 236 | 237 | 9. The Free Software Foundation may publish revised and/or new versions 238 | of the General Public License from time to time. Such new versions will 239 | be similar in spirit to the present version, but may differ in detail to 240 | address new problems or concerns. 241 | 242 | Each version is given a distinguishing version number. If the Program 243 | specifies a version number of this License which applies to it and "any 244 | later version", you have the option of following the terms and conditions 245 | either of that version or of any later version published by the Free 246 | Software Foundation. If the Program does not specify a version number of 247 | this License, you may choose any version ever published by the Free Software 248 | Foundation. 249 | 250 | 10. If you wish to incorporate parts of the Program into other free 251 | programs whose distribution conditions are different, write to the author 252 | to ask for permission. For software which is copyrighted by the Free 253 | Software Foundation, write to the Free Software Foundation; we sometimes 254 | make exceptions for this. Our decision will be guided by the two goals 255 | of preserving the free status of all derivatives of our free software and 256 | of promoting the sharing and reuse of software generally. 257 | 258 | NO WARRANTY 259 | 260 | 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY 261 | FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN 262 | OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES 263 | PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED 264 | OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 265 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS 266 | TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE 267 | PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, 268 | REPAIR OR CORRECTION. 269 | 270 | 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 271 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR 272 | REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, 273 | INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING 274 | OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED 275 | TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY 276 | YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER 277 | PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE 278 | POSSIBILITY OF SUCH DAMAGES. 279 | 280 | END OF TERMS AND CONDITIONS 281 | 282 | How to Apply These Terms to Your New Programs 283 | 284 | If you develop a new program, and you want it to be of the greatest 285 | possible use to the public, the best way to achieve this is to make it 286 | free software which everyone can redistribute and change under these terms. 287 | 288 | To do so, attach the following notices to the program. It is safest 289 | to attach them to the start of each source file to most effectively 290 | convey the exclusion of warranty; and each file should have at least 291 | the "copyright" line and a pointer to where the full notice is found. 292 | 293 | 294 | Copyright (C) 295 | 296 | This program is free software; you can redistribute it and/or modify 297 | it under the terms of the GNU General Public License as published by 298 | the Free Software Foundation; either version 2 of the License, or 299 | (at your option) any later version. 300 | 301 | This program is distributed in the hope that it will be useful, 302 | but WITHOUT ANY WARRANTY; without even the implied warranty of 303 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 304 | GNU General Public License for more details. 305 | 306 | You should have received a copy of the GNU General Public License along 307 | with this program; if not, write to the Free Software Foundation, Inc., 308 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 309 | 310 | Also add information on how to contact you by electronic and paper mail. 311 | 312 | If the program is interactive, make it output a short notice like this 313 | when it starts in an interactive mode: 314 | 315 | Gnomovision version 69, Copyright (C) year name of author 316 | Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 317 | This is free software, and you are welcome to redistribute it 318 | under certain conditions; type `show c' for details. 319 | 320 | The hypothetical commands `show w' and `show c' should show the appropriate 321 | parts of the General Public License. Of course, the commands you use may 322 | be called something other than `show w' and `show c'; they could even be 323 | mouse-clicks or menu items--whatever suits your program. 324 | 325 | You should also get your employer (if you work as a programmer) or your 326 | school, if any, to sign a "copyright disclaimer" for the program, if 327 | necessary. Here is a sample; alter the names: 328 | 329 | Yoyodyne, Inc., hereby disclaims all copyright interest in the program 330 | `Gnomovision' (which makes passes at compilers) written by James Hacker. 331 | 332 | , 1 April 1989 333 | Ty Coon, President of Vice 334 | 335 | This General Public License does not permit incorporating your program into 336 | proprietary programs. If your program is a subroutine library, you may 337 | consider it more useful to permit linking proprietary applications with the 338 | library. If this is what you want to do, use the GNU Lesser General 339 | Public License instead of this License. 340 | --------------------------------------------------------------------------------