├── 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 |
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 |
--------------------------------------------------------------------------------